Fixes #1575 - GitHub integration.

This commit is contained in:
Thorsten Eckel 2021-03-10 16:25:26 +00:00
parent 97c9d541e9
commit 12a5e9dba3
23 changed files with 827 additions and 6 deletions

View file

@ -274,6 +274,7 @@ RSpec/ExampleLength:
- 'spec/requests/integration/check_mk_spec.rb' - 'spec/requests/integration/check_mk_spec.rb'
- 'spec/requests/integration/cti_spec.rb' - 'spec/requests/integration/cti_spec.rb'
- 'spec/requests/integration/idoit_spec.rb' - 'spec/requests/integration/idoit_spec.rb'
- 'spec/requests/integration/github_spec.rb'
- 'spec/requests/integration/gitlab_spec.rb' - 'spec/requests/integration/gitlab_spec.rb'
- 'spec/requests/integration/monitoring_spec.rb' - 'spec/requests/integration/monitoring_spec.rb'
- 'spec/requests/integration/object_manager_attributes_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/check_mk_spec.rb'
- 'spec/requests/integration/cti_spec.rb' - 'spec/requests/integration/cti_spec.rb'
- 'spec/requests/integration/idoit_spec.rb' - 'spec/requests/integration/idoit_spec.rb'
- 'spec/requests/integration/github_spec.rb'
- 'spec/requests/integration/gitlab_spec.rb' - 'spec/requests/integration/gitlab_spec.rb'
- 'spec/requests/integration/monitoring_spec.rb' - 'spec/requests/integration/monitoring_spec.rb'
- 'spec/requests/integration/object_manager_attributes_spec.rb' - 'spec/requests/integration/object_manager_attributes_spec.rb'

View file

@ -224,6 +224,11 @@
"url": "", "url": "",
"license": "" "license": ""
}, },
"github-logo.svg": {
"author": "Github",
"url": "",
"license": ""
},
"gitlab-button.svg": { "gitlab-button.svg": {
"author": "GitLab", "author": "GitLab",
"url": "", "url": "",
@ -579,6 +584,11 @@
"url": "", "url": "",
"license": "MIT" "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": { "status-modified-outer-circle.svg": {
"author": "Zammad", "author": "Zammad",
"url": "", "url": "",
@ -688,10 +698,5 @@
"author": "Felix Niklas", "author": "Felix Niklas",
"url": "", "url": "",
"license": "MIT" "license": "MIT"
},
"sso-button.svg": {
"author": "Tanu Doank",
"url": "https://thenounproject.com/term/key/1247931/",
"license": "CC 3.0 Attribution"
} }
} }

View file

@ -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'
)

View file

@ -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')

View 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>

View file

@ -43,6 +43,7 @@
.icon-forward { width: 16px; height: 17px; } .icon-forward { width: 16px; height: 17px; }
.icon-full-logo { width: 175px; height: 50px; } .icon-full-logo { width: 175px; height: 50px; }
.icon-github-button { width: 29px; height: 24px; } .icon-github-button { width: 29px; height: 24px; }
.icon-github-logo { width: 24px; height: 24px; }
.icon-gitlab-button { width: 29px; height: 24px; } .icon-gitlab-button { width: 29px; height: 24px; }
.icon-gitlab-logo { width: 24px; height: 24px; } .icon-gitlab-logo { width: 24px; height: 24px; }
.icon-google-button { width: 29px; height: 24px; } .icon-google-button { width: 29px; height: 24px; }

View 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

View file

@ -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

View file

@ -22,4 +22,5 @@ ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.singular(/(knowledge_base)s$/i, '\1') inflect.singular(/(knowledge_base)s$/i, '\1')
inflect.acronym 'SMIME' inflect.acronym 'SMIME'
inflect.acronym 'GitLab' inflect.acronym 'GitLab'
inflect.acronym 'GitHub'
end end

View 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

View 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

View file

@ -4088,6 +4088,48 @@ Setting.create_if_not_exists(
}, },
frontend: false, 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( Setting.create_if_not_exists(
title: 'Defines sync transaction backend.', title: 'Defines sync transaction backend.',
name: '0100_trigger', name: '0100_trigger',

27
lib/github.rb Normal file
View 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
View 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
View 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
View 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

View file

@ -310,6 +310,11 @@
github-button github-button
</title> </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"/> <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"> </symbol><symbol id="icon-gitlab-button" viewBox="0 0 29 24">
<title> <title>
gitlab-button gitlab-button

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 89 KiB

View 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

View 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

View 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

View file

@ -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_IGNORE_MATCHING_REGEXPS = [/^192\.168\.\d+\.\d+$/].freeze
VCR.configure do |config| VCR.configure do |config|

View file

@ -399,4 +399,62 @@ RSpec.describe 'Ticket Create', type: :system do
end end
end 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 end

View file

@ -1538,4 +1538,68 @@ RSpec.describe 'Ticket zoom', type: :system do
end end
end 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 end