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 ...@@ -48,6 +48,7 @@ module Types
mount_mutation Mutations::Snippets::Create mount_mutation Mutations::Snippets::Create
mount_mutation Mutations::Snippets::MarkAsSpam mount_mutation Mutations::Snippets::MarkAsSpam
mount_mutation Mutations::JiraImport::Start mount_mutation Mutations::JiraImport::Start
mount_mutation Mutations::JiraImport::ImportUsers
mount_mutation Mutations::DesignManagement::Upload, calls_gitaly: true mount_mutation Mutations::DesignManagement::Upload, calls_gitaly: true
mount_mutation Mutations::DesignManagement::Delete, calls_gitaly: true mount_mutation Mutations::DesignManagement::Delete, calls_gitaly: true
mount_mutation Mutations::ContainerExpirationPolicies::Update mount_mutation Mutations::ContainerExpirationPolicies::Update
......
...@@ -22,6 +22,8 @@ module JiraImport ...@@ -22,6 +22,8 @@ module JiraImport
rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, URI::InvalidURIError, JIRA::HTTPError, OpenSSL::SSL::SSLError => error 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) Gitlab::ErrorTracking.track_exception(error, project_id: project.id, request: url)
ServiceResponse.error(message: "There was an error when communicating to Jira: #{error.message}") ServiceResponse.error(message: "There was an error when communicating to Jira: #{error.message}")
rescue Projects::ImportService::Error => error
ServiceResponse.error(message: error.message)
end end
private private
......
...@@ -26,6 +26,12 @@ class ServiceResponse ...@@ -26,6 +26,12 @@ class ServiceResponse
status == :error status == :error
end end
def errors
return [] unless error?
Array.wrap(message)
end
private private
attr_writer :status, :message, :http_status, :payload 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 { ...@@ -5933,6 +5933,46 @@ type JiraImportStartPayload {
jiraImport: JiraImport 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 { type JiraProject {
""" """
Key of the Jira project Key of the Jira project
...@@ -6027,6 +6067,28 @@ type JiraService implements Service { ...@@ -6027,6 +6067,28 @@ type JiraService implements Service {
type: String 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 { type Label {
""" """
Background color of the label Background color of the label
...@@ -7271,6 +7333,7 @@ type Mutation { ...@@ -7271,6 +7333,7 @@ type Mutation {
issueSetIteration(input: IssueSetIterationInput!): IssueSetIterationPayload issueSetIteration(input: IssueSetIterationInput!): IssueSetIterationPayload
issueSetWeight(input: IssueSetWeightInput!): IssueSetWeightPayload issueSetWeight(input: IssueSetWeightInput!): IssueSetWeightPayload
jiraImportStart(input: JiraImportStartInput!): JiraImportStartPayload jiraImportStart(input: JiraImportStartInput!): JiraImportStartPayload
jiraImportUsers(input: JiraImportUsersInput!): JiraImportUsersPayload
markAsSpamSnippet(input: MarkAsSpamSnippetInput!): MarkAsSpamSnippetPayload markAsSpamSnippet(input: MarkAsSpamSnippetInput!): MarkAsSpamSnippetPayload
mergeRequestCreate(input: MergeRequestCreateInput!): MergeRequestCreatePayload mergeRequestCreate(input: MergeRequestCreateInput!): MergeRequestCreatePayload
mergeRequestSetAssignees(input: MergeRequestSetAssigneesInput!): MergeRequestSetAssigneesPayload mergeRequestSetAssignees(input: MergeRequestSetAssigneesInput!): MergeRequestSetAssigneesPayload
......
...@@ -16358,6 +16358,126 @@ ...@@ -16358,6 +16358,126 @@
"enumValues": null, "enumValues": null,
"possibleTypes": 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", "kind": "OBJECT",
"name": "JiraProject", "name": "JiraProject",
...@@ -16641,6 +16761,83 @@ ...@@ -16641,6 +16761,83 @@
"enumValues": null, "enumValues": null,
"possibleTypes": 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", "kind": "OBJECT",
"name": "Label", "name": "Label",
...@@ -21053,6 +21250,33 @@ ...@@ -21053,6 +21250,33 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "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", "name": "markAsSpamSnippet",
"description": null, "description": null,
...@@ -869,6 +869,16 @@ Autogenerated return type of JiraImportStart ...@@ -869,6 +869,16 @@ Autogenerated return type of JiraImportStart
| `errors` | String! => Array | Errors encountered during execution of the mutation. | | `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `jiraImport` | JiraImport | The Jira import data after 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 ## JiraProject
| Name | Type | Description | | Name | Type | Description |
...@@ -885,6 +895,15 @@ Autogenerated return type of JiraImportStart ...@@ -885,6 +895,15 @@ Autogenerated return type of JiraImportStart
| `projects` | JiraProjectConnection | List of Jira projects fetched through Jira REST API | | `projects` | JiraProjectConnection | List of Jira projects fetched through Jira REST API |
| `type` | String | Class name of the service | | `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 ## Label
| Name | Type | Description | | 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 ...@@ -20,8 +20,8 @@ describe JiraImport::UsersImporter do
end end
context 'when Jira import is not configured properly' do context 'when Jira import is not configured properly' do
it 'raises an error' do it 'returns an error' do
expect { subject }.to raise_error(Projects::ImportService::Error) expect(subject.errors).to eq(['Jira integration not configured.'])
end end
end end
......
...@@ -84,4 +84,14 @@ describe ServiceResponse do ...@@ -84,4 +84,14 @@ describe ServiceResponse do
expect(described_class.error(message: 'Bad apple').error?).to eq(true) expect(described_class.error(message: 'Bad apple').error?).to eq(true)
end end
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 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