Commit 5b5df7be authored by Alexandru Croitor's avatar Alexandru Croitor

Handle user mapping for Jira server

Jra Server REST API endpoint for fetching users is different from
Jira Cloud version, however we need to support both. We ca find out
Jira deployment type ad hadle user mappings different for each
version.
parent eb25db6a
...@@ -8,6 +8,12 @@ class JiraService < IssueTrackerService ...@@ -8,6 +8,12 @@ class JiraService < IssueTrackerService
PROJECTS_PER_PAGE = 50 PROJECTS_PER_PAGE = 50
# TODO: use jira_service.deployment_type enum when https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37003 is merged
DEPLOYMENT_TYPES = {
server: 'SERVER',
cloud: 'CLOUD'
}.freeze
validates :url, public_url: true, presence: true, if: :activated? validates :url, public_url: true, presence: true, if: :activated?
validates :api_url, public_url: true, allow_blank: true validates :api_url, public_url: true, allow_blank: true
validates :username, presence: true, if: :activated? validates :username, presence: true, if: :activated?
......
# frozen_string_literal: true
module JiraImport
class CloudUsersMapperService < UsersMapperService
private
def url
"/rest/api/2/users?maxResults=#{MAX_USERS}&startAt=#{start_at.to_i}"
end
def jira_user_id(jira_user)
jira_user['accountId']
end
def jira_user_name(jira_user)
jira_user['displayName']
end
end
end
# frozen_string_literal: true
module JiraImport
class ServerUsersMapperService < UsersMapperService
private
def url
"/rest/api/2/user/search?username=''&maxResults=#{MAX_USERS}&startAt=#{start_at.to_i}"
end
def jira_user_id(jira_user)
jira_user['key']
end
def jira_user_name(jira_user)
jira_user['name']
end
end
end
...@@ -2,9 +2,7 @@ ...@@ -2,9 +2,7 @@
module JiraImport module JiraImport
class UsersImporter class UsersImporter
attr_reader :user, :project, :start_at, :result attr_reader :user, :project, :start_at
MAX_USERS = 50
def initialize(user, project, start_at) def initialize(user, project, start_at)
@project = project @project = project
...@@ -15,29 +13,43 @@ module JiraImport ...@@ -15,29 +13,43 @@ module JiraImport
def execute def execute
Gitlab::JiraImport.validate_project_settings!(project, user: user) Gitlab::JiraImport.validate_project_settings!(project, user: user)
return ServiceResponse.success(payload: nil) if users.blank? ServiceResponse.success(payload: mapped_users)
result = UsersMapper.new(project, users).execute
ServiceResponse.success(payload: result)
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)
ServiceResponse.error(message: "There was an error when communicating to Jira: #{error.message}") ServiceResponse.error(message: "There was an error when communicating to Jira")
rescue Projects::ImportService::Error => error rescue Projects::ImportService::Error => error
ServiceResponse.error(message: error.message) ServiceResponse.error(message: error.message)
end end
private private
def users def mapped_users
@users ||= client.get(url) users_mapper_service.execute
end
def users_mapper_service
@users_mapper_service ||= user_mapper_service_factory
end end
def url def deployment_type
"/rest/api/2/users?maxResults=#{MAX_USERS}&startAt=#{start_at.to_i}" # TODO: use project.jira_service.deployment_type value when https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37003 is merged
@deployment_type ||= client.ServerInfo.all.deploymentType
end end
def client def client
@client ||= project.jira_service.client @client ||= project.jira_service.client
end end
def user_mapper_service_factory
# TODO: use deployment_type enum from jira service when https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37003 is merged
case deployment_type.upcase
when JiraService::DEPLOYMENT_TYPES[:server]
ServerUsersMapperService.new(project.jira_service, start_at)
when JiraService::DEPLOYMENT_TYPES[:cloud]
CloudUsersMapperService.new(project.jira_service, start_at)
else
raise ArgumentError
end
end
end end
end end
# frozen_string_literal: true # frozen_string_literal: true
module JiraImport module JiraImport
class UsersMapper class UsersMapperService
attr_reader :project, :jira_users MAX_USERS = 50
def initialize(project, jira_users) attr_reader :jira_service, :start_at
@project = project
@jira_users = jira_users def initialize(jira_service, start_at)
@jira_service = jira_service
@start_at = start_at
end end
def execute def execute
jira_users.to_a.map do |jira_user| users.to_a.map do |jira_user|
{ {
jira_account_id: jira_user['accountId'], jira_account_id: jira_user_id(jira_user),
jira_display_name: jira_user['displayName'], jira_display_name: jira_user_name(jira_user),
jira_email: jira_user['emailAddress'] jira_email: jira_user['emailAddress']
}.merge(match_user(jira_user)) }.merge(match_user(jira_user))
end end
...@@ -21,6 +23,26 @@ module JiraImport ...@@ -21,6 +23,26 @@ module JiraImport
private private
def users
@users ||= client.get(url)
end
def client
@client ||= jira_service.client
end
def url
raise NotImplementedError
end
def jira_user_id(jira_user)
raise NotImplementedError
end
def jira_user_name(jira_user)
raise NotImplementedError
end
# TODO: Matching user by email and displayName will be done as the part # TODO: Matching user by email and displayName will be done as the part
# of follow-up issue: https://gitlab.com/gitlab-org/gitlab/-/issues/219023 # of follow-up issue: https://gitlab.com/gitlab-org/gitlab/-/issues/219023
def match_user(jira_user) def match_user(jira_user)
......
---
title: Handle user mapping for Jira server instances
merge_request: 39362
author:
type: fixed
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe JiraImport::CloudUsersMapperService do
let(:start_at) { 7 }
let(:url) { "/rest/api/2/users?maxResults=50&startAt=#{start_at}" }
let(:jira_users) do
[
{ 'accountId' => 'abcd', 'displayName' => 'user1' },
{ 'accountId' => 'efg' },
{ 'accountId' => 'hij', 'displayName' => 'user3', 'emailAddress' => 'user3@example.com' }
]
end
describe '#execute' do
it_behaves_like 'mapping jira users'
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe JiraImport::ServerUsersMapperService do
let(:start_at) { 7 }
let(:url) { "/rest/api/2/user/search?username=''&maxResults=50&startAt=#{start_at}" }
let(:jira_users) do
[
{ 'key' => 'abcd', 'name' => 'user1' },
{ 'key' => 'efg' },
{ 'key' => 'hij', 'name' => 'user3', 'emailAddress' => 'user3@example.com' }
]
end
describe '#execute' do
it_behaves_like 'mapping jira users'
end
end
...@@ -14,6 +14,27 @@ RSpec.describe JiraImport::UsersImporter do ...@@ -14,6 +14,27 @@ RSpec.describe JiraImport::UsersImporter do
subject { importer.execute } subject { importer.execute }
describe '#execute' do describe '#execute' do
let(:mapped_users) do
[
{
jira_account_id: 'acc1',
jira_display_name: 'user1',
jira_email: 'sample@jira.com',
gitlab_id: nil,
gitlab_username: nil,
gitlab_name: nil
},
{
jira_account_id: 'acc2',
jira_display_name: 'user2',
jira_email: nil,
gitlab_id: nil,
gitlab_username: nil,
gitlab_name: nil
}
]
end
before do before do
stub_jira_service_test stub_jira_service_test
project.add_maintainer(user) project.add_maintainer(user)
...@@ -25,53 +46,83 @@ RSpec.describe JiraImport::UsersImporter do ...@@ -25,53 +46,83 @@ RSpec.describe JiraImport::UsersImporter do
end end
end end
context 'when Jira import is configured correctly' do RSpec.shared_examples 'maps jira users to gitlab users' do
let_it_be(:jira_service) { create(:jira_service, project: project, active: true) } context 'when Jira import is configured correctly' do
let(:client) { double } let_it_be(:jira_service) { create(:jira_service, project: project, active: true) }
let(:client) { double }
before do
expect(importer).to receive(:client).and_return(client)
end
context 'when jira client raises an error' do
it 'returns an error response' do
expect(client).to receive(:get).and_raise(Timeout::Error)
expect(subject.error?).to be_truthy
expect(subject.message).to include('There was an error when communicating to Jira')
end
end
context 'when jira client returns result' do
before do before do
allow(client).to receive(:get).with('/rest/api/2/users?maxResults=50&startAt=7') expect(importer).to receive(:client).at_least(1).and_return(client)
.and_return(jira_users) allow(client).to receive_message_chain(:ServerInfo, :all, :deploymentType).and_return(deployment_type)
end end
context 'when jira client returns an empty array' do context 'when jira client raises an error' do
let(:jira_users) { [] } it 'returns an error response' do
expect(client).to receive(:get).and_raise(Timeout::Error)
it 'retturns nil payload' do expect(subject.error?).to be_truthy
expect(subject.success?).to be_truthy expect(subject.message).to include('There was an error when communicating to Jira')
expect(subject.payload).to be_nil
end end
end end
context 'when jira client returns an results' do context 'when jira client returns result' do
let(:jira_users) { [{ 'name' => 'user1' }, { 'name' => 'user2' }] } context 'when jira client returns an empty array' do
let(:mapped_users) { [{ jira_display_name: 'user1', gitlab_id: 5 }] } let(:jira_users) { [] }
before do it 'retturns nil payload' do
expect(JiraImport::UsersMapper).to receive(:new).with(project, jira_users) expect(subject.success?).to be_truthy
.and_return(double(execute: mapped_users)) expect(subject.payload).to be_empty
end
end end
it 'returns the mapped users' do context 'when jira client returns an results' do
expect(subject.success?).to be_truthy it 'returns the mapped users' do
expect(subject.payload).to eq(mapped_users) expect(subject.success?).to be_truthy
expect(subject.payload).to eq(mapped_users)
end
end end
end end
end end
end end
context 'when Jira instance is of Server deployment type' do
let(:deployment_type) { 'Server' }
let(:url) { "/rest/api/2/user/search?username=''&maxResults=50&startAt=#{start_at}" }
let(:jira_users) do
[
{ 'key' => 'acc1', 'name' => 'user1', 'emailAddress' => 'sample@jira.com' },
{ 'key' => 'acc2', 'name' => 'user2' }
]
end
before do
allow_next_instance_of(JiraImport::ServerUsersMapperService) do |instance|
allow(instance).to receive(:client).and_return(client)
allow(client).to receive(:get).with(url).and_return(jira_users)
end
end
it_behaves_like 'maps jira users to gitlab users'
end
context 'when Jira instance is of Cloud deploymet type' do
let(:deployment_type) { 'Cloud' }
let(:url) { "/rest/api/2/users?maxResults=50&startAt=#{start_at}" }
let(:jira_users) do
[
{ 'accountId' => 'acc1', 'displayName' => 'user1', 'emailAddress' => 'sample@jira.com' },
{ 'accountId' => 'acc2', 'displayName' => 'user2' }
]
end
before do
allow_next_instance_of(JiraImport::CloudUsersMapperService) do |instance|
allow(instance).to receive(:client).and_return(client)
allow(client).to receive(:get).with(url).and_return(jira_users)
end
end
it_behaves_like 'maps jira users to gitlab users'
end
end end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe JiraImport::UsersMapper do
let_it_be(:project) { create(:project) }
subject { described_class.new(project, jira_users).execute }
describe '#execute' do
context 'jira_users is nil' do
let(:jira_users) { nil }
it 'returns an empty array' do
expect(subject).to be_empty
end
end
context 'when jira_users is present' do
let(:jira_users) do
[
{ 'accountId' => 'abcd', 'displayName' => 'user1' },
{ 'accountId' => 'efg' },
{ 'accountId' => 'hij', 'displayName' => 'user3', 'emailAddress' => 'user3@example.com' }
]
end
# TODO: now we only create an array in a proper format
# mapping is tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/219023
let(:mapped_users) do
[
{ jira_account_id: 'abcd', jira_display_name: 'user1', jira_email: nil, gitlab_id: nil, gitlab_username: nil, gitlab_name: nil },
{ jira_account_id: 'efg', jira_display_name: nil, jira_email: nil, gitlab_id: nil, gitlab_username: nil, gitlab_name: nil },
{ jira_account_id: 'hij', jira_display_name: 'user3', jira_email: 'user3@example.com', gitlab_id: nil, gitlab_username: nil, gitlab_name: nil }
]
end
it 'returns users mapped to Gitlab' do
expect(subject).to eq(mapped_users)
end
end
end
end
# frozen_string_literal: true
RSpec.shared_examples 'mapping jira users' do
let(:client) { double }
let_it_be(:project) { create(:project) }
let_it_be(:jira_service) { create(:jira_service, project: project, active: true) }
before do
allow(subject).to receive(:client).and_return(client)
allow(client).to receive(:get).with(url).and_return(jira_users)
end
subject { described_class.new(jira_service, start_at) }
context 'jira_users is nil' do
let(:jira_users) { nil }
it 'returns an empty array' do
expect(subject.execute).to be_empty
end
end
context 'when jira_users is present' do
# TODO: now we only create an array in a proper format
# mapping is tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/219023
let(:mapped_users) do
[
{ jira_account_id: 'abcd', jira_display_name: 'user1', jira_email: nil, gitlab_id: nil, gitlab_username: nil, gitlab_name: nil },
{ jira_account_id: 'efg', jira_display_name: nil, jira_email: nil, gitlab_id: nil, gitlab_username: nil, gitlab_name: nil },
{ jira_account_id: 'hij', jira_display_name: 'user3', jira_email: 'user3@example.com', gitlab_id: nil, gitlab_username: nil, gitlab_name: nil }
]
end
it 'returns users mapped to Gitlab' do
expect(subject.execute).to eq(mapped_users)
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