Commit 11fefb3c authored by Alexandru Croitor's avatar Alexandru Croitor

Add Jira issue finder service

Adding a service for quring Jira REST API and fetching issues to
be presented for jira issues integration.
parent 5d0b22da
# frozen_string_literal: true
module Projects
module Integrations
module Jira
IntegrationError = Class.new(StandardError)
RequestError = Class.new(StandardError)
class IssuesFinder
attr_reader :issues, :total_count
def initialize(project, params = {})
@project = project
@jira_service = project.jira_service
@page = params[:page].presence || 1
@params = params
end
def execute
return [] unless Feature.enabled?(:jira_integration, project)
raise IntegrationError, _('Jira service not configured.') unless jira_service&.active?
project_key = jira_service.project_key
raise IntegrationError, _('Jira project key is not configured') if project_key.blank?
fetch_issues(project_key)
end
private
attr_reader :project, :jira_service, :page, :params
# rubocop: disable CodeReuse/ServiceClass
def fetch_issues(project_key)
jql = ::Jira::JqlBuilderService.new(project_key, params).execute
response = ::Jira::Requests::Issues::ListService.new(jira_service, { jql: jql, page: page }).execute
if response.success?
@total_count = response.payload[:total_count]
@issues = response.payload[:issues]
else
raise RequestError, response.message
end
end
# rubocop: enable CodeReuse/ServiceClass
end
end
end
end
......@@ -37,7 +37,7 @@ module Resolvers
def jira_projects(name:)
args = { query: name }.compact
return Jira::Requests::Projects.new(project.jira_service, args).execute
return Jira::Requests::Projects::ListService.new(project.jira_service, args).execute
end
end
end
......
......@@ -23,7 +23,7 @@ class JiraService < IssueTrackerService
# TODO: we can probably just delegate as part of
# https://gitlab.com/gitlab-org/gitlab/issues/29404
data_field :username, :password, :url, :api_url, :jira_issue_transition_id
data_field :username, :password, :url, :api_url, :jira_issue_transition_id, :project_key
before_update :reset_password
......
# frozen_string_literal: true
module Jira
class JqlBuilderService
DEFAULT_SORT = "created"
DEFAULT_SORT_DIRECTION = "DESC"
def initialize(jira_project_key, params = {})
@jira_project_key = jira_project_key
@sort = params[:sort] || DEFAULT_SORT
@sort_direction = params[:sort_direction] || DEFAULT_SORT_DIRECTION
end
def execute
[by_project, order_by].join(' ')
end
private
attr_reader :jira_project_key, :sort, :sort_direction
def by_project
"project = #{jira_project_key}"
end
def order_by
"order by #{sort} #{sort_direction}"
end
end
end
......@@ -5,12 +5,11 @@ module Jira
class Base
include ProjectServicesLoggable
attr_reader :jira_service, :project, :query
JIRA_API_VERSION = 2
def initialize(jira_service, query: nil)
def initialize(jira_service, params = {})
@project = jira_service&.project
@jira_service = jira_service
@query = query
end
def execute
......@@ -19,8 +18,19 @@ module Jira
request
end
def base_api_url
"/rest/api/#{api_version}"
end
private
attr_reader :jira_service, :project
# override this method in the specific request class implementation if a differnt API version is required
def api_version
JIRA_API_VERSION
end
def client
@client ||= jira_service.client
end
......
# frozen_string_literal: true
module Jira
module Requests
module Issues
class ListService < Base
extend ::Gitlab::Utils::Override
PER_PAGE = 100
def initialize(jira_service, params = {})
super(jira_service, params)
@jql = params[:jql].to_s
@page = params[:page].to_i || 1
end
private
attr_reader :jql, :page
override :url
def url
"#{base_api_url}/search?jql=#{CGI.escape(jql)}&startAt=#{start_at}&maxResults=#{PER_PAGE}&fields=*all"
end
override :build_service_response
def build_service_response(response)
return ServiceResponse.success(payload: empty_payload) if response.blank? || response["issues"].blank?
ServiceResponse.success(payload: {
issues: map_issues(response["issues"]),
is_last: last?(response),
total_count: response["total"].to_i
})
end
def map_issues(response)
response.map { |v| JIRA::Resource::Issue.build(client, v) }
end
def empty_payload
{ issues: [], is_last: true, total_count: 0 }
end
def last?(response)
response["total"].to_i <= response["startAt"].to_i + response["issues"].size
end
def start_at
(page - 1) * PER_PAGE
end
end
end
end
end
# frozen_string_literal: true
module Jira
module Requests
class Projects < Base
extend ::Gitlab::Utils::Override
private
override :url
def url
'/rest/api/2/project'
end
override :build_service_response
def build_service_response(response)
return ServiceResponse.success(payload: empty_payload) unless response.present?
ServiceResponse.success(payload: { projects: map_projects(response), is_last: true })
end
def map_projects(response)
response.map { |v| JIRA::Resource::Project.build(client, v) }.select(&method(:match_query?))
end
def match_query?(jira_project)
query = self.query.to_s.downcase
jira_project&.key&.downcase&.include?(query) || jira_project&.name&.downcase&.include?(query)
end
def empty_payload
{ projects: [], is_last: true }
end
end
end
end
# frozen_string_literal: true
module Jira
module Requests
module Projects
class ListService < Base
extend ::Gitlab::Utils::Override
def initialize(jira_service, params: {})
super(jira_service, params)
@query = params[:query]
end
private
attr_reader :query
override :url
def url
"#{base_api_url}/project"
end
override :build_service_response
def build_service_response(response)
return ServiceResponse.success(payload: empty_payload) unless response.present?
ServiceResponse.success(payload: { projects: map_projects(response), is_last: true })
end
def map_projects(response)
response.map { |v| JIRA::Resource::Project.build(client, v) }.select(&method(:match_query?))
end
def match_query?(jira_project)
query = query.to_s.downcase
jira_project&.key&.downcase&.include?(query) || jira_project&.name&.downcase&.include?(query)
end
def empty_payload
{ projects: [], is_last: true }
end
end
end
end
end
......@@ -12725,6 +12725,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
msgid "Jira project key is not configured"
msgstr ""
msgid "Jira project: %{importProject}"
msgstr ""
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::Integrations::Jira::IssuesFinder do
let_it_be(:project, reload: true) { create(:project) }
let_it_be(:jira_service, reload: true) { create(:jira_service, project: project) }
let(:params) { {} }
let(:service) { described_class.new(project, params) }
describe '#execute' do
subject(:issues) { service.execute }
context 'when jira_integration feature flag is not enabled' do
before do
stub_feature_flags(jira_integration: false)
end
it 'exits early and returns no issues' do
expect(issues.size).to eq 0
expect(service.total_count).to be_nil
end
end
context 'when jira service integration does not have project_key' do
it 'raises error' do
expect { subject }.to raise_error(Projects::Integrations::Jira::IntegrationError, 'Jira project key is not configured')
end
end
context 'when jira service integration is not active' do
before do
jira_service.update!(active: false)
end
it 'raises error' do
expect { subject }.to raise_error(Projects::Integrations::Jira::IntegrationError, 'Jira service not configured.')
end
end
context 'when jira service integration has project_key' do
let(:params) { {} }
let(:client) { double(options: { site: 'https://jira.example.com' }) }
before do
jira_service.update!(project_key: 'TEST')
expect_next_instance_of(Jira::Requests::Issues::ListService) do |instance|
expect(instance).to receive(:client).at_least(:once).and_return(client)
end
end
context 'when Jira API request fails' do
before do
expect(client).to receive(:get).and_raise(Timeout::Error)
end
it 'raises error', :aggregate_failures do
expect { subject }.to raise_error(Projects::Integrations::Jira::RequestError)
end
end
context 'when Jira API request succeeds' do
before do
expect(client).to receive(:get).and_return(
{
"total" => 375,
"startAt" => 0,
"issues" => [{ "key" => 'TEST-1' }, { "key" => 'TEST-2' }]
}
)
end
it 'return service response with issues', :aggregate_failures do
expect(issues.size).to eq 2
expect(service.total_count).to eq 375
expect(issues.map(&:key)).to eq(%w[TEST-1 TEST-2])
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Jira::JqlBuilderService do
describe '#execute' do
subject { described_class.new('PROJECT_KEY', params).execute }
context 'when no params' do
let(:params) { {} }
it 'builds jql with default ordering' do
expect(subject).to eq("project = PROJECT_KEY order by created DESC")
end
end
context 'with sort params' do
let(:params) { { sort: 'updated', sort_direction: 'ASC' } }
it 'builds jql' do
expect(subject).to eq("project = PROJECT_KEY order by updated ASC")
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Jira::Requests::Issues::ListService do
let(:jira_service) { create(:jira_service) }
let(:params) { {} }
describe '#execute' do
let(:service) { described_class.new(jira_service, params) }
subject { service.execute }
context 'without jira_service' do
before do
jira_service.update!(active: false)
end
it 'returns an error response' do
expect(subject.error?).to be_truthy
expect(subject.message).to eq('Jira service not configured.')
end
end
context 'when jira_service is nil' do
let(:jira_service) { nil }
it 'returns an error response' do
expect(subject.error?).to be_truthy
expect(subject.message).to eq('Jira service not configured.')
end
end
context 'with jira_service' do
context 'when validations and params are ok' do
let(:client) { double(options: { site: 'https://jira.example.com' }) }
before do
expect(service).to receive(:client).at_least(:once).and_return(client)
end
context 'when the request to Jira returns an error' do
before do
expect(client).to receive(:get).and_raise(Timeout::Error)
end
it 'returns an error response' do
expect(subject.error?).to be_truthy
expect(subject.message).to eq('Jira request error: Timeout::Error')
end
end
context 'when the request does not return any values' do
before do
expect(client).to receive(:get).and_return([])
end
it 'returns a paylod with no issues' do
payload = subject.payload
expect(subject.success?).to be_truthy
expect(payload[:issues]).to be_empty
expect(payload[:is_last]).to be_truthy
end
end
context 'when the request returns values' do
before do
expect(client).to receive(:get).and_return(
{
"total" => 375,
"startAt" => 0,
"issues" => [{ "key" => 'TST-1' }, { "key" => 'TST-2' }]
}
)
end
it 'returns a paylod with jira issues' do
payload = subject.payload
expect(subject.success?).to be_truthy
expect(payload[:issues].map(&:key)).to eq(%w[TST-1 TST-2])
expect(payload[:is_last]).to be_falsy
end
end
end
end
end
end
......@@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Jira::Requests::Projects do
RSpec.describe Jira::Requests::Projects::ListService do
let(:jira_service) { create(:jira_service) }
let(:params) { {} }
......
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