Commit 1905324d authored by Jarka Košanová's avatar Jarka Košanová

Merge branch '220232-get-all-jira-projects' into 'master'

Expose all Jira projects endpoint through a GraphQL endpoint

Closes #220232

See merge request gitlab-org/gitlab!33861
parents 53ad352d 9e5a576a
......@@ -13,11 +13,10 @@ module Resolvers
def resolve(name: nil, **args)
authorize!(project)
response, start_cursor, end_cursor = jira_projects(name: name, **compute_pagination_params(args))
end_cursor = nil if !!response.payload[:is_last]
response = jira_projects(name: name)
if response.success?
Gitlab::Graphql::ExternallyPaginatedArray.new(start_cursor, end_cursor, *response.payload[:projects])
response.payload[:projects]
else
raise Gitlab::Graphql::Errors::BaseError, response.message
end
......@@ -35,41 +34,10 @@ module Resolvers
jira_service&.project
end
def compute_pagination_params(params)
after_cursor = Base64.decode64(params[:after].to_s)
before_cursor = Base64.decode64(params[:before].to_s)
def jira_projects(name:)
args = { query: name }.compact
# differentiate between 0 cursor and nil or invalid cursor that decodes into zero.
after_index = after_cursor.to_i == 0 && after_cursor != "0" ? nil : after_cursor.to_i
before_index = before_cursor.to_i == 0 && before_cursor != "0" ? nil : before_cursor.to_i
if after_index.present? && before_index.present?
if after_index >= before_index
{ start_at: 0, limit: 0 }
else
{ start_at: after_index + 1, limit: before_index - after_index - 1 }
end
elsif after_index.present?
{ start_at: after_index + 1, limit: nil }
elsif before_index.present?
{ start_at: 0, limit: before_index - 1 }
else
{ start_at: 0, limit: nil }
end
end
def jira_projects(name:, start_at:, limit:)
args = { query: name, start_at: start_at, limit: limit }.compact
response = Jira::Requests::Projects.new(project.jira_service, args).execute
return [response, nil, nil] if response.error?
projects = response.payload[:projects]
start_cursor = start_at == 0 ? nil : Base64.encode64((start_at - 1).to_s)
end_cursor = Base64.encode64((start_at + projects.size - 1).to_s)
[response, start_cursor, end_cursor]
return Jira::Requests::Projects.new(project.jira_service, args).execute
end
end
end
......
......@@ -15,7 +15,7 @@ module Types
null: true,
connection: false,
extensions: [Gitlab::Graphql::Extensions::ExternallyPaginatedArrayExtension],
description: 'List of Jira projects fetched through Jira REST API',
description: 'List of all Jira projects fetched through Jira REST API',
resolver: Resolvers::Projects::JiraProjectsResolver
end
end
......
......@@ -5,22 +5,16 @@ module Jira
class Base
include ProjectServicesLoggable
PER_PAGE = 50
attr_reader :jira_service, :project, :query
attr_reader :jira_service, :project, :limit, :start_at, :query
def initialize(jira_service, limit: PER_PAGE, start_at: 0, query: nil)
def initialize(jira_service, query: nil)
@project = jira_service&.project
@jira_service = jira_service
@limit = limit
@start_at = start_at
@query = query
@query = query
end
def execute
return ServiceResponse.error(message: _('Jira service not configured.')) unless jira_service&.active?
return ServiceResponse.success(payload: empty_payload) if limit.to_i <= 0
request
end
......
......@@ -9,19 +9,24 @@ module Jira
override :url
def url
'/rest/api/2/project/search?query=%{query}&maxResults=%{limit}&startAt=%{start_at}' %
{ query: CGI.escape(query.to_s), limit: limit.to_i, start_at: start_at.to_i }
'/rest/api/2/project'
end
override :build_service_response
def build_service_response(response)
return ServiceResponse.success(payload: empty_payload) unless response['values'].present?
return ServiceResponse.success(payload: empty_payload) unless response.present?
ServiceResponse.success(payload: { projects: map_projects(response), is_last: response['isLast'] })
ServiceResponse.success(payload: { projects: map_projects(response), is_last: true })
end
def map_projects(response)
response['values'].map { |v| JIRA::Resource::Project.build(client, v) }
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
......
---
title: Expose all Jira projects endpoint through a GraphQL
merge_request: 33861
author:
type: added
......@@ -6274,7 +6274,7 @@ type JiraService implements Service {
active: Boolean
"""
List of Jira projects fetched through Jira REST API
List of all Jira projects fetched through Jira REST API
"""
projects(
"""
......
......@@ -17342,7 +17342,7 @@
},
{
"name": "projects",
"description": "List of Jira projects fetched through Jira REST API",
"description": "List of all Jira projects fetched through Jira REST API",
"args": [
{
"name": "name",
......@@ -926,7 +926,7 @@ Autogenerated return type of JiraImportUsers
| Name | Type | Description |
| --- | ---- | ---------- |
| `active` | Boolean | Indicates if the service is active |
| `projects` | JiraProjectConnection | List of Jira projects fetched through Jira REST API |
| `projects` | JiraProjectConnection | List of all Jira projects fetched through Jira REST API |
| `type` | String | Class name of the service |
## JiraUser
......
......@@ -63,7 +63,7 @@ describe Resolvers::Projects::JiraProjectsResolver do
context 'when Jira connection is not valid' do
before do
WebMock.stub_request(:get, 'https://jira.example.com/rest/api/2/project/search?maxResults=50&query=&startAt=0')
WebMock.stub_request(:get, 'https://jira.example.com/rest/api/2/project')
.to_raise(JIRA::HTTPError.new(double(message: 'Some failure.')))
end
......
......@@ -80,34 +80,6 @@ describe 'query Jira projects' do
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
......
......@@ -32,14 +32,6 @@ describe Jira::Requests::Projects do
end
context 'with jira_service' do
context 'when limit is invalid' do
let(:params) { { limit: 0 } }
it 'returns a paylod with no projects returned' do
expect(subject.payload[:projects]).to be_empty
end
end
context 'when validations and params are ok' do
let(:client) { double(options: { site: 'https://jira.example.com' }) }
......@@ -60,7 +52,7 @@ describe Jira::Requests::Projects do
context 'when the request does not return any values' do
before do
expect(client).to receive(:get).and_return({ 'someKey' => 'value' })
expect(client).to receive(:get).and_return([])
end
it 'returns a paylod with no projects returned' do
......@@ -74,19 +66,15 @@ describe Jira::Requests::Projects do
context 'when the request returns values' do
before do
expect(client).to receive(:get).and_return(
{ 'values' => %w(project1 project2), 'isLast' => false }
)
expect(JIRA::Resource::Project).to receive(:build).with(client, 'project1').and_return('jira_project1')
expect(JIRA::Resource::Project).to receive(:build).with(client, 'project2').and_return('jira_project2')
expect(client).to receive(:get).and_return([{ "key" => 'project1' }, { "key" => 'project2' }])
end
it 'returns a paylod with jira projets' do
payload = subject.payload
expect(subject.success?).to be_truthy
expect(payload[:projects]).to eq(%w(jira_project1 jira_project2))
expect(payload[:is_last]).to be_falsey
expect(payload[:projects].map(&:key)).to eq(%w(project1 project2))
expect(payload[:is_last]).to be_truthy
end
end
end
......
......@@ -74,6 +74,48 @@ shared_context 'jira projects request context' do
}'
end
let_it_be(:all_jira_projects_json) do
'[{
"expand": "description,lead,issueTypes,url,projectKeys,permissions,insight",
"self": "https://gitlab-jira.atlassian.net/rest/api/2/project/10000",
"id": "10000",
"key": "EX",
"name": "Example",
"avatarUrls": {
"48x48": "https://gitlab-jira.atlassian.net/secure/projectavatar?pid=10000&avatarId=10425",
"24x24": "https://gitlab-jira.atlassian.net/secure/projectavatar?size=small&s=small&pid=10000&avatarId=10425",
"16x16": "https://gitlab-jira.atlassian.net/secure/projectavatar?size=xsmall&s=xsmall&pid=10000&avatarId=10425",
"32x32": "https://gitlab-jira.atlassian.net/secure/projectavatar?size=medium&s=medium&pid=10000&avatarId=10425"
},
"projectTypeKey": "software",
"simplified": false,
"style": "classic",
"isPrivate": false,
"properties": {
}
},
{
"expand": "description,lead,issueTypes,url,projectKeys,permissions,insight",
"self": "https://gitlab-jira.atlassian.net/rest/api/2/project/10001",
"id": "10001",
"key": "ABC",
"name": "Alphabetical",
"avatarUrls": {
"48x48": "https://gitlab-jira.atlassian.net/secure/projectavatar?pid=10001&avatarId=10405",
"24x24": "https://gitlab-jira.atlassian.net/secure/projectavatar?size=small&s=small&pid=10001&avatarId=10405",
"16x16": "https://gitlab-jira.atlassian.net/secure/projectavatar?size=xsmall&s=xsmall&pid=10001&avatarId=10405",
"32x32": "https://gitlab-jira.atlassian.net/secure/projectavatar?size=medium&s=medium&pid=10001&avatarId=10405"
},
"projectTypeKey": "software",
"simplified": true,
"style": "next-gen",
"isPrivate": false,
"properties": {
},
"entityId": "14935009-f8aa-481e-94bc-f7251f320b0e",
"uuid": "14935009-f8aa-481e-94bc-f7251f320b0e"
}]'
end
let_it_be(:empty_jira_projects_json) do
'{
"self": "https://your-domain.atlassian.net/rest/api/2/project/search?startAt=0&maxResults=2",
......@@ -86,10 +128,32 @@ shared_context 'jira projects request context' do
}'
end
let(:server_info_json) do
'{
"baseUrl": "https://gitlab-jira.atlassian.net",
"version": "1001.0.0-SNAPSHOT",
"versionNumbers": [
1001,
0,
0
],
"deploymentType": "Cloud",
"buildNumber": 100128,
"buildDate": "2020-06-03T01:58:44.000-0700",
"serverTime": "2020-06-04T06:15:13.686-0700",
"scmInfo": "e736ab140ddb281c7cf5dcf9062c9ce2c08b3c1c",
"serverTitle": "Jira",
"defaultLocale": {
"locale": "en_US"
}
}'
end
let(:test_url) { "#{url}/rest/api/2/project/search?maxResults=50&query=&startAt=0" }
let(:start_at_20_url) { "#{url}/rest/api/2/project/search?maxResults=50&query=&startAt=20" }
let(:start_at_1_url) { "#{url}/rest/api/2/project/search?maxResults=50&query=&startAt=1" }
let(:max_results_1_url) { "#{url}/rest/api/2/project/search?maxResults=1&query=&startAt=0" }
let(:all_projects_url) { "#{url}/rest/api/2/project" }
before do
WebMock.stub_request(:get, test_url).with(basic_auth: [username, password])
......@@ -100,5 +164,9 @@ shared_context 'jira projects request context' do
.to_return(body: jira_projects_json, headers: { "Content-Type": "application/json" })
WebMock.stub_request(:get, max_results_1_url).with(basic_auth: [username, password])
.to_return(body: jira_projects_json, headers: { "Content-Type": "application/json" })
WebMock.stub_request(:get, all_projects_url).with(basic_auth: [username, password])
.to_return(body: all_jira_projects_json, headers: { "Content-Type": "application/json" })
WebMock.stub_request(:get, 'https://jira.example.com/rest/api/2/serverInfo')
.to_return(status: 200, body: server_info_json, headers: {})
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