From 02417225f7d3060cddda062cc1bd51b32e75deaf Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Tue, 23 Feb 2021 15:52:16 +0100 Subject: [PATCH] Fixes #3372 - Reasoning about Webhooks activity. --- .../generic_index.coffee | 14 +++ .../_ui_element/ticket_perform_action.coffee | 104 ++++++++++-------- .../app/controllers/webhook.coffee | 41 +++++++ .../controllers/widget/payload_example.coffee | 37 +++++++ .../javascripts/app/models/webhook.coffee | 29 +++++ .../app/views/generic/admin/index.jst.eco | 4 +- .../ticket_perform_action/webhook.jst.eco | 22 +--- .../app/views/widget/payload_example.jst.eco | 10 ++ app/controllers/webhooks_controller.rb | 37 +++++++ app/jobs/trigger_webhook_job.rb | 48 ++++++-- .../concerns/checks_perform_validation.rb | 2 +- app/models/trigger/assets.rb | 6 + app/models/webhook.rb | 23 ++++ .../controllers/webhooks_controller_policy.rb | 3 + config/routes/webhook.rb | 12 ++ db/migrate/20120101000010_create_ticket.rb | 14 +++ ...18095820_issue_3372_webhooks_admin_view.rb | 58 ++++++++++ db/seeds/permissions.rb | 7 ++ .../issue_3372_webhooks_admin_view_spec.rb | 44 ++++++++ spec/factories/webhook.rb | 9 ++ spec/jobs/trigger_webhook_job_spec.rb | 7 +- spec/models/ticket_spec.rb | 6 +- spec/models/user_spec.rb | 1 + spec/models/webhook_spec.rb | 41 +++++++ 24 files changed, 495 insertions(+), 84 deletions(-) create mode 100644 app/assets/javascripts/app/controllers/webhook.coffee create mode 100644 app/assets/javascripts/app/controllers/widget/payload_example.coffee create mode 100644 app/assets/javascripts/app/models/webhook.coffee create mode 100644 app/assets/javascripts/app/views/widget/payload_example.jst.eco create mode 100644 app/controllers/webhooks_controller.rb create mode 100644 app/models/webhook.rb create mode 100644 app/policies/controllers/webhooks_controller_policy.rb create mode 100644 config/routes/webhook.rb create mode 100644 db/migrate/20210118095820_issue_3372_webhooks_admin_view.rb create mode 100644 spec/db/migrate/issue_3372_webhooks_admin_view_spec.rb create mode 100644 spec/factories/webhook.rb create mode 100644 spec/models/webhook_spec.rb diff --git a/app/assets/javascripts/app/controllers/_application_controller/generic_index.coffee b/app/assets/javascripts/app/controllers/_application_controller/generic_index.coffee index bf1fb8d89..9daed4a70 100644 --- a/app/assets/javascripts/app/controllers/_application_controller/generic_index.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller/generic_index.coffee @@ -2,6 +2,7 @@ class App.ControllerGenericIndex extends App.Controller events: 'click [data-type=edit]': 'edit' 'click [data-type=new]': 'new' + 'click [data-type=payload]': 'payload' 'click [data-type=import]': 'import' 'click .js-description': 'description' @@ -152,6 +153,12 @@ class App.ControllerGenericIndex extends App.Controller else @table.update(objects: objects, pagerSelected: @pageData.pagerSelected, pagerTotalCount: @pageData.pagerTotalCount) + if @pageData.logFacility + new App.HttpLog( + el: @$('.page-footer') + facility: @pageData.logFacility + ) + edit: (id, e) => e.preventDefault() item = App[ @genericObject ].find(id) @@ -181,6 +188,13 @@ class App.ControllerGenericIndex extends App.Controller veryLarge: @veryLarge ) + payload: (e) -> + e.preventDefault() + new App.WidgetPayloadExample( + baseUrl: @payloadExampleUrl + container: @el.closest('.content') + ) + import: (e) -> e.preventDefault() @importCallback() diff --git a/app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee b/app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee index 42b80571b..325da089b 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee @@ -406,55 +406,73 @@ class App.UiElement.ticket_perform_action selectionRecipient = columnSelectRecipient.element() - elementTemplate = 'notification' if notificationType is 'webhook' - elementTemplate = 'webhook' + notificationElement = $( App.view('generic/ticket_perform_action/webhook')( + attribute: attribute + name: name + notificationType: notificationType + meta: meta || {} + )) - notificationElement = $( App.view("generic/ticket_perform_action/#{elementTemplate}")( - attribute: attribute - name: name - notificationType: notificationType - meta: meta || {} - )) + notificationElement.find('.js-recipient select').replaceWith(selectionRecipient) - notificationElement.find('.js-recipient select').replaceWith(selectionRecipient) + webhookSelection = App.UiElement.select.render( + name: "#{name}::webhook_id" + multiple: false + null: false + relation: 'Webhook' + value: meta.webhook_id + translate: false + ) - visibilitySelection = App.UiElement.select.render( - name: "#{name}::internal" - multiple: false - null: false - options: { true: 'internal', false: 'public' } - value: meta.internal || 'false' - translate: true - ) + notificationElement.find('.js-webhooks').html(webhookSelection) - notificationElement.find('.js-internal').html(visibilitySelection) + else + notificationElement = $( App.view('generic/ticket_perform_action/notification')( + attribute: attribute + name: name + notificationType: notificationType + meta: meta || {} + )) - notificationElement.find('.js-body div[contenteditable="true"]').ce( - mode: 'richtext' - placeholder: 'message' - maxlength: messageLength - ) - new App.WidgetPlaceholder( - el: notificationElement.find('.js-body div[contenteditable="true"]').parent() - objects: [ - { - prefix: 'ticket' - object: 'Ticket' - display: 'Ticket' - }, - { - prefix: 'article' - object: 'TicketArticle' - display: 'Article' - }, - { - prefix: 'user' - object: 'User' - display: 'Current User' - }, - ] - ) + notificationElement.find('.js-recipient select').replaceWith(selectionRecipient) + + visibilitySelection = App.UiElement.select.render( + name: "#{name}::internal" + multiple: false + null: false + options: { true: 'internal', false: 'public' } + value: meta.internal || 'false' + translate: true + ) + + notificationElement.find('.js-internal').html(visibilitySelection) + + notificationElement.find('.js-body div[contenteditable="true"]').ce( + mode: 'richtext' + placeholder: 'message' + maxlength: messageLength + ) + new App.WidgetPlaceholder( + el: notificationElement.find('.js-body div[contenteditable="true"]').parent() + objects: [ + { + prefix: 'ticket' + object: 'Ticket' + display: 'Ticket' + }, + { + prefix: 'article' + object: 'TicketArticle' + display: 'Article' + }, + { + prefix: 'user' + object: 'User' + display: 'Current User' + }, + ] + ) elementRow.find('.js-setNotification').html(notificationElement).removeClass('hide') diff --git a/app/assets/javascripts/app/controllers/webhook.coffee b/app/assets/javascripts/app/controllers/webhook.coffee new file mode 100644 index 000000000..96b79cb2a --- /dev/null +++ b/app/assets/javascripts/app/controllers/webhook.coffee @@ -0,0 +1,41 @@ +class Index extends App.ControllerSubContent + requiredPermission: 'admin.webhook' + header: 'Webhooks' + constructor: -> + super + + @genericController = new App.ControllerGenericIndex( + el: @el + id: @id + genericObject: 'Webhook' + defaultSortBy: 'name' + pageData: + home: 'webhooks' + object: 'Webhook' + objects: 'Webhooks' + pagerAjax: true + pagerBaseUrl: '#manage/webhook/' + pagerSelected: ( @page || 1 ) + pagerPerPage: 150 + navupdate: '#webhooks' + notes: [ + 'Webhooks are ...' + ] + buttons: [ + { name: 'Example Payload', 'data-type': 'payload', class: 'btn' } + { name: 'New Webhook', 'data-type': 'new', class: 'btn--success' } + ] + logFacility: 'webhook' + payloadExampleUrl: '/api/v1/webhooks/preview' + container: @el.closest('.content') + veryLarge: true + ) + + show: (params) => + for key, value of params + if key isnt 'el' && key isnt 'shown' && key isnt 'match' + @[key] = value + + @genericController.paginate( @page || 1 ) + +App.Config.set('Webhook', { prio: 3350, name: 'Webhook', parent: '#manage', target: '#manage/webhook', controller: Index, permission: ['admin.webhook'] }, 'NavBarAdmin') diff --git a/app/assets/javascripts/app/controllers/widget/payload_example.coffee b/app/assets/javascripts/app/controllers/widget/payload_example.coffee new file mode 100644 index 000000000..2597e7054 --- /dev/null +++ b/app/assets/javascripts/app/controllers/widget/payload_example.coffee @@ -0,0 +1,37 @@ +class App.WidgetPayloadExample extends App.ControllerModal + buttonClose: true + buttonCancel: true + buttonSubmit: false + head: 'Example Payload' + large: true + + content: => + if !@payloadExample + @load() + return + + @payloadExample + + load: => + @ajax( + id: 'example_payload' + type: 'get' + url: @baseUrl + processData: false + contentType: 'text/plain' + dataType: 'text' + cache: false + success: (data, status, xhr) => + @payloadExample = $(App.view('widget/payload_example')( + payload: data + )) + + @update() + error: (data) => + details = data.responseJSON || {} + @notify + type: 'error' + msg: App.i18n.translateContent(details.error_human || details.error || 'Unable to load example payload!') + timeout: 6000 + ) + diff --git a/app/assets/javascripts/app/models/webhook.coffee b/app/assets/javascripts/app/models/webhook.coffee new file mode 100644 index 000000000..fead97e46 --- /dev/null +++ b/app/assets/javascripts/app/models/webhook.coffee @@ -0,0 +1,29 @@ +class App.Webhook extends App.Model + @configure 'Webhook', 'name', 'endpoint', 'signature_token', 'ssl_verify', 'note', 'active' + @extend Spine.Model.Ajax + @url: @apiPath + '/webhooks' + @configure_attributes = [ + { name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, null: false }, + { name: 'endpoint', display: 'Endpoint', tag: 'input', type: 'text', limit: 300, null: false, placeholder: 'https://target.example.com/webhook' }, + { name: 'signature_token', display: 'HMAC SHA1 Signature Token', tag: 'input', type: 'text', limit: 100, null: true }, + { name: 'ssl_verify', display: 'SSL Verify', tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, default: true }, + { name: 'note', display: 'Note', tag: 'textarea', note: '', limit: 250, null: true }, + { name: 'active', display: 'Active', tag: 'active', default: true }, + { name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 }, + ] + @configure_delete = true + @configure_clone = true + @configure_overview = [ + 'name', + 'endpoint', + ] + + @description = ''' +Webhooks make it easy to send information about events within Zammad to third party systems via HTTP(S). + +You can use Webhooks in Zammad to send Ticket, Article and Attachment data whenever a Trigger is performed. Just create and configure your Webhook with an HTTP(S) endpoint and relevant security settings, configure a Trigger to perform it. +''' + + displayName: -> + return @name if !@endpoint + "#{@name} (#{@endpoint})" diff --git a/app/assets/javascripts/app/views/generic/admin/index.jst.eco b/app/assets/javascripts/app/views/generic/admin/index.jst.eco index 19012867a..ff0f5dfbb 100644 --- a/app/assets/javascripts/app/views/generic/admin/index.jst.eco +++ b/app/assets/javascripts/app/views/generic/admin/index.jst.eco @@ -16,4 +16,6 @@
-
\ No newline at end of file + + + diff --git a/app/assets/javascripts/app/views/generic/ticket_perform_action/webhook.jst.eco b/app/assets/javascripts/app/views/generic/ticket_perform_action/webhook.jst.eco index 9cb3d179f..214c867b9 100644 --- a/app/assets/javascripts/app/views/generic/ticket_perform_action/webhook.jst.eco +++ b/app/assets/javascripts/app/views/generic/ticket_perform_action/webhook.jst.eco @@ -1,24 +1,6 @@
- -
-
- -
-
-
-
- -
-
- -
-
-
-
- -
-
- checked<% end %>> +
+
diff --git a/app/assets/javascripts/app/views/widget/payload_example.jst.eco b/app/assets/javascripts/app/views/widget/payload_example.jst.eco new file mode 100644 index 000000000..862e4cca8 --- /dev/null +++ b/app/assets/javascripts/app/views/widget/payload_example.jst.eco @@ -0,0 +1,10 @@ +
+<%- @T('Header') %> +
+X-Zammad-Trigger:  Name of the Trigger
+X-Zammad-Delivery: 6d600811-06a3-40af-aebd-a2d8213e85aa
+X-Hub-Signature:   sha1=06007ef23c38e435f49091cdfa3c770b3d85d7be
+
+<%- @T('Body') %> +
<%= @payload %>
+
diff --git a/app/controllers/webhooks_controller.rb b/app/controllers/webhooks_controller.rb new file mode 100644 index 000000000..9a0ac2f5e --- /dev/null +++ b/app/controllers/webhooks_controller.rb @@ -0,0 +1,37 @@ +# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ + +class WebhooksController < ApplicationController + prepend_before_action { authentication_check && authorize! } + + def preview + access_condition = Ticket.access_condition(current_user, 'read') + + ticket = Ticket.where(access_condition).last + + render json: JSON.pretty_generate({ + ticket: TriggerWebhookJob::RecordPayload.generate(ticket), + article: TriggerWebhookJob::RecordPayload.generate(ticket.articles.last), + }), + status: :ok + end + + def index + model_index_render(Webhook, params) + end + + def show + model_show_render(Webhook, params) + end + + def create + model_create_render(Webhook, params) + end + + def update + model_update_render(Webhook, params) + end + + def destroy + model_destroy_render(Webhook, params) + end +end diff --git a/app/jobs/trigger_webhook_job.rb b/app/jobs/trigger_webhook_job.rb index 3ffe65cf6..8122589c8 100644 --- a/app/jobs/trigger_webhook_job.rb +++ b/app/jobs/trigger_webhook_job.rb @@ -27,6 +27,7 @@ class TriggerWebhookJob < ApplicationJob @ticket = ticket @article = article + return if abort? return if request.success? raise TriggerWebhookJob::RequestError @@ -34,9 +35,42 @@ class TriggerWebhookJob < ApplicationJob private + def abort? + if webhook_id.blank? + log_wrong_trigger_config + return true + elsif webhook.blank? + log_not_existing_webhook + return true + end + + false + end + + def webhook_id + @webhook_id ||= trigger.perform.dig('notification.webhook', 'webhook_id') + end + + def webhook + @webhook ||= begin + Webhook.find_by( + id: webhook_id, + active: true + ) + end + end + + def log_wrong_trigger_config + Rails.logger.error "Can't find webhook_id for Trigger '#{trigger.name}' with ID #{trigger.id}" + end + + def log_not_existing_webhook + Rails.logger.error "Can't find Webhook for ID #{webhook_id} configured in Trigger '#{trigger.name}' with ID #{trigger.id}" + end + def request UserAgent.post( - config['endpoint'], + webhook.endpoint, payload, { json: true, @@ -45,8 +79,8 @@ class TriggerWebhookJob < ApplicationJob read_timeout: 30, total_timeout: 60, headers: headers, - signature_token: config['token'], - verify_ssl: verify_ssl?, + signature_token: webhook.signature_token, + verify_ssl: webhook.ssl_verify, log: { facility: 'webhook', }, @@ -54,14 +88,6 @@ class TriggerWebhookJob < ApplicationJob ) end - def config - @config ||= trigger.perform['notification.webhook'] - end - - def verify_ssl? - config.fetch('verify_ssl', false).present? - end - def headers { 'X-Zammad-Trigger' => trigger.name, diff --git a/app/models/concerns/checks_perform_validation.rb b/app/models/concerns/checks_perform_validation.rb index 8cdb4487d..d856a0e66 100644 --- a/app/models/concerns/checks_perform_validation.rb +++ b/app/models/concerns/checks_perform_validation.rb @@ -15,7 +15,7 @@ module ChecksPerformValidation 'article.note' => %w[body subject internal], 'notification.email' => %w[body recipient subject], 'notification.sms' => %w[body recipient], - 'notification.webhook' => %w[endpoint], + 'notification.webhook' => %w[webhook_id], } check_present.each do |key, values| diff --git a/app/models/trigger/assets.rb b/app/models/trigger/assets.rb index 4b9a21fd9..1ec395cdb 100644 --- a/app/models/trigger/assets.rb +++ b/app/models/trigger/assets.rb @@ -39,6 +39,12 @@ returns data = calendar.assets(data) end + app_model_webhook = Webhook.to_app_model + data[ app_model_webhook ] ||= {} + Webhook.find_each do |webhook| + data = webhook.assets(data) + end + app_model_user = User.to_app_model data[ app_model_user ] ||= {} diff --git a/app/models/webhook.rb b/app/models/webhook.rb new file mode 100644 index 000000000..4285ec1a3 --- /dev/null +++ b/app/models/webhook.rb @@ -0,0 +1,23 @@ +# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ + +class Webhook < ApplicationModel + include ChecksClientNotification + include ChecksLatestChangeObserved + include HasCollectionUpdate + + before_create :validate_endpoint + before_update :validate_endpoint + + validates :name, presence: true + + private + + def validate_endpoint + uri = URI.parse(endpoint) + raise Exceptions::UnprocessableEntity, 'Invalid endpoint (no http/https)!' if !uri.is_a?(URI::HTTP) + raise Exceptions::UnprocessableEntity, 'Invalid endpoint (no hostname)!' if uri.host.nil? + rescue URI::InvalidURIError + raise Exceptions::UnprocessableEntity, 'Invalid endpoint!' + end + +end diff --git a/app/policies/controllers/webhooks_controller_policy.rb b/app/policies/controllers/webhooks_controller_policy.rb new file mode 100644 index 000000000..a962c198d --- /dev/null +++ b/app/policies/controllers/webhooks_controller_policy.rb @@ -0,0 +1,3 @@ +class Controllers::WebhooksControllerPolicy < Controllers::ApplicationControllerPolicy + default_permit!('admin.webhook') +end diff --git a/config/routes/webhook.rb b/config/routes/webhook.rb new file mode 100644 index 000000000..40bff17ae --- /dev/null +++ b/config/routes/webhook.rb @@ -0,0 +1,12 @@ +Zammad::Application.routes.draw do + api_path = Rails.configuration.api_path + + # webhooks + match api_path + '/webhooks/preview', to: 'webhooks#preview', via: :get + match api_path + '/webhooks', to: 'webhooks#index', via: :get + match api_path + '/webhooks/:id', to: 'webhooks#show', via: :get + match api_path + '/webhooks', to: 'webhooks#create', via: :post + match api_path + '/webhooks/:id', to: 'webhooks#update', via: :put + match api_path + '/webhooks/:id', to: 'webhooks#destroy', via: :delete + +end diff --git a/db/migrate/20120101000010_create_ticket.rb b/db/migrate/20120101000010_create_ticket.rb index a661222d9..832463fa1 100644 --- a/db/migrate/20120101000010_create_ticket.rb +++ b/db/migrate/20120101000010_create_ticket.rb @@ -586,6 +586,19 @@ class CreateTicket < ActiveRecord::Migration[4.2] add_index :karma_activity_logs, %i[o_id object_lookup_id] add_foreign_key :karma_activity_logs, :users add_foreign_key :karma_activity_logs, :karma_activities, column: :activity_id + + create_table :webhooks do |t| + t.column :name, :string, limit: 250, null: false + t.column :endpoint, :string, limit: 300, null: false + t.column :signature_token, :string, limit: 200, null: true + t.column :ssl_verify, :boolean, null: false, default: true + t.column :note, :string, limit: 500, null: true + t.column :active, :boolean, null: false, default: true + t.column :updated_by_id, :integer, null: false + t.column :created_by_id, :integer, null: false + t.timestamps limit: 3, null: false + end + end def self.down @@ -623,5 +636,6 @@ class CreateTicket < ActiveRecord::Migration[4.2] drop_table :ticket_priorities drop_table :ticket_states drop_table :ticket_state_types + drop_table :webhooks end end diff --git a/db/migrate/20210118095820_issue_3372_webhooks_admin_view.rb b/db/migrate/20210118095820_issue_3372_webhooks_admin_view.rb new file mode 100644 index 000000000..e56b99f76 --- /dev/null +++ b/db/migrate/20210118095820_issue_3372_webhooks_admin_view.rb @@ -0,0 +1,58 @@ +class Issue3372WebhooksAdminView < ActiveRecord::Migration[5.2] + + def up + return if !Setting.exists?(name: 'system_init_done') + + create_webhooks_table + + record_upgrade + + Permission.create_if_not_exists( + name: 'admin.webhook', + note: 'Manage %s', + preferences: { + translations: ['Webhooks'] + }, + ) + end + + def create_webhooks_table + create_table :webhooks do |t| + t.column :name, :string, limit: 250, null: false + t.column :endpoint, :string, limit: 300, null: false + t.column :signature_token, :string, limit: 200, null: true + t.column :ssl_verify, :boolean, null: false, default: true + t.column :note, :string, limit: 500, null: true + t.column :active, :boolean, null: false, default: true + t.column :updated_by_id, :integer, null: false + t.column :created_by_id, :integer, null: false + t.timestamps limit: 3, null: false + end + end + + def record_upgrade + Trigger.all.find_each do |trigger| + next if trigger.perform.dig('notification.webhook', 'endpoint').blank? + + webhook = webhook_create( + source: trigger.name, + config: trigger.perform['notification.webhook'], + ) + trigger.perform['notification.webhook'] = { webhook_id: webhook.id } + trigger.save! + end + end + + def webhook_create(source:, config:) + Webhook.create!( + name: "Webhook '#{source}'", + endpoint: config['endpoint'], + signature_token: config['token'], + ssl_verify: config['verify_ssl'], + active: true, + created_by_id: 1, + updated_by_id: 1, + ) + end + +end diff --git a/db/seeds/permissions.rb b/db/seeds/permissions.rb index 0d81da3bc..88d67ae2d 100644 --- a/db/seeds/permissions.rb +++ b/db/seeds/permissions.rb @@ -262,6 +262,13 @@ Permission.create_if_not_exists( translations: ['Sessions'] }, ) +Permission.create_if_not_exists( + name: 'admin.webhook', + note: 'Manage %s', + preferences: { + translations: ['Webhooks'] + }, +) Permission.create_if_not_exists( name: 'user_preferences', note: 'User Preferences', diff --git a/spec/db/migrate/issue_3372_webhooks_admin_view_spec.rb b/spec/db/migrate/issue_3372_webhooks_admin_view_spec.rb new file mode 100644 index 000000000..44ee228c7 --- /dev/null +++ b/spec/db/migrate/issue_3372_webhooks_admin_view_spec.rb @@ -0,0 +1,44 @@ +require 'rails_helper' + +RSpec.describe Issue3372WebhooksAdminView, type: :db_migration do + + let(:trigger_webhook_config) do + { + 'endpoint' => 'https://example.com/webhook', + 'token' => '53Cr3T', + 'verify_ssl' => false, + } + end + + let(:webhook_attributes) do + { + endpoint: trigger_webhook_config['endpoint'], + signature_token: trigger_webhook_config['token'], + ssl_verify: trigger_webhook_config['verify_ssl'], + } + end + + let!(:trigger) do + Trigger.without_callback(:create, :before, :validate_perform) do + create(:trigger, perform: { + 'notification.webhook' => trigger_webhook_config + }) + end + end + + it 'Creates Webhook object from mapped Trigger configuration' do + migrate do |migration| + allow(migration).to receive(:create_webhooks_table) + end + + expect(Webhook.last).to have_attributes(**webhook_attributes) + end + + it 'Migrates Trigger#perform Webhook configuration to new structure' do + migrate do |migration| + allow(migration).to receive(:create_webhooks_table) + end + + expect(trigger.reload.perform['notification.webhook']['webhook_id']).to eq(Webhook.last.id) + end +end diff --git a/spec/factories/webhook.rb b/spec/factories/webhook.rb new file mode 100644 index 000000000..4230500d5 --- /dev/null +++ b/spec/factories/webhook.rb @@ -0,0 +1,9 @@ +FactoryBot.define do + factory :webhook do + sequence(:name) { |n| "Test webhook #{n}" } + ssl_verify { true } + active { true } + created_by_id { 1 } + updated_by_id { 1 } + end +end diff --git a/spec/jobs/trigger_webhook_job_spec.rb b/spec/jobs/trigger_webhook_job_spec.rb index 4d8e78d3c..3b71085c9 100644 --- a/spec/jobs/trigger_webhook_job_spec.rb +++ b/spec/jobs/trigger_webhook_job_spec.rb @@ -62,13 +62,12 @@ RSpec.describe TriggerWebhookJob, type: :job do let!(:ticket) { create(:ticket) } let!(:article) { create(:'ticket/article') } + let(:webhook) { create(:webhook, endpoint: endpoint, signature_token: token) } + let(:trigger) do create(:trigger, perform: { - 'notification.webhook' => { - endpoint: endpoint, - token: token - } + 'notification.webhook' => { 'webhook_id' => webhook.id } }) end diff --git a/spec/models/ticket_spec.rb b/spec/models/ticket_spec.rb index c21061802..313fe7413 100644 --- a/spec/models/ticket_spec.rb +++ b/spec/models/ticket_spec.rb @@ -502,13 +502,11 @@ RSpec.describe Ticket, type: :model do end context 'with a "notification.webhook" trigger', performs_jobs: true do + let(:webhook) { create(:webhook, endpoint: 'http://api.example.com/webhook', signature_token: '53CR3t') } let(:trigger) do create(:trigger, perform: { - 'notification.webhook' => { - endpoint: 'http://api.example.com/webhook', - token: '53CR3t' - } + 'notification.webhook' => { 'webhook_id' => webhook.id } }) end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index bde1a3711..fcbf74a07 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -870,6 +870,7 @@ RSpec.describe User, type: :model do 'Channel' => { 'created_by_id' => 0, 'updated_by_id' => 0 }, 'Role' => { 'created_by_id' => 0, 'updated_by_id' => 0 }, 'History' => { 'created_by_id' => 1 }, + 'Webhook' => { 'created_by_id' => 0, 'updated_by_id' => 0 }, 'Overview' => { 'created_by_id' => 1, 'updated_by_id' => 0 }, 'ActivityStream' => { 'created_by_id' => 0 }, 'StatsStore' => { 'created_by_id' => 0 }, diff --git a/spec/models/webhook_spec.rb b/spec/models/webhook_spec.rb new file mode 100644 index 000000000..f19522ef4 --- /dev/null +++ b/spec/models/webhook_spec.rb @@ -0,0 +1,41 @@ +require 'rails_helper' + +RSpec.describe Webhook, type: :model do + + describe 'check endpoint' do + subject(:webhook) { create(:webhook, endpoint: endpoint) } + + let(:endpoint) { 'example.com' } + + context 'with missing http type' do + it 'raise an error' do + expect { webhook }.to raise_error(Exceptions::UnprocessableEntity, 'Invalid endpoint (no http/https)!') + end + end + + context 'with spaces in invalid hostname' do + let(:endpoint) { 'http:// example.com' } + + it 'raise an error' do + expect { webhook }.to raise_error(Exceptions::UnprocessableEntity, 'Invalid endpoint!') + end + end + + context 'with ? in hostname' do + let(:endpoint) { 'http://?example.com' } + + it 'raise an error' do + expect { webhook }.to raise_error(Exceptions::UnprocessableEntity, 'Invalid endpoint (no hostname)!') + end + end + + context 'with nil in endpoint' do + let(:endpoint) { nil } + + it 'raise an error' do + expect { webhook }.to raise_error(Exceptions::UnprocessableEntity, 'Invalid endpoint!') + end + end + + end +end