<%- @T('Client ID') %>
@@ -22,4 +22,4 @@
<%- @T('Save') %>
-
\ No newline at end of file
+
diff --git a/app/assets/javascripts/app/views/ticket_zoom/sidebar_git_issue.jst.eco b/app/assets/javascripts/app/views/ticket_zoom/sidebar_git_issue.jst.eco
new file mode 100644
index 000000000..a13e624be
--- /dev/null
+++ b/app/assets/javascripts/app/views/ticket_zoom/sidebar_git_issue.jst.eco
@@ -0,0 +1,36 @@
+<% for issue in @issues: %>
+
+
+
+<% end %>
diff --git a/app/assets/stylesheets/svg-dimensions.css b/app/assets/stylesheets/svg-dimensions.css
index 10d16a36e..0bebf8c11 100644
--- a/app/assets/stylesheets/svg-dimensions.css
+++ b/app/assets/stylesheets/svg-dimensions.css
@@ -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; }
diff --git a/app/assets/stylesheets/zammad.scss b/app/assets/stylesheets/zammad.scss
index 088eb7855..fdb56649b 100644
--- a/app/assets/stylesheets/zammad.scss
+++ b/app/assets/stylesheets/zammad.scss
@@ -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;
diff --git a/app/controllers/integration/gitlab_controller.rb b/app/controllers/integration/gitlab_controller.rb
new file mode 100644
index 000000000..abc2e8e61
--- /dev/null
+++ b/app/controllers/integration/gitlab_controller.rb
@@ -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
diff --git a/app/policies/controllers/integration/gitlab_controller_policy.rb b/app/policies/controllers/integration/gitlab_controller_policy.rb
new file mode 100644
index 000000000..13b9cca5b
--- /dev/null
+++ b/app/policies/controllers/integration/gitlab_controller_policy.rb
@@ -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
diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb
index 87061c42a..6987cc812 100644
--- a/config/initializers/inflections.rb
+++ b/config/initializers/inflections.rb
@@ -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
diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb
index ed8e270f5..b8903d95f 100644
--- a/config/initializers/omniauth.rb
+++ b/config/initializers/omniauth.rb
@@ -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',
diff --git a/config/routes/integration_gitlab.rb b/config/routes/integration_gitlab.rb
new file mode 100644
index 000000000..7aaea77d0
--- /dev/null
+++ b/config/routes/integration_gitlab.rb
@@ -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
diff --git a/contrib/icon-sprite.sketch b/contrib/icon-sprite.sketch
index 6f00dab67..2c4bb6f60 100644
Binary files a/contrib/icon-sprite.sketch and b/contrib/icon-sprite.sketch differ
diff --git a/db/migrate/20190903165443_issue_2595_gitlab_placeholder.rb b/db/migrate/20190903165443_issue_2595_gitlab_placeholder.rb
index fbba634a2..0e2e858fd 100644
--- a/db/migrate/20190903165443_issue_2595_gitlab_placeholder.rb
+++ b/db/migrate/20190903165443_issue_2595_gitlab_placeholder.rb
@@ -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')
diff --git a/db/migrate/20210113000001_gitlab_support.rb b/db/migrate/20210113000001_gitlab_support.rb
new file mode 100644
index 000000000..e1b82cb0c
--- /dev/null
+++ b/db/migrate/20210113000001_gitlab_support.rb
@@ -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
diff --git a/db/seeds/settings.rb b/db/seeds/settings.rb
index 0156000be..cd1670a1c 100644
--- a/db/seeds/settings.rb
+++ b/db/seeds/settings.rb
@@ -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',
diff --git a/lib/gitlab.rb b/lib/gitlab.rb
new file mode 100644
index 000000000..81d96aa05
--- /dev/null
+++ b/lib/gitlab.rb
@@ -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
diff --git a/lib/gitlab/client.rb b/lib/gitlab/client.rb
new file mode 100644
index 000000000..7fc371361
--- /dev/null
+++ b/lib/gitlab/client.rb
@@ -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
diff --git a/lib/gitlab/http_client.rb b/lib/gitlab/http_client.rb
new file mode 100644
index 000000000..8a6fc6825
--- /dev/null
+++ b/lib/gitlab/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 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
diff --git a/lib/gitlab/linked_issue.rb b/lib/gitlab/linked_issue.rb
new file mode 100644
index 000000000..27a618a25
--- /dev/null
+++ b/lib/gitlab/linked_issue.rb
@@ -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
diff --git a/lib/omniauth/gitlab_database.rb b/lib/omniauth/gitlab_database.rb
index 799f06e90..f9c04d6f7 100644
--- a/lib/omniauth/gitlab_database.rb
+++ b/lib/omniauth/gitlab_database.rb
@@ -1,4 +1,4 @@
-class GitlabDatabase < OmniAuth::Strategies::GitLab
+class GitLabDatabase < OmniAuth::Strategies::GitLab
option :name, 'gitlab'
def initialize(app, *args, &block)
diff --git a/public/assets/images/icons.svg b/public/assets/images/icons.svg
index 1f6d347f3..0dd6507ae 100644
--- a/public/assets/images/icons.svg
+++ b/public/assets/images/icons.svg
@@ -324,6 +324,11 @@
+
+
+ gitlab-logo
+
+
google-button
diff --git a/public/assets/images/icons/gitlab-logo.svg b/public/assets/images/icons/gitlab-logo.svg
new file mode 100644
index 000000000..d2afbf2e2
--- /dev/null
+++ b/public/assets/images/icons/gitlab-logo.svg
@@ -0,0 +1,7 @@
+
+
+ gitlab-logo
+
+
+
+
\ No newline at end of file
diff --git a/spec/integration/gitlab_spec.rb b/spec/integration/gitlab_spec.rb
new file mode 100644
index 000000000..6c598cb78
--- /dev/null
+++ b/spec/integration/gitlab_spec.rb
@@ -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
diff --git a/spec/lib/core_ext/active_record/calculations/pluck_as_hash_spec.rb b/spec/lib/core_ext/active_record/calculations/pluck_as_hash_spec.rb
index d775e68ae..0bb26e3d0 100644
--- a/spec/lib/core_ext/active_record/calculations/pluck_as_hash_spec.rb
+++ b/spec/lib/core_ext/active_record/calculations/pluck_as_hash_spec.rb
@@ -1,5 +1,3 @@
-# frozen_string_literal: true
-
require 'rails_helper'
RSpec.describe ActiveRecord::Calculations do # rubocop:disable RSpec/FilePath
diff --git a/spec/requests/integration/gitlab_spec.rb b/spec/requests/integration/gitlab_spec.rb
new file mode 100644
index 000000000..914e179a6
--- /dev/null
+++ b/spec/requests/integration/gitlab_spec.rb
@@ -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
diff --git a/spec/support/vcr.rb b/spec/support/vcr.rb
index f71d62542..88b75201f 100644
--- a/spec/support/vcr.rb
+++ b/spec/support/vcr.rb
@@ -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|
diff --git a/spec/system/ticket/create_spec.rb b/spec/system/ticket/create_spec.rb
index b6bb0857a..31a7787d9 100644
--- a/spec/system/ticket/create_spec.rb
+++ b/spec/system/ticket/create_spec.rb
@@ -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
diff --git a/spec/system/ticket/zoom_spec.rb b/spec/system/ticket/zoom_spec.rb
index ca23f3ec7..4c801477a 100644
--- a/spec/system/ticket/zoom_spec.rb
+++ b/spec/system/ticket/zoom_spec.rb
@@ -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