From 29a033680932835e43aaf5ea2e9aedcec905e006 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Tue, 29 May 2018 01:45:29 +0200 Subject: [PATCH] Implemented issue #2044 - Generic CTI integration. --- .../app/controllers/_integration/cti.coffee | 158 +++++++ .../javascripts/app/controllers/cti.coffee | 1 + .../app/views/integration/cti.jst.eco | 97 ++++ app/controllers/cti_controller.rb | 5 + app/controllers/integration/cti_controller.rb | 117 +++++ app/models/cti/log.rb | 13 +- config/routes/integration_cti.rb | 5 + db/migrate/20180420000001_setting_cti.rb | 70 +++ db/seeds/settings.rb | 66 ++- script/build/cleanup.sh | 1 + script/build/test_slice_tests.sh | 18 +- ...ti_notify_not_clearing_on_leftside_test.rb | 65 +++ ...te_notify_not_clearing_on_leftside_test.rb | 58 +++ .../integration_cti_controller_test.rb | 415 ++++++++++++++++++ 14 files changed, 1075 insertions(+), 14 deletions(-) create mode 100644 app/assets/javascripts/app/controllers/_integration/cti.coffee create mode 100644 app/assets/javascripts/app/views/integration/cti.jst.eco create mode 100644 app/controllers/integration/cti_controller.rb create mode 100644 config/routes/integration_cti.rb create mode 100644 db/migrate/20180420000001_setting_cti.rb create mode 100644 test/browser/integration_cti_notify_not_clearing_on_leftside_test.rb create mode 100644 test/browser/integration_sipgate_notify_not_clearing_on_leftside_test.rb create mode 100644 test/controllers/integration_cti_controller_test.rb diff --git a/app/assets/javascripts/app/controllers/_integration/cti.coffee b/app/assets/javascripts/app/controllers/_integration/cti.coffee new file mode 100644 index 000000000..88fb87b43 --- /dev/null +++ b/app/assets/javascripts/app/controllers/_integration/cti.coffee @@ -0,0 +1,158 @@ +class Index extends App.ControllerIntegrationBase + featureIntegration: 'cti_integration' + featureName: 'CTI (generic)' + featureConfig: 'cti_config' + description: [ + ['This service shows you contacts of incoming calls and a caller list in realtime.'] + ['Also caller id of outbound calls can be changed.'] + ] + events: + 'click .js-select': 'selectAll' + 'change .js-switch input': 'switch' + + render: => + super + new Form( + el: @$('.js-form') + ) + + new App.HttpLog( + el: @$('.js-log') + facility: 'cti' + ) + +class Form extends App.Controller + events: + 'submit form': 'update' + 'click .js-inboundBlockCallerId .js-add': 'addInboundBlockCallerId' + 'click .js-outboundRouting .js-add': 'addOutboundRouting' + 'click .js-inboundBlockCallerId .js-remove': 'removeInboundBlockCallerId' + 'click .js-outboundRouting .js-remove': 'removeOutboundRouting' + + constructor: -> + super + @render() + + currentConfig: -> + config = App.Setting.get('cti_config') + if !config.outbound + config.outbound = {} + if !config.outbound.routing_table + config.outbound.routing_table = [] + if !config.inbound + config.inbound = {} + if !config.inbound.block_caller_ids + config.inbound.block_caller_ids = [] + config + + setConfig: (value) -> + App.Setting.set('cti_config', value, {notify: true}) + + render: => + @config = @currentConfig() + + @html App.view('integration/cti')( + config: @config + cti_token: App.Setting.get('cti_token') + ) + + updateCurrentConfig: => + config = @config + cleanupInput = @cleanupInput + + # default caller_id + default_caller_id = @$('input[name=default_caller_id]').val() + config.outbound.default_caller_id = cleanupInput(default_caller_id) + + # routing table + config.outbound.routing_table = [] + @$('.js-outboundRouting .js-row').each(-> + dest = cleanupInput($(@).find('input[name="dest"]').val()) + caller_id = cleanupInput($(@).find('input[name="caller_id"]').val()) + note = $(@).find('input[name="note"]').val() + config.outbound.routing_table.push { + dest: dest + caller_id: caller_id + note: note + } + ) + + # blocked caller ids + config.inbound.block_caller_ids = [] + @$('.js-inboundBlockCallerId .js-row').each(-> + caller_id = $(@).find('input[name="caller_id"]').val() + note = $(@).find('input[name="note"]').val() + config.inbound.block_caller_ids.push { + caller_id: cleanupInput(caller_id) + note: note + } + ) + + @config = config + + update: (e) => + e.preventDefault() + @updateCurrentConfig() + @setConfig(@config) + + cleanupInput: (value) -> + return value if !value + value.replace(/\s/g, '').trim() + + addInboundBlockCallerId: (e) => + e.preventDefault() + @updateCurrentConfig() + element = $(e.currentTarget).closest('tr') + caller_id = element.find('input[name="caller_id"]').val() + note = element.find('input[name="note"]').val() + return if _.isEmpty(caller_id) || _.isEmpty(note) + @config.inbound.block_caller_ids.push { + caller_id: @cleanupInput(caller_id) + note: note + } + @render() + + addOutboundRouting: (e) => + e.preventDefault() + @updateCurrentConfig() + element = $(e.currentTarget).closest('tr') + dest = @cleanupInput(element.find('input[name="dest"]').val()) + caller_id = @cleanupInput(element.find('input[name="caller_id"]').val()) + note = element.find('input[name="note"]').val() + return if _.isEmpty(caller_id) || _.isEmpty(dest) || _.isEmpty(note) + @config.outbound.routing_table.push { + dest: dest + caller_id: caller_id + note: note + } + @render() + + removeInboundBlockCallerId: (e) => + e.preventDefault() + @updateCurrentConfig() + element = $(e.currentTarget).closest('tr') + element.remove() + @updateCurrentConfig() + + removeOutboundRouting: (e) => + e.preventDefault() + @updateCurrentConfig() + element = $(e.currentTarget).closest('tr') + element.remove() + @updateCurrentConfig() + +class State + @current: -> + App.Setting.get('cti_integration') + +App.Config.set( + 'IntegrationCti' + { + name: 'CTI (generic)' + target: '#system/integration/cti' + description: 'Generic API to integrate VoIP service provider with realtime push.' + controller: Index + state: State + } + 'NavBarIntegrations' +) diff --git a/app/assets/javascripts/app/controllers/cti.coffee b/app/assets/javascripts/app/controllers/cti.coffee index 0fa50580d..ad01c9860 100644 --- a/app/assets/javascripts/app/controllers/cti.coffee +++ b/app/assets/javascripts/app/controllers/cti.coffee @@ -112,6 +112,7 @@ class App.CTI extends App.Controller featureActive: => return true if @Config.get('sipgate_integration') + return true if @Config.get('cti_integration') false render: -> diff --git a/app/assets/javascripts/app/views/integration/cti.jst.eco b/app/assets/javascripts/app/views/integration/cti.jst.eco new file mode 100644 index 000000000..d8ed35186 --- /dev/null +++ b/app/assets/javascripts/app/views/integration/cti.jst.eco @@ -0,0 +1,97 @@ +
+ +

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

+ +

<%- @T('You need to configure the Zammad endpoints in the %s', 'PBX') %>:

+ +

+ + + + + + + +
<%- @T('Type') %> + <%- @T('URL') %> +
<%- @T('Endpoint') %> + +
+
+ +

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

+ +

<%- @T('Blocked caller ids based on sender caller id.') %> + +

+ + + + + +<% for row in @config.inbound.block_caller_ids: %> + + + +
<%- @T('Caller id to block') %> + <%- @T('Note') %> + <%- @T('Action') %> +
+ +
<%- @Icon('trash') %> <%- @T('Remove') %>
+<% end %> +
+ +
<%- @Icon('plus-small') %> <%- @T('Add') %>
+
+
+ +

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

+ +

<%- @T('Set caller id of outbound calls based on destination caller id.') %> + +

+ + + + + +<% for row in @config.outbound.routing_table: %> + + + +
<%- @T('Destination caller id') %> + <%- @T('Set outbound caller id') %> + <%- @T('Note') %> + <%- @T('Action') %> +
+ + +
<%- @Icon('trash') %> <%- @T('Remove') %>
+<% end %> +
+ + +
<%- @Icon('plus-small') %> <%- @T('Add') %>
+
+
+ +

<%- @T('Default caller id.') %> + +

+ + + + + + + +
<%- @T('Default caller id') %> + <%- @T('Note') %> +
+ <%- @T('Default caller id for outbound calls.') %> +
+
+ + +
\ No newline at end of file diff --git a/app/controllers/cti_controller.rb b/app/controllers/cti_controller.rb index 03ac19142..2fdc71b4d 100644 --- a/app/controllers/cti_controller.rb +++ b/app/controllers/cti_controller.rb @@ -6,6 +6,11 @@ class CtiController < ApplicationController # list current caller log def index backends = [ + { + name: 'CTI (generic)', + enabled: Setting.get('cti_integration'), + url: '#system/integration/cti', + }, { name: 'sipgate.io', enabled: Setting.get('sipgate_integration'), diff --git a/app/controllers/integration/cti_controller.rb b/app/controllers/integration/cti_controller.rb new file mode 100644 index 000000000..33a1bd9ea --- /dev/null +++ b/app/controllers/integration/cti_controller.rb @@ -0,0 +1,117 @@ +# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ + +class Integration::CtiController < ApplicationController + skip_before_action :verify_csrf_token + before_action :check_configured, :check_token + + # notify about inbound call / block inbound call + def event + if params['direction'] == 'in' + if params['event'] == 'newCall' + config_inbound = config_integration[:inbound] || {} + block_caller_ids = config_inbound[:block_caller_ids] || [] + + # check if call need to be blocked + block_caller_ids.each do |item| + next unless item[:caller_id] == params['from'] + + render json: { action: 'reject', reason: 'busy' }, status: :ok + + #params['Reject'] = 'busy' + params['comment'] = 'reject, busy' + if params['user'] + params['comment'] = "#{params['user']} -> reject, busy" + end + Cti::Log.process(params) + return true + end + end + + Cti::Log.process(params) + + render json: {}, status: :ok + return true + elsif params['direction'] == 'out' + config_outbound = config_integration[:outbound] + routing_table = nil + default_caller_id = nil + if config_outbound.present? + routing_table = config_outbound[:routing_table] + default_caller_id = config_outbound[:default_caller_id] + end + + # set callerId + data = {} + to = params[:to] + from = nil + if to && routing_table.present? + routing_table.each do |row| + dest = row[:dest].gsub(/\*/, '.+?') + next if to !~ /^#{dest}$/ + from = row[:caller_id] + data = { + action: 'dial', + caller_id: from, + number: params[:to] + } + break + end + if data.blank? && default_caller_id.present? + from = default_caller_id + data = { + action: 'dial', + caller_id: default_caller_id, + number: params[:to] + } + end + end + render json: data, status: :ok + + if from.present? + params['from'] = from + end + Cti::Log.process(params) + return true + end + render json: { error: 'Invalid direction!' }, status: :unprocessable_entity + end + + private + + def check_token + if Setting.get('cti_token') != params[:token] + response_unauthorized('Invalid token, please contact your admin!') + return + end + + true + end + + def check_configured + http_log_config facility: 'cti' + + if !Setting.get('cti_integration') + response_error('Feature is disable, please contact your admin to enable it!') + return + end + if config_integration.blank? || config_integration[:inbound].blank? || config_integration[:outbound].blank? + response_error('Feature not configured, please contact your admin!') + return + end + + true + end + + def config_integration + @config_integration ||= Setting.get('cti_config') + end + + def response_error(error) + render json: { error: error }, status: :unprocessable_entity + end + + def response_unauthorized(error) + render json: { error: error }, status: :unauthorized + end + +end diff --git a/app/models/cti/log.rb b/app/models/cti/log.rb index 9b043207f..af35c5cc7 100644 --- a/app/models/cti/log.rb +++ b/app/models/cti/log.rb @@ -276,7 +276,7 @@ Cti::Log.process( 'user' => 'user 1', 'from' => '4912347114711', 'to' => '4930600000000', - 'callId' => '4991155921769858278-1', + 'callId' => '4991155921769858278-1', # or call_id 'direction' => 'in', ) @@ -286,6 +286,7 @@ Cti::Log.process( comment = params['cause'] event = params['event'] user = params['user'] + call_id = params['callId'] || params['call_id'] if user.class == Array user = user.join(', ') end @@ -309,14 +310,14 @@ Cti::Log.process( from_comment: from_comment, to: params['to'], to_comment: to_comment, - call_id: params['callId'], + call_id: call_id, comment: comment, state: event, preferences: preferences, ) when 'answer' - log = find_by(call_id: params['callId']) - raise "No such call_id #{params['callId']}" if !log + log = find_by(call_id: call_id) + raise "No such call_id #{call_id}" if !log log.state = 'answer' log.start = Time.zone.now if user @@ -325,8 +326,8 @@ Cti::Log.process( log.comment = comment log.save when 'hangup' - log = find_by(call_id: params['callId']) - raise "No such call_id #{params['callId']}" if !log + log = find_by(call_id: call_id) + raise "No such call_id #{call_id}" if !log if params['direction'] == 'in' && log.state == 'newCall' log.done = false end diff --git a/config/routes/integration_cti.rb b/config/routes/integration_cti.rb new file mode 100644 index 000000000..29784f3d7 --- /dev/null +++ b/config/routes/integration_cti.rb @@ -0,0 +1,5 @@ +Zammad::Application.routes.draw do + + match '/api/v1/cti/:token', to: 'integration/cti#event', via: :post + +end diff --git a/db/migrate/20180420000001_setting_cti.rb b/db/migrate/20180420000001_setting_cti.rb new file mode 100644 index 000000000..24968b377 --- /dev/null +++ b/db/migrate/20180420000001_setting_cti.rb @@ -0,0 +1,70 @@ +class SettingCti < ActiveRecord::Migration[5.1] + def up + + # return if it's a new setup + return if !Setting.find_by(name: 'system_init_done') + + Setting.create_if_not_exists( + title: 'cti integration', + name: 'cti_integration', + area: 'Integration::Switch', + description: 'Defines if generic CTI is enabled or not.', + options: { + form: [ + { + display: '', + null: true, + name: 'cti_integration', + tag: 'boolean', + options: { + true => 'yes', + false => 'no', + }, + }, + ], + }, + state: false, + preferences: { + prio: 1, + trigger: ['menu:render', 'cti:reload'], + authentication: true, + permission: ['admin.integration'], + }, + frontend: true + ) + Setting.create_if_not_exists( + title: 'cti config', + name: 'cti_config', + area: 'Integration::Cti', + description: 'Defines the cti config.', + options: {}, + state: { 'outbound' => { 'routing_table' => [], 'default_caller_id' => '' }, 'inbound' => { 'block_caller_ids' => [] } }, + preferences: { + prio: 2, + permission: ['admin.integration'], + }, + frontend: false, + ) + Setting.create_if_not_exists( + title: 'CTI Token', + name: 'cti_token', + area: 'Integration::Cti', + description: 'Token for cti.', + options: { + form: [ + { + display: '', + null: false, + name: 'cti_token', + tag: 'input', + }, + ], + }, + state: SecureRandom.urlsafe_base64(20), + preferences: { + permission: ['admin.integration'], + }, + frontend: false + ) + end +end diff --git a/db/seeds/settings.rb b/db/seeds/settings.rb index 3b3d339e4..95f6efa6a 100644 --- a/db/seeds/settings.rb +++ b/db/seeds/settings.rb @@ -2485,7 +2485,7 @@ Setting.create_if_not_exists( }, ], }, - state: SecureRandom.urlsafe_base64(40), + state: ENV['MONITORING_TOKEN'] || SecureRandom.urlsafe_base64(40), preferences: { permission: ['admin.monitoring'], }, @@ -3377,7 +3377,7 @@ Setting.create_if_not_exists( area: 'Core', description: 'Defines the Check_MK token for allowing updates.', options: {}, - state: SecureRandom.hex(16), + state: ENV['CHECK_MK_TOKEN'] || SecureRandom.hex(16), preferences: { permission: ['admin.integration'], }, @@ -3722,6 +3722,68 @@ Setting.create_if_not_exists( }, frontend: false, ) +Setting.create_if_not_exists( + title: 'cti integration', + name: 'cti_integration', + area: 'Integration::Switch', + description: 'Defines if generic CTI is enabled or not.', + options: { + form: [ + { + display: '', + null: true, + name: 'cti_integration', + tag: 'boolean', + options: { + true => 'yes', + false => 'no', + }, + }, + ], + }, + state: false, + preferences: { + prio: 1, + trigger: ['menu:render', 'cti:reload'], + authentication: true, + permission: ['admin.integration'], + }, + frontend: true +) +Setting.create_if_not_exists( + title: 'cti config', + name: 'cti_config', + area: 'Integration::Cti', + description: 'Defines the cti config.', + options: {}, + state: { 'outbound' => { 'routing_table' => [], 'default_caller_id' => '' }, 'inbound' => { 'block_caller_ids' => [] } }, + preferences: { + prio: 2, + permission: ['admin.integration'], + }, + frontend: false, +) +Setting.create_if_not_exists( + title: 'CTI Token', + name: 'cti_token', + area: 'Integration::Cti', + description: 'Token for cti.', + options: { + form: [ + { + display: '', + null: false, + name: 'cti_token', + tag: 'input', + }, + ], + }, + state: ENV['CTI_TOKEN'] || SecureRandom.urlsafe_base64(20), + preferences: { + permission: ['admin.integration'], + }, + frontend: false +) Setting.create_if_not_exists( title: 'Clearbit integration', name: 'clearbit_integration', diff --git a/script/build/cleanup.sh b/script/build/cleanup.sh index a4287ae7f..7f6ec5439 100755 --- a/script/build/cleanup.sh +++ b/script/build/cleanup.sh @@ -5,3 +5,4 @@ set -ex rm app/assets/javascripts/app/controllers/layout_ref.coffee rm -rf app/assets/javascripts/app/views/layout_ref/ rm app/assets/javascripts/app/controllers/karma.coffee +rm app/assets/javascripts/app/controllers/_integration/cti.coffee diff --git a/script/build/test_slice_tests.sh b/script/build/test_slice_tests.sh index daec7e3d8..4a30d2fdf 100755 --- a/script/build/test_slice_tests.sh +++ b/script/build/test_slice_tests.sh @@ -65,7 +65,8 @@ if [ "$LEVEL" == '1' ]; then # test/browser/maintenance_session_message_test.rb # test/browser/manage_test.rb # test/browser/monitoring_test.rb - rm test/browser/phone_notify_not_clearing_on_leftside_test.rb + rm test/browser/integration_sipgate_notify_not_clearing_on_leftside_test.rb + rm test/browser/integration_cti_notify_not_clearing_on_leftside_test.rb rm test/browser/preferences_language_test.rb rm test/browser/preferences_permission_check_test.rb rm test/browser/preferences_token_access_test.rb @@ -138,7 +139,8 @@ elif [ "$LEVEL" == '2' ]; then rm test/browser/maintenance_session_message_test.rb rm test/browser/manage_test.rb rm test/browser/monitoring_test.rb - rm test/browser/phone_notify_not_clearing_on_leftside_test.rb + rm test/browser/integration_sipgate_notify_not_clearing_on_leftside_test.rb + rm test/browser/integration_cti_notify_not_clearing_on_leftside_test.rb rm test/browser/preferences_language_test.rb rm test/browser/preferences_permission_check_test.rb rm test/browser/preferences_token_access_test.rb @@ -211,7 +213,8 @@ elif [ "$LEVEL" == '3' ]; then rm test/browser/maintenance_session_message_test.rb rm test/browser/manage_test.rb rm test/browser/monitoring_test.rb - rm test/browser/phone_notify_not_clearing_on_leftside_test.rb + rm test/browser/integration_sipgate_notify_not_clearing_on_leftside_test.rb + rm test/browser/integration_cti_notify_not_clearing_on_leftside_test.rb rm test/browser/preferences_language_test.rb rm test/browser/preferences_permission_check_test.rb rm test/browser/preferences_token_access_test.rb @@ -284,7 +287,8 @@ elif [ "$LEVEL" == '4' ]; then rm test/browser/maintenance_session_message_test.rb rm test/browser/manage_test.rb rm test/browser/monitoring_test.rb - rm test/browser/phone_notify_not_clearing_on_leftside_test.rb + rm test/browser/integration_sipgate_notify_not_clearing_on_leftside_test.rb + rm test/browser/integration_cti_notify_not_clearing_on_leftside_test.rb rm test/browser/preferences_language_test.rb rm test/browser/preferences_permission_check_test.rb rm test/browser/preferences_token_access_test.rb @@ -356,7 +360,8 @@ elif [ "$LEVEL" == '5' ]; then rm test/browser/maintenance_session_message_test.rb rm test/browser/manage_test.rb rm test/browser/monitoring_test.rb - rm test/browser/phone_notify_not_clearing_on_leftside_test.rb + rm test/browser/integration_sipgate_notify_not_clearing_on_leftside_test.rb + rm test/browser/integration_cti_notify_not_clearing_on_leftside_test.rb rm test/browser/preferences_language_test.rb rm test/browser/preferences_permission_check_test.rb rm test/browser/preferences_token_access_test.rb @@ -431,7 +436,8 @@ elif [ "$LEVEL" == '6' ]; then rm test/browser/maintenance_session_message_test.rb rm test/browser/manage_test.rb rm test/browser/monitoring_test.rb - # rm test/browser/phone_notify_not_clearing_on_leftside_test.rb + # rm test/browser/integration_sipgate_notify_not_clearing_on_leftside_test.rb + # rm test/browser/integration_cti_notify_not_clearing_on_leftside_test.rb # test/browser/preferences_language_test.rb # test/browser/preferences_permission_check_test.rb # test/browser/preferences_token_access_test.rb diff --git a/test/browser/integration_cti_notify_not_clearing_on_leftside_test.rb b/test/browser/integration_cti_notify_not_clearing_on_leftside_test.rb new file mode 100644 index 000000000..ce222186e --- /dev/null +++ b/test/browser/integration_cti_notify_not_clearing_on_leftside_test.rb @@ -0,0 +1,65 @@ +require 'browser_test_helper' + +# Regression test for #2017 + +class IntegrationCtiNotifyNotClearingOnLeftsideTest < TestCase + setup do + if !ENV['CTI_TOKEN'] + raise "ERROR: Need CTI_TOKEN - hint CTI_TOKEN='some_token'" + end + + end + + def test_notify_badge + id = rand(99_999_999) + + @browser = browser_instance + login( + username: 'master@example.com', + password: 'test', + url: browser_url, + ) + + click(css: 'a[href="#manage"]') + click(css: 'a[href="#system/integration"]') + click(css: 'a[href="#system/integration/cti"]') + + switch( + css: '.content.active .js-switch', + type: 'on' + ) + + watch_for( + css: 'a[href="#cti"]' + ) + + click(css: 'a[href="#cti"]') + + # simulate cti callbacks + + url = URI.join(browser_url, "api/v1/cti/#{ENV['CTI_TOKEN']}") + params = { direction: 'in', from: '491715000002', to: '4930600000000', callId: "4991155921769858278-#{id}", cause: 'busy' } + Net::HTTP.post_form(url, params.merge(event: 'newCall')) + Net::HTTP.post_form(url, params.merge(event: 'hangup')) + + watch_for( + css: '.js-phoneMenuItem .counter', + value: '1' + ) + + click(css: '.content.active .table-checkbox label') + + watch_for_disappear( + css: '.js-phoneMenuItem .counter' + ) + + click(css: 'a[href="#manage"]') + click(css: 'a[href="#system/integration"]') + click(css: 'a[href="#system/integration/cti"]') + + switch( + css: '.content.active .js-switch', + type: 'off' + ) + end +end diff --git a/test/browser/integration_sipgate_notify_not_clearing_on_leftside_test.rb b/test/browser/integration_sipgate_notify_not_clearing_on_leftside_test.rb new file mode 100644 index 000000000..7f8ee8cfc --- /dev/null +++ b/test/browser/integration_sipgate_notify_not_clearing_on_leftside_test.rb @@ -0,0 +1,58 @@ +require 'browser_test_helper' + +# Regression test for #2017 + +class IntegrationSipgateNotifyNotClearingOnLeftsideTest < TestCase + def test_notify_badge + id = rand(99_999_999) + + @browser = browser_instance + login( + username: 'master@example.com', + password: 'test', + url: browser_url, + ) + + click(css: 'a[href="#manage"]') + click(css: 'a[href="#system/integration"]') + click(css: 'a[href="#system/integration/sipgate"]') + + switch( + css: '.content.active .js-switch', + type: 'on' + ) + + watch_for( + css: 'a[href="#cti"]' + ) + + click(css: 'a[href="#cti"]') + + # simulate sipgate callbacks + + url = URI.join(browser_url, 'api/v1/sipgate/in') + params = { direction: 'in', from: '491715000002', to: '4930600000000', callId: "4991155921769858278-#{id}", cause: 'busy' } + Net::HTTP.post_form(url, params.merge(event: 'newCall')) + Net::HTTP.post_form(url, params.merge(event: 'hangup')) + + watch_for( + css: '.js-phoneMenuItem .counter', + value: '1' + ) + + click(css: '.content.active .table-checkbox label') + + watch_for_disappear( + css: '.js-phoneMenuItem .counter' + ) + + click(css: 'a[href="#manage"]') + click(css: 'a[href="#system/integration"]') + click(css: 'a[href="#system/integration/cti"]') + + switch( + css: '.content.active .js-switch', + type: 'off' + ) + end +end diff --git a/test/controllers/integration_cti_controller_test.rb b/test/controllers/integration_cti_controller_test.rb new file mode 100644 index 000000000..7a358bc5e --- /dev/null +++ b/test/controllers/integration_cti_controller_test.rb @@ -0,0 +1,415 @@ + +require 'test_helper' +require 'rexml/document' + +class IntegrationCtiControllerTest < ActionDispatch::IntegrationTest + setup do + + Cti::Log.destroy_all + + Setting.set('cti_integration', true) + Setting.set('cti_config', { + outbound: { + routing_table: [ + { + dest: '41*', + caller_id: '41715880339000', + }, + { + dest: '491714000000', + caller_id: '41715880339000', + }, + ], + default_caller_id: '4930777000000', + }, + inbound: { + block_caller_ids: [ + { + caller_id: '491715000000', + note: 'some note', + } + ], + notify_user_ids: { + 2 => true, + 4 => false, + }, + } + },) + + groups = Group.where(name: 'Users') + roles = Role.where(name: %w[Agent]) + agent = User.create_or_update( + login: 'cti-agent@example.com', + firstname: 'E', + lastname: 'S', + email: 'cti-agent@example.com', + password: 'agentpw', + active: true, + roles: roles, + groups: groups, + updated_by_id: 1, + created_by_id: 1, + ) + + customer1 = User.create_or_update( + login: 'ticket-caller_id_cti-customer1@example.com', + firstname: 'CallerId', + lastname: 'Customer1', + email: 'ticket-caller_id_cti-customer1@example.com', + password: 'customerpw', + active: true, + phone: '+49 99999 222222', + fax: '+49 99999 222223', + mobile: '+4912347114711', + note: 'Phone at home: +49 99999 222224', + updated_by_id: 1, + created_by_id: 1, + ) + customer2 = User.create_or_update( + login: 'ticket-caller_id_cti-customer2@example.com', + firstname: 'CallerId', + lastname: 'Customer2', + email: 'ticket-caller_id_cti-customer2@example.com', + password: 'customerpw', + active: true, + phone: '+49 99999 222222 2', + updated_by_id: 1, + created_by_id: 1, + ) + customer3 = User.create_or_update( + login: 'ticket-caller_id_cti-customer3@example.com', + firstname: 'CallerId', + lastname: 'Customer3', + email: 'ticket-caller_id_cti-customer3@example.com', + password: 'customerpw', + active: true, + phone: '+49 99999 222222 2', + updated_by_id: 1, + created_by_id: 1, + ) + Cti::CallerId.rebuild + + end + + test 'token check' do + params = 'event=newCall&direction=in&from=4912347114711&to=4930600000000&call_id=4991155921769858278-1&user%5B%5D=user+1&user%5B%5D=user+2' + post '/api/v1/cti/not_existing_token', params: params + assert_response(401) + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_equal('Invalid token, please contact your admin!', result['error']) + end + + test 'basic call' do + token = Setting.get('cti_token') + + # inbound - I + params = 'event=newCall&direction=in&from=4912347114711&to=4930600000000&call_id=4991155921769858278-1&user%5B%5D=user+1&user%5B%5D=user+2' + post "/api/v1/cti/#{token}", params: params + assert_response(200) + + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert(result.blank?) + + # inbound - II - block caller + params = 'event=newCall&direction=in&from=491715000000&to=4930600000000&call_id=4991155921769858278-2&user%5B%5D=user+1&user%5B%5D=user+2' + post "/api/v1/cti/#{token}", params: params + assert_response(200) + + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_equal('reject', result['action']) + assert_equal('busy', result['reason']) + + # outbound - I - set default_caller_id + params = 'event=newCall&direction=out&from=4930600000000&to=4912347114711&call_id=8621106404543334274-3&user%5B%5D=user+1' + post "/api/v1/cti/#{token}", params: params + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_equal('dial', result['action']) + assert_equal('4912347114711', result['number']) + assert_equal('4930777000000', result['caller_id']) + + # outbound - II - set caller_id based on routing_table by explicite number + params = 'event=newCall&direction=out&from=4930600000000&to=491714000000&call_id=8621106404543334274-4&user%5B%5D=user+1' + post "/api/v1/cti/#{token}", params: params + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_equal('dial', result['action']) + assert_equal('491714000000', result['number']) + assert_equal('41715880339000', result['caller_id']) + + # outbound - III - set caller_id based on routing_table by 41* + params = 'event=newCall&direction=out&from=4930600000000&to=4147110000000&call_id=8621106404543334274-5&user%5B%5D=user+1' + post "/api/v1/cti/#{token}", params: params + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_equal('dial', result['action']) + assert_equal('4147110000000', result['number']) + assert_equal('41715880339000', result['caller_id']) + + # no config + Setting.set('cti_config', {}) + params = 'event=newCall&direction=in&from=4912347114711&to=4930600000000&call_id=4991155921769858278-6&user%5B%5D=user+1&user%5B%5D=user+2' + post "/api/v1/cti/#{token}", params: params + assert_response(422) + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_equal('Feature not configured, please contact your admin!', result['error']) + + end + + test 'log call' do + token = Setting.get('cti_token') + + # outbound - I - new call + params = 'event=newCall&direction=out&from=4930600000000&to=4912347114711&call_id=1234567890-1&user%5B%5D=user+1' + post "/api/v1/cti/#{token}", params: params + assert_response(200) + log = Cti::Log.find_by(call_id: '1234567890-1') + assert(log) + assert_equal('4930777000000', log.from) + assert_equal('4912347114711', log.to) + assert_equal('out', log.direction) + assert_equal('user 1', log.from_comment) + assert_equal('CallerId Customer1', log.to_comment) + assert_nil(log.comment) + assert_equal('newCall', log.state) + assert_equal(true, log.done) + + # outbound - I - hangup by agent + params = 'event=hangup&direction=out&call_id=1234567890-1&cause=cancel' + post "/api/v1/cti/#{token}", params: params + assert_response(200) + log = Cti::Log.find_by(call_id: '1234567890-1') + assert(log) + assert_equal('4930777000000', log.from) + assert_equal('4912347114711', log.to) + assert_equal('out', log.direction) + assert_equal('user 1', log.from_comment) + assert_equal('CallerId Customer1', log.to_comment) + assert_equal('cancel', log.comment) + assert_equal('hangup', log.state) + assert_equal(true, log.done) + + # outbound - II - new call + params = 'event=newCall&direction=out&from=4930600000000&to=4912347114711&call_id=1234567890-2&user%5B%5D=user+1' + post "/api/v1/cti/#{token}", params: params + assert_response(200) + log = Cti::Log.find_by(call_id: '1234567890-2') + assert(log) + assert_equal('4930777000000', log.from) + assert_equal('4912347114711', log.to) + assert_equal('out', log.direction) + assert_equal('user 1', log.from_comment) + assert_equal('CallerId Customer1', log.to_comment) + assert_nil(log.comment) + assert_equal('newCall', log.state) + assert_equal(true, log.done) + + # outbound - II - answer by customer + params = 'event=answer&direction=out&call_id=1234567890-2&from=4930600000000&to=4912347114711' + post "/api/v1/cti/#{token}", params: params + assert_response(200) + log = Cti::Log.find_by(call_id: '1234567890-2') + assert(log) + assert_equal('4930777000000', log.from) + assert_equal('4912347114711', log.to) + assert_equal('out', log.direction) + assert_equal('user 1', log.from_comment) + assert_equal('CallerId Customer1', log.to_comment) + assert_nil(log.comment) + assert_equal('answer', log.state) + assert_equal(true, log.done) + + # outbound - II - hangup by customer + params = 'event=hangup&direction=out&call_id=1234567890-2&cause=normalClearing&from=4930600000000&to=4912347114711' + post "/api/v1/cti/#{token}", params: params + assert_response(200) + log = Cti::Log.find_by(call_id: '1234567890-2') + assert(log) + assert_equal('4930777000000', log.from) + assert_equal('4912347114711', log.to) + assert_equal('out', log.direction) + assert_equal('user 1', log.from_comment) + assert_equal('CallerId Customer1', log.to_comment) + assert_equal('normalClearing', log.comment) + assert_equal('hangup', log.state) + assert_equal(true, log.done) + + # inbound - I - new call + params = 'event=newCall&direction=in&to=4930600000000&from=4912347114711&call_id=1234567890-3&user%5B%5D=user+1' + post "/api/v1/cti/#{token}", params: params + assert_response(200) + log = Cti::Log.find_by(call_id: '1234567890-3') + assert(log) + assert_equal('4930600000000', log.to) + assert_equal('4912347114711', log.from) + assert_equal('in', log.direction) + assert_equal('user 1', log.to_comment) + assert_equal('CallerId Customer1', log.from_comment) + assert_nil(log.comment) + assert_equal('newCall', log.state) + assert_equal(true, log.done) + + # inbound - I - answer by customer + params = 'event=answer&direction=in&call_id=1234567890-3&to=4930600000000&from=4912347114711' + post "/api/v1/cti/#{token}", params: params + assert_response(200) + log = Cti::Log.find_by(call_id: '1234567890-3') + assert(log) + assert_equal('4930600000000', log.to) + assert_equal('4912347114711', log.from) + assert_equal('in', log.direction) + assert_equal('user 1', log.to_comment) + assert_equal('CallerId Customer1', log.from_comment) + assert_nil(log.comment) + assert_equal('answer', log.state) + assert_equal(true, log.done) + + # inbound - I - hangup by customer + params = 'event=hangup&direction=in&call_id=1234567890-3&cause=normalClearing&to=4930600000000&from=4912347114711' + post "/api/v1/cti/#{token}", params: params + assert_response(200) + log = Cti::Log.find_by(call_id: '1234567890-3') + assert(log) + assert_equal('4930600000000', log.to) + assert_equal('4912347114711', log.from) + assert_equal('in', log.direction) + assert_equal('user 1', log.to_comment) + assert_equal('CallerId Customer1', log.from_comment) + assert_equal('normalClearing', log.comment) + assert_equal('hangup', log.state) + assert_equal(true, log.done) + + # inbound - II - new call + params = 'event=newCall&direction=in&to=4930600000000&from=4912347114711&call_id=1234567890-4&user%5B%5D=user+1,user+2' + post "/api/v1/cti/#{token}", params: params + assert_response(200) + log = Cti::Log.find_by(call_id: '1234567890-4') + assert(log) + assert_equal('4930600000000', log.to) + assert_equal('4912347114711', log.from) + assert_equal('in', log.direction) + assert_equal('user 1,user 2', log.to_comment) + assert_equal('CallerId Customer1', log.from_comment) + assert_nil(log.comment) + assert_equal('newCall', log.state) + assert_equal(true, log.done) + + # inbound - II - answer by voicemail + params = 'event=answer&direction=in&call_id=1234567890-4&to=4930600000000&from=4912347114711&user=voicemail' + post "/api/v1/cti/#{token}", params: params + assert_response(200) + log = Cti::Log.find_by(call_id: '1234567890-4') + assert(log) + assert_equal('4930600000000', log.to) + assert_equal('4912347114711', log.from) + assert_equal('in', log.direction) + assert_equal('voicemail', log.to_comment) + assert_equal('CallerId Customer1', log.from_comment) + assert_nil(log.comment) + assert_equal('answer', log.state) + assert_equal(true, log.done) + + # inbound - II - hangup by customer + params = 'event=hangup&direction=in&call_id=1234567890-4&cause=normalClearing&to=4930600000000&from=4912347114711' + post "/api/v1/cti/#{token}", params: params + assert_response(200) + log = Cti::Log.find_by(call_id: '1234567890-4') + assert(log) + assert_equal('4930600000000', log.to) + assert_equal('4912347114711', log.from) + assert_equal('in', log.direction) + assert_equal('voicemail', log.to_comment) + assert_equal('CallerId Customer1', log.from_comment) + assert_equal('normalClearing', log.comment) + assert_equal('hangup', log.state) + assert_equal(false, log.done) + + # inbound - III - new call + params = 'event=newCall&direction=in&to=4930600000000&from=4912347114711&call_id=1234567890-5&user%5B%5D=user+1,user+2' + post "/api/v1/cti/#{token}", params: params + assert_response(200) + log = Cti::Log.find_by(call_id: '1234567890-5') + assert(log) + assert_equal('4930600000000', log.to) + assert_equal('4912347114711', log.from) + assert_equal('in', log.direction) + assert_equal('user 1,user 2', log.to_comment) + assert_equal('CallerId Customer1', log.from_comment) + assert_nil(log.comment) + assert_equal('newCall', log.state) + assert_equal(true, log.done) + + # inbound - III - hangup by customer + params = 'event=hangup&direction=in&call_id=1234567890-5&cause=normalClearing&to=4930600000000&from=4912347114711' + post "/api/v1/cti/#{token}", params: params + assert_response(200) + log = Cti::Log.find_by(call_id: '1234567890-5') + assert(log) + assert_equal('4930600000000', log.to) + assert_equal('4912347114711', log.from) + assert_equal('in', log.direction) + assert_equal('user 1,user 2', log.to_comment) + assert_equal('CallerId Customer1', log.from_comment) + assert_equal('normalClearing', log.comment) + assert_equal('hangup', log.state) + assert_equal(false, log.done) + + # inbound - IV - new call + params = 'event=newCall&direction=in&to=4930600000000&from=49999992222222&call_id=1234567890-6&user%5B%5D=user+1,user+2' + post "/api/v1/cti/#{token}", params: params + assert_response(200) + log = Cti::Log.find_by(call_id: '1234567890-6') + assert(log) + assert_equal('4930600000000', log.to) + assert_equal('49999992222222', log.from) + assert_equal('in', log.direction) + assert_equal('user 1,user 2', log.to_comment) + assert_equal('CallerId Customer3,CallerId Customer2', log.from_comment) + assert_not(log.preferences['to']) + assert(log.preferences['from']) + assert_nil(log.comment) + assert_equal('newCall', log.state) + assert_equal(true, log.done) + + # get caller list + get '/api/v1/cti/log' + assert_response(401) + + customer2 = User.lookup(login: 'ticket-caller_id_cti-customer2@example.com') + customer3 = User.lookup(login: 'ticket-caller_id_cti-customer3@example.com') + + headers = { 'ACCEPT' => 'application/json', 'CONTENT_TYPE' => 'application/json' } + credentials = ActionController::HttpAuthentication::Basic.encode_credentials('cti-agent@example.com', 'agentpw') + get '/api/v1/cti/log', headers: headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(result['list'].class, Array) + assert_equal(6, result['list'].count) + assert(result['assets']) + assert(result['assets']['User']) + assert(result['assets']['User'][customer2.id.to_s]) + assert(result['assets']['User'][customer3.id.to_s]) + assert_equal('1234567890-6', result['list'][0]['call_id']) + assert_equal('1234567890-5', result['list'][1]['call_id']) + assert_equal('1234567890-4', result['list'][2]['call_id']) + assert_equal('1234567890-3', result['list'][3]['call_id']) + assert_equal('1234567890-2', result['list'][4]['call_id']) + assert_equal('hangup', result['list'][4]['state']) + assert_equal('4930777000000', result['list'][4]['from']) + assert_equal('user 1', result['list'][4]['from_comment']) + assert_equal('4912347114711', result['list'][4]['to']) + assert_equal('CallerId Customer1', result['list'][4]['to_comment']) + assert_equal('normalClearing', result['list'][4]['comment']) + assert_equal('hangup', result['list'][4]['state']) + assert_equal('1234567890-1', result['list'][5]['call_id']) + + end + +end