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:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-06-18 14:18:50 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-06-18 14:18:50 +0300
commit8c7f4e9d5f36cff46365a7f8c4b9c21578c1e781 (patch)
treea77e7fe7a93de11213032ed4ab1f33a3db51b738 /spec/requests/api/graphql
parent00b35af3db1abfe813a778f643dad221aad51fca (diff)
Add latest changes from gitlab-org/gitlab@13-1-stable-ee
Diffstat (limited to 'spec/requests/api/graphql')
-rw-r--r--spec/requests/api/graphql/boards/board_lists_query_spec.rb64
-rw-r--r--spec/requests/api/graphql/group/labels_query_spec.rb19
-rw-r--r--spec/requests/api/graphql/metrics/dashboard_query_spec.rb56
-rw-r--r--spec/requests/api/graphql/mutations/alert_management/alerts/create_alert_issue_spec.rb74
-rw-r--r--spec/requests/api/graphql/mutations/alert_management/alerts/set_assignees_spec.rb65
-rw-r--r--spec/requests/api/graphql/mutations/alert_management/alerts/update_alert_status_spec.rb20
-rw-r--r--spec/requests/api/graphql/mutations/commits/create_spec.rb52
-rw-r--r--spec/requests/api/graphql/mutations/container_expiration_policy/update_spec.rb110
-rw-r--r--spec/requests/api/graphql/mutations/discussions/toggle_resolve_spec.rb49
-rw-r--r--spec/requests/api/graphql/mutations/jira_import/import_users_spec.rb104
-rw-r--r--spec/requests/api/graphql/mutations/jira_import/start_spec.rb62
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/create_spec.rb51
-rw-r--r--spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb80
-rw-r--r--spec/requests/api/graphql/mutations/snippets/create_spec.rb40
-rw-r--r--spec/requests/api/graphql/project/alert_management/alert/assignees_spec.rb78
-rw-r--r--spec/requests/api/graphql/project/alert_management/alert/notes_spec.rb67
-rw-r--r--spec/requests/api/graphql/project/alert_management/alert_status_counts_spec.rb16
-rw-r--r--spec/requests/api/graphql/project/alert_management/alerts_spec.rb3
-rw-r--r--spec/requests/api/graphql/project/container_expiration_policy_spec.rb30
-rw-r--r--spec/requests/api/graphql/project/issues_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/jira_import_spec.rb36
-rw-r--r--spec/requests/api/graphql/project/jira_projects_spec.rb114
-rw-r--r--spec/requests/api/graphql/project/labels_query_spec.rb19
-rw-r--r--spec/requests/api/graphql/project/merge_request_spec.rb24
-rw-r--r--spec/requests/api/graphql/project/merge_requests_spec.rb174
-rw-r--r--spec/requests/api/graphql/project/pipeline_spec.rb32
-rw-r--r--spec/requests/api/graphql/project/release_spec.rb206
-rw-r--r--spec/requests/api/graphql/project_query_spec.rb48
-rw-r--r--spec/requests/api/graphql/tasks/task_completion_status_spec.rb8
-rw-r--r--spec/requests/api/graphql/user/group_member_query_spec.rb32
-rw-r--r--spec/requests/api/graphql/user/project_member_query_spec.rb32
-rw-r--r--spec/requests/api/graphql/user_query_spec.rb260
-rw-r--r--spec/requests/api/graphql/user_spec.rb55
-rw-r--r--spec/requests/api/graphql/users_spec.rb90
34 files changed, 2039 insertions, 133 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 f0927487f85..3cc1468be02 100644
--- a/spec/requests/api/graphql/boards/board_lists_query_spec.rb
+++ b/spec/requests/api/graphql/boards/board_lists_query_spec.rb
@@ -65,41 +65,41 @@ describe 'get board lists' do
end
describe 'sorting and pagination' do
+ let_it_be(:current_user) { user }
+ let(:data_path) { [board_parent_type, :boards, :edges, 0, :node, :lists] }
+
+ def pagination_query(params, page_info)
+ 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 } }")}
+ }
+ }
+ }
+ 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) }
let!(:backlog_list) { create(:backlog_list, board: board) }
let(:closed_list) { board.lists.find_by(list_type: :closed) }
-
- before do
- post_graphql(query, current_user: user)
- end
-
- it_behaves_like 'a working graphql query'
+ let(:lists) { [backlog_list, label_list2, label_list, closed_list] }
context 'when ascending' do
- let(:lists) { [backlog_list, label_list2, label_list, closed_list] }
- let(:expected_list_gids) do
- lists.map { |list| list.to_global_id.to_s }
- end
-
- it 'sorts lists' do
- expect(grab_ids).to eq expected_list_gids
- end
-
- context 'when paginating' do
- let(:params) { 'first: 2' }
-
- it 'sorts boards' do
- expect(grab_ids).to eq expected_list_gids.first(2)
-
- cursored_query = query("after: \"#{end_cursor}\"")
- post_graphql(cursored_query, current_user: user)
-
- response_data = grab_list_data(response.body)
-
- expect(grab_ids(response_data)).to eq expected_list_gids.drop(2).first(2)
- end
+ 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 } }
end
end
end
@@ -126,12 +126,4 @@ describe 'get board lists' do
it_behaves_like 'group and project board lists query'
end
-
- def grab_ids(data = lists_data)
- data.map { |list| list.dig('node', 'id') }
- end
-
- def grab_list_data(response_body)
- Gitlab::Json.parse(response_body)['data'][board_parent_type]['boards']['edges'][0]['node']['lists']['edges']
- end
end
diff --git a/spec/requests/api/graphql/group/labels_query_spec.rb b/spec/requests/api/graphql/group/labels_query_spec.rb
new file mode 100644
index 00000000000..6c34cbadf95
--- /dev/null
+++ b/spec/requests/api/graphql/group/labels_query_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'getting group label information' do
+ include GraphqlHelpers
+
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be(:label_factory) { :group_label }
+ let_it_be(:label_attrs) { { group: group } }
+
+ it_behaves_like 'querying a GraphQL type with labels' do
+ let(:path_prefix) { ['group'] }
+
+ def make_query(fields)
+ graphql_query_for('group', { full_path: group.full_path }, fields)
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/metrics/dashboard_query_spec.rb b/spec/requests/api/graphql/metrics/dashboard_query_spec.rb
index 8b0965a815b..d9d9ea9ad61 100644
--- a/spec/requests/api/graphql/metrics/dashboard_query_spec.rb
+++ b/spec/requests/api/graphql/metrics/dashboard_query_spec.rb
@@ -9,25 +9,19 @@ describe 'Getting Metrics Dashboard' do
let(:project) { create(:project) }
let!(:environment) { create(:environment, project: project) }
- let(:fields) do
- <<~QUERY
- #{all_graphql_fields_for('MetricsDashboard'.classify)}
- QUERY
- end
-
let(:query) do
- %(
- query {
- project(fullPath:"#{project.full_path}") {
- environments(name: "#{environment.name}") {
- nodes {
- metricsDashboard(path: "#{path}"){
- #{fields}
- }
- }
- }
- }
- }
+ graphql_query_for(
+ 'project', { 'fullPath' => project.full_path },
+ query_graphql_field(
+ :environments, { 'name' => environment.name },
+ query_graphql_field(
+ :nodes, nil,
+ query_graphql_field(
+ :metricsDashboard, { 'path' => path },
+ all_graphql_fields_for('MetricsDashboard'.classify)
+ )
+ )
+ )
)
end
@@ -63,7 +57,29 @@ describe 'Getting Metrics Dashboard' do
it 'returns metrics dashboard' do
dashboard = graphql_data.dig('project', 'environments', 'nodes')[0]['metricsDashboard']
- expect(dashboard).to eql("path" => path)
+ expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => nil)
+ end
+
+ context 'invalid dashboard' do
+ let(:path) { '.gitlab/dashboards/metrics.yml' }
+ let(:project) { create(:project, :repository, :custom_repo, namespace: current_user.namespace, files: { path => "---\ndasboard: ''" }) }
+
+ it 'returns metrics dashboard' do
+ dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
+
+ expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["dashboard: can't be blank", "panel_groups: can't be blank"])
+ end
+ end
+
+ context 'empty dashboard' do
+ let(:path) { '.gitlab/dashboards/metrics.yml' }
+ let(:project) { create(:project, :repository, :custom_repo, namespace: current_user.namespace, files: { path => "" }) }
+
+ it 'returns metrics dashboard' do
+ dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
+
+ expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["dashboard: can't be blank", "panel_groups: can't be blank"])
+ end
end
end
@@ -72,7 +88,7 @@ describe 'Getting Metrics Dashboard' do
it_behaves_like 'a working graphql query'
- it 'return snil' do
+ it 'returns nil' do
dashboard = graphql_data.dig('project', 'environments', 'nodes')[0]['metricsDashboard']
expect(dashboard).to be_nil
diff --git a/spec/requests/api/graphql/mutations/alert_management/alerts/create_alert_issue_spec.rb b/spec/requests/api/graphql/mutations/alert_management/alerts/create_alert_issue_spec.rb
new file mode 100644
index 00000000000..5b5b2ec8788
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/alert_management/alerts/create_alert_issue_spec.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Create an alert issue from an alert' do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:alert) { create(:alert_management_alert, project: project) }
+
+ let(:mutation) do
+ variables = {
+ project_path: project.full_path,
+ iid: alert.iid.to_s
+ }
+ graphql_mutation(:create_alert_issue, variables,
+ <<~QL
+ clientMutationId
+ errors
+ alert {
+ iid
+ issueIid
+ }
+ issue {
+ iid
+ title
+ }
+ QL
+ )
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:create_alert_issue) }
+
+ before do
+ project.add_developer(user)
+ end
+
+ context 'when there is no issue associated with the alert' do
+ it 'creates an alert issue' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ new_issue = Issue.last!
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response.slice('alert', 'issue')).to eq(
+ 'alert' => {
+ 'iid' => alert.iid.to_s,
+ 'issueIid' => new_issue.iid.to_s
+ },
+ 'issue' => {
+ 'iid' => new_issue.iid.to_s,
+ 'title' => new_issue.title
+ }
+ )
+ end
+ end
+
+ context 'when there is an issue already associated with the alert' do
+ before do
+ AlertManagement::CreateAlertIssueService.new(alert, user).execute
+ end
+
+ it 'responds with an error' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response.slice('errors', 'issue')).to eq(
+ 'errors' => ['An issue already exists'],
+ 'issue' => nil
+ )
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/alert_management/alerts/set_assignees_spec.rb b/spec/requests/api/graphql/mutations/alert_management/alerts/set_assignees_spec.rb
new file mode 100644
index 00000000000..6663281e093
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/alert_management/alerts/set_assignees_spec.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Setting assignees of an alert' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:alert) { create(:alert_management_alert, project: project) }
+ let(:input) { { assignee_usernames: [current_user.username] } }
+
+ let(:mutation) do
+ graphql_mutation(
+ :alert_set_assignees,
+ { project_path: project.full_path, iid: alert.iid.to_s }.merge(input),
+ <<~QL
+ clientMutationId
+ errors
+ alert {
+ assignees {
+ nodes {
+ username
+ }
+ }
+ }
+ QL
+ )
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:alert_set_assignees) }
+
+ before_all do
+ project.add_developer(current_user)
+ end
+
+ it 'updates the assignee of the alert' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['alert']['assignees']['nodes'].first['username']).to eq(current_user.username)
+ expect(alert.reload.assignees).to contain_exactly(current_user)
+ end
+
+ context 'with operation_mode specified' do
+ let(:input) do
+ {
+ assignee_usernames: [current_user.username],
+ operation_mode: Types::MutationOperationModeEnum.enum[:remove]
+ }
+ end
+
+ before do
+ alert.assignees = [current_user]
+ end
+
+ it 'updates the assignee of the alert' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['alert']['assignees']['nodes']).to be_empty
+ expect(alert.reload.assignees).to be_empty
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/alert_management/alerts/update_alert_status_spec.rb b/spec/requests/api/graphql/mutations/alert_management/alerts/update_alert_status_spec.rb
index fe50468134c..2a470bda689 100644
--- a/spec/requests/api/graphql/mutations/alert_management/alerts/update_alert_status_spec.rb
+++ b/spec/requests/api/graphql/mutations/alert_management/alerts/update_alert_status_spec.rb
@@ -15,16 +15,16 @@ describe 'Setting the status of an alert' do
project_path: project.full_path,
iid: alert.iid.to_s
}
- graphql_mutation(:update_alert_status, variables.merge(input),
- <<~QL
- clientMutationId
- errors
- alert {
- iid
- status
- }
- QL
- )
+ graphql_mutation(:update_alert_status, variables.merge(input)) do
+ <<~QL
+ clientMutationId
+ errors
+ alert {
+ iid
+ status
+ }
+ QL
+ end
end
let(:mutation_response) { graphql_mutation_response(:update_alert_status) }
diff --git a/spec/requests/api/graphql/mutations/commits/create_spec.rb b/spec/requests/api/graphql/mutations/commits/create_spec.rb
new file mode 100644
index 00000000000..10a69932948
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/commits/create_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Creation of a new commit' do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let(:input) { { project_path: project.full_path, branch: branch, message: message, actions: actions } }
+ let(:branch) { 'master' }
+ let(:message) { 'Commit message' }
+ let(:actions) do
+ [
+ {
+ action: 'CREATE',
+ filePath: 'NEW_FILE.md',
+ content: 'Hello'
+ }
+ ]
+ end
+
+ let(:mutation) { graphql_mutation(:commit_create, input) }
+ let(:mutation_response) { graphql_mutation_response(:commit_create) }
+
+ context 'the user is not allowed to create a commit' do
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action']
+ end
+
+ context 'when user has permissions to create a commit' do
+ before do
+ project.add_developer(current_user)
+ end
+
+ it 'creates a new commit' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['commit']).to include(
+ 'title' => message
+ )
+ end
+
+ context 'when branch is not correct' do
+ let(:branch) { 'unknown' }
+
+ it_behaves_like 'a mutation that returns errors in the response',
+ errors: ['You can only create or edit files when you are on a branch']
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/container_expiration_policy/update_spec.rb b/spec/requests/api/graphql/mutations/container_expiration_policy/update_spec.rb
new file mode 100644
index 00000000000..bc256a08f00
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/container_expiration_policy/update_spec.rb
@@ -0,0 +1,110 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Updating the container expiration policy' do
+ include GraphqlHelpers
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:project, reload: true) { create(:project) }
+ let_it_be(:user) { create(:user) }
+
+ let(:container_expiration_policy) { project.container_expiration_policy.reload }
+ let(:params) do
+ {
+ project_path: project.full_path,
+ cadence: 'EVERY_THREE_MONTHS',
+ keep_n: 'ONE_HUNDRED_TAGS',
+ older_than: 'FOURTEEN_DAYS'
+ }
+ end
+ let(:mutation) do
+ graphql_mutation(:update_container_expiration_policy, params,
+ <<~QL
+ containerExpirationPolicy {
+ cadence
+ keepN
+ nameRegexKeep
+ nameRegex
+ olderThan
+ }
+ errors
+ QL
+ )
+ end
+ let(:mutation_response) { graphql_mutation_response(:update_container_expiration_policy) }
+ let(:container_expiration_policy_response) { mutation_response['containerExpirationPolicy'] }
+
+ RSpec.shared_examples 'returning a success' do
+ it_behaves_like 'returning response status', :success
+
+ it 'returns the updated container expiration policy' do
+ subject
+
+ expect(mutation_response['errors']).to be_empty
+ expect(container_expiration_policy_response['cadence']).to eq(params[:cadence])
+ expect(container_expiration_policy_response['keepN']).to eq(params[:keep_n])
+ expect(container_expiration_policy_response['olderThan']).to eq(params[:older_than])
+ end
+ end
+
+ RSpec.shared_examples 'updating the container expiration policy' do
+ it_behaves_like 'updating the container expiration policy attributes', mode: :update, from: { cadence: '1d', keep_n: 10, older_than: '90d' }, to: { cadence: '3month', keep_n: 100, older_than: '14d' }
+
+ it_behaves_like 'returning a success'
+ end
+
+ RSpec.shared_examples 'denying access to container expiration policy' do
+ it_behaves_like 'not creating the container expiration policy'
+
+ it_behaves_like 'returning response status', :success
+
+ it 'returns no response' do
+ subject
+
+ expect(mutation_response).to be_nil
+ end
+ end
+
+ describe 'post graphql mutation' do
+ subject { post_graphql_mutation(mutation, current_user: user) }
+
+ context 'with existing container expiration policy' do
+ where(:user_role, :shared_examples_name) do
+ :maintainer | 'updating the container expiration policy'
+ :developer | 'updating the container expiration policy'
+ :reporter | 'denying access to container expiration policy'
+ :guest | 'denying access to container expiration policy'
+ :anonymous | 'denying access to container expiration policy'
+ 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 'without existing container expiration policy' do
+ let_it_be(:project, reload: true) { create(:project, :without_container_expiration_policy) }
+
+ where(:user_role, :shared_examples_name) do
+ :maintainer | 'creating the container expiration policy'
+ :developer | 'creating the container expiration policy'
+ :reporter | 'denying access to container expiration policy'
+ :guest | 'denying access to container expiration policy'
+ :anonymous | 'denying access to container expiration policy'
+ 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
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/discussions/toggle_resolve_spec.rb b/spec/requests/api/graphql/mutations/discussions/toggle_resolve_spec.rb
new file mode 100644
index 00000000000..95e967c039d
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/discussions/toggle_resolve_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Toggling the resolve status of a discussion' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let_it_be(:noteable) { create(:merge_request, source_project: project) }
+ let(:discussion) do
+ create(:diff_note_on_merge_request, noteable: noteable, project: project).to_discussion
+ end
+ let(:mutation) do
+ graphql_mutation(:discussion_toggle_resolve, { id: discussion.to_global_id.to_s, resolve: true })
+ end
+ let(:mutation_response) { graphql_mutation_response(:discussion_toggle_resolve) }
+
+ context 'when the user does not have permission' do
+ let_it_be(:current_user) { create(:user) }
+
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: ["The resource that you are attempting to access does not exist or you don't have permission to perform this action"]
+ end
+
+ context 'when user has permission' do
+ let_it_be(:current_user) { create(:user, developer_projects: [project]) }
+
+ it 'returns the discussion without errors', :aggregate_failures do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response).to include(
+ 'discussion' => be_present,
+ 'errors' => be_empty
+ )
+ end
+
+ context 'when an error is encountered' do
+ before do
+ allow_next_instance_of(::Discussions::ResolveService) do |service|
+ allow(service).to receive(:execute).and_raise(ActiveRecord::RecordNotSaved)
+ end
+ end
+
+ it_behaves_like 'a mutation that returns errors in the response',
+ errors: ['Discussion failed to be resolved']
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/jira_import/import_users_spec.rb b/spec/requests/api/graphql/mutations/jira_import/import_users_spec.rb
new file mode 100644
index 00000000000..be0d843d5ff
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/jira_import/import_users_spec.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Importing Jira Users' do
+ include JiraServiceHelper
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let(:project_path) { project.full_path }
+ let(:start_at) { 7 }
+
+ let(:mutation) do
+ variables = {
+ start_at: start_at,
+ project_path: project_path
+ }
+
+ graphql_mutation(:jira_import_users, variables)
+ end
+
+ def mutation_response
+ graphql_mutation_response(:jira_import_users)
+ end
+
+ def jira_import
+ mutation_response['jiraUsers']
+ end
+
+ context 'with anonymous user' do
+ let(:current_user) { nil }
+
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+ end
+
+ context 'with user without permissions' do
+ let(:current_user) { user }
+
+ before do
+ project.add_developer(current_user)
+ end
+
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+ end
+
+ context 'when the user has permissions' do
+ let(:current_user) { user }
+
+ before do
+ project.add_maintainer(current_user)
+ end
+
+ context 'when the project path is invalid' do
+ let(:project_path) { 'foobar' }
+
+ it 'returns an an error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ errors = json_response['errors']
+
+ expect(errors.first['message']).to eq(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR)
+ end
+ end
+
+ context 'when all params and permissions are ok' do
+ let(:importer) { instance_double(JiraImport::UsersImporter) }
+
+ before do
+ expect(JiraImport::UsersImporter).to receive(:new).with(current_user, project, 7)
+ .and_return(importer)
+ end
+
+ context 'when service returns a successful response' do
+ it 'returns imported users' do
+ users = [{ jira_account_id: '12a', jira_display_name: 'user 1' }]
+ result = ServiceResponse.success(payload: users)
+
+ expect(importer).to receive(:execute).and_return(result)
+
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(jira_import.length).to eq(1)
+ expect(jira_import.first['jiraAccountId']).to eq('12a')
+ expect(jira_import.first['jiraDisplayName']).to eq('user 1')
+ end
+ end
+
+ context 'when service returns an error response' do
+ it 'returns an error messaege' do
+ result = ServiceResponse.error(message: 'Some error')
+
+ expect(importer).to receive(:execute).and_return(result)
+
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(mutation_response['errors']).to eq(['Some error'])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/jira_import/start_spec.rb b/spec/requests/api/graphql/mutations/jira_import/start_spec.rb
index 84110098400..296d33aec5d 100644
--- a/spec/requests/api/graphql/mutations/jira_import/start_spec.rb
+++ b/spec/requests/api/graphql/mutations/jira_import/start_spec.rb
@@ -29,10 +29,6 @@ describe 'Starting a Jira Import' do
end
context 'when the user does not have permission' do
- before do
- stub_feature_flags(jira_issue_import: true)
- end
-
shared_examples 'Jira import does not start' do
it 'does not start the Jira import' do
post_graphql_mutation(mutation, current_user: current_user)
@@ -83,53 +79,39 @@ describe 'Starting a Jira Import' do
end
end
- context 'when feature jira_issue_import feature flag is disabled' do
- before do
- stub_feature_flags(jira_issue_import: false)
- end
-
- it_behaves_like 'a mutation that returns errors in the response', errors: ['Jira import feature is disabled.']
+ context 'when project has no Jira service' do
+ it_behaves_like 'a mutation that returns errors in the response', errors: ['Jira integration not configured.']
end
- context 'when feature jira_issue_import feature flag is enabled' do
+ context 'when when project has Jira service' do
+ let!(:service) { create(:jira_service, project: project) }
+
before do
- stub_feature_flags(jira_issue_import: true)
- end
+ project.reload
- context 'when project has no Jira service' do
- it_behaves_like 'a mutation that returns errors in the response', errors: ['Jira integration not configured.']
+ stub_jira_service_test
end
- context 'when when project has Jira service' do
- let!(:service) { create(:jira_service, project: project) }
+ context 'when issues feature are disabled' do
+ let_it_be(:project, reload: true) { create(:project, :issues_disabled) }
- before do
- project.reload
-
- stub_jira_service_test
- end
-
- context 'when issues feature are disabled' do
- let_it_be(:project, reload: true) { create(:project, :issues_disabled) }
-
- it_behaves_like 'a mutation that returns errors in the response', errors: ['Cannot import because issues are not available in this project.']
- end
+ it_behaves_like 'a mutation that returns errors in the response', errors: ['Cannot import because issues are not available in this project.']
+ end
- context 'when jira_project_key not provided' do
- let(:jira_project_key) { '' }
+ context 'when jira_project_key not provided' do
+ let(:jira_project_key) { '' }
- it_behaves_like 'a mutation that returns errors in the response', errors: ['Unable to find Jira project to import data from.']
- end
+ it_behaves_like 'a mutation that returns errors in the response', errors: ['Unable to find Jira project to import data from.']
+ end
- context 'when Jira import successfully scheduled' do
- it 'schedules a Jira import' do
- post_graphql_mutation(mutation, current_user: current_user)
+ context 'when Jira import successfully scheduled' do
+ it 'schedules a Jira import' do
+ post_graphql_mutation(mutation, current_user: current_user)
- expect(jira_import['jiraProjectKey']).to eq 'AA'
- expect(jira_import['scheduledBy']['username']).to eq current_user.username
- expect(project.latest_jira_import).not_to be_nil
- expect(project.latest_jira_import).to be_scheduled
- end
+ expect(jira_import['jiraProjectKey']).to eq 'AA'
+ expect(jira_import['scheduledBy']['username']).to eq current_user.username
+ expect(project.latest_jira_import).not_to be_nil
+ expect(project.latest_jira_import).to be_scheduled
end
end
end
diff --git a/spec/requests/api/graphql/mutations/merge_requests/create_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/create_spec.rb
new file mode 100644
index 00000000000..5c63f655f1d
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/merge_requests/create_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Creation of a new merge request' do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let(:project) { create(:project, :public, :repository) }
+ let(:input) do
+ {
+ project_path: project.full_path,
+ title: title,
+ source_branch: source_branch,
+ target_branch: target_branch
+ }
+ end
+ let(:title) { 'MergeRequest' }
+ let(:source_branch) { 'new_branch' }
+ let(:target_branch) { 'master' }
+
+ let(:mutation) { graphql_mutation(:merge_request_create, input) }
+ let(:mutation_response) { graphql_mutation_response(:merge_request_create) }
+
+ context 'the user is not allowed to create a branch' do
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action']
+ end
+
+ context 'when user has permissions to create a merge request' do
+ before do
+ project.add_developer(current_user)
+ end
+
+ it 'creates a new merge request' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['mergeRequest']).to include(
+ 'title' => title
+ )
+ end
+
+ context 'when source branch is equal to the target branch' do
+ let(:source_branch) { target_branch }
+
+ it_behaves_like 'a mutation that returns errors in the response',
+ errors: ['Branch conflict You can\'t use same project/branch for source and target']
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb
new file mode 100644
index 00000000000..217f538c53e
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Mutations::Metrics::Dashboard::Annotations::Delete do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:project) { create(:project, :private, :repository) }
+ let_it_be(:environment) { create(:environment, project: project) }
+ let_it_be(:annotation) { create(:metrics_dashboard_annotation, environment: environment) }
+ let(:mutation) do
+ variables = {
+ id: GitlabSchema.id_from_object(annotation).to_s
+ }
+
+ graphql_mutation(:delete_annotation, variables)
+ end
+
+ def mutation_response
+ graphql_mutation_response(:delete_annotation)
+ end
+
+ specify { expect(described_class).to require_graphql_authorizations(:delete_metrics_dashboard_annotation) }
+
+ context 'when the user has permission to delete the annotation' do
+ before do
+ project.add_developer(current_user)
+ end
+
+ context 'with valid params' do
+ it 'deletes the annotation' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.to change { Metrics::Dashboard::Annotation.count }.by(-1)
+ end
+ end
+
+ context 'with invalid params' do
+ let(:mutation) do
+ variables = {
+ id: 'invalid_id'
+ }
+
+ graphql_mutation(:delete_annotation, variables)
+ end
+
+ it_behaves_like 'a mutation that returns top-level errors', errors: ['invalid_id is not a valid GitLab id.']
+ end
+
+ context 'when the delete fails' do
+ let(:service_response) { { message: 'Annotation has not been deleted', status: :error, last_step: :delete } }
+
+ before do
+ allow_next_instance_of(Metrics::Dashboard::Annotations::DeleteService) do |delete_service|
+ allow(delete_service).to receive(:execute).and_return(service_response)
+ end
+ end
+ it 'returns the error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(mutation_response['errors']).to eq([service_response[:message]])
+ end
+ end
+ end
+
+ context 'when the user does not have permission to delete the annotation' do
+ before do
+ project.add_reporter(current_user)
+ end
+
+ it_behaves_like 'a mutation that returns top-level errors', errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+
+ it 'does not delete the annotation' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.not_to change { Metrics::Dashboard::Annotation.count }
+ 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 e1e5fe22887..9052f54b171 100644
--- a/spec/requests/api/graphql/mutations/snippets/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/snippets/create_spec.rb
@@ -109,31 +109,21 @@ describe 'Creating a Snippet' do
context 'when the project path is invalid' do
let(:project_path) { 'foobar' }
- it 'returns an an error' do
- subject
- errors = json_response['errors']
-
- expect(errors.first['message']).to eq(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR)
- end
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
end
context 'when the feature is disabled' do
- it 'returns an an error' do
+ before do
project.project_feature.update_attribute(:snippets_access_level, ProjectFeature::DISABLED)
-
- subject
- errors = json_response['errors']
-
- expect(errors.first['message']).to eq(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR)
end
+
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
end
end
- context 'when there are ActiveRecord validation errors' do
- let(:title) { '' }
-
- it_behaves_like 'a mutation that returns errors in the response', errors: ["Title can't be blank"]
-
+ shared_examples 'does not create snippet' do
it 'does not create the Snippet' do
expect do
subject
@@ -147,7 +137,21 @@ describe 'Creating a Snippet' do
end
end
- context 'when there uploaded files' do
+ context 'when there are ActiveRecord validation errors' do
+ let(:title) { '' }
+
+ it_behaves_like 'a mutation that returns errors in the response', errors: ["Title can't be blank"]
+ it_behaves_like 'does not create snippet'
+ end
+
+ context 'when there non ActiveRecord errors' do
+ let(:file_name) { 'invalid://file/path' }
+
+ it_behaves_like 'a mutation that returns errors in the response', errors: ['Repository Error creating the snippet - Invalid file name']
+ it_behaves_like 'does not create snippet'
+ end
+
+ context 'when there are uploaded files' do
shared_examples 'expected files argument' do |file_value, expected_value|
let(:uploaded_files) { file_value }
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
new file mode 100644
index 00000000000..4c048caaeee
--- /dev/null
+++ b/spec/requests/api/graphql/project/alert_management/alert/assignees_spec.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'getting Alert Management Alert Assignees' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:first_alert) { create(:alert_management_alert, project: project, assignees: [current_user]) }
+ let_it_be(:second_alert) { create(:alert_management_alert, project: project) }
+
+ let(:params) { {} }
+
+ let(:fields) do
+ <<~QUERY
+ nodes {
+ iid
+ assignees {
+ nodes {
+ username
+ }
+ }
+ }
+ QUERY
+ end
+
+ let(:query) do
+ graphql_query_for(
+ 'project',
+ { 'fullPath' => project.full_path },
+ query_graphql_field('alertManagementAlerts', params, fields)
+ )
+ end
+
+ let(:alerts) { graphql_data.dig('project', 'alertManagementAlerts', 'nodes') }
+ let(:assignees) { alerts.map { |alert| [alert['iid'], alert['assignees']['nodes']] }.to_h }
+ let(:first_assignees) { assignees[first_alert.iid.to_s] }
+ let(:second_assignees) { assignees[second_alert.iid.to_s] }
+
+ before do
+ project.add_developer(current_user)
+ end
+
+ it 'returns the correct assignees' do
+ post_graphql(query, current_user: current_user)
+
+ expect(first_assignees.length).to eq(1)
+ expect(first_assignees.first).to include('username' => current_user.username)
+ expect(second_assignees).to be_empty
+ end
+
+ it 'applies appropriate filters for non-visible users' do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?).with(current_user, :read_user, current_user).and_return(false)
+
+ post_graphql(query, current_user: current_user)
+
+ expect(first_assignees).to be_empty
+ expect(second_assignees).to be_empty
+ end
+
+ it 'avoids N+1 queries' do
+ base_count = ActiveRecord::QueryRecorder.new do
+ post_graphql(query, current_user: current_user)
+ end
+
+ # An N+1 would mean a new alert would increase the query count
+ third_alert = create(:alert_management_alert, project: project, assignees: [current_user])
+
+ expect { post_graphql(query, current_user: current_user) }.not_to exceed_query_limit(base_count)
+
+ third_assignees = assignees[third_alert.iid.to_s]
+
+ expect(third_assignees.length).to eq(1)
+ expect(third_assignees.first).to include('username' => current_user.username)
+ end
+end
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
new file mode 100644
index 00000000000..df6bfa8c97b
--- /dev/null
+++ b/spec/requests/api/graphql/project/alert_management/alert/notes_spec.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'getting Alert Management Alert Notes' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:first_alert) { create(:alert_management_alert, project: project, assignees: [current_user]) }
+ let_it_be(:second_alert) { create(:alert_management_alert, project: project) }
+ let_it_be(:first_system_note) { create(:note_on_alert, noteable: first_alert, project: project) }
+ let_it_be(:second_system_note) { create(:note_on_alert, noteable: first_alert, project: project) }
+
+ let(:params) { {} }
+
+ let(:fields) do
+ <<~QUERY
+ nodes {
+ iid
+ notes {
+ nodes {
+ id
+ }
+ }
+ }
+ QUERY
+ end
+
+ let(:query) do
+ graphql_query_for(
+ 'project',
+ { 'fullPath' => project.full_path },
+ query_graphql_field('alertManagementAlerts', params, fields)
+ )
+ 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(:first_notes_result) { notes_result[first_alert.iid.to_s] }
+ let(:second_notes_result) { notes_result[second_alert.iid.to_s] }
+
+ before do
+ project.add_developer(current_user)
+ end
+
+ it 'returns the notes ordered by createdAt' do
+ post_graphql(query, current_user: current_user)
+
+ expect(first_notes_result.length).to eq(2)
+ expect(first_notes_result.first).to include('id' => first_system_note.to_global_id.to_s)
+ expect(first_notes_result.second).to include('id' => second_system_note.to_global_id.to_s)
+ expect(second_notes_result).to be_empty
+ end
+
+ it 'avoids N+1 queries' do
+ base_count = ActiveRecord::QueryRecorder.new do
+ post_graphql(query, current_user: current_user)
+ end
+
+ # An N+1 would mean a new alert would increase the query count
+ create(:alert_management_alert, project: project)
+
+ expect { post_graphql(query, current_user: current_user) }.not_to exceed_query_limit(base_count)
+ expect(alerts_result.length).to eq(3)
+ end
+end
diff --git a/spec/requests/api/graphql/project/alert_management/alert_status_counts_spec.rb b/spec/requests/api/graphql/project/alert_management/alert_status_counts_spec.rb
index ffd328429ef..a0d1ff7efc5 100644
--- a/spec/requests/api/graphql/project/alert_management/alert_status_counts_spec.rb
+++ b/spec/requests/api/graphql/project/alert_management/alert_status_counts_spec.rb
@@ -56,6 +56,22 @@ describe 'getting Alert Management Alert counts by status' do
'ignored' => 0
)
end
+
+ context 'with search criteria' do
+ let(:params) { { search: alert_1.title } }
+
+ it_behaves_like 'a working graphql query'
+ it 'returns the correct counts for each status' do
+ expect(alert_counts).to eq(
+ 'open' => 0,
+ 'all' => 1,
+ 'triggered' => 0,
+ 'acknowledged' => 0,
+ 'resolved' => 1,
+ 'ignored' => 0
+ )
+ end
+ end
end
end
end
diff --git a/spec/requests/api/graphql/project/alert_management/alerts_spec.rb b/spec/requests/api/graphql/project/alert_management/alerts_spec.rb
index c226e659364..c591895f295 100644
--- a/spec/requests/api/graphql/project/alert_management/alerts_spec.rb
+++ b/spec/requests/api/graphql/project/alert_management/alerts_spec.rb
@@ -10,12 +10,13 @@ describe 'getting Alert Management Alerts' do
let_it_be(:resolved_alert) { create(:alert_management_alert, :all_fields, :resolved, project: project, issue: nil, severity: :low) }
let_it_be(:triggered_alert) { create(:alert_management_alert, :all_fields, project: project, severity: :critical, payload: payload) }
let_it_be(:other_project_alert) { create(:alert_management_alert, :all_fields) }
+
let(:params) { {} }
let(:fields) do
<<~QUERY
nodes {
- #{all_graphql_fields_for('AlertManagementAlert'.classify)}
+ #{all_graphql_fields_for('AlertManagementAlert', excluded: ['assignees'])}
}
QUERY
end
diff --git a/spec/requests/api/graphql/project/container_expiration_policy_spec.rb b/spec/requests/api/graphql/project/container_expiration_policy_spec.rb
new file mode 100644
index 00000000000..d0563f9ff05
--- /dev/null
+++ b/spec/requests/api/graphql/project/container_expiration_policy_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe 'getting a repository in a project' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:current_user) { project.owner }
+ let_it_be(:container_expiration_policy) { project.container_expiration_policy }
+
+ let(:fields) do
+ <<~QUERY
+ #{all_graphql_fields_for('container_expiration_policy'.classify)}
+ QUERY
+ end
+ let(:query) do
+ graphql_query_for(
+ 'project',
+ { 'fullPath' => project.full_path },
+ query_graphql_field('containerExpirationPolicy', {}, fields)
+ )
+ end
+
+ before do
+ stub_config(registry: { enabled: true })
+ post_graphql(query, current_user: current_user)
+ end
+
+ it_behaves_like 'a working graphql query'
+end
diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb
index 91fce3eed92..3128f527356 100644
--- a/spec/requests/api/graphql/project/issues_spec.rb
+++ b/spec/requests/api/graphql/project/issues_spec.rb
@@ -124,7 +124,7 @@ describe 'getting an issue list for a project' do
graphql_query_for(
'project',
{ 'fullPath' => sort_project.full_path },
- "issues(#{params}) { #{page_info} edges { node { iid dueDate } } }"
+ query_graphql_field('issues', params, "#{page_info} edges { node { iid dueDate} }")
)
end
diff --git a/spec/requests/api/graphql/project/jira_import_spec.rb b/spec/requests/api/graphql/project/jira_import_spec.rb
index e063068eb1a..7be14696963 100644
--- a/spec/requests/api/graphql/project/jira_import_spec.rb
+++ b/spec/requests/api/graphql/project/jira_import_spec.rb
@@ -7,9 +7,30 @@ describe 'query Jira import data' do
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project, :private, :import_started, import_type: 'jira') }
- let_it_be(:jira_import1) { create(:jira_import_state, :finished, project: project, jira_project_key: 'AA', user: current_user, created_at: 2.days.ago) }
- let_it_be(:jira_import2) { create(:jira_import_state, :finished, project: project, jira_project_key: 'BB', user: current_user, created_at: 5.days.ago) }
-
+ let_it_be(:jira_import1) do
+ create(
+ :jira_import_state, :finished,
+ project: project,
+ jira_project_key: 'AA',
+ user: current_user,
+ created_at: 2.days.ago,
+ failed_to_import_count: 2,
+ imported_issues_count: 2,
+ total_issue_count: 4
+ )
+ end
+ let_it_be(:jira_import2) do
+ create(
+ :jira_import_state, :finished,
+ project: project,
+ jira_project_key: 'BB',
+ user: current_user,
+ created_at: 5.days.ago,
+ failed_to_import_count: 1,
+ imported_issues_count: 2,
+ total_issue_count: 3
+ )
+ end
let(:query) do
%(
query {
@@ -23,6 +44,9 @@ describe 'query Jira import data' do
scheduledBy {
username
}
+ importedIssuesCount
+ failedToImportCount
+ totalIssueCount
}
}
}
@@ -64,10 +88,16 @@ describe 'query Jira import data' do
it 'retuns list of jira imports' do
jira_proket_keys = jira_imports.map {|ji| ji['jiraProjectKey']}
usernames = jira_imports.map {|ji| ji.dig('scheduledBy', 'username')}
+ imported_issues_count = jira_imports.map {|ji| ji.dig('importedIssuesCount')}
+ failed_issues_count = jira_imports.map {|ji| ji.dig('failedToImportCount')}
+ total_issue_count = jira_imports.map {|ji| ji.dig('totalIssueCount')}
expect(jira_imports.size).to eq 2
expect(jira_proket_keys).to eq %w(BB AA)
expect(usernames).to eq [current_user.username, current_user.username]
+ expect(imported_issues_count).to eq [2, 2]
+ expect(failed_issues_count).to eq [1, 2]
+ expect(total_issue_count).to eq [3, 4]
end
end
diff --git a/spec/requests/api/graphql/project/jira_projects_spec.rb b/spec/requests/api/graphql/project/jira_projects_spec.rb
new file mode 100644
index 00000000000..d67c89f18c9
--- /dev/null
+++ b/spec/requests/api/graphql/project/jira_projects_spec.rb
@@ -0,0 +1,114 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'query Jira projects' do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+
+ include_context 'jira projects request context'
+
+ let(:services) { graphql_data_at(:project, :services, :edges) }
+ let(:jira_projects) { services.first.dig('node', 'projects', 'nodes') }
+ let(:projects_query) { 'projects' }
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ services(active: true, type: JIRA_SERVICE) {
+ edges {
+ node {
+ ... on JiraService {
+ %{projects_query} {
+ nodes {
+ key
+ name
+ projectId
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ) % { projects_query: projects_query }
+ end
+
+ context 'when user does not have access' do
+ it_behaves_like 'unauthorized users cannot read services'
+ end
+
+ context 'when user can access project services' do
+ before do
+ project.add_maintainer(current_user)
+ post_graphql(query, current_user: current_user)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'retuns list of jira projects' do
+ project_keys = jira_projects.map { |jp| jp['key'] }
+ project_names = jira_projects.map { |jp| jp['name'] }
+ project_ids = jira_projects.map { |jp| jp['projectId'] }
+
+ expect(jira_projects.size).to eq(2)
+ expect(project_keys).to eq(%w(EX ABC))
+ expect(project_names).to eq(%w(Example Alphabetical))
+ expect(project_ids).to eq([10000, 10001])
+ end
+
+ context 'with pagination' do
+ context 'when fetching limited number of projects' do
+ shared_examples_for 'fetches first project' do
+ it 'retuns first project from list of fetched projects' do
+ project_keys = jira_projects.map { |jp| jp['key'] }
+ project_names = jira_projects.map { |jp| jp['name'] }
+ project_ids = jira_projects.map { |jp| jp['projectId'] }
+
+ expect(jira_projects.size).to eq(1)
+ expect(project_keys).to eq(%w(EX))
+ expect(project_names).to eq(%w(Example))
+ expect(project_ids).to eq([10000])
+ end
+ end
+
+ context 'without cursor' do
+ let(:projects_query) { 'projects(first: 1)' }
+
+ it_behaves_like 'fetches first project'
+ end
+
+ context 'with before cursor' do
+ let(:projects_query) { 'projects(before: "Mg==", first: 1)' }
+
+ it_behaves_like 'fetches first project'
+ end
+
+ context 'with after cursor' do
+ let(:projects_query) { 'projects(after: "MA==", first: 1)' }
+
+ it_behaves_like 'fetches first project'
+ end
+ end
+
+ context 'with valid but inexistent after cursor' do
+ let(:projects_query) { 'projects(after: "MTk==")' }
+
+ it 'retuns empty list of jira projects' do
+ expect(jira_projects.size).to eq(0)
+ end
+ end
+
+ context 'with invalid after cursor' do
+ let(:projects_query) { 'projects(after: "invalid==")' }
+
+ it 'treats the invalid cursor as no cursor and returns list of jira projects' do
+ expect(jira_projects.size).to eq(2)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/labels_query_spec.rb b/spec/requests/api/graphql/project/labels_query_spec.rb
new file mode 100644
index 00000000000..ecc43e0a3db
--- /dev/null
+++ b/spec/requests/api/graphql/project/labels_query_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'getting project label information' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:label_factory) { :label }
+ let_it_be(:label_attrs) { { project: project } }
+
+ it_behaves_like 'querying a GraphQL type with labels' do
+ let(:path_prefix) { ['project'] }
+
+ def make_query(fields)
+ graphql_query_for('project', { full_path: project.full_path }, fields)
+ 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 8d8c31c335d..643532bf2e2 100644
--- a/spec/requests/api/graphql/project/merge_request_spec.rb
+++ b/spec/requests/api/graphql/project/merge_request_spec.rb
@@ -37,6 +37,30 @@ 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)
+
+ expect(merge_request_graphql_data['author']['username']).to eq(merge_request.author.username)
+ 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)
+
+ post_graphql(query, current_user: current_user)
+ retrieved = merge_request_graphql_data['mergedAt']
+
+ expect(Time.zone.parse(retrieved)).to be_within(1.second).of(time)
+ end
+
+ it 'includes nil mergedAt value when not merged' do
+ post_graphql(query, current_user: current_user)
+ retrieved = merge_request_graphql_data['mergedAt']
+
+ expect(retrieved).to be_nil
+ end
+
context 'permissions on the merge request' do
it 'includes the permissions for the current user on a public project' do
expected_permissions = {
diff --git a/spec/requests/api/graphql/project/merge_requests_spec.rb b/spec/requests/api/graphql/project/merge_requests_spec.rb
new file mode 100644
index 00000000000..49fdfe29874
--- /dev/null
+++ b/spec/requests/api/graphql/project/merge_requests_spec.rb
@@ -0,0 +1,174 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'getting merge request listings nested in a project' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :repository, :public) }
+ let_it_be(:current_user) { create(:user) }
+
+ let_it_be(:label) { create(:label) }
+ let_it_be(:merge_request_a) { create(:labeled_merge_request, :unique_branches, source_project: project, labels: [label]) }
+ let_it_be(:merge_request_b) { create(:merge_request, :closed, :unique_branches, source_project: project) }
+ let_it_be(:merge_request_c) { create(:labeled_merge_request, :closed, :unique_branches, source_project: project, labels: [label]) }
+ let_it_be(:merge_request_d) { create(:merge_request, :locked, :unique_branches, source_project: project) }
+
+ let(:results) { graphql_data.dig('project', 'mergeRequests', 'nodes') }
+
+ let(:search_params) { nil }
+
+ def query_merge_requests(fields)
+ graphql_query_for(
+ :project,
+ { full_path: project.full_path },
+ query_graphql_field(:merge_requests, search_params, [
+ query_graphql_field(:nodes, nil, fields)
+ ])
+ )
+ end
+
+ let(:query) do
+ query_merge_requests(all_graphql_fields_for('MergeRequest', max_depth: 1))
+ end
+
+ it_behaves_like 'a working graphql query' do
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+ end
+
+ # The following tests are needed to guarantee that we have correctly annotated
+ # all the gitaly calls. Selecting combinations of fields may mask this due to
+ # memoization.
+ context 'requesting a single field' do
+ let_it_be(:fresh_mr) { create(:merge_request, :unique_branches, source_project: project) }
+ let(:search_params) { { iids: [fresh_mr.iid.to_s] } }
+
+ before do
+ project.repository.expire_branches_cache
+ end
+
+ context 'selecting any single scalar field' do
+ where(:field) do
+ scalar_fields_of('MergeRequest').map { |name| [name] }
+ end
+
+ with_them do
+ it_behaves_like 'a working graphql query' do
+ let(:query) do
+ query_merge_requests([:iid, field].uniq)
+ end
+
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ it 'selects the correct MR' do
+ expect(results).to contain_exactly(a_hash_including('iid' => fresh_mr.iid.to_s))
+ end
+ end
+ end
+ end
+
+ context 'selecting any single nested field' do
+ where(:field, :subfield, :is_connection) do
+ nested_fields_of('MergeRequest').flat_map do |name, field|
+ type = field_type(field)
+ is_connection = type.name.ends_with?('Connection')
+ type = field_type(type.fields['nodes']) if is_connection
+
+ type.fields
+ .select { |_, field| !nested_fields?(field) && !required_arguments?(field) }
+ .map(&:first)
+ .map { |subfield| [name, subfield, is_connection] }
+ end
+ end
+
+ with_them do
+ it_behaves_like 'a working graphql query' do
+ let(:query) do
+ fld = is_connection ? query_graphql_field(:nodes, nil, [subfield]) : subfield
+ query_merge_requests([:iid, query_graphql_field(field, nil, [fld])])
+ end
+
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ it 'selects the correct MR' do
+ expect(results).to contain_exactly(a_hash_including('iid' => fresh_mr.iid.to_s))
+ end
+ end
+ end
+ end
+ end
+
+ shared_examples 'searching with parameters' do
+ let(:expected) do
+ mrs.map { |mr| a_hash_including('iid' => mr.iid.to_s, 'title' => mr.title) }
+ end
+
+ it 'finds the right mrs' do
+ post_graphql(query, current_user: current_user)
+
+ expect(results).to match_array(expected)
+ end
+ end
+
+ context 'there are no search params' do
+ let(:search_params) { nil }
+ let(:mrs) { [merge_request_a, merge_request_b, merge_request_c, merge_request_d] }
+
+ it_behaves_like 'searching with parameters'
+ end
+
+ context 'the search params do not match anything' do
+ let(:search_params) { { iids: %w(foo bar baz) } }
+ let(:mrs) { [] }
+
+ it_behaves_like 'searching with parameters'
+ end
+
+ context 'searching by iids' do
+ let(:search_params) { { iids: mrs.map(&:iid).map(&:to_s) } }
+ let(:mrs) { [merge_request_a, merge_request_c] }
+
+ it_behaves_like 'searching with parameters'
+ end
+
+ context 'searching by state' do
+ let(:search_params) { { state: :closed } }
+ let(:mrs) { [merge_request_b, merge_request_c] }
+
+ it_behaves_like 'searching with parameters'
+ end
+
+ context 'searching by source_branch' do
+ let(:search_params) { { source_branches: mrs.map(&:source_branch) } }
+ let(:mrs) { [merge_request_b, merge_request_c] }
+
+ it_behaves_like 'searching with parameters'
+ end
+
+ context 'searching by target_branch' do
+ let(:search_params) { { target_branches: mrs.map(&:target_branch) } }
+ let(:mrs) { [merge_request_a, merge_request_d] }
+
+ it_behaves_like 'searching with parameters'
+ end
+
+ context 'searching by label' do
+ let(:search_params) { { labels: [label.title] } }
+ let(:mrs) { [merge_request_a, merge_request_c] }
+
+ it_behaves_like 'searching with parameters'
+ end
+
+ context 'searching by combination' do
+ let(:search_params) { { state: :closed, labels: [label.title] } }
+ let(:mrs) { [merge_request_c] }
+
+ it_behaves_like 'searching with parameters'
+ end
+end
diff --git a/spec/requests/api/graphql/project/pipeline_spec.rb b/spec/requests/api/graphql/project/pipeline_spec.rb
new file mode 100644
index 00000000000..bed9a18577f
--- /dev/null
+++ b/spec/requests/api/graphql/project/pipeline_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+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
+ graphql_query_for(
+ 'project',
+ { 'fullPath' => project.full_path },
+ query_graphql_field('pipeline', iid: pipeline.iid.to_s)
+ )
+ end
+
+ it_behaves_like 'a working graphql query' do
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+ end
+
+ it 'contains pipeline information' do
+ post_graphql(query, current_user: current_user)
+
+ expect(pipeline_graphql_data).not_to be_nil
+ end
+end
diff --git a/spec/requests/api/graphql/project/release_spec.rb b/spec/requests/api/graphql/project/release_spec.rb
new file mode 100644
index 00000000000..f8624a97a2b
--- /dev/null
+++ b/spec/requests/api/graphql/project/release_spec.rb
@@ -0,0 +1,206 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'pp'
+
+describe 'Query.project(fullPath).release(tagName)' do
+ include GraphqlHelpers
+ include Presentable
+
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:milestone_1) { create(:milestone, project: project) }
+ let_it_be(:milestone_2) { create(:milestone, project: project) }
+ let_it_be(:release) { create(:release, :with_evidence, project: project, milestones: [milestone_1, milestone_2]) }
+ let_it_be(:release_link_1) { create(:release_link, release: release) }
+ let_it_be(:release_link_2) { create(:release_link, release: release) }
+ let_it_be(:developer) { create(:user) }
+
+ let(:current_user) { developer }
+
+ def query(rq = release_fields)
+ graphql_query_for(:project, { fullPath: project.full_path },
+ query_graphql_field(:release, { tagName: release.tag }, rq))
+ end
+
+ let(:post_query) { post_graphql(query, current_user: current_user) }
+ let(:path_prefix) { %w[project release] }
+
+ let(:data) { graphql_data.dig(*path) }
+
+ before do
+ project.add_developer(developer)
+ end
+
+ describe 'scalar fields' do
+ let(:path) { path_prefix }
+ let(:release_fields) do
+ query_graphql_field(%{
+ tagName
+ tagPath
+ description
+ descriptionHtml
+ name
+ createdAt
+ releasedAt
+ })
+ end
+
+ before do
+ post_query
+ end
+
+ it 'finds all release data' do
+ expect(data).to eq({
+ 'tagName' => release.tag,
+ 'tagPath' => project_tag_path(project, release.tag),
+ 'description' => release.description,
+ 'descriptionHtml' => release.description_html,
+ 'name' => release.name,
+ 'createdAt' => release.created_at.iso8601,
+ 'releasedAt' => release.released_at.iso8601
+ })
+ end
+ end
+
+ describe 'milestones' do
+ let(:path) { path_prefix + %w[milestones nodes] }
+ let(:release_fields) do
+ query_graphql_field(:milestones, nil, 'nodes { id title }')
+ end
+
+ it 'finds all milestones associated to a release' do
+ post_query
+
+ expected = release.milestones.map do |milestone|
+ { 'id' => global_id_of(milestone), 'title' => milestone.title }
+ end
+
+ expect(data).to match_array(expected)
+ end
+ end
+
+ describe 'author' do
+ let(:path) { path_prefix + %w[author] }
+ let(:release_fields) do
+ query_graphql_field(:author, nil, 'id username')
+ end
+
+ it 'finds the author of the release' do
+ post_query
+
+ expect(data).to eq({
+ 'id' => global_id_of(release.author),
+ 'username' => release.author.username
+ })
+ end
+ end
+
+ describe 'commit' do
+ let(:path) { path_prefix + %w[commit] }
+ let(:release_fields) do
+ query_graphql_field(:commit, nil, 'sha')
+ end
+
+ it 'finds the commit associated with the release' do
+ post_query
+
+ expect(data).to eq({ 'sha' => release.commit.sha })
+ end
+ end
+
+ describe 'assets' do
+ describe 'assetsCount' do
+ let(:path) { path_prefix + %w[assets] }
+ let(:release_fields) do
+ query_graphql_field(:assets, nil, 'assetsCount')
+ end
+
+ it 'returns the number of assets associated to the release' do
+ post_query
+
+ expect(data).to eq({ 'assetsCount' => release.sources.size + release.links.size })
+ end
+ end
+
+ describe 'links' do
+ let(:path) { path_prefix + %w[assets links nodes] }
+ let(:release_fields) do
+ query_graphql_field(:assets, nil,
+ query_graphql_field(:links, nil, 'nodes { id name url external }'))
+ end
+
+ it 'finds all release links' do
+ post_query
+
+ expected = release.links.map do |link|
+ {
+ 'id' => global_id_of(link),
+ 'name' => link.name,
+ 'url' => link.url,
+ 'external' => link.external?
+ }
+ end
+
+ expect(data).to match_array(expected)
+ end
+ end
+
+ describe 'sources' do
+ let(:path) { path_prefix + %w[assets sources nodes] }
+ let(:release_fields) do
+ query_graphql_field(:assets, nil,
+ query_graphql_field(:sources, nil, 'nodes { format url }'))
+ end
+
+ it 'finds all release sources' do
+ post_query
+
+ expected = release.sources.map do |source|
+ {
+ 'format' => source.format,
+ 'url' => source.url
+ }
+ end
+
+ expect(data).to match_array(expected)
+ end
+ end
+
+ describe 'evidences' do
+ let(:path) { path_prefix + %w[evidences] }
+ let(:release_fields) do
+ query_graphql_field(:evidences, nil, 'nodes { id sha filepath collectedAt }')
+ end
+
+ context 'for a developer' do
+ it 'finds all evidence fields' do
+ post_query
+
+ evidence = release.evidences.first.present
+ expected = {
+ 'id' => global_id_of(evidence),
+ 'sha' => evidence.sha,
+ 'filepath' => evidence.filepath,
+ 'collectedAt' => evidence.collected_at.utc.iso8601
+ }
+
+ expect(data["nodes"].first).to eq(expected)
+ end
+ end
+
+ context 'for a guest' do
+ let(:current_user) { create :user }
+
+ before do
+ project.add_guest(current_user)
+ end
+
+ it 'denies access' do
+ post_query
+
+ expect(data['node']).to be_nil
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project_query_spec.rb b/spec/requests/api/graphql/project_query_spec.rb
index 035894c8022..9a88b47eea6 100644
--- a/spec/requests/api/graphql/project_query_spec.rb
+++ b/spec/requests/api/graphql/project_query_spec.rb
@@ -62,6 +62,54 @@ describe 'getting project information' do
end
end
+ describe 'performance' do
+ before do
+ project.add_developer(current_user)
+ mrs = create_list(:merge_request, 10, :closed, :with_head_pipeline,
+ source_project: project,
+ author: current_user)
+ mrs.each do |mr|
+ mr.assignees << create(:user)
+ mr.assignees << current_user
+ end
+ end
+
+ def run_query(number)
+ q = <<~GQL
+ query {
+ project(fullPath: "#{project.full_path}") {
+ mergeRequests(first: #{number}) {
+ nodes {
+ assignees { nodes { username } }
+ headPipeline { status }
+ }
+ }
+ }
+ }
+ GQL
+
+ post_graphql(q, current_user: current_user)
+ end
+
+ it 'returns appropriate results' do
+ run_query(2)
+
+ mrs = graphql_data.dig('project', 'mergeRequests', 'nodes')
+
+ expect(mrs.size).to eq(2)
+ expect(mrs).to all(
+ match(
+ a_hash_including(
+ 'assignees' => { 'nodes' => all(match(a_hash_including('username' => be_present))) },
+ 'headPipeline' => { 'status' => be_present }
+ )))
+ 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
+ end
+ end
+
context 'when the user does not have access to the project' do
it 'returns an empty field' do
post_graphql(query, current_user: current_user)
diff --git a/spec/requests/api/graphql/tasks/task_completion_status_spec.rb b/spec/requests/api/graphql/tasks/task_completion_status_spec.rb
index c727750c0ce..c47406ea534 100644
--- a/spec/requests/api/graphql/tasks/task_completion_status_spec.rb
+++ b/spec/requests/api/graphql/tasks/task_completion_status_spec.rb
@@ -5,9 +5,9 @@ require 'spec_helper'
describe 'getting task completion status information' do
include GraphqlHelpers
- DESCRIPTION_0_DONE = '- [ ] task 1\n- [ ] task 2'
- DESCRIPTION_1_DONE = '- [x] task 1\n- [ ] task 2'
- DESCRIPTION_2_DONE = '- [x] task 1\n- [x] task 2'
+ description_0_done = '- [ ] task 1\n- [ ] task 2'
+ description_1_done = '- [x] task 1\n- [ ] task 2'
+ description_2_done = '- [x] task 1\n- [x] task 2'
let_it_be(:user1) { create(:user) }
let_it_be(:project) { create(:project, :repository, :public) }
@@ -42,7 +42,7 @@ describe 'getting task completion status information' do
end
end
- [DESCRIPTION_0_DONE, DESCRIPTION_1_DONE, DESCRIPTION_2_DONE].each do |desc|
+ [description_0_done, description_1_done, description_2_done].each do |desc|
context "with description #{desc}" do
context 'when type is issue' do
it_behaves_like 'graphql task completion status provider', 'issue' do
diff --git a/spec/requests/api/graphql/user/group_member_query_spec.rb b/spec/requests/api/graphql/user/group_member_query_spec.rb
new file mode 100644
index 00000000000..022ee79297c
--- /dev/null
+++ b/spec/requests/api/graphql/user/group_member_query_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'GroupMember' do
+ include GraphqlHelpers
+
+ let_it_be(:member) { create(:group_member, :developer) }
+ let_it_be(:fields) do
+ <<~HEREDOC
+ nodes {
+ accessLevel {
+ integerValue
+ stringValue
+ }
+ group {
+ id
+ }
+ }
+ HEREDOC
+ end
+ let_it_be(:query) do
+ graphql_query_for('user', { id: member.user.to_global_id.to_s }, query_graphql_field("groupMemberships", {}, fields))
+ end
+
+ before do
+ post_graphql(query, current_user: member.user)
+ end
+
+ it_behaves_like 'a working graphql query'
+ it_behaves_like 'a working membership object query'
+end
diff --git a/spec/requests/api/graphql/user/project_member_query_spec.rb b/spec/requests/api/graphql/user/project_member_query_spec.rb
new file mode 100644
index 00000000000..397d2872189
--- /dev/null
+++ b/spec/requests/api/graphql/user/project_member_query_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'ProjectMember' do
+ include GraphqlHelpers
+
+ let_it_be(:member) { create(:project_member, :developer) }
+ let_it_be(:fields) do
+ <<~HEREDOC
+ nodes {
+ accessLevel {
+ integerValue
+ stringValue
+ }
+ project {
+ id
+ }
+ }
+ HEREDOC
+ end
+ let_it_be(:query) do
+ graphql_query_for('user', { id: member.user.to_global_id.to_s }, query_graphql_field("projectMemberships", {}, fields))
+ end
+
+ before do
+ post_graphql(query, current_user: member.user)
+ end
+
+ it_behaves_like 'a working graphql query'
+ it_behaves_like 'a working membership object query'
+end
diff --git a/spec/requests/api/graphql/user_query_spec.rb b/spec/requests/api/graphql/user_query_spec.rb
new file mode 100644
index 00000000000..5ac94bc7323
--- /dev/null
+++ b/spec/requests/api/graphql/user_query_spec.rb
@@ -0,0 +1,260 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'getting user information' do
+ include GraphqlHelpers
+
+ let(:query) do
+ graphql_query_for(:user, user_params, user_fields)
+ end
+
+ let(:user_fields) { all_graphql_fields_for('User', max_depth: 2) }
+
+ context 'no parameters are provided' do
+ let(:user_params) { nil }
+
+ it 'mentions the missing required parameters' do
+ post_graphql(query)
+
+ expect_graphql_errors_to_include(/username/)
+ end
+ end
+
+ context 'looking up a user by username' do
+ let_it_be(:project_a) { create(:project, :repository) }
+ let_it_be(:project_b) { create(:project, :repository) }
+ let_it_be(:user, reload: true) { create(:user, developer_projects: [project_a, project_b]) }
+ let_it_be(:authorised_user) { create(:user, developer_projects: [project_a, project_b]) }
+ let_it_be(:unauthorized_user) { create(:user) }
+
+ let_it_be(:assigned_mr) do
+ create(:merge_request, :unique_branches,
+ source_project: project_a, assignees: [user])
+ end
+ let_it_be(:assigned_mr_b) do
+ create(:merge_request, :unique_branches,
+ source_project: project_b, assignees: [user])
+ end
+ let_it_be(:assigned_mr_c) do
+ create(:merge_request, :unique_branches,
+ source_project: project_b, assignees: [user])
+ end
+ let_it_be(:authored_mr) do
+ create(:merge_request, :unique_branches,
+ source_project: project_a, author: user)
+ end
+ let_it_be(:authored_mr_b) do
+ create(:merge_request, :unique_branches,
+ source_project: project_b, author: user)
+ end
+ let_it_be(:authored_mr_c) do
+ create(:merge_request, :unique_branches,
+ source_project: project_b, author: 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(:user_params) { { username: user.username } }
+
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ context 'the user is an active user' do
+ it_behaves_like 'a working graphql query'
+
+ it 'can access user profile fields' do
+ presenter = UserPresenter.new(user)
+
+ expect(graphql_data['user']).to match(
+ a_hash_including(
+ 'id' => global_id_of(user),
+ 'state' => presenter.state,
+ 'name' => presenter.name,
+ 'username' => presenter.username,
+ 'webUrl' => presenter.web_url,
+ 'avatarUrl' => presenter.avatar_url
+ ))
+ end
+
+ describe 'assignedMergeRequests' do
+ let(:user_fields) do
+ query_graphql_field(:assigned_merge_requests, mr_args, 'nodes { id }')
+ end
+ let(:mr_args) { nil }
+
+ it_behaves_like 'a working graphql query'
+
+ it 'can be found' do
+ expect(assigned_mrs).to contain_exactly(
+ a_hash_including('id' => global_id_of(assigned_mr)),
+ a_hash_including('id' => global_id_of(assigned_mr_b)),
+ a_hash_including('id' => global_id_of(assigned_mr_c))
+ )
+ end
+
+ context 'applying filters' do
+ context 'filtering by IID without specifying a project' do
+ let(:mr_args) do
+ { iids: [assigned_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: [assigned_mr_b.iid.to_s] }
+ end
+
+ it 'selects the correct MRs' do
+ expect(assigned_mrs).to contain_exactly(
+ a_hash_including('id' => global_id_of(assigned_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(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
+ let(:current_user) { unauthorized_user }
+
+ it 'cannot be found' do
+ expect(assigned_mrs).to be_empty
+ end
+ end
+ end
+
+ describe 'authoredMergeRequests' do
+ let(:user_fields) do
+ query_graphql_field(:authored_merge_requests, mr_args, 'nodes { id }')
+ end
+ let(:mr_args) { nil }
+
+ it_behaves_like 'a working graphql query'
+
+ it 'can be found' do
+ expect(authored_mrs).to contain_exactly(
+ a_hash_including('id' => global_id_of(authored_mr)),
+ a_hash_including('id' => global_id_of(authored_mr_b)),
+ a_hash_including('id' => global_id_of(authored_mr_c))
+ )
+ end
+
+ context 'applying filters' do
+ context 'filtering by IID without specifying a project' do
+ let(:mr_args) do
+ { iids: [authored_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: [authored_mr_b.iid.to_s] }
+ end
+
+ it 'selects the correct MRs' do
+ expect(authored_mrs).to contain_exactly(
+ a_hash_including('id' => global_id_of(authored_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(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
+ end
+
+ context 'the current user does not have access' do
+ let(:current_user) { unauthorized_user }
+
+ it 'cannot be found' do
+ expect(authored_mrs).to be_empty
+ end
+ end
+ end
+ end
+
+ context 'the user is private' do
+ before do
+ user.update(private_profile: true)
+ post_graphql(query, current_user: current_user)
+ end
+
+ context 'we only request basic fields' do
+ let(:user_fields) { %i[id name username state web_url avatar_url] }
+
+ it_behaves_like 'a working graphql query'
+ end
+
+ context 'we request the authoredMergeRequests' do
+ let(:user_fields) { 'authoredMergeRequests { nodes { id } }' }
+
+ it_behaves_like 'a working graphql query'
+
+ it 'cannot be found' do
+ expect(authored_mrs).to be_empty
+ end
+
+ context 'the current user is the user' do
+ let(:current_user) { user }
+
+ it 'can be found' do
+ expect(authored_mrs).to include(
+ a_hash_including('id' => global_id_of(authored_mr))
+ )
+ end
+ end
+ end
+
+ context 'we request the assignedMergeRequests' do
+ let(:user_fields) { 'assignedMergeRequests { nodes { id } }' }
+
+ it_behaves_like 'a working graphql query'
+
+ it 'cannot be found' do
+ expect(assigned_mrs).to be_empty
+ end
+
+ context 'the current user is the user' do
+ let(:current_user) { user }
+
+ it 'can be found' do
+ expect(assigned_mrs).to include(
+ a_hash_including('id' => global_id_of(assigned_mr))
+ )
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/user_spec.rb b/spec/requests/api/graphql/user_spec.rb
new file mode 100644
index 00000000000..097c75b3541
--- /dev/null
+++ b/spec/requests/api/graphql/user_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'User' do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+
+ shared_examples 'a working user query' do
+ it_behaves_like 'a working graphql query' do
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+ end
+
+ it 'includes the user' do
+ post_graphql(query, current_user: current_user)
+
+ expect(graphql_data['user']).not_to be_nil
+ end
+
+ it 'returns no user when global restricted_visibility_levels includes PUBLIC' do
+ stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
+
+ post_graphql(query)
+
+ expect(graphql_data['user']).to be_nil
+ end
+ end
+
+ context 'when id parameter is used' do
+ let(:query) { graphql_query_for(:user, { id: current_user.to_global_id.to_s }) }
+
+ it_behaves_like 'a working user query'
+ end
+
+ context 'when username parameter is used' do
+ let(:query) { graphql_query_for(:user, { username: current_user.username.to_s }) }
+
+ it_behaves_like 'a working user query'
+ 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') }
+
+ it 'displays an error' do
+ post_graphql(query)
+
+ expect(graphql_errors).to include(
+ a_hash_including('message' => a_string_matching(%r{Provide either a single username or id}))
+ )
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/users_spec.rb b/spec/requests/api/graphql/users_spec.rb
new file mode 100644
index 00000000000..1e6d73cbd7d
--- /dev/null
+++ b/spec/requests/api/graphql/users_spec.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Users' do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user, created_at: 1.day.ago) }
+ let_it_be(:user1) { create(:user, created_at: 2.days.ago) }
+ let_it_be(:user2) { create(:user, created_at: 3.days.ago) }
+ let_it_be(:user3) { create(:user, created_at: 4.days.ago) }
+
+ describe '.users' do
+ shared_examples 'a working users query' do
+ it_behaves_like 'a working graphql query' do
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+ end
+
+ it 'includes a list of users' do
+ post_graphql(query)
+
+ expect(graphql_data.dig('users', 'nodes')).not_to be_empty
+ end
+ end
+
+ context 'with no arguments' do
+ let_it_be(:query) { graphql_query_for(:users, { usernames: [user1.username] }, 'nodes { id }') }
+
+ it_behaves_like 'a working users query'
+ end
+
+ context 'with a list of usernames' do
+ let(:query) { graphql_query_for(:users, { usernames: [user1.username] }, 'nodes { id }') }
+
+ it_behaves_like 'a working users query'
+ end
+
+ context 'with a list of IDs' do
+ let(:query) { graphql_query_for(:users, { ids: [user1.to_global_id.to_s] }, 'nodes { id }') }
+
+ it_behaves_like 'a working users query'
+ end
+
+ context 'when usernames and ids parameter are used' do
+ let_it_be(:query) { graphql_query_for(:users, { ids: user1.to_global_id.to_s, usernames: user1.username }, 'nodes { id }') }
+
+ it 'displays an error' do
+ post_graphql(query)
+
+ expect(graphql_errors).to include(
+ a_hash_including('message' => a_string_matching(%r{Provide either a list of usernames or ids}))
+ )
+ end
+ end
+ end
+
+ 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') }
+ 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) }
+
+ context 'when ascending' do
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_param) { 'created_asc' }
+ let(:first_param) { 1 }
+ let(:expected_results) { ascending_users }
+ end
+ end
+
+ context 'when descending' do
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_param) { 'created_desc' }
+ let(:first_param) { 1 }
+ let(:expected_results) { ascending_users.reverse }
+ end
+ end
+ end
+ end
+end