From 12a5e9dba33a43c84f574f68bd57c25871f812a7 Mon Sep 17 00:00:00 2001 From: Thorsten Eckel Date: Wed, 10 Mar 2021 16:25:26 +0000 Subject: [PATCH] Fixes #1575 - GitHub integration. --- .rubocop/todo.rspec.yml | 2 + LICENSE-ICONS-3RD-PARTY.json | 15 ++- .../controllers/_integration/github.coffee | 82 ++++++++++++ .../ticket_zoom/sidebar_github.coffee | 6 + .../app/views/integration/github.jst.eco | 22 ++++ app/assets/stylesheets/svg-dimensions.css | 1 + .../integration/github_controller.rb | 53 ++++++++ .../integration/github_controller_policy.rb | 5 + config/initializers/inflections.rb | 1 + config/routes/integration_github.rb | 9 ++ db/migrate/20210308000001_github_support.rb | 51 ++++++++ db/seeds/settings.rb | 42 ++++++ lib/github.rb | 27 ++++ lib/github/client.rb | 38 ++++++ lib/github/http_client.rb | 23 ++++ lib/github/linked_issue.rb | 120 ++++++++++++++++++ public/assets/images/icons.svg | 5 + public/assets/images/icons/github-logo.svg | 7 + spec/integration/github_spec.rb | 90 +++++++++++++ spec/requests/integration/github_spec.rb | 110 ++++++++++++++++ spec/support/vcr.rb | 2 +- spec/system/ticket/create_spec.rb | 58 +++++++++ spec/system/ticket/zoom_spec.rb | 64 ++++++++++ 23 files changed, 827 insertions(+), 6 deletions(-) create mode 100644 app/assets/javascripts/app/controllers/_integration/github.coffee create mode 100644 app/assets/javascripts/app/controllers/ticket_zoom/sidebar_github.coffee create mode 100644 app/assets/javascripts/app/views/integration/github.jst.eco create mode 100644 app/controllers/integration/github_controller.rb create mode 100644 app/policies/controllers/integration/github_controller_policy.rb create mode 100644 config/routes/integration_github.rb create mode 100644 db/migrate/20210308000001_github_support.rb create mode 100644 lib/github.rb create mode 100644 lib/github/client.rb create mode 100644 lib/github/http_client.rb create mode 100644 lib/github/linked_issue.rb create mode 100644 public/assets/images/icons/github-logo.svg create mode 100644 spec/integration/github_spec.rb create mode 100644 spec/requests/integration/github_spec.rb diff --git a/.rubocop/todo.rspec.yml b/.rubocop/todo.rspec.yml index f8448ee64..0e105c1fc 100644 --- a/.rubocop/todo.rspec.yml +++ b/.rubocop/todo.rspec.yml @@ -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' diff --git a/LICENSE-ICONS-3RD-PARTY.json b/LICENSE-ICONS-3RD-PARTY.json index 56779c6db..bfe559355 100644 --- a/LICENSE-ICONS-3RD-PARTY.json +++ b/LICENSE-ICONS-3RD-PARTY.json @@ -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" } } diff --git a/app/assets/javascripts/app/controllers/_integration/github.coffee b/app/assets/javascripts/app/controllers/_integration/github.coffee new file mode 100644 index 000000000..e3ace7e5a --- /dev/null +++ b/app/assets/javascripts/app/controllers/_integration/github.coffee @@ -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' +) diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_github.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_github.coffee new file mode 100644 index 000000000..41d4a70c8 --- /dev/null +++ b/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_github.coffee @@ -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') diff --git a/app/assets/javascripts/app/views/integration/github.jst.eco b/app/assets/javascripts/app/views/integration/github.jst.eco new file mode 100644 index 000000000..2239c3654 --- /dev/null +++ b/app/assets/javascripts/app/views/integration/github.jst.eco @@ -0,0 +1,22 @@ +
+

<%- @T('Settings') %>

+
+ + + + + + + + +
<%- @T('Name') %> + <%- @T('Value') %> +
<%- @T('Endpoint') %> * + +
<%- @T('API token') %> * + +
+
+ + +
diff --git a/app/assets/stylesheets/svg-dimensions.css b/app/assets/stylesheets/svg-dimensions.css index 0bebf8c11..a2b6a1dc2 100644 --- a/app/assets/stylesheets/svg-dimensions.css +++ b/app/assets/stylesheets/svg-dimensions.css @@ -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; } diff --git a/app/controllers/integration/github_controller.rb b/app/controllers/integration/github_controller.rb new file mode 100644 index 000000000..2cab8e130 --- /dev/null +++ b/app/controllers/integration/github_controller.rb @@ -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 diff --git a/app/policies/controllers/integration/github_controller_policy.rb b/app/policies/controllers/integration/github_controller_policy.rb new file mode 100644 index 000000000..d0cebd74d --- /dev/null +++ b/app/policies/controllers/integration/github_controller_policy.rb @@ -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 diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index 6987cc812..ab5925cb1 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -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 diff --git a/config/routes/integration_github.rb b/config/routes/integration_github.rb new file mode 100644 index 000000000..0775f869a --- /dev/null +++ b/config/routes/integration_github.rb @@ -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 diff --git a/db/migrate/20210308000001_github_support.rb b/db/migrate/20210308000001_github_support.rb new file mode 100644 index 000000000..ead086fb4 --- /dev/null +++ b/db/migrate/20210308000001_github_support.rb @@ -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 diff --git a/db/seeds/settings.rb b/db/seeds/settings.rb index cd1670a1c..32de72086 100644 --- a/db/seeds/settings.rb +++ b/db/seeds/settings.rb @@ -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', diff --git a/lib/github.rb b/lib/github.rb new file mode 100644 index 000000000..de35f63c6 --- /dev/null +++ b/lib/github.rb @@ -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 diff --git a/lib/github/client.rb b/lib/github/client.rb new file mode 100644 index 000000000..adbcfde6d --- /dev/null +++ b/lib/github/client.rb @@ -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 diff --git a/lib/github/http_client.rb b/lib/github/http_client.rb new file mode 100644 index 000000000..e00c3da54 --- /dev/null +++ b/lib/github/http_client.rb @@ -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 diff --git a/lib/github/linked_issue.rb b/lib/github/linked_issue.rb new file mode 100644 index 000000000..1dd647c89 --- /dev/null +++ b/lib/github/linked_issue.rb @@ -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 diff --git a/public/assets/images/icons.svg b/public/assets/images/icons.svg index 0dd6507ae..922167867 100644 --- a/public/assets/images/icons.svg +++ b/public/assets/images/icons.svg @@ -310,6 +310,11 @@ github-button + gitlab-button diff --git a/public/assets/images/icons/github-logo.svg b/public/assets/images/icons/github-logo.svg new file mode 100644 index 000000000..b42368601 --- /dev/null +++ b/public/assets/images/icons/github-logo.svg @@ -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 + + \ No newline at end of file diff --git a/spec/integration/github_spec.rb b/spec/integration/github_spec.rb new file mode 100644 index 000000000..008bde099 --- /dev/null +++ b/spec/integration/github_spec.rb @@ -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 diff --git a/spec/requests/integration/github_spec.rb b/spec/requests/integration/github_spec.rb new file mode 100644 index 000000000..9bb20f5b4 --- /dev/null +++ b/spec/requests/integration/github_spec.rb @@ -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 diff --git a/spec/support/vcr.rb b/spec/support/vcr.rb index 88b75201f..bc7d4e158 100644 --- a/spec/support/vcr.rb +++ b/spec/support/vcr.rb @@ -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| diff --git a/spec/system/ticket/create_spec.rb b/spec/system/ticket/create_spec.rb index 31a7787d9..2f6ef8dc3 100644 --- a/spec/system/ticket/create_spec.rb +++ b/spec/system/ticket/create_spec.rb @@ -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 diff --git a/spec/system/ticket/zoom_spec.rb b/spec/system/ticket/zoom_spec.rb index 4c801477a..62f9874a7 100644 --- a/spec/system/ticket/zoom_spec.rb +++ b/spec/system/ticket/zoom_spec.rb @@ -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