- Fixes #3455: Gitlab and Github integration cause application reload when using [Enter]
- Fixes #3456: Wrong API-Key for Gitlab does not throw error message - Fixes #3458: Gitlab and Github integration should provide visual feedback during adding issue. - Fixes #3459: Provide error message if user provided wrong information for Github and Gitlab - Refactoring: Multiple loads of GitLab/GitHub GraphQL schema causes Memory leak.
This commit is contained in:
parent
92eea19f8a
commit
1d2a1a1163
24 changed files with 287 additions and 265 deletions
1
Gemfile
1
Gemfile
|
@ -123,7 +123,6 @@ gem 'acts_as_list'
|
||||||
|
|
||||||
# integrations
|
# integrations
|
||||||
gem 'clearbit'
|
gem 'clearbit'
|
||||||
gem 'graphql-client'
|
|
||||||
gem 'net-ldap'
|
gem 'net-ldap'
|
||||||
gem 'slack-notifier'
|
gem 'slack-notifier'
|
||||||
gem 'zendesk_api'
|
gem 'zendesk_api'
|
||||||
|
|
|
@ -218,10 +218,6 @@ GEM
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
gmail_xoauth (0.4.2)
|
gmail_xoauth (0.4.2)
|
||||||
oauth (>= 0.3.6)
|
oauth (>= 0.3.6)
|
||||||
graphql (1.11.6)
|
|
||||||
graphql-client (0.16.0)
|
|
||||||
activesupport (>= 3.0)
|
|
||||||
graphql (~> 1.8)
|
|
||||||
guard (2.15.0)
|
guard (2.15.0)
|
||||||
formatador (>= 0.2.4)
|
formatador (>= 0.2.4)
|
||||||
listen (>= 2.7, < 4.0)
|
listen (>= 2.7, < 4.0)
|
||||||
|
@ -617,7 +613,6 @@ DEPENDENCIES
|
||||||
faker
|
faker
|
||||||
github_changelog_generator
|
github_changelog_generator
|
||||||
gmail_xoauth
|
gmail_xoauth
|
||||||
graphql-client
|
|
||||||
guard
|
guard
|
||||||
guard-livereload
|
guard-livereload
|
||||||
guard-symlink
|
guard-symlink
|
||||||
|
|
|
@ -51,8 +51,7 @@ class Form extends App.Controller
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
config.schema = data.response
|
App.Setting.set('github_config', config, notify: true)
|
||||||
App.Setting.set('github_config', config)
|
|
||||||
|
|
||||||
error: (data, status) ->
|
error: (data, status) ->
|
||||||
|
|
||||||
|
|
|
@ -51,8 +51,7 @@ class Form extends App.Controller
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
config.schema = data.response
|
App.Setting.set('gitlab_config', config, notify: true)
|
||||||
App.Setting.set('gitlab_config', config)
|
|
||||||
|
|
||||||
error: (data, status) ->
|
error: (data, status) ->
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,8 @@ class App.SidebarGitIssue extends App.Controller
|
||||||
|
|
||||||
constructor: ->
|
constructor: ->
|
||||||
super
|
super
|
||||||
@issueLinks = []
|
@issueLinks = []
|
||||||
|
@issueLinkData = []
|
||||||
@providerIdentifier = @provider.toLowerCase()
|
@providerIdentifier = @provider.toLowerCase()
|
||||||
|
|
||||||
sidebarItem: =>
|
sidebarItem: =>
|
||||||
|
@ -13,7 +14,7 @@ class App.SidebarGitIssue extends App.Controller
|
||||||
name: @providerIdentifier
|
name: @providerIdentifier
|
||||||
badgeCallback: @badgeRender
|
badgeCallback: @badgeRender
|
||||||
sidebarHead: @provider
|
sidebarHead: @provider
|
||||||
sidebarCallback: @showObjects
|
sidebarCallback: @reloadIssues
|
||||||
sidebarActions: [
|
sidebarActions: [
|
||||||
{
|
{
|
||||||
title: 'Link issue'
|
title: 'Link issue'
|
||||||
|
@ -26,8 +27,8 @@ class App.SidebarGitIssue extends App.Controller
|
||||||
|
|
||||||
shown: ->
|
shown: ->
|
||||||
return if !@ticket
|
return if !@ticket
|
||||||
return if !@ticket.id
|
|
||||||
@showIssues()
|
@listIssues()
|
||||||
|
|
||||||
metaBadge: =>
|
metaBadge: =>
|
||||||
counter = ''
|
counter = ''
|
||||||
|
@ -45,6 +46,7 @@ class App.SidebarGitIssue extends App.Controller
|
||||||
@badgeRenderLocal()
|
@badgeRenderLocal()
|
||||||
|
|
||||||
badgeRenderLocal: =>
|
badgeRenderLocal: =>
|
||||||
|
return if !@badgeEl
|
||||||
@badgeEl.html(App.view('generic/sidebar_tabs_item')(@metaBadge()))
|
@badgeEl.html(App.view('generic/sidebar_tabs_item')(@metaBadge()))
|
||||||
|
|
||||||
linkIssue: =>
|
linkIssue: =>
|
||||||
|
@ -54,86 +56,141 @@ class App.SidebarGitIssue extends App.Controller
|
||||||
taskKey: @taskKey
|
taskKey: @taskKey
|
||||||
container: @el.closest('.content')
|
container: @el.closest('.content')
|
||||||
callback: (link, ui) =>
|
callback: (link, ui) =>
|
||||||
if @ticket && @ticket.id
|
@getIssues(
|
||||||
@saveTicketIssues = true
|
links: [link]
|
||||||
ui.close()
|
success: (result) =>
|
||||||
@showIssues([link])
|
if !_.contains(@issueLinks, link)
|
||||||
|
@issueLinks.push(result[0].url)
|
||||||
|
@issueLinkData = @issueLinkData.concat(result)
|
||||||
|
|
||||||
|
if @ticket && @ticket.id
|
||||||
|
@saveIssues(
|
||||||
|
ticket_id: @ticket.id
|
||||||
|
links: @issueLinks
|
||||||
|
success: =>
|
||||||
|
ui.close()
|
||||||
|
@renderIssues()
|
||||||
|
error: (message = 'Unable to save issue') =>
|
||||||
|
ui.showAlert(App.i18n.translatePlain(message))
|
||||||
|
form = ui.el.find('.js-result')
|
||||||
|
@formEnable(form)
|
||||||
|
)
|
||||||
|
else
|
||||||
|
ui.close()
|
||||||
|
@renderIssues()
|
||||||
|
error: (message = 'Unable to load issues') =>
|
||||||
|
ui.showAlert(App.i18n.translatePlain(message))
|
||||||
|
form = ui.el.find('.js-result')
|
||||||
|
@formEnable(form)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
showObjects: (el) =>
|
reloadIssues: (el) =>
|
||||||
@el = el
|
if el
|
||||||
|
@el = el
|
||||||
|
|
||||||
# show placeholder
|
return @renderIssues() if !@ticket
|
||||||
if @ticket && @ticket.preferences && @ticket.preferences[@providerIdentifier] && @ticket.preferences[@providerIdentifier].issue_links
|
|
||||||
@issueLinks = @ticket.preferences[@providerIdentifier].issue_links
|
|
||||||
queryParams = @queryParam()
|
|
||||||
|
|
||||||
# TODO: what is 'gitlab_issue_links'
|
ticketLinks = @ticket?.preferences?[@providerIdentifier]?.issue_links || []
|
||||||
if queryParams && queryParams.gitlab_issue_links
|
return @renderIssues() if _.isEqual(@issueLinks, ticketLinks)
|
||||||
@issueLinks.push queryParams.gitlab_issue_links
|
|
||||||
@showIssues()
|
|
||||||
|
|
||||||
showIssues: (issueLinks) =>
|
@issueLinks = ticketLinks
|
||||||
if issueLinks
|
@listIssues(true)
|
||||||
@issueLinks = _.uniq(@issueLinks.concat(issueLinks))
|
|
||||||
|
|
||||||
# show placeholder
|
renderIssues: =>
|
||||||
if _.isEmpty(@issueLinks)
|
if _.isEmpty(@issueLinkData)
|
||||||
@html("<div>#{App.i18n.translateInline('No linked issues')}</div>")
|
@showEmpty()
|
||||||
return
|
return
|
||||||
|
|
||||||
# AJAX call to show items
|
|
||||||
@ajax(
|
|
||||||
id: "#{@providerIdentifier}-#{@taskKey}"
|
|
||||||
type: 'POST'
|
|
||||||
url: "#{@apiPath}/integration/#{@providerIdentifier}"
|
|
||||||
data: JSON.stringify(links: @issueLinks)
|
|
||||||
success: (data, status, xhr) =>
|
|
||||||
if data.response
|
|
||||||
@showList(data.response)
|
|
||||||
if @saveTicketIssues
|
|
||||||
@saveTicketIssues = false
|
|
||||||
@issueLinks = data.response.map((issue) -> issue.url)
|
|
||||||
@updateTicket(@ticket.id, @issueLinks)
|
|
||||||
return
|
|
||||||
@showError('Unable to load data...')
|
|
||||||
|
|
||||||
error: (xhr, status, error) =>
|
|
||||||
|
|
||||||
# do not close window if request is aborted
|
|
||||||
return if status is 'abort'
|
|
||||||
|
|
||||||
# show error message
|
|
||||||
@showError('Unable to load data...')
|
|
||||||
)
|
|
||||||
|
|
||||||
showList: (issues) =>
|
|
||||||
list = $(App.view('ticket_zoom/sidebar_git_issue')(
|
list = $(App.view('ticket_zoom/sidebar_git_issue')(
|
||||||
issues: issues
|
issues: @issueLinkData
|
||||||
))
|
))
|
||||||
list.delegate('.js-delete', 'click', (e) =>
|
list.delegate('.js-delete', 'click', (e) =>
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
issueLink = $(e.currentTarget).attr 'data-issue-id'
|
issueLink = $(e.currentTarget).attr 'data-issue-id'
|
||||||
@delete(issueLink)
|
@deleteIssue(issueLink)
|
||||||
)
|
)
|
||||||
@html(list)
|
@html(list)
|
||||||
@badgeRenderLocal()
|
@badgeRenderLocal()
|
||||||
|
|
||||||
|
listIssues: (force = false) =>
|
||||||
|
return @renderIssues() if !force && @fetchFullActive && @fetchFullActive > new Date().getTime() - 5000
|
||||||
|
@fetchFullActive = new Date().getTime()
|
||||||
|
|
||||||
|
return @renderIssues() if _.isEmpty(@issueLinks)
|
||||||
|
|
||||||
|
@getIssues(
|
||||||
|
links: @issueLinks
|
||||||
|
success: (result) =>
|
||||||
|
@issueLinks = result.map((element) -> element.url)
|
||||||
|
@issueLinkData = result
|
||||||
|
@renderIssues()
|
||||||
|
error: =>
|
||||||
|
@showError(App.i18n.translateInline('Unable to load issues'))
|
||||||
|
)
|
||||||
|
|
||||||
|
getIssues: (params) ->
|
||||||
|
@ajax(
|
||||||
|
id: "#{@providerIdentifier}-#{@taskKey}"
|
||||||
|
type: 'POST'
|
||||||
|
url: "#{@apiPath}/integration/#{@providerIdentifier}"
|
||||||
|
data: JSON.stringify(links: params.links)
|
||||||
|
success: (data, status, xhr) ->
|
||||||
|
if data.response
|
||||||
|
|
||||||
|
# some issues redirect to pull requests like
|
||||||
|
# https://github.com/zammad/zammad/issues/1574
|
||||||
|
# in this case throw error
|
||||||
|
return params.error('Unable to load issues') if _.isEmpty(data.response)
|
||||||
|
|
||||||
|
params.success(data.response)
|
||||||
|
else
|
||||||
|
params.error(data.message)
|
||||||
|
error: (xhr, status, error) ->
|
||||||
|
return if status is 'abort'
|
||||||
|
|
||||||
|
params.error()
|
||||||
|
)
|
||||||
|
|
||||||
|
saveIssues: (params) ->
|
||||||
|
App.Ajax.request(
|
||||||
|
id: "#{@providerIdentifier}-update-#{params.ticket_id}"
|
||||||
|
type: 'POST'
|
||||||
|
url: "#{@apiPath}/integration/#{@providerIdentifier}_ticket_update"
|
||||||
|
data: JSON.stringify(ticket_id: params.ticket_id, issue_links: params.links)
|
||||||
|
success: (data, status, xhr) ->
|
||||||
|
params.success(data)
|
||||||
|
error: (xhr, status, details) ->
|
||||||
|
return if status is 'abort'
|
||||||
|
|
||||||
|
params.error()
|
||||||
|
)
|
||||||
|
|
||||||
|
deleteIssue: (link) ->
|
||||||
|
@issueLinks = _.filter(@issueLinks, (element) -> element isnt link)
|
||||||
|
@issueLinkData = _.filter(@issueLinkData, (element) -> element.url isnt link)
|
||||||
|
|
||||||
|
if @ticket && @ticket.id
|
||||||
|
@saveIssues(
|
||||||
|
ticket_id: @ticket.id
|
||||||
|
links: @issueLinks
|
||||||
|
success: =>
|
||||||
|
@renderIssues()
|
||||||
|
error: (message = 'Unable to save issue') =>
|
||||||
|
@showError(App.i18n.translateInline(message))
|
||||||
|
)
|
||||||
|
else
|
||||||
|
@renderIssues()
|
||||||
|
|
||||||
|
showEmpty: ->
|
||||||
|
@html("<div>#{App.i18n.translateInline('No linked issues')}</div>")
|
||||||
|
@badgeRenderLocal()
|
||||||
|
|
||||||
showError: (message) =>
|
showError: (message) =>
|
||||||
@html App.i18n.translateInline(message)
|
@html App.i18n.translateInline(message)
|
||||||
|
|
||||||
reload: =>
|
reload: =>
|
||||||
@showIssues()
|
@reloadIssues()
|
||||||
|
|
||||||
delete: (issueLink) =>
|
|
||||||
localLinks = []
|
|
||||||
for localLink in @issueLinks
|
|
||||||
if issueLink.toString() isnt localLink.toString()
|
|
||||||
localLinks.push localLink
|
|
||||||
@issueLinks = localLinks
|
|
||||||
if @ticket && @ticket.id
|
|
||||||
@updateTicket(@ticket.id, @issueLinks)
|
|
||||||
@showIssues()
|
|
||||||
|
|
||||||
postParams: (args) =>
|
postParams: (args) =>
|
||||||
return if !args.ticket
|
return if !args.ticket
|
||||||
|
@ -143,25 +200,3 @@ class App.SidebarGitIssue extends App.Controller
|
||||||
args.ticket.preferences ||= {}
|
args.ticket.preferences ||= {}
|
||||||
args.ticket.preferences[@providerIdentifier] ||= {}
|
args.ticket.preferences[@providerIdentifier] ||= {}
|
||||||
args.ticket.preferences[@providerIdentifier].issue_links = @issueLinks
|
args.ticket.preferences[@providerIdentifier].issue_links = @issueLinks
|
||||||
|
|
||||||
updateTicket: (ticket_id, issueLinks) =>
|
|
||||||
App.Ajax.request(
|
|
||||||
id: "#{@providerIdentifier}-update-#{ticket_id}"
|
|
||||||
type: 'POST'
|
|
||||||
url: "#{@apiPath}/integration/#{@providerIdentifier}_ticket_update"
|
|
||||||
data: JSON.stringify(ticket_id: ticket_id, issue_links: issueLinks)
|
|
||||||
success: (data, status, xhr) =>
|
|
||||||
@badgeRenderLocal()
|
|
||||||
error: (xhr, status, details) =>
|
|
||||||
|
|
||||||
# do not close window if request is aborted
|
|
||||||
return if status is 'abort'
|
|
||||||
|
|
||||||
# show error message
|
|
||||||
@log 'errors', details
|
|
||||||
@notify(
|
|
||||||
type: 'error'
|
|
||||||
msg: App.i18n.translateContent(details.error_human || details.error || 'Unable to update object!')
|
|
||||||
timeout: 6000
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
<form class="flex horizontal js-result">
|
<div class="flex horizontal js-result">
|
||||||
<input type="text" name="link" value="" autocomplete="off" placeholder="<%= @placeholder %>" class="form-control"/>
|
<input type="text" name="link" value="" autocomplete="off" placeholder="<%= @placeholder %>" class="form-control"/>
|
||||||
</form>
|
</div>
|
||||||
|
|
|
@ -5,9 +5,11 @@ class Integration::GitHubController < ApplicationController
|
||||||
|
|
||||||
def verify
|
def verify
|
||||||
github = ::GitHub.new(params[:endpoint], params[:api_token])
|
github = ::GitHub.new(params[:endpoint], params[:api_token])
|
||||||
|
|
||||||
|
github.verify!
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
result: 'ok',
|
result: 'ok',
|
||||||
response: github.schema.to_json,
|
|
||||||
}
|
}
|
||||||
rescue => e
|
rescue => e
|
||||||
logger.error e
|
logger.error e
|
||||||
|
@ -21,7 +23,7 @@ class Integration::GitHubController < ApplicationController
|
||||||
def query
|
def query
|
||||||
config = Setting.get('github_config')
|
config = Setting.get('github_config')
|
||||||
|
|
||||||
github = ::GitHub.new(config['endpoint'], config['api_token'], schema: config['schema'])
|
github = ::GitHub.new(config['endpoint'], config['api_token'])
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
result: 'ok',
|
result: 'ok',
|
||||||
|
|
|
@ -5,9 +5,11 @@ class Integration::GitLabController < ApplicationController
|
||||||
|
|
||||||
def verify
|
def verify
|
||||||
gitlab = ::GitLab.new(params[:endpoint], params[:api_token])
|
gitlab = ::GitLab.new(params[:endpoint], params[:api_token])
|
||||||
|
|
||||||
|
gitlab.verify!
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
result: 'ok',
|
result: 'ok',
|
||||||
response: gitlab.schema.to_json,
|
|
||||||
}
|
}
|
||||||
rescue => e
|
rescue => e
|
||||||
logger.error e
|
logger.error e
|
||||||
|
@ -21,7 +23,7 @@ class Integration::GitLabController < ApplicationController
|
||||||
def query
|
def query
|
||||||
config = Setting.get('gitlab_config')
|
config = Setting.get('gitlab_config')
|
||||||
|
|
||||||
gitlab = ::GitLab.new(config['endpoint'], config['api_token'], schema: config['schema'])
|
gitlab = ::GitLab.new(config['endpoint'], config['api_token'])
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
result: 'ok',
|
result: 'ok',
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
class GitHub
|
class GitHub
|
||||||
extend Forwardable
|
|
||||||
|
|
||||||
attr_reader :client
|
attr_reader :client
|
||||||
|
|
||||||
def_delegator :client, :schema
|
def initialize(endpoint, api_token)
|
||||||
|
@client = GitHub::HttpClient.new(endpoint, api_token)
|
||||||
|
end
|
||||||
|
|
||||||
def initialize(*args, **kargs)
|
def verify!
|
||||||
@client = GitHub::Client.new(*args, **kargs)
|
GitHub::Credentials.new(client).verify!
|
||||||
end
|
end
|
||||||
|
|
||||||
def issues_by_urls(urls)
|
def issues_by_urls(urls)
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
|
||||||
require 'graphql/client'
|
|
||||||
|
|
||||||
class GitHub
|
|
||||||
class Client
|
|
||||||
|
|
||||||
delegate_missing_to :client
|
|
||||||
|
|
||||||
attr_reader :endpoint
|
|
||||||
|
|
||||||
def initialize(endpoint, api_token, schema: nil)
|
|
||||||
@endpoint = endpoint
|
|
||||||
@api_token = api_token
|
|
||||||
schema(schema) if schema.present?
|
|
||||||
end
|
|
||||||
|
|
||||||
def schema(source = http_client)
|
|
||||||
@schema ||= ::GraphQL::Client.load_schema(source)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def http_client
|
|
||||||
@http_client ||= GitHub::HttpClient.new(@endpoint, @api_token)
|
|
||||||
end
|
|
||||||
|
|
||||||
def client
|
|
||||||
@client ||= begin
|
|
||||||
GraphQL::Client.new(
|
|
||||||
schema: schema,
|
|
||||||
execute: http_client,
|
|
||||||
).tap do |client|
|
|
||||||
client.allow_dynamic_queries = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
28
lib/github/credentials.rb
Normal file
28
lib/github/credentials.rb
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
class GitHub
|
||||||
|
class Credentials
|
||||||
|
|
||||||
|
QUERY = <<-'GRAPHQL'.freeze
|
||||||
|
query {
|
||||||
|
viewer {
|
||||||
|
login
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GRAPHQL
|
||||||
|
|
||||||
|
attr_reader :client
|
||||||
|
|
||||||
|
def initialize(client)
|
||||||
|
@client = client
|
||||||
|
end
|
||||||
|
|
||||||
|
def verify!
|
||||||
|
response = client.perform(
|
||||||
|
query: GitHub::Credentials::QUERY,
|
||||||
|
)
|
||||||
|
return if response.dig('data', 'viewer', 'login').present?
|
||||||
|
|
||||||
|
raise 'Invalid GitHub GraphQL API token'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,22 +1,45 @@
|
||||||
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
||||||
require 'graphql/client'
|
|
||||||
require 'graphql/client/http'
|
|
||||||
|
|
||||||
class GitHub
|
class GitHub
|
||||||
class HttpClient < ::GraphQL::Client::HTTP
|
class HttpClient
|
||||||
|
attr_reader :api_token, :endpoint
|
||||||
|
|
||||||
def initialize(endpoint, api_token)
|
def initialize(endpoint, api_token)
|
||||||
raise 'api_token required' if api_token.blank?
|
raise 'api_token required' if api_token.blank?
|
||||||
raise 'endpoint required' if endpoint.blank?
|
raise 'endpoint required' if endpoint.blank?
|
||||||
|
|
||||||
@api_token = api_token
|
@api_token = api_token
|
||||||
|
@endpoint = endpoint
|
||||||
super(endpoint)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def headers(_context)
|
def perform(payload)
|
||||||
|
response = UserAgent.post(
|
||||||
|
endpoint,
|
||||||
|
payload,
|
||||||
|
{
|
||||||
|
headers: headers,
|
||||||
|
json: true,
|
||||||
|
open_timeout: 6,
|
||||||
|
read_timeout: 16,
|
||||||
|
log: {
|
||||||
|
facility: 'GitHub',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if !response.success?
|
||||||
|
Rails.logger.error response.error
|
||||||
|
raise "Error while requesting GitHub GraphQL API: #{response.error}"
|
||||||
|
end
|
||||||
|
|
||||||
|
response.data
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def headers
|
||||||
{
|
{
|
||||||
Authorization: "bearer #{@api_token}"
|
Authorization: "bearer #{api_token}"
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -87,28 +87,28 @@ class GitHub
|
||||||
@result.dig('milestone', 'title')
|
@result.dig('milestone', 'title')
|
||||||
end
|
end
|
||||||
|
|
||||||
def query
|
|
||||||
@query ||= client.parse GitHub::LinkedIssue::QUERY
|
|
||||||
end
|
|
||||||
|
|
||||||
def query_by_url(url)
|
def query_by_url(url)
|
||||||
variables = variables(url)
|
response = client.perform(
|
||||||
return if variables.blank?
|
query: GitHub::LinkedIssue::QUERY,
|
||||||
|
variables: variables!(url)
|
||||||
|
)
|
||||||
|
|
||||||
response = client.query(query, variables: variables)
|
response.dig('data', 'repository', 'issue')
|
||||||
|
|
||||||
response&.data&.repository&.issue&.to_h&.deep_dup
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def variables(url)
|
def variables!(url)
|
||||||
return if url !~ %r{^https://([^/]+)/([^/]+)/([^/]+)/issues/(\d+)$}
|
if url !~ %r{^https://([^/]+)/([^/]+)/([^/]+)/issues/(\d+)$}
|
||||||
|
raise Exceptions::UnprocessableEntity, 'Invalid GitHub issue link format'
|
||||||
|
end
|
||||||
|
|
||||||
host = $1
|
host = $1
|
||||||
repositor_owner = $2
|
repositor_owner = $2
|
||||||
repository_name = $3
|
repository_name = $3
|
||||||
id = $4
|
id = $4
|
||||||
|
|
||||||
return if client.endpoint.exclude?(host)
|
if client.endpoint.exclude?(host)
|
||||||
|
raise Exceptions::UnprocessableEntity, "Issue link doesn't match configured GitHub endpoint '#{client.endpoint}'"
|
||||||
|
end
|
||||||
|
|
||||||
{
|
{
|
||||||
repositor_owner: repositor_owner,
|
repositor_owner: repositor_owner,
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
class GitLab
|
class GitLab
|
||||||
extend Forwardable
|
|
||||||
|
|
||||||
attr_reader :client
|
attr_reader :client
|
||||||
|
|
||||||
def_delegator :client, :schema
|
def initialize(endpoint, api_token)
|
||||||
|
@client = GitLab::HttpClient.new(endpoint, api_token)
|
||||||
|
end
|
||||||
|
|
||||||
def initialize(*args, **kargs)
|
def verify!
|
||||||
@client = GitLab::Client.new(*args, **kargs)
|
GitLab::Credentials.new(client).verify!
|
||||||
end
|
end
|
||||||
|
|
||||||
def issues_by_urls(urls)
|
def issues_by_urls(urls)
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
|
||||||
require 'graphql/client'
|
|
||||||
|
|
||||||
class GitLab
|
|
||||||
class Client
|
|
||||||
|
|
||||||
delegate_missing_to :client
|
|
||||||
|
|
||||||
attr_reader :endpoint
|
|
||||||
|
|
||||||
def initialize(endpoint, api_token, schema: nil)
|
|
||||||
@endpoint = endpoint
|
|
||||||
@api_token = api_token
|
|
||||||
schema(schema) if schema.present?
|
|
||||||
end
|
|
||||||
|
|
||||||
def schema(source = http_client)
|
|
||||||
@schema ||= ::GraphQL::Client.load_schema(source)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def http_client
|
|
||||||
@http_client ||= GitLab::HttpClient.new(@endpoint, @api_token)
|
|
||||||
end
|
|
||||||
|
|
||||||
def client
|
|
||||||
@client ||= begin
|
|
||||||
GraphQL::Client.new(
|
|
||||||
schema: schema,
|
|
||||||
execute: http_client,
|
|
||||||
).tap do |client|
|
|
||||||
client.allow_dynamic_queries = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
28
lib/gitlab/credentials.rb
Normal file
28
lib/gitlab/credentials.rb
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
class GitLab
|
||||||
|
class Credentials
|
||||||
|
|
||||||
|
QUERY = <<-'GRAPHQL'.freeze
|
||||||
|
query {
|
||||||
|
currentUser {
|
||||||
|
username
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GRAPHQL
|
||||||
|
|
||||||
|
attr_reader :client
|
||||||
|
|
||||||
|
def initialize(client)
|
||||||
|
@client = client
|
||||||
|
end
|
||||||
|
|
||||||
|
def verify!
|
||||||
|
response = client.perform(
|
||||||
|
query: GitLab::Credentials::QUERY,
|
||||||
|
)
|
||||||
|
return if response.dig('data', 'currentUser', 'username').present?
|
||||||
|
|
||||||
|
raise 'Invalid GitLab GraphQL API token'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,20 +1,43 @@
|
||||||
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
||||||
require 'graphql/client'
|
|
||||||
require 'graphql/client/http'
|
|
||||||
|
|
||||||
class GitLab
|
class GitLab
|
||||||
class HttpClient < ::GraphQL::Client::HTTP
|
class HttpClient
|
||||||
|
attr_reader :api_token, :endpoint
|
||||||
|
|
||||||
def initialize(endpoint, api_token)
|
def initialize(endpoint, api_token)
|
||||||
raise 'api_token required' if api_token.blank?
|
raise 'api_token required' if api_token.blank?
|
||||||
raise 'endpoint required' if endpoint.blank?
|
raise 'endpoint required' if endpoint.blank?
|
||||||
|
|
||||||
@api_token = api_token
|
@api_token = api_token
|
||||||
|
@endpoint = endpoint
|
||||||
super(endpoint)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def headers(_context)
|
def perform(payload)
|
||||||
|
response = UserAgent.post(
|
||||||
|
endpoint,
|
||||||
|
payload,
|
||||||
|
{
|
||||||
|
headers: headers,
|
||||||
|
json: true,
|
||||||
|
open_timeout: 6,
|
||||||
|
read_timeout: 16,
|
||||||
|
log: {
|
||||||
|
facility: 'GitLab',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if !response.success?
|
||||||
|
Rails.logger.error response.error
|
||||||
|
raise "Error while requesting GitLab GraphQL API: #{response.error}"
|
||||||
|
end
|
||||||
|
|
||||||
|
response.data
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def headers
|
||||||
{
|
{
|
||||||
"PRIVATE-TOKEN": @api_token
|
"PRIVATE-TOKEN": @api_token
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,27 +84,30 @@ class GitLab
|
||||||
@result.dig('milestone', 'title')
|
@result.dig('milestone', 'title')
|
||||||
end
|
end
|
||||||
|
|
||||||
def query
|
|
||||||
@query ||= client.parse GitLab::LinkedIssue::QUERY
|
|
||||||
end
|
|
||||||
|
|
||||||
def query_by_url(url)
|
def query_by_url(url)
|
||||||
variables = variables(url)
|
variables = variables(url)
|
||||||
return if variables.blank?
|
return if variables.blank?
|
||||||
|
|
||||||
response = client.query(query, variables: variables)
|
response = client.perform(
|
||||||
|
query: GitLab::LinkedIssue::QUERY,
|
||||||
|
variables: variables
|
||||||
|
)
|
||||||
|
|
||||||
response&.data&.project&.issue&.to_h&.deep_dup
|
response.dig('data', 'project', 'issue')
|
||||||
end
|
end
|
||||||
|
|
||||||
def variables(url)
|
def variables(url)
|
||||||
return if url !~ %r{^https://([^/]+)/(.*)/-/issues/(\d+)$}
|
if url !~ %r{^https://([^/]+)/(.*)/-/issues/(\d+)$}
|
||||||
|
raise Exceptions::UnprocessableEntity, 'Invalid GitLab issue link format'
|
||||||
|
end
|
||||||
|
|
||||||
host = $1
|
host = $1
|
||||||
fullpath = $2
|
fullpath = $2
|
||||||
id = $3
|
id = $3
|
||||||
|
|
||||||
return if client.endpoint.exclude?(host)
|
if client.endpoint.exclude?(host)
|
||||||
|
raise Exceptions::UnprocessableEntity, "Issue link doesn't match configured GitLab endpoint '#{client.endpoint}'"
|
||||||
|
end
|
||||||
|
|
||||||
{
|
{
|
||||||
fullpath: fullpath,
|
fullpath: fullpath,
|
||||||
|
|
|
@ -6,13 +6,9 @@ RSpec.describe GitHub, type: :integration do # rubocop:disable RSpec/FilePath
|
||||||
required_envs.each do |key|
|
required_envs.each do |key|
|
||||||
skip("NOTICE: Missing environment variable #{key} for test! (Please fill up: #{required_envs.join(' && ')})") if ENV[key].blank?
|
skip("NOTICE: Missing environment variable #{key} for test! (Please fill up: #{required_envs.join(' && ')})") if ENV[key].blank?
|
||||||
end
|
end
|
||||||
|
|
||||||
# request schema only once for performance reasons
|
|
||||||
@cached_schema = described_class.new(ENV['GITHUB_ENDPOINT'], ENV['GITHUB_APITOKEN']).schema
|
|
||||||
end
|
end
|
||||||
|
|
||||||
let(:instance) { described_class.new(ENV['GITHUB_ENDPOINT'], ENV['GITHUB_APITOKEN'], schema: schema) }
|
let(:instance) { described_class.new(ENV['GITHUB_ENDPOINT'], ENV['GITHUB_APITOKEN']) }
|
||||||
let(:schema) { @cached_schema } # rubocop:disable RSpec/InstanceVariable
|
|
||||||
let(:issue_data) do
|
let(:issue_data) do
|
||||||
{
|
{
|
||||||
id: '1575',
|
id: '1575',
|
||||||
|
@ -37,12 +33,6 @@ RSpec.describe GitHub, type: :integration do # rubocop:disable RSpec/FilePath
|
||||||
end
|
end
|
||||||
let(:invalid_issue_url) { 'https://github.com/organization/repository/issues/42' }
|
let(:invalid_issue_url) { 'https://github.com/organization/repository/issues/42' }
|
||||||
|
|
||||||
describe '#schema' do
|
|
||||||
it 'returns GraphQL schema' do
|
|
||||||
expect(instance.schema).to respond_to(:to_graphql)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#issues_by_urls' do
|
describe '#issues_by_urls' do
|
||||||
let(:result) { instance.issues_by_urls([ issue_url ]) }
|
let(:result) { instance.issues_by_urls([ issue_url ]) }
|
||||||
|
|
||||||
|
|
|
@ -6,13 +6,9 @@ RSpec.describe GitLab, type: :integration do # rubocop:disable RSpec/FilePath
|
||||||
required_envs.each do |key|
|
required_envs.each do |key|
|
||||||
skip("NOTICE: Missing environment variable #{key} for test! (Please fill up: #{required_envs.join(' && ')})") if ENV[key].blank?
|
skip("NOTICE: Missing environment variable #{key} for test! (Please fill up: #{required_envs.join(' && ')})") if ENV[key].blank?
|
||||||
end
|
end
|
||||||
|
|
||||||
# request schema only once for performance reasons
|
|
||||||
@cached_schema = described_class.new(ENV['GITLAB_ENDPOINT'], ENV['GITLAB_APITOKEN']).schema
|
|
||||||
end
|
end
|
||||||
|
|
||||||
let(:instance) { described_class.new(ENV['GITLAB_ENDPOINT'], ENV['GITLAB_APITOKEN'], schema: schema) }
|
let(:instance) { described_class.new(ENV['GITLAB_ENDPOINT'], ENV['GITLAB_APITOKEN']) }
|
||||||
let(:schema) { @cached_schema } # rubocop:disable RSpec/InstanceVariable
|
|
||||||
let(:issue_data) do
|
let(:issue_data) do
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
|
@ -40,13 +36,7 @@ RSpec.describe GitLab, type: :integration do # rubocop:disable RSpec/FilePath
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
let(:invalid_issue_url) { 'https://git.example.com/group/project/-/issues/1' }
|
let(:invalid_issue_url) { "https://#{URI.parse(ENV['GITLAB_ISSUE_LINK']).host}/group/project/-/issues/1" }
|
||||||
|
|
||||||
describe '#schema' do
|
|
||||||
it 'returns GraphQL schema' do
|
|
||||||
expect(instance.schema).to respond_to(:to_graphql)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#issues_by_urls' do
|
describe '#issues_by_urls' do
|
||||||
let(:result) { instance.issues_by_urls([ issue_url ]) }
|
let(:result) { instance.issues_by_urls([ issue_url ]) }
|
||||||
|
|
|
@ -60,14 +60,13 @@ RSpec.describe 'GitHub', type: :request do
|
||||||
authenticated_as(admin)
|
authenticated_as(admin)
|
||||||
instance = instance_double('GitHub')
|
instance = instance_double('GitHub')
|
||||||
expect(GitHub).to receive(:new).with(endpoint, token).and_return instance
|
expect(GitHub).to receive(:new).with(endpoint, token).and_return instance
|
||||||
expect(instance).to receive(:schema).and_return(dummy_schema)
|
expect(instance).to receive(:verify!).and_return(true)
|
||||||
|
|
||||||
post '/api/v1/integration/github/verify', params: params, as: :json
|
post '/api/v1/integration/github/verify', params: params, as: :json
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
expect(json_response).to be_a_kind_of(Hash)
|
expect(json_response).to be_a_kind_of(Hash)
|
||||||
expect(json_response).not_to be_blank
|
expect(json_response).not_to be_blank
|
||||||
expect(json_response['result']).to eq('ok')
|
expect(json_response['result']).to eq('ok')
|
||||||
expect(json_response['response']).to eq(dummy_schema.to_json)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does query objects' do
|
it 'does query objects' do
|
||||||
|
|
|
@ -65,14 +65,13 @@ RSpec.describe 'GitLab', type: :request do
|
||||||
authenticated_as(admin)
|
authenticated_as(admin)
|
||||||
instance = instance_double('GitLab')
|
instance = instance_double('GitLab')
|
||||||
expect(GitLab).to receive(:new).with(endpoint, token).and_return instance
|
expect(GitLab).to receive(:new).with(endpoint, token).and_return instance
|
||||||
expect(instance).to receive(:schema).and_return(dummy_schema)
|
expect(instance).to receive(:verify!).and_return(true)
|
||||||
|
|
||||||
post '/api/v1/integration/gitlab/verify', params: params, as: :json
|
post '/api/v1/integration/gitlab/verify', params: params, as: :json
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
expect(json_response).to be_a_kind_of(Hash)
|
expect(json_response).to be_a_kind_of(Hash)
|
||||||
expect(json_response).not_to be_blank
|
expect(json_response).not_to be_blank
|
||||||
expect(json_response['result']).to eq('ok')
|
expect(json_response['result']).to eq('ok')
|
||||||
expect(json_response['response']).to eq(dummy_schema.to_json)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does query objects' do
|
it 'does query objects' do
|
||||||
|
|
|
@ -352,9 +352,6 @@ RSpec.describe 'Ticket Create', type: :system do
|
||||||
required_envs.each do |key|
|
required_envs.each do |key|
|
||||||
skip("NOTICE: Missing environment variable #{key} for test! (Please fill up: #{required_envs.join(' && ')})") if ENV[key].blank?
|
skip("NOTICE: Missing environment variable #{key} for test! (Please fill up: #{required_envs.join(' && ')})") if ENV[key].blank?
|
||||||
end
|
end
|
||||||
|
|
||||||
# request schema only once for performance reasons
|
|
||||||
@cached_schema = GitLab.new(ENV['GITLAB_ENDPOINT'], ENV['GITLAB_APITOKEN']).schema.to_json
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def authenticate
|
def authenticate
|
||||||
|
@ -362,7 +359,6 @@ RSpec.describe 'Ticket Create', type: :system do
|
||||||
Setting.set('gitlab_config', {
|
Setting.set('gitlab_config', {
|
||||||
api_token: ENV['GITLAB_APITOKEN'],
|
api_token: ENV['GITLAB_APITOKEN'],
|
||||||
endpoint: ENV['GITLAB_ENDPOINT'],
|
endpoint: ENV['GITLAB_ENDPOINT'],
|
||||||
schema: @cached_schema, # rubocop:disable RSpec/InstanceVariable
|
|
||||||
})
|
})
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
@ -410,9 +406,6 @@ RSpec.describe 'Ticket Create', type: :system do
|
||||||
required_envs.each do |key|
|
required_envs.each do |key|
|
||||||
skip("NOTICE: Missing environment variable #{key} for test! (Please fill up: #{required_envs.join(' && ')})") if ENV[key].blank?
|
skip("NOTICE: Missing environment variable #{key} for test! (Please fill up: #{required_envs.join(' && ')})") if ENV[key].blank?
|
||||||
end
|
end
|
||||||
|
|
||||||
# request schema only once for performance reasons
|
|
||||||
@cached_schema = GitHub.new(ENV['GITHUB_ENDPOINT'], ENV['GITHUB_APITOKEN']).schema.to_json
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def authenticate
|
def authenticate
|
||||||
|
@ -420,7 +413,6 @@ RSpec.describe 'Ticket Create', type: :system do
|
||||||
Setting.set('github_config', {
|
Setting.set('github_config', {
|
||||||
api_token: ENV['GITHUB_APITOKEN'],
|
api_token: ENV['GITHUB_APITOKEN'],
|
||||||
endpoint: ENV['GITHUB_ENDPOINT'],
|
endpoint: ENV['GITHUB_ENDPOINT'],
|
||||||
schema: @cached_schema, # rubocop:disable RSpec/InstanceVariable
|
|
||||||
})
|
})
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
|
@ -1513,9 +1513,6 @@ RSpec.describe 'Ticket zoom', type: :system do
|
||||||
required_envs.each do |key|
|
required_envs.each do |key|
|
||||||
skip("NOTICE: Missing environment variable #{key} for test! (Please fill up: #{required_envs.join(' && ')})") if ENV[key].blank?
|
skip("NOTICE: Missing environment variable #{key} for test! (Please fill up: #{required_envs.join(' && ')})") if ENV[key].blank?
|
||||||
end
|
end
|
||||||
|
|
||||||
# request schema only once for performance reasons
|
|
||||||
@cached_schema = GitLab.new(ENV['GITLAB_ENDPOINT'], ENV['GITLAB_APITOKEN']).schema.to_json
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def authenticate
|
def authenticate
|
||||||
|
@ -1523,7 +1520,6 @@ RSpec.describe 'Ticket zoom', type: :system do
|
||||||
Setting.set('gitlab_config', {
|
Setting.set('gitlab_config', {
|
||||||
api_token: ENV['GITLAB_APITOKEN'],
|
api_token: ENV['GITLAB_APITOKEN'],
|
||||||
endpoint: ENV['GITLAB_ENDPOINT'],
|
endpoint: ENV['GITLAB_ENDPOINT'],
|
||||||
schema: @cached_schema, # rubocop:disable RSpec/InstanceVariable
|
|
||||||
})
|
})
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
@ -1577,9 +1573,6 @@ RSpec.describe 'Ticket zoom', type: :system do
|
||||||
required_envs.each do |key|
|
required_envs.each do |key|
|
||||||
skip("NOTICE: Missing environment variable #{key} for test! (Please fill up: #{required_envs.join(' && ')})") if ENV[key].blank?
|
skip("NOTICE: Missing environment variable #{key} for test! (Please fill up: #{required_envs.join(' && ')})") if ENV[key].blank?
|
||||||
end
|
end
|
||||||
|
|
||||||
# request schema only once for performance reasons
|
|
||||||
@cached_schema = GitHub.new(ENV['GITHUB_ENDPOINT'], ENV['GITHUB_APITOKEN']).schema.to_json
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def authenticate
|
def authenticate
|
||||||
|
@ -1587,7 +1580,6 @@ RSpec.describe 'Ticket zoom', type: :system do
|
||||||
Setting.set('github_config', {
|
Setting.set('github_config', {
|
||||||
api_token: ENV['GITHUB_APITOKEN'],
|
api_token: ENV['GITHUB_APITOKEN'],
|
||||||
endpoint: ENV['GITHUB_ENDPOINT'],
|
endpoint: ENV['GITHUB_ENDPOINT'],
|
||||||
schema: @cached_schema, # rubocop:disable RSpec/InstanceVariable
|
|
||||||
})
|
})
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue