Fixes #435 - GitLab integration.
This commit is contained in:
parent
20d3c5027f
commit
97c9d541e9
42 changed files with 1116 additions and 20 deletions
|
@ -70,7 +70,7 @@ include:
|
|||
RAILS_ENV: "test"
|
||||
script:
|
||||
- bundle exec rake zammad:ci:test:prepare
|
||||
- bundle exec rspec --fail-fast -t type:system
|
||||
- bundle exec rspec --fail-fast -t type:system -t ~integration
|
||||
|
||||
# we need at least one job to store and include this template
|
||||
# $IGNORE is not defined
|
||||
|
|
|
@ -1,12 +1,29 @@
|
|||
include:
|
||||
|
||||
# browser-integration
|
||||
- local: '/.gitlab/ci/browser-integration/capybara_chrome.yml'
|
||||
- local: '/.gitlab/ci/browser-integration/capybara_ff.yml'
|
||||
- local: '/.gitlab/ci/browser-integration/facebook_chrome.yml'
|
||||
- local: '/.gitlab/ci/browser-integration/facebook_ff.yml'
|
||||
- local: '/.gitlab/ci/browser-integration/idoit_chrome.yml'
|
||||
- local: '/.gitlab/ci/browser-integration/otrs_chrome.yml'
|
||||
- local: '/.gitlab/ci/browser-integration/zendesk_chrome.yml'
|
||||
|
||||
.template_browser-integration_capybara: &template_browser-integration_capybara
|
||||
stage: browser-integration
|
||||
dependencies:
|
||||
- browser:build
|
||||
extends:
|
||||
- .env_base
|
||||
- .variables_app_restart_cmd
|
||||
- .variables_es
|
||||
- .services_mysql_postgresql_elasticsearch_selenium_imap
|
||||
variables:
|
||||
RAILS_ENV: "test"
|
||||
script:
|
||||
- bundle exec rake zammad:ci:test:prepare
|
||||
- bundle exec rspec --fail-fast --pattern "spec/system/**/*_spec.rb" -t integration
|
||||
|
||||
.template_browser-integration: &template_browser-integration
|
||||
stage: browser-integration
|
||||
dependencies:
|
||||
|
|
5
.gitlab/ci/browser-integration/capybara_chrome.yml
Normal file
5
.gitlab/ci/browser-integration/capybara_chrome.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
integration_capybara_chrome:
|
||||
extends:
|
||||
- .template_browser-integration_capybara
|
||||
variables:
|
||||
BROWSER: "chrome"
|
5
.gitlab/ci/browser-integration/capybara_ff.yml
Normal file
5
.gitlab/ci/browser-integration/capybara_ff.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
integration_capybara_ff:
|
||||
extends:
|
||||
- .template_browser-integration_capybara
|
||||
variables:
|
||||
BROWSER: "firefox"
|
|
@ -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/gitlab_spec.rb'
|
||||
- 'spec/requests/integration/monitoring_spec.rb'
|
||||
- 'spec/requests/integration/object_manager_attributes_spec.rb'
|
||||
- 'spec/requests/integration/placetel_spec.rb'
|
||||
|
@ -554,6 +555,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/gitlab_spec.rb'
|
||||
- 'spec/requests/integration/monitoring_spec.rb'
|
||||
- 'spec/requests/integration/object_manager_attributes_spec.rb'
|
||||
- 'spec/requests/integration/placetel_spec.rb'
|
||||
|
|
1
Gemfile
1
Gemfile
|
@ -124,6 +124,7 @@ gem 'acts_as_list'
|
|||
|
||||
# integrations
|
||||
gem 'clearbit'
|
||||
gem 'graphql-client'
|
||||
gem 'net-ldap'
|
||||
gem 'slack-notifier'
|
||||
gem 'zendesk_api'
|
||||
|
|
|
@ -218,6 +218,10 @@ GEM
|
|||
activesupport (>= 4.2.0)
|
||||
gmail_xoauth (0.4.2)
|
||||
oauth (>= 0.3.6)
|
||||
graphql (1.11.6)
|
||||
graphql-client (0.16.0)
|
||||
activesupport (>= 3.0)
|
||||
graphql (~> 1.8)
|
||||
guard (2.15.0)
|
||||
formatador (>= 0.2.4)
|
||||
listen (>= 2.7, < 4.0)
|
||||
|
@ -613,6 +617,7 @@ DEPENDENCIES
|
|||
faker
|
||||
github_changelog_generator
|
||||
gmail_xoauth
|
||||
graphql-client
|
||||
guard
|
||||
guard-livereload
|
||||
guard-symlink
|
||||
|
|
|
@ -225,7 +225,7 @@
|
|||
"license": ""
|
||||
},
|
||||
"gitlab-button.svg": {
|
||||
"author": "Gitlab",
|
||||
"author": "GitLab",
|
||||
"url": "",
|
||||
"license": ""
|
||||
},
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
class GitLab extends App.ControllerIntegrationBase
|
||||
featureIntegration: 'gitlab_integration'
|
||||
featureName: 'GitLab'
|
||||
featureConfig: 'gitlab_config'
|
||||
description: [
|
||||
['This service allows you to connect %s with %s.', 'GitLab', '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('gitlab_config')
|
||||
|
||||
@html App.view('integration/gitlab')(
|
||||
config: config
|
||||
)
|
||||
|
||||
update: (e) =>
|
||||
e.preventDefault()
|
||||
config = @formParam(e.target)
|
||||
@validateAndSave(config)
|
||||
|
||||
validateAndSave: (config) =>
|
||||
App.Ajax.request(
|
||||
id: 'gitlab'
|
||||
type: 'POST'
|
||||
url: "#{@apiPath}/integration/gitlab/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('gitlab_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('gitlab_integration')
|
||||
|
||||
App.Config.set(
|
||||
'IntegrationGitLab'
|
||||
{
|
||||
name: 'GitLab'
|
||||
target: '#system/integration/gitlab'
|
||||
description: 'Link GitLab issues to your tickets.'
|
||||
controller: GitLab
|
||||
state: State
|
||||
}
|
||||
'NavBarIntegrations'
|
||||
)
|
|
@ -0,0 +1,23 @@
|
|||
class App.GitIssueLinkModal extends App.ControllerModal
|
||||
buttonClose: true
|
||||
buttonCancel: true
|
||||
buttonSubmit: true
|
||||
|
||||
constructor: (params) ->
|
||||
@placeholder = params.placeholder
|
||||
@head = params.head
|
||||
super
|
||||
|
||||
content: ->
|
||||
$(App.view('integration/git_issue_link_modal')(
|
||||
placeholder: @placeholder
|
||||
))
|
||||
|
||||
onSubmit: (e) =>
|
||||
form = @el.find('.js-result')
|
||||
params = @formParam(form)
|
||||
return if _.isEmpty(params.link)
|
||||
|
||||
@formDisable(form)
|
||||
@callback(params.link, @)
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
class App.SidebarGitIssue extends App.Controller
|
||||
provider: '_need_to_be_defined_' # GitLab
|
||||
urlPlaceholder: '_need_to_be_defined_' # https://git.example.com/group1/project1/-/issues/1
|
||||
|
||||
constructor: ->
|
||||
super
|
||||
@issueLinks = []
|
||||
@providerIdentifier = @provider.toLowerCase()
|
||||
|
||||
sidebarItem: =>
|
||||
return if !@Config.get("#{@providerIdentifier}_integration")
|
||||
@item = {
|
||||
name: @providerIdentifier
|
||||
badgeCallback: @badgeRender
|
||||
sidebarHead: @provider
|
||||
sidebarCallback: @showObjects
|
||||
sidebarActions: [
|
||||
{
|
||||
title: 'Link issue'
|
||||
name: 'link-issue'
|
||||
callback: @linkIssue
|
||||
},
|
||||
]
|
||||
}
|
||||
@item
|
||||
|
||||
shown: ->
|
||||
return if !@ticket
|
||||
return if !@ticket.id
|
||||
@showIssues()
|
||||
|
||||
metaBadge: =>
|
||||
counter = ''
|
||||
counter = @issueLinks.length
|
||||
|
||||
{
|
||||
name: 'customer'
|
||||
icon: "#{@providerIdentifier}-logo"
|
||||
counterPossible: true
|
||||
counter: counter
|
||||
}
|
||||
|
||||
badgeRender: (el) =>
|
||||
@badgeEl = el
|
||||
@badgeRenderLocal()
|
||||
|
||||
badgeRenderLocal: =>
|
||||
@badgeEl.html(App.view('generic/sidebar_tabs_item')(@metaBadge()))
|
||||
|
||||
linkIssue: =>
|
||||
new App.GitIssueLinkModal(
|
||||
head: @provider
|
||||
placeholder: @urlPlaceholder
|
||||
taskKey: @taskKey
|
||||
container: @el.closest('.content')
|
||||
callback: (link, ui) =>
|
||||
if @ticket && @ticket.id
|
||||
@saveTicketIssues = true
|
||||
ui.close()
|
||||
@showIssues([link])
|
||||
)
|
||||
|
||||
showObjects: (el) =>
|
||||
@el = el
|
||||
|
||||
# show placeholder
|
||||
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'
|
||||
if queryParams && queryParams.gitlab_issue_links
|
||||
@issueLinks.push queryParams.gitlab_issue_links
|
||||
@showIssues()
|
||||
|
||||
showIssues: (issueLinks) =>
|
||||
if issueLinks
|
||||
@issueLinks = _.uniq(@issueLinks.concat(issueLinks))
|
||||
|
||||
# show placeholder
|
||||
if _.isEmpty(@issueLinks)
|
||||
@html("<div>#{App.i18n.translateInline('No linked issues')}</div>")
|
||||
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')(
|
||||
issues: issues
|
||||
))
|
||||
list.delegate('.js-delete', 'click', (e) =>
|
||||
e.preventDefault()
|
||||
issueLink = $(e.currentTarget).attr 'data-issue-id'
|
||||
@delete(issueLink)
|
||||
)
|
||||
@html(list)
|
||||
@badgeRenderLocal()
|
||||
|
||||
showError: (message) =>
|
||||
@html App.i18n.translateInline(message)
|
||||
|
||||
reload: =>
|
||||
@showIssues()
|
||||
|
||||
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) =>
|
||||
return if !args.ticket
|
||||
return if args.ticket.created_at
|
||||
return if !@issueLinks
|
||||
return if _.isEmpty(@issueLinks)
|
||||
args.ticket.preferences ||= {}
|
||||
args.ticket.preferences[@providerIdentifier] ||= {}
|
||||
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
|
||||
)
|
||||
)
|
|
@ -0,0 +1,6 @@
|
|||
class SidebarGitLab extends App.SidebarGitIssue
|
||||
provider: 'GitLab'
|
||||
urlPlaceholder: 'https://git.example.com/group1/project1/-/issues/1'
|
||||
|
||||
App.Config.set('500-GitLab', SidebarGitLab, 'TicketCreateSidebar')
|
||||
App.Config.set('500-GitLab', SidebarGitLab, 'TicketZoomSidebar')
|
|
@ -122,6 +122,12 @@ class App.Sidebar extends App.Controller
|
|||
# remember current tab
|
||||
@currentTab = name
|
||||
|
||||
# get current sidebar controller
|
||||
for item in @items
|
||||
itemLocal = item.sidebarItem()
|
||||
if itemLocal && itemLocal.name && itemLocal.name is @currentTab && item.shown
|
||||
item.shown()
|
||||
|
||||
# show sidebar if not shown
|
||||
@showSidebar()
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<% if @counterPossible is true: %>
|
||||
<div class="tabsSidebar-tab-count js-tabCounter <% if !@counter || @counter is 0: %>hide<% end %><% if @cssClass: %><%= @cssClass %><% end %>"><%= @counter %></div>
|
||||
<% if @counterPossible is true and @counter and @counter > 0: %>
|
||||
<div class="tabsSidebar-tab-count js-tabCounter <% if @cssClass: %><%= @cssClass %><% end %>"><%= @counter %></div>
|
||||
<% end %>
|
||||
<%- @Icon(@icon) %>
|
|
@ -0,0 +1,3 @@
|
|||
<form class="flex horizontal js-result">
|
||||
<input type="text" name="link" value="" autocomplete="off" placeholder="<%= @placeholder %>" class="form-control"/>
|
||||
</form>
|
22
app/assets/javascripts/app/views/integration/gitlab.jst.eco
Normal file
22
app/assets/javascripts/app/views/integration/gitlab.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://git.example.com/api/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>
|
|
@ -8,12 +8,12 @@
|
|||
<th width="80%"><%- @T('Value') %>
|
||||
</thead>
|
||||
<tbody>
|
||||
<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">
|
||||
<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://idoit.example.com/i-doit/" 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">
|
||||
<tr>
|
||||
<td class="settings-list-row-control"><%- @T('Client ID') %>
|
||||
<td class="settings-list-control-cell"><input type="text" class="form-control form-control--small" value="<%= @config.client_id %>" name="client_id">
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
<% for issue in @issues: %>
|
||||
<div class="sidebar-git-issue-delete">
|
||||
<span class="list-item-delete js-delete" data-issue-id="<%= issue.url %>" data-type="remove">
|
||||
<%- @Icon('diagonal-cross') %>
|
||||
</span>
|
||||
</div>
|
||||
<div class="sidebar-git-issue-content">
|
||||
<div class="sidebar-block">
|
||||
<a href="<%- issue.url %>" target="_blank">
|
||||
<span class="icon-holder" title="<%= @T(issue.icon_state) %>">
|
||||
<%- @Icon('task-state', issue.icon_state) %>
|
||||
</span>
|
||||
#<%= issue.id %> <%= issue.title %>
|
||||
</a>
|
||||
</div>
|
||||
<% if issue.milestone: %>
|
||||
<div class="sidebar-block">
|
||||
<label><%- @T('Milestone') %></label>
|
||||
<%= @T(issue.milestone) %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if issue.assignees.length > 0: %>
|
||||
<div class="sidebar-block">
|
||||
<label><%- @T('Assignee') %></label>
|
||||
<%= issue.assignees.join(', ') %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if issue.labels.length > 0: %>
|
||||
<div class="sidebar-block">
|
||||
<label><%- @T('Labels') %></label>
|
||||
<% for label in issue.labels: %><span class="badge" style="color: <%- label.text_color %>; background-color: <%- label.color %>"><%= label.title %></span> <% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<hr class="clearfix">
|
||||
<% end %>
|
|
@ -44,6 +44,7 @@
|
|||
.icon-full-logo { width: 175px; height: 50px; }
|
||||
.icon-github-button { width: 29px; height: 24px; }
|
||||
.icon-gitlab-button { width: 29px; height: 24px; }
|
||||
.icon-gitlab-logo { width: 24px; height: 24px; }
|
||||
.icon-google-button { width: 29px; height: 24px; }
|
||||
.icon-group { width: 24px; height: 24px; }
|
||||
.icon-help { width: 16px; height: 16px; }
|
||||
|
|
|
@ -5108,6 +5108,15 @@ footer {
|
|||
}
|
||||
}
|
||||
|
||||
.sidebar-git-issue-delete {
|
||||
text-align: right;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.sidebar-git-issue-content {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.main + .sidebar {
|
||||
border-right: none;
|
||||
border-left: 1px solid #e6e6e6;
|
||||
|
|
53
app/controllers/integration/gitlab_controller.rb
Normal file
53
app/controllers/integration/gitlab_controller.rb
Normal file
|
@ -0,0 +1,53 @@
|
|||
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
||||
|
||||
class Integration::GitLabController < ApplicationController
|
||||
prepend_before_action { authentication_check && authorize! }
|
||||
|
||||
def verify
|
||||
gitlab = ::GitLab.new(params[:endpoint], params[:api_token])
|
||||
render json: {
|
||||
result: 'ok',
|
||||
response: gitlab.schema.to_json,
|
||||
}
|
||||
rescue => e
|
||||
logger.error e
|
||||
|
||||
render json: {
|
||||
result: 'failed',
|
||||
message: e.message,
|
||||
}
|
||||
end
|
||||
|
||||
def query
|
||||
config = Setting.get('gitlab_config')
|
||||
|
||||
gitlab = ::GitLab.new(config['endpoint'], config['api_token'], schema: config['schema'])
|
||||
|
||||
render json: {
|
||||
result: 'ok',
|
||||
response: gitlab.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[:gitlab] ||= {}
|
||||
ticket.preferences[:gitlab][:issue_links] = Array(params[:issue_links]).uniq
|
||||
ticket.save!
|
||||
end
|
||||
|
||||
render json: {
|
||||
result: 'ok',
|
||||
}
|
||||
end
|
||||
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
class Controllers::Integration::GitLabControllerPolicy < Controllers::ApplicationControllerPolicy
|
||||
permit! %i[query update], to: 'ticket.agent'
|
||||
permit! :verify, to: 'admin.integration.gitlab'
|
||||
default_permit!(['agent.integration.gitlab', 'admin.integration.gitlab'])
|
||||
end
|
|
@ -21,4 +21,5 @@ ActiveSupport::Inflector.inflections(:en) do |inflect|
|
|||
# see: KnowledgeBase.table_name.singularize
|
||||
inflect.singular(/(knowledge_base)s$/i, '\1')
|
||||
inflect.acronym 'SMIME'
|
||||
inflect.acronym 'GitLab'
|
||||
end
|
||||
|
|
|
@ -32,7 +32,7 @@ Rails.application.config.middleware.use OmniAuth::Builder do
|
|||
provider :github_database, 'not_change_will_be_set_by_database', 'not_change_will_be_set_by_database'
|
||||
|
||||
# gitlab database connect
|
||||
provider :gitlab_database, 'not_change_will_be_set_by_database', 'not_change_will_be_set_by_database', {
|
||||
provider :git_lab_database, 'not_change_will_be_set_by_database', 'not_change_will_be_set_by_database', {
|
||||
client_options: {
|
||||
site: 'https://not_change_will_be_set_by_database',
|
||||
authorize_url: '/oauth/authorize',
|
||||
|
|
9
config/routes/integration_gitlab.rb
Normal file
9
config/routes/integration_gitlab.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
Zammad::Application.routes.draw do
|
||||
api_path = Rails.configuration.api_path
|
||||
|
||||
match api_path + '/integration/gitlab', to: 'integration/gitlab#query', via: :post
|
||||
match api_path + '/integration/gitlab', to: 'integration/gitlab#query', via: :get
|
||||
match api_path + '/integration/gitlab/verify', to: 'integration/gitlab#verify', via: :post
|
||||
match api_path + '/integration/gitlab_ticket_update', to: 'integration/gitlab#update', via: :post
|
||||
|
||||
end
|
Binary file not shown.
|
@ -1,4 +1,4 @@
|
|||
class Issue2595GitlabPlaceholder < ActiveRecord::Migration[5.2]
|
||||
class Issue2595GitLabPlaceholder < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
# return if it's a new setup
|
||||
return if !Setting.exists?(name: 'system_init_done')
|
||||
|
|
51
db/migrate/20210113000001_gitlab_support.rb
Normal file
51
db/migrate/20210113000001_gitlab_support.rb
Normal file
|
@ -0,0 +1,51 @@
|
|||
class GitLabSupport < 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: 'GitLab integration',
|
||||
name: 'gitlab_integration',
|
||||
area: 'Integration::Switch',
|
||||
description: 'Defines if the GitLab (http://www.gitlab.com) integration is enabled or not.',
|
||||
options: {
|
||||
form: [
|
||||
{
|
||||
display: '',
|
||||
null: true,
|
||||
name: 'gitlab_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: 'GitLab config',
|
||||
name: 'gitlab_config',
|
||||
area: 'Integration::GitLab',
|
||||
description: 'Stores the GitLab configuration.',
|
||||
options: {},
|
||||
state: {
|
||||
endpoint: 'https://gitlab.com/api/graphql',
|
||||
},
|
||||
preferences: {
|
||||
prio: 2,
|
||||
permission: ['admin.integration'],
|
||||
},
|
||||
frontend: false,
|
||||
)
|
||||
end
|
||||
|
||||
end
|
|
@ -1427,18 +1427,18 @@ Setting.create_if_not_exists(
|
|||
preferences: {
|
||||
controller: 'SettingsAreaSwitch',
|
||||
sub: ['auth_gitlab_credentials'],
|
||||
title_i18n: ['Gitlab'],
|
||||
description_i18n: ['Gitlab', 'Gitlab Applications', 'https://your-gitlab-host/admin/applications'],
|
||||
title_i18n: ['GitLab'],
|
||||
description_i18n: ['GitLab', 'GitLab Applications', 'https://your-gitlab-host/admin/applications'],
|
||||
permission: ['admin.security'],
|
||||
},
|
||||
state: false,
|
||||
frontend: true
|
||||
)
|
||||
Setting.create_if_not_exists(
|
||||
title: 'Gitlab App Credentials',
|
||||
title: 'GitLab App Credentials',
|
||||
name: 'auth_gitlab_credentials',
|
||||
area: 'Security::ThirdPartyAuthentication::Gitlab',
|
||||
description: 'Enables user authentication via Gitlab.',
|
||||
area: 'Security::ThirdPartyAuthentication::GitLab',
|
||||
description: 'Enables user authentication via GitLab.',
|
||||
options: {
|
||||
form: [
|
||||
{
|
||||
|
@ -4046,6 +4046,48 @@ Setting.create_if_not_exists(
|
|||
},
|
||||
frontend: false,
|
||||
)
|
||||
Setting.create_if_not_exists(
|
||||
title: 'GitLab integration',
|
||||
name: 'gitlab_integration',
|
||||
area: 'Integration::Switch',
|
||||
description: 'Defines if the GitLab (http://www.gitlab.com) integration is enabled or not.',
|
||||
options: {
|
||||
form: [
|
||||
{
|
||||
display: '',
|
||||
null: true,
|
||||
name: 'gitlab_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: 'GitLab config',
|
||||
name: 'gitlab_config',
|
||||
area: 'Integration::GitLab',
|
||||
description: 'Stores the GitLab configuration.',
|
||||
options: {},
|
||||
state: {
|
||||
endpoint: 'https://gitlab.com/api/graphql',
|
||||
},
|
||||
preferences: {
|
||||
prio: 2,
|
||||
permission: ['admin.integration'],
|
||||
},
|
||||
frontend: false,
|
||||
)
|
||||
Setting.create_if_not_exists(
|
||||
title: 'Defines sync transaction backend.',
|
||||
name: '0100_trigger',
|
||||
|
|
27
lib/gitlab.rb
Normal file
27
lib/gitlab.rb
Normal file
|
@ -0,0 +1,27 @@
|
|||
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
||||
|
||||
class GitLab
|
||||
extend Forwardable
|
||||
|
||||
attr_reader :client
|
||||
|
||||
def_delegator :client, :schema
|
||||
|
||||
def initialize(*args, **kargs)
|
||||
@client = GitLab::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 = GitLab::LinkedIssue.new(client)
|
||||
issue.find_by(url)&.to_h
|
||||
end
|
||||
end
|
38
lib/gitlab/client.rb
Normal file
38
lib/gitlab/client.rb
Normal file
|
@ -0,0 +1,38 @@
|
|||
# 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
|
23
lib/gitlab/http_client.rb
Normal file
23
lib/gitlab/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 GitLab
|
||||
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)
|
||||
{
|
||||
"PRIVATE-TOKEN": @api_token
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
115
lib/gitlab/linked_issue.rb
Normal file
115
lib/gitlab/linked_issue.rb
Normal file
|
@ -0,0 +1,115 @@
|
|||
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
||||
class GitLab
|
||||
class LinkedIssue
|
||||
|
||||
STATES_MAPPING = {
|
||||
'opened' => 'open'
|
||||
}.freeze
|
||||
|
||||
QUERY = <<-'GRAPHQL'.freeze
|
||||
query($fullpath: ID!, $issue_id: String) {
|
||||
project(fullPath: $fullpath) {
|
||||
issue(iid: $issue_id) {
|
||||
iid
|
||||
title
|
||||
state
|
||||
milestone {
|
||||
title
|
||||
}
|
||||
assignees {
|
||||
edges {
|
||||
node {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
labels {
|
||||
edges {
|
||||
node {
|
||||
title
|
||||
color
|
||||
textColor
|
||||
description
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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['iid'],
|
||||
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: label['node']['textColor'],
|
||||
color: label['node']['color'],
|
||||
title: label['node']['title']
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def milestone
|
||||
@result['milestone']['title']
|
||||
end
|
||||
|
||||
def query
|
||||
@query ||= client.parse GitLab::LinkedIssue::QUERY
|
||||
end
|
||||
|
||||
def query_by_url(url)
|
||||
variables = variables(url)
|
||||
return if variables.blank?
|
||||
|
||||
response = client.query(query, variables: variables)
|
||||
|
||||
response&.data&.project&.issue&.to_h&.deep_dup
|
||||
end
|
||||
|
||||
def variables(url)
|
||||
return if url !~ %r{^https://([^/]+)/(.*)/-/issues/(\d+)$}
|
||||
|
||||
host = $1
|
||||
fullpath = $2
|
||||
id = $3
|
||||
|
||||
return if client.endpoint.exclude?(host)
|
||||
|
||||
{
|
||||
fullpath: fullpath,
|
||||
issue_id: id
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,4 +1,4 @@
|
|||
class GitlabDatabase < OmniAuth::Strategies::GitLab
|
||||
class GitLabDatabase < OmniAuth::Strategies::GitLab
|
||||
option :name, 'gitlab'
|
||||
|
||||
def initialize(app, *args, &block)
|
||||
|
|
|
@ -324,6 +324,11 @@
|
|||
<path d="M24.906 9.481l1.333 4.101a.908.908 0 0 1-.33 1.015l-11.533 8.38L24.906 9.48z" fill="#FF998A"/>
|
||||
<path d="M25.066 10.022H17.99l3.41-8.668a.454.454 0 0 1 .864 0l2.8 8.668z" fill="#FFF"/>
|
||||
</g>
|
||||
</symbol><symbol id="icon-gitlab-logo" viewBox="0 0 24 24">
|
||||
<title>
|
||||
gitlab-logo
|
||||
</title>
|
||||
<path d="M23.955 13.64l-1.343-4.133-2.661-8.19a.457.457 0 0 0-.87 0l-2.876 8.726H7.599l-2.68-8.727a.457.457 0 0 0-.87 0L1.388 9.507.045 13.64a.915.915 0 0 0 .332 1.023L12 23.108l11.623-8.445a.915.915 0 0 0 .332-1.023" fill-rule="evenodd"/>
|
||||
</symbol><symbol id="icon-google-button" viewBox="0 0 29 24">
|
||||
<title>
|
||||
google-button
|
||||
|
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
7
public/assets/images/icons/gitlab-logo.svg
Normal file
7
public/assets/images/icons/gitlab-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>gitlab-logo</title>
|
||||
<g id="gitlab-logo" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M23.9551616,10.4673469 L22.6122253,14.6003733 L19.9507177,22.7917715 C19.8138186,23.2131893 19.2175573,23.2131893 19.0805932,22.7917715 L16.2048292,14.0650133 L7.59876258,14.0650133 L4.91936909,22.7917715 C4.78247003,23.2131893 4.18620865,23.2131893 4.04924461,22.7917715 L1.38773702,14.6003733 L0.0448656821,10.4673469 C-0.0776742518,10.0903709 0.0565609036,9.67739968 0.377204898,9.44440484 L11.9999811,1 L23.6228224,9.44440484 C23.9434664,9.67739968 24.0776366,10.0903709 23.9551616,10.4673469" fill="#50E3C2" transform="translate(12.000000, 12.053917) scale(1, -1) translate(-12.000000, -12.053917) "></path>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 948 B |
95
spec/integration/gitlab_spec.rb
Normal file
95
spec/integration/gitlab_spec.rb
Normal file
|
@ -0,0 +1,95 @@
|
|||
require 'rails_helper'
|
||||
RSpec.describe GitLab, type: :integration do # rubocop:disable RSpec/FilePath
|
||||
|
||||
before(:all) do # rubocop:disable RSpec/BeforeAfterAll
|
||||
required_envs = %w[GITLAB_ENDPOINT GITLAB_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['GITLAB_ENDPOINT'], ENV['GITLAB_APITOKEN']).schema
|
||||
end
|
||||
|
||||
let(:instance) { described_class.new(ENV['GITLAB_ENDPOINT'], ENV['GITLAB_APITOKEN'], schema: schema) }
|
||||
let(:schema) { @cached_schema } # rubocop:disable RSpec/InstanceVariable
|
||||
let(:issue_data) do
|
||||
{
|
||||
id: '1',
|
||||
title: 'Example issue',
|
||||
url: ENV['GITLAB_ISSUE_LINK'],
|
||||
icon_state: 'open',
|
||||
milestone: 'important milestone',
|
||||
assignees: ['zammad-robot'],
|
||||
labels: [
|
||||
{
|
||||
color: '#FF0000',
|
||||
text_color: '#FFFFFF',
|
||||
title: 'critical'
|
||||
},
|
||||
{
|
||||
color: '#0033CC',
|
||||
text_color: '#FFFFFF',
|
||||
title: 'label1'
|
||||
},
|
||||
{
|
||||
color: '#D1D100',
|
||||
text_color: '#FFFFFF',
|
||||
title: 'special'
|
||||
}
|
||||
],
|
||||
}
|
||||
end
|
||||
let(:invalid_issue_url) { 'https://git.example.com/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
|
||||
let(:result) { instance.issues_by_urls([ issue_url ]) }
|
||||
|
||||
context 'when issue exists' do
|
||||
let(:issue_url) { ENV['GITLAB_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['GITLAB_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
|
|
@ -1,5 +1,3 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ActiveRecord::Calculations do # rubocop:disable RSpec/FilePath
|
||||
|
|
115
spec/requests/integration/gitlab_spec.rb
Normal file
115
spec/requests/integration/gitlab_spec.rb
Normal file
|
@ -0,0 +1,115 @@
|
|||
require 'rails_helper'
|
||||
|
||||
# rubocop:disable RSpec/StubbedMock,RSpec/MessageSpies
|
||||
|
||||
RSpec.describe 'GitLab', type: :request do
|
||||
|
||||
let(:token) { 't0k3N' }
|
||||
let(:endpoint) { 'https://git.example.com/api/graphql' }
|
||||
|
||||
let!(:admin) do
|
||||
create(:admin, groups: Group.all)
|
||||
end
|
||||
|
||||
let!(:agent) do
|
||||
create(:agent, groups: Group.all)
|
||||
end
|
||||
|
||||
let(:issue_data) do
|
||||
{
|
||||
id: '1',
|
||||
title: 'Example issue',
|
||||
url: ENV['GITLAB_ISSUE_LINK'],
|
||||
icon_state: 'open',
|
||||
milestone: 'important milestone',
|
||||
assignees: ['zammad-robot'],
|
||||
labels: [
|
||||
{
|
||||
color: '#FF0000',
|
||||
text_color: '#FFFFFF',
|
||||
title: 'critical'
|
||||
},
|
||||
{
|
||||
color: '#0033CC',
|
||||
text_color: '#FFFFFF',
|
||||
title: 'label1'
|
||||
},
|
||||
{
|
||||
color: '#D1D100',
|
||||
text_color: '#FFFFFF',
|
||||
title: 'special'
|
||||
}
|
||||
],
|
||||
}
|
||||
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/gitlab/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('GitLab')
|
||||
expect(GitLab).to receive(:new).with(endpoint, token).and_return instance
|
||||
expect(instance).to receive(:schema).and_return(dummy_schema)
|
||||
|
||||
post '/api/v1/integration/gitlab/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['GITLAB_ISSUE_LINK'] ],
|
||||
}
|
||||
authenticated_as(agent)
|
||||
instance = instance_double('GitLab')
|
||||
expect(GitLab).to receive(:new).and_return instance
|
||||
expect(instance).to receive(:issues_by_urls).and_return([issue_data])
|
||||
|
||||
post '/api/v1/integration/gitlab', 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['GITLAB_ISSUE_LINK'] ],
|
||||
}
|
||||
authenticated_as(agent)
|
||||
post '/api/v1/integration/gitlab_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[:gitlab][: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[zammad.com google.com elasticsearch selenium login.microsoftonline.com zammad.org].freeze
|
||||
VCR_IGNORE_MATCHING_HOSTS = %w[elasticsearch selenium zammad.org zammad.com znuny.com google.com login.microsoftonline.com].freeze
|
||||
VCR_IGNORE_MATCHING_REGEXPS = [/^192\.168\.\d+\.\d+$/].freeze
|
||||
|
||||
VCR.configure do |config|
|
||||
|
|
|
@ -341,4 +341,62 @@ RSpec.describe 'Ticket Create', type: :system do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GitLab 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[GITLAB_ENDPOINT GITLAB_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 = GitLab.new(ENV['GITLAB_ENDPOINT'], ENV['GITLAB_APITOKEN']).schema.to_json
|
||||
end
|
||||
|
||||
def authenticate
|
||||
Setting.set('gitlab_integration', true)
|
||||
Setting.set('gitlab_config', {
|
||||
api_token: ENV['GITLAB_APITOKEN'],
|
||||
endpoint: ENV['GITLAB_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 gitlab sidebar
|
||||
click('.tabsSidebar-tab[data-tab=gitlab]')
|
||||
click('.sidebar-header-headline.js-headline')
|
||||
|
||||
# add issue
|
||||
click_on 'Link issue'
|
||||
fill_in 'link', with: ENV['GITLAB_ISSUE_LINK']
|
||||
click_on 'Submit'
|
||||
await_empty_ajax_queue
|
||||
|
||||
# verify issue
|
||||
content = find('.sidebar-git-issue-content')
|
||||
expect(content).to have_text('#1 Example issue')
|
||||
expect(content).to have_text('critical')
|
||||
expect(content).to have_text('special')
|
||||
expect(content).to have_text('important milestone')
|
||||
expect(content).to have_text('zammad-robot')
|
||||
|
||||
# create Ticket
|
||||
click '.js-submit'
|
||||
await_empty_ajax_queue
|
||||
|
||||
# check stored data
|
||||
expect(Ticket.last.preferences[:gitlab][:issue_links][0]).to eq(ENV['GITLAB_ISSUE_LINK'])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1474,4 +1474,68 @@ RSpec.describe 'Ticket zoom', type: :system do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GitLab 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[GITLAB_ENDPOINT GITLAB_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 = GitLab.new(ENV['GITLAB_ENDPOINT'], ENV['GITLAB_APITOKEN']).schema.to_json
|
||||
end
|
||||
|
||||
def authenticate
|
||||
Setting.set('gitlab_integration', true)
|
||||
Setting.set('gitlab_config', {
|
||||
api_token: ENV['GITLAB_APITOKEN'],
|
||||
endpoint: ENV['GITLAB_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 GitLab sidebar
|
||||
click('.tabsSidebar-tab[data-tab=gitlab]')
|
||||
click('.sidebar-header-headline.js-headline')
|
||||
|
||||
# add issue
|
||||
click_on 'Link issue'
|
||||
fill_in 'link', with: ENV['GITLAB_ISSUE_LINK']
|
||||
click_on 'Submit'
|
||||
await_empty_ajax_queue
|
||||
|
||||
# verify issue
|
||||
content = find('.sidebar-git-issue-content')
|
||||
expect(content).to have_text('#1 Example issue')
|
||||
expect(content).to have_text('critical')
|
||||
expect(content).to have_text('special')
|
||||
expect(content).to have_text('important milestone')
|
||||
expect(content).to have_text('zammad-robot')
|
||||
|
||||
expect(ticket.reload.preferences[:gitlab][:issue_links][0]).to eq(ENV['GITLAB_ISSUE_LINK'])
|
||||
|
||||
# check sidebar counter increased to 1
|
||||
expect(find('.tabsSidebar-tab[data-tab=gitlab] .js-tabCounter')).to have_text('1')
|
||||
|
||||
# delete issue
|
||||
click(".sidebar-git-issue-delete span[data-issue-id='#{ENV['GITLAB_ISSUE_LINK']}']")
|
||||
await_empty_ajax_queue
|
||||
|
||||
content = find('.sidebar[data-tab=gitlab] .sidebar-content')
|
||||
expect(content).to have_text('No linked issues')
|
||||
expect(ticket.reload.preferences[:gitlab][:issue_links][0]).to be nil
|
||||
|
||||
# check that counter got removed
|
||||
expect(page).to have_no_selector('.tabsSidebar-tab[data-tab=gitlab] .js-tabCounter')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue