Commit 0eb4b6f3 authored by Oswaldo Ferreira's avatar Oswaldo Ferreira

Merge branch '216145-jira-users-import-endpoint' into 'master'

GraphQL endpoint for Jira users import

See merge request gitlab-org/gitlab!33501
parents 8b0c4e74 297aa92b
# frozen_string_literal: true
module Mutations
module JiraImport
class ImportUsers < BaseMutation
include ResolvesProject
graphql_name 'JiraImportUsers'
field :jira_users,
[Types::JiraUserType],
null: true,
description: 'Users returned from Jira, matched by email and name if possible.'
argument :project_path, GraphQL::ID_TYPE,
required: true,
description: 'The project to import the Jira users into'
argument :start_at, GraphQL::INT_TYPE,
required: false,
description: 'The index of the record the import should started at, default 0 (50 records returned)'
def resolve(project_path:, start_at:)
project = authorized_find!(full_path: project_path)
service_response = ::JiraImport::UsersImporter.new(context[:current_user], project, start_at).execute
{
jira_users: service_response.payload,
errors: service_response.errors
}
end
private
def find_object(full_path:)
resolve_project(full_path: full_path)
end
def authorized_resource?(project)
Ability.allowed?(context[:current_user], :admin_project, project)
end
end
end
end
# frozen_string_literal: true
module Types
# rubocop: disable Graphql/AuthorizeTypes
# Authorization is at project level for owners or admins on mutation level
class JiraUserType < BaseObject
graphql_name 'JiraUser'
field :jira_account_id, GraphQL::STRING_TYPE, null: false,
description: 'Account id of the Jira user'
field :jira_display_name, GraphQL::STRING_TYPE, null: false,
description: 'Display name of the Jira user'
field :jira_email, GraphQL::STRING_TYPE, null: true,
description: 'Email of the Jira user, returned only for users with public emails'
field :gitlab_id, GraphQL::INT_TYPE, null: true,
description: 'Id of the matched GitLab user'
end
# rubocop: enable Graphql/AuthorizeTypes
end
......@@ -48,6 +48,7 @@ module Types
mount_mutation Mutations::Snippets::Create
mount_mutation Mutations::Snippets::MarkAsSpam
mount_mutation Mutations::JiraImport::Start
mount_mutation Mutations::JiraImport::ImportUsers
mount_mutation Mutations::DesignManagement::Upload, calls_gitaly: true
mount_mutation Mutations::DesignManagement::Delete, calls_gitaly: true
mount_mutation Mutations::ContainerExpirationPolicies::Update
......
......@@ -22,6 +22,8 @@ module JiraImport
rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, URI::InvalidURIError, JIRA::HTTPError, OpenSSL::SSL::SSLError => error
Gitlab::ErrorTracking.track_exception(error, project_id: project.id, request: url)
ServiceResponse.error(message: "There was an error when communicating to Jira: #{error.message}")
rescue Projects::ImportService::Error => error
ServiceResponse.error(message: error.message)
end
private
......
......@@ -26,6 +26,12 @@ class ServiceResponse
status == :error
end
def errors
return [] unless error?
Array.wrap(message)
end
private
attr_writer :status, :message, :http_status, :payload
......
---
title: Create graphQL endpoint for Jira users import
merge_request: 33501
author:
type: added
......@@ -5933,6 +5933,46 @@ type JiraImportStartPayload {
jiraImport: JiraImport
}
"""
Autogenerated input type of JiraImportUsers
"""
input JiraImportUsersInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The project to import the Jira users into
"""
projectPath: ID!
"""
The index of the record the import should started at, default 0 (50 records returned)
"""
startAt: Int
}
"""
Autogenerated return type of JiraImportUsers
"""
type JiraImportUsersPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
"""
Users returned from Jira, matched by email and name if possible.
"""
jiraUsers: [JiraUser!]
}
type JiraProject {
"""
Key of the Jira project
......@@ -6027,6 +6067,28 @@ type JiraService implements Service {
type: String
}
type JiraUser {
"""
Id of the matched GitLab user
"""
gitlabId: Int
"""
Account id of the Jira user
"""
jiraAccountId: String!
"""
Display name of the Jira user
"""
jiraDisplayName: String!
"""
Email of the Jira user, returned only for users with public emails
"""
jiraEmail: String
}
type Label {
"""
Background color of the label
......@@ -7271,6 +7333,7 @@ type Mutation {
issueSetIteration(input: IssueSetIterationInput!): IssueSetIterationPayload
issueSetWeight(input: IssueSetWeightInput!): IssueSetWeightPayload
jiraImportStart(input: JiraImportStartInput!): JiraImportStartPayload
jiraImportUsers(input: JiraImportUsersInput!): JiraImportUsersPayload
markAsSpamSnippet(input: MarkAsSpamSnippetInput!): MarkAsSpamSnippetPayload
mergeRequestCreate(input: MergeRequestCreateInput!): MergeRequestCreatePayload
mergeRequestSetAssignees(input: MergeRequestSetAssigneesInput!): MergeRequestSetAssigneesPayload
......
......@@ -16358,6 +16358,126 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "JiraImportUsersInput",
"description": "Autogenerated input type of JiraImportUsers",
"fields": null,
"inputFields": [
{
"name": "projectPath",
"description": "The project to import the Jira users into",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "startAt",
"description": "The index of the record the import should started at, default 0 (50 records returned)",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "JiraImportUsersPayload",
"description": "Autogenerated return type of JiraImportUsers",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Errors encountered during execution of the mutation.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "jiraUsers",
"description": "Users returned from Jira, matched by email and name if possible.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "JiraUser",
"ofType": null
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "JiraProject",
......@@ -16641,6 +16761,83 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "JiraUser",
"description": null,
"fields": [
{
"name": "gitlabId",
"description": "Id of the matched GitLab user",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "jiraAccountId",
"description": "Account id of the Jira user",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "jiraDisplayName",
"description": "Display name of the Jira user",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "jiraEmail",
"description": "Email of the Jira user, returned only for users with public emails",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "Label",
......@@ -21053,6 +21250,33 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "jiraImportUsers",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "JiraImportUsersInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "JiraImportUsersPayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "markAsSpamSnippet",
"description": null,
......@@ -869,6 +869,16 @@ Autogenerated return type of JiraImportStart
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `jiraImport` | JiraImport | The Jira import data after mutation |
## JiraImportUsersPayload
Autogenerated return type of JiraImportUsers
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `jiraUsers` | JiraUser! => Array | Users returned from Jira, matched by email and name if possible. |
## JiraProject
| Name | Type | Description |
......@@ -885,6 +895,15 @@ Autogenerated return type of JiraImportStart
| `projects` | JiraProjectConnection | List of Jira projects fetched through Jira REST API |
| `type` | String | Class name of the service |
## JiraUser
| Name | Type | Description |
| --- | ---- | ---------- |
| `gitlabId` | Int | Id of the matched GitLab user |
| `jiraAccountId` | String! | Account id of the Jira user |
| `jiraDisplayName` | String! | Display name of the Jira user |
| `jiraEmail` | String | Email of the Jira user, returned only for users with public emails |
## Label
| Name | Type | Description |
......
# 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
......@@ -20,8 +20,8 @@ describe JiraImport::UsersImporter do
end
context 'when Jira import is not configured properly' do
it 'raises an error' do
expect { subject }.to raise_error(Projects::ImportService::Error)
it 'returns an error' do
expect(subject.errors).to eq(['Jira integration not configured.'])
end
end
......
......@@ -84,4 +84,14 @@ describe ServiceResponse do
expect(described_class.error(message: 'Bad apple').error?).to eq(true)
end
end
describe '#errors' do
it 'returns an empty array for a successful response' do
expect(described_class.success.errors).to be_empty
end
it 'returns an array with a correct message for an error response' do
expect(described_class.error(message: 'error message').errors).to eq(['error message'])
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment