Fixes #1575 - GitHub integration.
This commit is contained in:
parent
97c9d541e9
commit
12a5e9dba3
23 changed files with 827 additions and 6 deletions
|
@ -274,6 +274,7 @@ RSpec/ExampleLength:
|
|||
- 'spec/requests/integration/check_mk_spec.rb'
|
||||
- 'spec/requests/integration/cti_spec.rb'
|
||||
- 'spec/requests/integration/idoit_spec.rb'
|
||||
- 'spec/requests/integration/github_spec.rb'
|
||||
- 'spec/requests/integration/gitlab_spec.rb'
|
||||
- 'spec/requests/integration/monitoring_spec.rb'
|
||||
- 'spec/requests/integration/object_manager_attributes_spec.rb'
|
||||
|
@ -555,6 +556,7 @@ RSpec/MultipleExpectations:
|
|||
- 'spec/requests/integration/check_mk_spec.rb'
|
||||
- 'spec/requests/integration/cti_spec.rb'
|
||||
- 'spec/requests/integration/idoit_spec.rb'
|
||||
- 'spec/requests/integration/github_spec.rb'
|
||||
- 'spec/requests/integration/gitlab_spec.rb'
|
||||
- 'spec/requests/integration/monitoring_spec.rb'
|
||||
- 'spec/requests/integration/object_manager_attributes_spec.rb'
|
||||
|
|
|
@ -224,6 +224,11 @@
|
|||
"url": "",
|
||||
"license": ""
|
||||
},
|
||||
"github-logo.svg": {
|
||||
"author": "Github",
|
||||
"url": "",
|
||||
"license": ""
|
||||
},
|
||||
"gitlab-button.svg": {
|
||||
"author": "GitLab",
|
||||
"url": "",
|
||||
|
@ -579,6 +584,11 @@
|
|||
"url": "",
|
||||
"license": "MIT"
|
||||
},
|
||||
"sso-button.svg": {
|
||||
"author": "Tanu Doank",
|
||||
"url": "https:\/\/thenounproject.com\/term\/key\/1247931\/",
|
||||
"license": "CC 3.0 Attribution"
|
||||
},
|
||||
"status-modified-outer-circle.svg": {
|
||||
"author": "Zammad",
|
||||
"url": "",
|
||||
|
@ -688,10 +698,5 @@
|
|||
"author": "Felix Niklas",
|
||||
"url": "",
|
||||
"license": "MIT"
|
||||
},
|
||||
"sso-button.svg": {
|
||||
"author": "Tanu Doank",
|
||||
"url": "https://thenounproject.com/term/key/1247931/",
|
||||
"license": "CC 3.0 Attribution"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
class GitHub extends App.ControllerIntegrationBase
|
||||
featureIntegration: 'github_integration'
|
||||
featureName: 'GitHub'
|
||||
featureConfig: 'github_config'
|
||||
description: [
|
||||
['This service allows you to connect %s with %s.', 'GitHub', 'Zammad']
|
||||
]
|
||||
events:
|
||||
'change .js-switch input': 'switch'
|
||||
|
||||
render: =>
|
||||
super
|
||||
new Form(
|
||||
el: @$('.js-form')
|
||||
)
|
||||
|
||||
class Form extends App.Controller
|
||||
events:
|
||||
'submit form': 'update'
|
||||
|
||||
constructor: ->
|
||||
super
|
||||
@render()
|
||||
|
||||
render: =>
|
||||
config = App.Setting.get('github_config')
|
||||
|
||||
@html App.view('integration/github')(
|
||||
config: config
|
||||
)
|
||||
|
||||
update: (e) =>
|
||||
e.preventDefault()
|
||||
config = @formParam(e.target)
|
||||
@validateAndSave(config)
|
||||
|
||||
validateAndSave: (config) =>
|
||||
App.Ajax.request(
|
||||
id: 'github'
|
||||
type: 'POST'
|
||||
url: "#{@apiPath}/integration/github/verify"
|
||||
data: JSON.stringify(
|
||||
api_token: config.api_token
|
||||
endpoint: config.endpoint
|
||||
)
|
||||
success: (data, status, xhr) =>
|
||||
if data.result is 'failed'
|
||||
new App.ControllerErrorModal(
|
||||
message: data.message
|
||||
container: @el.closest('.content')
|
||||
)
|
||||
return
|
||||
|
||||
config.schema = data.response
|
||||
App.Setting.set('github_config', config)
|
||||
|
||||
error: (data, status) ->
|
||||
|
||||
return if status is 'abort'
|
||||
|
||||
details = data.responseJSON || {}
|
||||
App.Event.trigger 'notify', {
|
||||
type: 'error'
|
||||
msg: App.i18n.translateContent(details.error_human || details.error || 'Unable to save!')
|
||||
}
|
||||
)
|
||||
|
||||
class State
|
||||
@current: ->
|
||||
App.Setting.get('github_integration')
|
||||
|
||||
App.Config.set(
|
||||
'IntegrationGitHub'
|
||||
{
|
||||
name: 'GitHub'
|
||||
target: '#system/integration/github'
|
||||
description: 'Link GitHub issues to your tickets.'
|
||||
controller: GitHub
|
||||
state: State
|
||||
}
|
||||
'NavBarIntegrations'
|
||||
)
|
|
@ -0,0 +1,6 @@
|
|||
class SidebarGitHub extends App.SidebarGitIssue
|
||||
provider: 'GitHub'
|
||||
urlPlaceholder: 'https://github.com/organization/repository/issues/42'
|
||||
|
||||
App.Config.set('500-GitHub', SidebarGitHub, 'TicketCreateSidebar')
|
||||
App.Config.set('500-GitHub', SidebarGitHub, 'TicketZoomSidebar')
|
22
app/assets/javascripts/app/views/integration/github.jst.eco
Normal file
22
app/assets/javascripts/app/views/integration/github.jst.eco
Normal file
|
@ -0,0 +1,22 @@
|
|||
<form>
|
||||
<h2><%- @T('Settings') %></h2>
|
||||
<div class="settings-entry">
|
||||
<table class="settings-list" style="width: 100%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="20%"><%- @T('Name') %>
|
||||
<th width="80%"><%- @T('Value') %>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="settings-list-row-control"><%- @T('Endpoint') %> *
|
||||
<td class="settings-list-control-cell"><input type="text" class="form-control form-control--small" value="<%= @config.endpoint %>" placeholder="https://api.github.com/graphql" name="endpoint">
|
||||
<tr>
|
||||
<td class="settings-list-row-control"><%- @T('API token') %> *
|
||||
<td class="settings-list-control-cell"><input type="text" class="form-control form-control--small" value="<%= @config.api_token %>" name="api_token">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn--primary js-submit"><%- @T('Save') %></button>
|
||||
</form>
|
|
@ -43,6 +43,7 @@
|
|||
.icon-forward { width: 16px; height: 17px; }
|
||||
.icon-full-logo { width: 175px; height: 50px; }
|
||||
.icon-github-button { width: 29px; height: 24px; }
|
||||
.icon-github-logo { width: 24px; height: 24px; }
|
||||
.icon-gitlab-button { width: 29px; height: 24px; }
|
||||
.icon-gitlab-logo { width: 24px; height: 24px; }
|
||||
.icon-google-button { width: 29px; height: 24px; }
|
||||
|
|
53
app/controllers/integration/github_controller.rb
Normal file
53
app/controllers/integration/github_controller.rb
Normal file
|
@ -0,0 +1,53 @@
|
|||
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
||||
|
||||
class Integration::GitHubController < ApplicationController
|
||||
prepend_before_action { authentication_check && authorize! }
|
||||
|
||||
def verify
|
||||
github = ::GitHub.new(params[:endpoint], params[:api_token])
|
||||
render json: {
|
||||
result: 'ok',
|
||||
response: github.schema.to_json,
|
||||
}
|
||||
rescue => e
|
||||
logger.error e
|
||||
|
||||
render json: {
|
||||
result: 'failed',
|
||||
message: e.message,
|
||||
}
|
||||
end
|
||||
|
||||
def query
|
||||
config = Setting.get('github_config')
|
||||
|
||||
github = ::GitHub.new(config['endpoint'], config['api_token'], schema: config['schema'])
|
||||
|
||||
render json: {
|
||||
result: 'ok',
|
||||
response: github.issues_by_urls(params[:links]),
|
||||
}
|
||||
rescue => e
|
||||
logger.error e
|
||||
|
||||
render json: {
|
||||
result: 'failed',
|
||||
message: e.message,
|
||||
}
|
||||
end
|
||||
|
||||
def update
|
||||
ticket = Ticket.find(params[:ticket_id])
|
||||
ticket.with_lock do
|
||||
authorize!(ticket, :show?)
|
||||
ticket.preferences[:github] ||= {}
|
||||
ticket.preferences[:github][:issue_links] = Array(params[:issue_links]).uniq
|
||||
ticket.save!
|
||||
end
|
||||
|
||||
render json: {
|
||||
result: 'ok',
|
||||
}
|
||||
end
|
||||
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
class Controllers::Integration::GitHubControllerPolicy < Controllers::ApplicationControllerPolicy
|
||||
permit! %i[query update], to: 'ticket.agent'
|
||||
permit! :verify, to: 'admin.integration.github'
|
||||
default_permit!(['agent.integration.github', 'admin.integration.github'])
|
||||
end
|
|
@ -22,4 +22,5 @@ ActiveSupport::Inflector.inflections(:en) do |inflect|
|
|||
inflect.singular(/(knowledge_base)s$/i, '\1')
|
||||
inflect.acronym 'SMIME'
|
||||
inflect.acronym 'GitLab'
|
||||
inflect.acronym 'GitHub'
|
||||
end
|
||||
|
|
9
config/routes/integration_github.rb
Normal file
9
config/routes/integration_github.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
Zammad::Application.routes.draw do
|
||||
api_path = Rails.configuration.api_path
|
||||
|
||||
match api_path + '/integration/github', to: 'integration/github#query', via: :post
|
||||
match api_path + '/integration/github', to: 'integration/github#query', via: :get
|
||||
match api_path + '/integration/github/verify', to: 'integration/github#verify', via: :post
|
||||
match api_path + '/integration/github_ticket_update', to: 'integration/github#update', via: :post
|
||||
|
||||
end
|
51
db/migrate/20210308000001_github_support.rb
Normal file
51
db/migrate/20210308000001_github_support.rb
Normal file
|
@ -0,0 +1,51 @@
|
|||
class GitHubSupport < ActiveRecord::Migration[4.2]
|
||||
def up
|
||||
|
||||
# return if it's a new setup
|
||||
return if !Setting.exists?(name: 'system_init_done')
|
||||
|
||||
Setting.create_if_not_exists(
|
||||
title: 'GitHub integration',
|
||||
name: 'github_integration',
|
||||
area: 'Integration::Switch',
|
||||
description: 'Defines if the GitHub (http://www.github.com) integration is enabled or not.',
|
||||
options: {
|
||||
form: [
|
||||
{
|
||||
display: '',
|
||||
null: true,
|
||||
name: 'github_integration',
|
||||
tag: 'boolean',
|
||||
options: {
|
||||
true => 'yes',
|
||||
false => 'no',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
state: false,
|
||||
preferences: {
|
||||
prio: 1,
|
||||
authentication: true,
|
||||
permission: ['admin.integration'],
|
||||
},
|
||||
frontend: true
|
||||
)
|
||||
Setting.create_if_not_exists(
|
||||
title: 'GitHub config',
|
||||
name: 'github_config',
|
||||
area: 'Integration::GitHub',
|
||||
description: 'Stores the GitHub configuration.',
|
||||
options: {},
|
||||
state: {
|
||||
endpoint: 'https://api.github.com/graphql',
|
||||
},
|
||||
preferences: {
|
||||
prio: 2,
|
||||
permission: ['admin.integration'],
|
||||
},
|
||||
frontend: false,
|
||||
)
|
||||
end
|
||||
|
||||
end
|
|
@ -4088,6 +4088,48 @@ Setting.create_if_not_exists(
|
|||
},
|
||||
frontend: false,
|
||||
)
|
||||
Setting.create_if_not_exists(
|
||||
title: 'GitHub integration',
|
||||
name: 'github_integration',
|
||||
area: 'Integration::Switch',
|
||||
description: 'Defines if the GitHub (http://www.github.com) integration is enabled or not.',
|
||||
options: {
|
||||
form: [
|
||||
{
|
||||
display: '',
|
||||
null: true,
|
||||
name: 'github_integration',
|
||||
tag: 'boolean',
|
||||
options: {
|
||||
true => 'yes',
|
||||
false => 'no',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
state: false,
|
||||
preferences: {
|
||||
prio: 1,
|
||||
authentication: true,
|
||||
permission: ['admin.integration'],
|
||||
},
|
||||
frontend: true
|
||||
)
|
||||
Setting.create_if_not_exists(
|
||||
title: 'GitHub config',
|
||||
name: 'github_config',
|
||||
area: 'Integration::GitHub',
|
||||
description: 'Stores the GitHub configuration.',
|
||||
options: {},
|
||||
state: {
|
||||
endpoint: 'https://api.github.com/graphql',
|
||||
},
|
||||
preferences: {
|
||||
prio: 2,
|
||||
permission: ['admin.integration'],
|
||||
},
|
||||
frontend: false,
|
||||
)
|
||||
Setting.create_if_not_exists(
|
||||
title: 'Defines sync transaction backend.',
|
||||
name: '0100_trigger',
|
||||
|
|
27
lib/github.rb
Normal file
27
lib/github.rb
Normal file
|
@ -0,0 +1,27 @@
|
|||
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
||||
|
||||
class GitHub
|
||||
extend Forwardable
|
||||
|
||||
attr_reader :client
|
||||
|
||||
def_delegator :client, :schema
|
||||
|
||||
def initialize(*args, **kargs)
|
||||
@client = GitHub::Client.new(*args, **kargs)
|
||||
end
|
||||
|
||||
def issues_by_urls(urls)
|
||||
urls.uniq.each_with_object([]) do |url, result|
|
||||
issue = issue_by_url(url)
|
||||
next if issue.blank?
|
||||
|
||||
result << issue
|
||||
end
|
||||
end
|
||||
|
||||
def issue_by_url(url)
|
||||
issue = GitHub::LinkedIssue.new(client)
|
||||
issue.find_by(url)&.to_h
|
||||
end
|
||||
end
|
38
lib/github/client.rb
Normal file
38
lib/github/client.rb
Normal file
|
@ -0,0 +1,38 @@
|
|||
# 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
|
23
lib/github/http_client.rb
Normal file
23
lib/github/http_client.rb
Normal file
|
@ -0,0 +1,23 @@
|
|||
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
||||
require 'graphql/client'
|
||||
require 'graphql/client/http'
|
||||
|
||||
class GitHub
|
||||
class HttpClient < ::GraphQL::Client::HTTP
|
||||
|
||||
def initialize(endpoint, api_token)
|
||||
raise 'api_token required' if api_token.blank?
|
||||
raise 'endpoint required' if endpoint.blank?
|
||||
|
||||
@api_token = api_token
|
||||
|
||||
super(endpoint)
|
||||
end
|
||||
|
||||
def headers(_context)
|
||||
{
|
||||
Authorization: "bearer #{@api_token}"
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
120
lib/github/linked_issue.rb
Normal file
120
lib/github/linked_issue.rb
Normal file
|
@ -0,0 +1,120 @@
|
|||
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
||||
class GitHub
|
||||
class LinkedIssue
|
||||
|
||||
STATES_MAPPING = {
|
||||
'OPEN' => 'open',
|
||||
'CLOSED' => 'closed'
|
||||
}.freeze
|
||||
|
||||
QUERY = <<-'GRAPHQL'.freeze
|
||||
query($repositor_owner: String!, $repository_name: String!, $issue_id: Int!) {
|
||||
repository(owner: $repositor_owner, name: $repository_name) {
|
||||
issue(number: $issue_id) {
|
||||
number
|
||||
title
|
||||
state
|
||||
milestone {
|
||||
title
|
||||
}
|
||||
assignees(last: 100) {
|
||||
edges {
|
||||
node {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
labels(last: 100) {
|
||||
edges {
|
||||
node {
|
||||
name
|
||||
color
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
GRAPHQL
|
||||
|
||||
attr_reader :client
|
||||
|
||||
def initialize(client)
|
||||
@client = client
|
||||
end
|
||||
|
||||
def find_by(url)
|
||||
@result = query_by_url(url)
|
||||
return if @result.blank?
|
||||
|
||||
to_h.merge(url: url)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def to_h
|
||||
{
|
||||
id: @result['number'].to_s,
|
||||
title: @result['title'],
|
||||
icon_state: STATES_MAPPING.fetch(@result['state'], @result['state']),
|
||||
milestone: milestone,
|
||||
assignees: assignees,
|
||||
labels: labels,
|
||||
}
|
||||
end
|
||||
|
||||
def assignees
|
||||
@result['assignees']['edges'].map do |assignee|
|
||||
assignee['node']['name']
|
||||
end
|
||||
end
|
||||
|
||||
def labels
|
||||
@result['labels']['edges'].map do |label|
|
||||
{
|
||||
text_color: text_color(label['node']['color']),
|
||||
color: "##{label['node']['color']}",
|
||||
title: label['node']['name']
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def text_color(background_color)
|
||||
background_color.to_i(16) > 0xFFF / 2 ? '#000000' : '#FFFFFF'
|
||||
end
|
||||
|
||||
def milestone
|
||||
@result['milestone']['title']
|
||||
end
|
||||
|
||||
def query
|
||||
@query ||= client.parse GitHub::LinkedIssue::QUERY
|
||||
end
|
||||
|
||||
def query_by_url(url)
|
||||
variables = variables(url)
|
||||
return if variables.blank?
|
||||
|
||||
response = client.query(query, variables: variables)
|
||||
|
||||
response&.data&.repository&.issue&.to_h&.deep_dup
|
||||
end
|
||||
|
||||
def variables(url)
|
||||
return if url !~ %r{^https://([^/]+)/([^/]+)/([^/]+)/issues/(\d+)$}
|
||||
|
||||
host = $1
|
||||
repositor_owner = $2
|
||||
repository_name = $3
|
||||
id = $4
|
||||
|
||||
return if client.endpoint.exclude?(host)
|
||||
|
||||
{
|
||||
repositor_owner: repositor_owner,
|
||||
repository_name: repository_name,
|
||||
issue_id: id.to_i,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -310,6 +310,11 @@
|
|||
github-button
|
||||
</title>
|
||||
<path d="M14.497 1C8.27 1 3.22 6.05 3.22 12.279c0 4.983 3.231 9.21 7.713 10.701.564.104.77-.244.77-.543 0-.268-.01-.977-.015-1.918-3.137.68-3.8-1.513-3.8-1.513-.512-1.303-1.252-1.65-1.252-1.65-1.024-.699.078-.685.078-.685 1.132.08 1.727 1.163 1.727 1.163 1.006 1.723 2.64 1.225 3.283.936.102-.728.394-1.225.716-1.507-2.505-.284-5.138-1.252-5.138-5.574 0-1.231.44-2.239 1.161-3.027-.116-.285-.503-1.432.111-2.984 0 0 .947-.304 3.101 1.156.9-.25 1.865-.375 2.824-.38.958.005 1.922.13 2.823.38 2.153-1.46 3.099-1.156 3.099-1.156.615 1.552.228 2.7.112 2.984.723.788 1.16 1.796 1.16 3.027 0 4.333-2.638 5.286-5.15 5.565.405.348.765 1.037.765 2.088 0 1.508-.013 2.725-.013 3.095 0 .301.203.652.775.542 4.478-1.495 7.707-5.719 7.707-10.7C25.777 6.049 20.727 1 14.497 1" fill="#FFF" fill-rule="evenodd"/>
|
||||
</symbol><symbol id="icon-github-logo" viewBox="0 0 24 24">
|
||||
<title>
|
||||
github-logo
|
||||
</title>
|
||||
<path d="M11.999 0C5.373 0 0 5.373 0 12c0 5.302 3.438 9.8 8.207 11.386.6.11.819-.26.819-.578 0-.285-.01-1.04-.017-2.04-3.337.724-4.042-1.61-4.042-1.61-.546-1.386-1.332-1.755-1.332-1.755-1.09-.744.082-.73.082-.73 1.205.086 1.838 1.238 1.838 1.238 1.07 1.833 2.81 1.304 3.493.996.109-.775.419-1.304.762-1.603C7.145 17 4.343 15.97 4.343 11.373c0-1.31.468-2.382 1.236-3.22-.124-.304-.536-1.524.118-3.176 0 0 1.007-.323 3.3 1.23.956-.266 1.983-.4 3.003-.404 1.02.005 2.046.138 3.005.404 2.29-1.553 3.296-1.23 3.296-1.23.655 1.652.243 2.872.12 3.176.77.838 1.233 1.91 1.233 3.22 0 4.61-2.806 5.624-5.478 5.921.43.37.814 1.103.814 2.222 0 1.604-.015 2.899-.015 3.292 0 .321.217.695.825.578C20.565 21.796 24 17.3 24 12c0-6.627-5.373-12-12.001-12" fill-rule="evenodd"/>
|
||||
</symbol><symbol id="icon-gitlab-button" viewBox="0 0 29 24">
|
||||
<title>
|
||||
gitlab-button
|
||||
|
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 89 KiB |
7
public/assets/images/icons/github-logo.svg
Normal file
7
public/assets/images/icons/github-logo.svg
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>github-logo</title>
|
||||
<g id="github-logo" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M11.998895,3.78002575e-15 C5.37327726,3.78002575e-15 1.79551223e-14,5.37254059 1.79551223e-14,12.0003683 C1.79551223e-14,17.3021885 3.43804291,21.7995641 8.2065134,23.3863532 C8.80690015,23.4968538 9.0256914,23.1263084 9.0256914,22.8080665 C9.0256914,22.5229749 9.015378,21.768624 9.00948464,20.7674883 C5.67162896,21.4923724 4.96737162,19.1585991 4.96737162,19.1585991 C4.42149851,17.7721845 3.63473403,17.4031124 3.63473403,17.4031124 C2.54519783,16.6590749 3.71724117,16.6738083 3.71724117,16.6738083 C4.92169803,16.7585254 5.55523497,17.9106787 5.55523497,17.9106787 C6.62561773,19.7442524 8.36416096,19.2145861 9.04779152,18.9073943 C9.15681881,18.1324166 9.46695724,17.6034869 9.80950919,17.3036619 C7.14497069,17.0008901 4.3434114,15.9710243 4.3434114,11.3727248 C4.3434114,10.062924 4.8111974,8.99106787 5.57880843,8.15273643 C5.45504773,7.84922803 5.04324872,6.62856441 5.69667577,4.97694834 C5.69667577,4.97694834 6.70370484,4.6542865 8.99622456,6.20718868 C9.95316001,5.94051383 10.9800792,5.80791307 12.0003683,5.80275638 C13.0199208,5.80791307 14.0461033,5.94051383 15.0045121,6.20718868 C17.2955585,4.6542865 18.3011142,4.97694834 18.3011142,4.97694834 C18.9560146,6.62856441 18.5442156,7.84922803 18.4211916,8.15273643 C19.1902759,8.99106787 19.6543786,10.062924 19.6543786,11.3727248 C19.6543786,15.982811 16.8483993,16.9972068 14.1757574,17.2940851 C14.6059732,17.6646306 14.9897787,18.3968814 14.9897787,19.5158845 C14.9897787,21.1203536 14.9750453,22.4146843 14.9750453,22.8080665 C14.9750453,23.129255 15.1916265,23.5027472 15.8001166,23.3856165 C20.5649038,21.7951441 24,17.3007152 24,12.0003683 C24,5.37254059 18.6267227,3.78002575e-15 11.998895,3.78002575e-15" fill="#50E3C2"></path>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2 KiB |
90
spec/integration/github_spec.rb
Normal file
90
spec/integration/github_spec.rb
Normal file
|
@ -0,0 +1,90 @@
|
|||
require 'rails_helper'
|
||||
RSpec.describe GitHub, type: :integration do # rubocop:disable RSpec/FilePath
|
||||
|
||||
before(:all) do # rubocop:disable RSpec/BeforeAfterAll
|
||||
required_envs = %w[GITHUB_ENDPOINT GITHUB_APITOKEN]
|
||||
required_envs.each do |key|
|
||||
skip("NOTICE: Missing environment variable #{key} for test! (Please fill up: #{required_envs.join(' && ')})") if ENV[key].blank?
|
||||
end
|
||||
|
||||
# request schema only once for performance reasons
|
||||
@cached_schema = described_class.new(ENV['GITHUB_ENDPOINT'], ENV['GITHUB_APITOKEN']).schema
|
||||
end
|
||||
|
||||
let(:instance) { described_class.new(ENV['GITHUB_ENDPOINT'], ENV['GITHUB_APITOKEN'], schema: schema) }
|
||||
let(:schema) { @cached_schema } # rubocop:disable RSpec/InstanceVariable
|
||||
let(:issue_data) do
|
||||
{
|
||||
id: '1575',
|
||||
title: 'GitHub integration',
|
||||
url: ENV['GITHUB_ISSUE_LINK'],
|
||||
icon_state: 'open',
|
||||
milestone: '4.0',
|
||||
assignees: ['Thorsten'],
|
||||
labels: [
|
||||
{
|
||||
color: '#fef2c0',
|
||||
text_color: '#000000',
|
||||
title: 'feature backlog'
|
||||
},
|
||||
{
|
||||
color: '#bfdadc',
|
||||
text_color: '#000000',
|
||||
title: 'integration'
|
||||
}
|
||||
],
|
||||
}
|
||||
end
|
||||
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
|
||||
let(:result) { instance.issues_by_urls([ issue_url ]) }
|
||||
|
||||
context 'when issue exists' do
|
||||
let(:issue_url) { ENV['GITHUB_ISSUE_LINK'] }
|
||||
|
||||
it 'returns a result list' do
|
||||
expect(result.size).to eq(1)
|
||||
end
|
||||
|
||||
it 'returns issue data in the result list' do
|
||||
expect(result[0]).to eq(issue_data)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when issue does not exists' do
|
||||
let(:issue_url) { invalid_issue_url }
|
||||
|
||||
it 'returns no result' do
|
||||
expect(result.size).to eq(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#issue_by_url' do
|
||||
|
||||
let(:result) { instance.issue_by_url(issue_url) }
|
||||
|
||||
context 'when issue exists' do
|
||||
let(:issue_url) { ENV['GITHUB_ISSUE_LINK'] }
|
||||
|
||||
it 'returns issue data' do
|
||||
expect(result).to eq(issue_data)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when issue does not exists' do
|
||||
let(:issue_url) { invalid_issue_url }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(result).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
110
spec/requests/integration/github_spec.rb
Normal file
110
spec/requests/integration/github_spec.rb
Normal file
|
@ -0,0 +1,110 @@
|
|||
require 'rails_helper'
|
||||
|
||||
# rubocop:disable RSpec/StubbedMock,RSpec/MessageSpies
|
||||
|
||||
RSpec.describe 'GitHub', type: :request do
|
||||
|
||||
let(:token) { 't0k3N' }
|
||||
let(:endpoint) { 'https://api.github.com/graphql' }
|
||||
|
||||
let!(:admin) do
|
||||
create(:admin, groups: Group.all)
|
||||
end
|
||||
|
||||
let!(:agent) do
|
||||
create(:agent, groups: Group.all)
|
||||
end
|
||||
|
||||
let(:issue_data) do
|
||||
{
|
||||
id: '1575',
|
||||
title: 'GitHub integration',
|
||||
url: ENV['GITHUB_ISSUE_LINK'],
|
||||
icon_state: 'open',
|
||||
milestone: '4.0',
|
||||
assignees: ['Thorsten'],
|
||||
labels: [
|
||||
{
|
||||
color: '#fef2c0',
|
||||
text_color: '#000000',
|
||||
title: 'feature backlog'
|
||||
},
|
||||
{
|
||||
color: '#bfdadc',
|
||||
text_color: '#000000',
|
||||
title: 'integration'
|
||||
}
|
||||
],
|
||||
}
|
||||
end
|
||||
|
||||
let(:dummy_schema) do
|
||||
{
|
||||
a: :b
|
||||
}
|
||||
end
|
||||
|
||||
describe 'request handling' do
|
||||
it 'does verify integration' do
|
||||
params = {
|
||||
endpoint: endpoint,
|
||||
api_token: token,
|
||||
}
|
||||
authenticated_as(agent)
|
||||
post '/api/v1/integration/github/verify', params: params, as: :json
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
expect(json_response).to be_a_kind_of(Hash)
|
||||
expect(json_response).not_to be_blank
|
||||
expect(json_response['error']).to eq('Not authorized (user)!')
|
||||
|
||||
authenticated_as(admin)
|
||||
instance = instance_double('GitHub')
|
||||
expect(GitHub).to receive(:new).with(endpoint, token).and_return instance
|
||||
expect(instance).to receive(:schema).and_return(dummy_schema)
|
||||
|
||||
post '/api/v1/integration/github/verify', params: params, as: :json
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json_response).to be_a_kind_of(Hash)
|
||||
expect(json_response).not_to be_blank
|
||||
expect(json_response['result']).to eq('ok')
|
||||
expect(json_response['response']).to eq(dummy_schema.to_json)
|
||||
end
|
||||
|
||||
it 'does query objects' do
|
||||
params = {
|
||||
links: [ ENV['GITHUB_ISSUE_LINK'] ],
|
||||
}
|
||||
authenticated_as(agent)
|
||||
instance = instance_double('GitHub')
|
||||
expect(GitHub).to receive(:new).and_return instance
|
||||
expect(instance).to receive(:issues_by_urls).and_return([issue_data])
|
||||
|
||||
post '/api/v1/integration/github', params: params, as: :json
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
||||
expect(json_response).to be_a_kind_of(Hash)
|
||||
expect(json_response).not_to be_blank
|
||||
expect(json_response['result']).to eq('ok')
|
||||
expect(json_response['response']).to eq([issue_data.deep_stringify_keys])
|
||||
end
|
||||
|
||||
it 'does save ticket issues' do
|
||||
ticket = create(:ticket, group: Group.first)
|
||||
|
||||
params = {
|
||||
ticket_id: ticket.id,
|
||||
issue_links: [ ENV['GITHUB_ISSUE_LINK'] ],
|
||||
}
|
||||
authenticated_as(agent)
|
||||
post '/api/v1/integration/github_ticket_update', params: params, as: :json
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json_response).to be_a_kind_of(Hash)
|
||||
expect(json_response).not_to be_blank
|
||||
expect(json_response['result']).to eq('ok')
|
||||
|
||||
expect(ticket.reload.preferences[:github][:issue_links]).to eq(params[:issue_links])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# rubocop:enable RSpec/StubbedMock,RSpec/MessageSpies
|
|
@ -1,4 +1,4 @@
|
|||
VCR_IGNORE_MATCHING_HOSTS = %w[elasticsearch selenium zammad.org zammad.com znuny.com google.com login.microsoftonline.com].freeze
|
||||
VCR_IGNORE_MATCHING_HOSTS = %w[elasticsearch selenium zammad.org zammad.com znuny.com google.com login.microsoftonline.com github.com].freeze
|
||||
VCR_IGNORE_MATCHING_REGEXPS = [/^192\.168\.\d+\.\d+$/].freeze
|
||||
|
||||
VCR.configure do |config|
|
||||
|
|
|
@ -399,4 +399,62 @@ RSpec.describe 'Ticket Create', type: :system do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GitHub Integration', :integration, authenticated_as: :authenticate do
|
||||
let(:customer) { create(:customer) }
|
||||
let(:agent) { create(:agent, groups: [Group.find_by(name: 'Users')]) }
|
||||
let!(:template) { create(:template, :dummy_data, group: Group.find_by(name: 'Users'), owner: agent, customer: customer) }
|
||||
|
||||
before(:all) do # rubocop:disable RSpec/BeforeAfterAll
|
||||
required_envs = %w[GITHUB_ENDPOINT GITHUB_APITOKEN]
|
||||
required_envs.each do |key|
|
||||
skip("NOTICE: Missing environment variable #{key} for test! (Please fill up: #{required_envs.join(' && ')})") if ENV[key].blank?
|
||||
end
|
||||
|
||||
# request schema only once for performance reasons
|
||||
@cached_schema = GitHub.new(ENV['GITHUB_ENDPOINT'], ENV['GITHUB_APITOKEN']).schema.to_json
|
||||
end
|
||||
|
||||
def authenticate
|
||||
Setting.set('github_integration', true)
|
||||
Setting.set('github_config', {
|
||||
api_token: ENV['GITHUB_APITOKEN'],
|
||||
endpoint: ENV['GITHUB_ENDPOINT'],
|
||||
schema: @cached_schema, # rubocop:disable RSpec/InstanceVariable
|
||||
})
|
||||
true
|
||||
end
|
||||
|
||||
it 'creates a ticket with links' do
|
||||
visit 'ticket/create'
|
||||
within(:active_content) do
|
||||
use_template(template)
|
||||
|
||||
# switch to github sidebar
|
||||
click('.tabsSidebar-tab[data-tab=github]')
|
||||
click('.sidebar-header-headline.js-headline')
|
||||
|
||||
# add issue
|
||||
click_on 'Link issue'
|
||||
fill_in 'link', with: ENV['GITHUB_ISSUE_LINK']
|
||||
click_on 'Submit'
|
||||
await_empty_ajax_queue
|
||||
|
||||
# verify issue
|
||||
content = find('.sidebar-git-issue-content')
|
||||
expect(content).to have_text('#1575 GitHub integration')
|
||||
expect(content).to have_text('feature backlog')
|
||||
expect(content).to have_text('integration')
|
||||
expect(content).to have_text('4.0')
|
||||
expect(content).to have_text('Thorsten')
|
||||
|
||||
# create Ticket
|
||||
click '.js-submit'
|
||||
await_empty_ajax_queue
|
||||
|
||||
# check stored data
|
||||
expect(Ticket.last.preferences[:github][:issue_links][0]).to eq(ENV['GITHUB_ISSUE_LINK'])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1538,4 +1538,68 @@ RSpec.describe 'Ticket zoom', type: :system do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GitHub Integration', :integration, authenticated_as: :authenticate do
|
||||
let!(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
|
||||
|
||||
before(:all) do # rubocop:disable RSpec/BeforeAfterAll
|
||||
required_envs = %w[GITHUB_ENDPOINT GITHUB_APITOKEN]
|
||||
required_envs.each do |key|
|
||||
skip("NOTICE: Missing environment variable #{key} for test! (Please fill up: #{required_envs.join(' && ')})") if ENV[key].blank?
|
||||
end
|
||||
|
||||
# request schema only once for performance reasons
|
||||
@cached_schema = GitHub.new(ENV['GITHUB_ENDPOINT'], ENV['GITHUB_APITOKEN']).schema.to_json
|
||||
end
|
||||
|
||||
def authenticate
|
||||
Setting.set('github_integration', true)
|
||||
Setting.set('github_config', {
|
||||
api_token: ENV['GITHUB_APITOKEN'],
|
||||
endpoint: ENV['GITHUB_ENDPOINT'],
|
||||
schema: @cached_schema, # rubocop:disable RSpec/InstanceVariable
|
||||
})
|
||||
true
|
||||
end
|
||||
|
||||
it 'creates links and removes them' do
|
||||
visit "#ticket/zoom/#{ticket.id}"
|
||||
within(:active_content) do
|
||||
|
||||
# switch to GitHub sidebar
|
||||
click('.tabsSidebar-tab[data-tab=github]')
|
||||
click('.sidebar-header-headline.js-headline')
|
||||
|
||||
# add issue
|
||||
click_on 'Link issue'
|
||||
fill_in 'link', with: ENV['GITHUB_ISSUE_LINK']
|
||||
click_on 'Submit'
|
||||
await_empty_ajax_queue
|
||||
|
||||
# verify issue
|
||||
content = find('.sidebar-git-issue-content')
|
||||
expect(content).to have_text('#1575 GitHub integration')
|
||||
expect(content).to have_text('feature backlog')
|
||||
expect(content).to have_text('integration')
|
||||
expect(content).to have_text('4.0')
|
||||
expect(content).to have_text('Thorsten')
|
||||
|
||||
expect(ticket.reload.preferences[:github][:issue_links][0]).to eq(ENV['GITHUB_ISSUE_LINK'])
|
||||
|
||||
# check sidebar counter increased to 1
|
||||
expect(find('.tabsSidebar-tab[data-tab=github] .js-tabCounter')).to have_text('1')
|
||||
|
||||
# delete issue
|
||||
click(".sidebar-git-issue-delete span[data-issue-id='#{ENV['GITHUB_ISSUE_LINK']}']")
|
||||
await_empty_ajax_queue
|
||||
|
||||
content = find('.sidebar[data-tab=github] .sidebar-content')
|
||||
expect(content).to have_text('No linked issues')
|
||||
expect(ticket.reload.preferences[:github][:issue_links][0]).to be nil
|
||||
|
||||
# check that counter got removed
|
||||
expect(page).to have_no_selector('.tabsSidebar-tab[data-tab=github] .js-tabCounter')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue