punchblock 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG.md +5 -0
- data/lib/punchblock.rb +1 -1
- data/lib/punchblock/connection.rb +1 -0
- data/lib/punchblock/connection/asterisk.rb +0 -1
- data/lib/punchblock/connection/freeswitch.rb +49 -0
- data/lib/punchblock/event/offer.rb +1 -1
- data/lib/punchblock/translator.rb +5 -0
- data/lib/punchblock/translator/asterisk.rb +16 -28
- data/lib/punchblock/translator/asterisk/call.rb +4 -21
- data/lib/punchblock/translator/asterisk/component.rb +0 -5
- data/lib/punchblock/translator/asterisk/component/asterisk/agi_command.rb +0 -3
- data/lib/punchblock/translator/asterisk/component/asterisk/ami_action.rb +0 -1
- data/lib/punchblock/translator/asterisk/component/input.rb +7 -97
- data/lib/punchblock/translator/asterisk/component/output.rb +0 -4
- data/lib/punchblock/translator/asterisk/component/record.rb +0 -2
- data/lib/punchblock/translator/freeswitch.rb +153 -0
- data/lib/punchblock/translator/freeswitch/call.rb +265 -0
- data/lib/punchblock/translator/freeswitch/component.rb +92 -0
- data/lib/punchblock/translator/freeswitch/component/abstract_output.rb +57 -0
- data/lib/punchblock/translator/freeswitch/component/flite_output.rb +17 -0
- data/lib/punchblock/translator/freeswitch/component/input.rb +29 -0
- data/lib/punchblock/translator/freeswitch/component/output.rb +56 -0
- data/lib/punchblock/translator/freeswitch/component/record.rb +79 -0
- data/lib/punchblock/translator/freeswitch/component/tts_output.rb +26 -0
- data/lib/punchblock/translator/input_component.rb +108 -0
- data/lib/punchblock/version.rb +1 -1
- data/punchblock.gemspec +3 -2
- data/spec/punchblock/connection/freeswitch_spec.rb +90 -0
- data/spec/punchblock/translator/asterisk/call_spec.rb +23 -2
- data/spec/punchblock/translator/asterisk/component/input_spec.rb +3 -3
- data/spec/punchblock/translator/asterisk_spec.rb +1 -1
- data/spec/punchblock/translator/freeswitch/call_spec.rb +922 -0
- data/spec/punchblock/translator/freeswitch/component/flite_output_spec.rb +279 -0
- data/spec/punchblock/translator/freeswitch/component/input_spec.rb +312 -0
- data/spec/punchblock/translator/freeswitch/component/output_spec.rb +369 -0
- data/spec/punchblock/translator/freeswitch/component/record_spec.rb +373 -0
- data/spec/punchblock/translator/freeswitch/component/tts_output_spec.rb +285 -0
- data/spec/punchblock/translator/freeswitch/component_spec.rb +118 -0
- data/spec/punchblock/translator/freeswitch_spec.rb +597 -0
- data/spec/punchblock_spec.rb +11 -0
- data/spec/spec_helper.rb +1 -0
- metadata +52 -7
data/lib/punchblock/version.rb
CHANGED
data/punchblock.gemspec
CHANGED
|
@@ -27,9 +27,10 @@ Gem::Specification.new do |s|
|
|
|
27
27
|
s.add_runtime_dependency %q<activesupport>, ["~> 3.0"]
|
|
28
28
|
s.add_runtime_dependency %q<state_machine>, ["~> 1.0"]
|
|
29
29
|
s.add_runtime_dependency %q<future-resource>, ["~> 1.0"]
|
|
30
|
-
s.add_runtime_dependency %q<has-guarded-handlers>, ["~> 1.
|
|
31
|
-
s.add_runtime_dependency %q<celluloid>, [">= 0.
|
|
30
|
+
s.add_runtime_dependency %q<has-guarded-handlers>, ["~> 1.3"]
|
|
31
|
+
s.add_runtime_dependency %q<celluloid>, [">= 0.11.0"]
|
|
32
32
|
s.add_runtime_dependency %q<ruby_ami>, ["~> 1.2", ">= 1.2.1"]
|
|
33
|
+
s.add_runtime_dependency %q<ruby_fs>, ["~> 1.0"]
|
|
33
34
|
s.add_runtime_dependency %q<ruby_speech>, ["~> 1.0"]
|
|
34
35
|
|
|
35
36
|
s.add_development_dependency %q<bundler>, ["~> 1.0"]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
module Punchblock
|
|
6
|
+
module Connection
|
|
7
|
+
describe Freeswitch do
|
|
8
|
+
let(:media_engine) { :flite }
|
|
9
|
+
let(:default_voice) { :hal }
|
|
10
|
+
let :options do
|
|
11
|
+
{
|
|
12
|
+
:host => '127.0.0.1',
|
|
13
|
+
:port => 8021,
|
|
14
|
+
:password => 'test',
|
|
15
|
+
:media_engine => media_engine,
|
|
16
|
+
:default_voice => default_voice
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
let(:mock_event_handler) { stub_everything 'Event Handler' }
|
|
21
|
+
|
|
22
|
+
let(:connection) { described_class.new options }
|
|
23
|
+
|
|
24
|
+
let(:mock_stream) { mock 'RubyFS::Stream' }
|
|
25
|
+
|
|
26
|
+
subject { connection }
|
|
27
|
+
|
|
28
|
+
before do
|
|
29
|
+
subject.event_handler = mock_event_handler
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'should set the connection on the translator' do
|
|
33
|
+
subject.translator.connection.should be subject
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'should set the media engine on the translator' do
|
|
37
|
+
subject.translator.media_engine.should be media_engine
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'should set the default voice on the translator' do
|
|
41
|
+
subject.translator.default_voice.should be default_voice
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
describe '#run' do
|
|
45
|
+
it 'starts a RubyFS stream' do
|
|
46
|
+
# subject.expects(:new_fs_stream).once.with('127.0.0.1', 8021, 'test').returns mock_stream
|
|
47
|
+
subject.stream.expects(:run).once
|
|
48
|
+
lambda { subject.run }.should raise_error(DisconnectedError)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
describe '#stop' do
|
|
53
|
+
it 'stops the RubyFS::Stream' do
|
|
54
|
+
subject.stream.expects(:shutdown).once
|
|
55
|
+
subject.stop
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'shuts down the translator' do
|
|
59
|
+
subject.translator.expects(:terminate).once
|
|
60
|
+
subject.stop
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it 'sends events from RubyFS to the translator' do
|
|
65
|
+
event = mock 'RubyFS::Event'
|
|
66
|
+
subject.translator.expects(:handle_es_event!).once.with event
|
|
67
|
+
subject.stream.fire_event event
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
describe '#write' do
|
|
71
|
+
it 'sends a command to the translator' do
|
|
72
|
+
command = mock 'Command'
|
|
73
|
+
options = {:foo => :bar}
|
|
74
|
+
subject.translator.expects(:execute_command!).once.with command, options
|
|
75
|
+
subject.write command, options
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
describe 'when a rayo event is received from the translator' do
|
|
80
|
+
it 'should call the event handler with the event' do
|
|
81
|
+
offer = Event::Offer.new
|
|
82
|
+
offer.target_call_id = '9f00061'
|
|
83
|
+
|
|
84
|
+
mock_event_handler.expects(:call).once.with offer
|
|
85
|
+
subject.handle_event offer
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -86,7 +86,7 @@ module Punchblock
|
|
|
86
86
|
it 'sends an offer to the translator' do
|
|
87
87
|
expected_offer = Punchblock::Event::Offer.new :target_call_id => subject.id,
|
|
88
88
|
:to => '1000',
|
|
89
|
-
:from => 'Jane Smith <
|
|
89
|
+
:from => 'Jane Smith <SIP/5678>',
|
|
90
90
|
:headers => sip_headers
|
|
91
91
|
translator.expects(:handle_pb_event).with expected_offer
|
|
92
92
|
subject.send_offer
|
|
@@ -151,8 +151,10 @@ module Punchblock
|
|
|
151
151
|
describe '#dial' do
|
|
152
152
|
let(:dial_command_options) { {} }
|
|
153
153
|
|
|
154
|
+
let(:to) { 'SIP/1234' }
|
|
155
|
+
|
|
154
156
|
let :dial_command do
|
|
155
|
-
Punchblock::Command::Dial.new({:to =>
|
|
157
|
+
Punchblock::Command::Dial.new({:to => to, :from => 'sip:foo@bar.com'}.merge(dial_command_options))
|
|
156
158
|
end
|
|
157
159
|
|
|
158
160
|
before { dial_command.request! }
|
|
@@ -172,6 +174,25 @@ module Punchblock
|
|
|
172
174
|
subject.dial dial_command
|
|
173
175
|
end
|
|
174
176
|
|
|
177
|
+
context 'with a name and channel in the to field' do
|
|
178
|
+
let(:to) { 'Jane Smith <SIP/5678>' }
|
|
179
|
+
|
|
180
|
+
it 'sends an Originate AMI action with only the channel' do
|
|
181
|
+
expected_action = Punchblock::Component::Asterisk::AMI::Action.new(:name => 'Originate',
|
|
182
|
+
:params => {
|
|
183
|
+
:async => true,
|
|
184
|
+
:application => 'AGI',
|
|
185
|
+
:data => 'agi:async',
|
|
186
|
+
:channel => 'SIP/5678',
|
|
187
|
+
:callerid => 'sip:foo@bar.com',
|
|
188
|
+
:variable => "punchblock_call_id=#{subject.id}"
|
|
189
|
+
}).tap { |a| a.request! }
|
|
190
|
+
|
|
191
|
+
translator.expects(:execute_global_command!).once.with expected_action
|
|
192
|
+
subject.dial dial_command
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
175
196
|
context 'with a timeout specified' do
|
|
176
197
|
let :dial_command_options do
|
|
177
198
|
{ :timeout => 10000 }
|
|
@@ -134,7 +134,7 @@ module Punchblock
|
|
|
134
134
|
let(:original_command_opts) { { :mode => nil } }
|
|
135
135
|
it "should return an error and not execute any actions" do
|
|
136
136
|
subject.execute
|
|
137
|
-
error = ProtocolError.new.setup 'option error', 'A mode value other than DTMF is unsupported
|
|
137
|
+
error = ProtocolError.new.setup 'option error', 'A mode value other than DTMF is unsupported.'
|
|
138
138
|
original_command.response(0.1).should be == error
|
|
139
139
|
end
|
|
140
140
|
end
|
|
@@ -143,7 +143,7 @@ module Punchblock
|
|
|
143
143
|
let(:original_command_opts) { { :mode => :any } }
|
|
144
144
|
it "should return an error and not execute any actions" do
|
|
145
145
|
subject.execute
|
|
146
|
-
error = ProtocolError.new.setup 'option error', 'A mode value other than DTMF is unsupported
|
|
146
|
+
error = ProtocolError.new.setup 'option error', 'A mode value other than DTMF is unsupported.'
|
|
147
147
|
original_command.response(0.1).should be == error
|
|
148
148
|
end
|
|
149
149
|
end
|
|
@@ -152,7 +152,7 @@ module Punchblock
|
|
|
152
152
|
let(:original_command_opts) { { :mode => :speech } }
|
|
153
153
|
it "should return an error and not execute any actions" do
|
|
154
154
|
subject.execute
|
|
155
|
-
error = ProtocolError.new.setup 'option error', 'A mode value other than DTMF is unsupported
|
|
155
|
+
error = ProtocolError.new.setup 'option error', 'A mode value other than DTMF is unsupported.'
|
|
156
156
|
original_command.response(0.1).should be == error
|
|
157
157
|
end
|
|
158
158
|
end
|
|
@@ -392,7 +392,7 @@ module Punchblock
|
|
|
392
392
|
it 'should be able to look up the call by channel ID' do
|
|
393
393
|
subject.handle_ami_event ami_event
|
|
394
394
|
call_actor = subject.call_for_channel('SIP/1234-00000000')
|
|
395
|
-
call_actor.
|
|
395
|
+
call_actor.should be_a Asterisk::Call
|
|
396
396
|
call_actor.agi_env.should be_a Hash
|
|
397
397
|
call_actor.agi_env.should be == {
|
|
398
398
|
:agi_request => 'async',
|
|
@@ -0,0 +1,922 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
module Punchblock
|
|
6
|
+
module Translator
|
|
7
|
+
class Freeswitch
|
|
8
|
+
describe Call do
|
|
9
|
+
let(:id) { Punchblock.new_uuid }
|
|
10
|
+
let(:stream) { stub_everything 'RubyFS::Stream' }
|
|
11
|
+
let(:media_engine) { :freeswitch }
|
|
12
|
+
let(:default_voice) { :hal }
|
|
13
|
+
let(:translator) { Freeswitch.new stub_everything('Connection::Freeswitch') }
|
|
14
|
+
let(:es_env) do
|
|
15
|
+
{
|
|
16
|
+
:variable_direction => "inbound",
|
|
17
|
+
:variable_uuid => "3f0e1e18-c056-11e1-b099-fffeda3ce54f",
|
|
18
|
+
:variable_session_id => "1",
|
|
19
|
+
:variable_sip_local_network_addr => "109.148.160.137",
|
|
20
|
+
:variable_sip_network_ip => "192.168.1.74",
|
|
21
|
+
:variable_sip_network_port => "59253",
|
|
22
|
+
:variable_sip_received_ip => "192.168.1.74",
|
|
23
|
+
:variable_sip_received_port => "59253",
|
|
24
|
+
:variable_sip_via_protocol => "udp",
|
|
25
|
+
:variable_sip_authorized => "true",
|
|
26
|
+
:variable_sip_number_alias => "1000",
|
|
27
|
+
:variable_sip_auth_username => "1000",
|
|
28
|
+
:variable_sip_auth_realm => "127.0.0.1",
|
|
29
|
+
:variable_number_alias => "1000",
|
|
30
|
+
:variable_user_name => "1000",
|
|
31
|
+
:variable_domain_name => "127.0.0.1",
|
|
32
|
+
:variable_record_stereo => "true",
|
|
33
|
+
:variable_default_gateway => "example.com",
|
|
34
|
+
:variable_default_areacode => "918",
|
|
35
|
+
:variable_transfer_fallback_extension => "operator",
|
|
36
|
+
:variable_toll_allow => "domestic,international,local",
|
|
37
|
+
:variable_accountcode => "1000",
|
|
38
|
+
:variable_user_context => "default",
|
|
39
|
+
:variable_effective_caller_id_name => "Extension 1000",
|
|
40
|
+
:variable_effective_caller_id_number => "1000",
|
|
41
|
+
:variable_outbound_caller_id_name => "FreeSWITCH",
|
|
42
|
+
:variable_outbound_caller_id_number => "0000000000",
|
|
43
|
+
:variable_callgroup => "techsupport",
|
|
44
|
+
:variable_sip_from_user => "1000",
|
|
45
|
+
:variable_sip_from_uri => "1000@127.0.0.1",
|
|
46
|
+
:variable_sip_from_host => "127.0.0.1",
|
|
47
|
+
:variable_sip_from_user_stripped => "1000",
|
|
48
|
+
:variable_sip_from_tag => "1248111553",
|
|
49
|
+
:variable_sofia_profile_name => "internal",
|
|
50
|
+
:variable_sip_full_via => "SIP/2.0/UDP 192.168.1.74:59253;rport=59253;branch=z9hG4bK2021947958",
|
|
51
|
+
:variable_sip_full_from => "<sip:1000@127.0.0.1>;tag=1248111553",
|
|
52
|
+
:variable_sip_full_to => "<sip:10@127.0.0.1>",
|
|
53
|
+
:variable_sip_req_user => "10",
|
|
54
|
+
:variable_sip_req_uri => "10@127.0.0.1",
|
|
55
|
+
:variable_sip_req_host => "127.0.0.1",
|
|
56
|
+
:variable_sip_to_user => "10",
|
|
57
|
+
:variable_sip_to_uri => "10@127.0.0.1",
|
|
58
|
+
:variable_sip_to_host => "127.0.0.1",
|
|
59
|
+
:variable_sip_contact_user => "1000",
|
|
60
|
+
:variable_sip_contact_port => "59253",
|
|
61
|
+
:variable_sip_contact_uri => "1000@192.168.1.74:59253",
|
|
62
|
+
:variable_sip_contact_host => "192.168.1.74",
|
|
63
|
+
:variable_channel_name => "sofia/internal/1000@127.0.0.1",
|
|
64
|
+
:variable_sip_call_id => "1251435211@127.0.0.1",
|
|
65
|
+
:variable_sip_user_agent => "YATE/4.1.0",
|
|
66
|
+
:variable_sip_via_host => "192.168.1.74",
|
|
67
|
+
:variable_sip_via_port => "59253",
|
|
68
|
+
:variable_sip_via_rport => "59253",
|
|
69
|
+
:variable_max_forwards => "20",
|
|
70
|
+
:variable_presence_id => "1000@127.0.0.1",
|
|
71
|
+
:variable_switch_r_sdp => "v=0\r\no=yate 1340801245 1340801245 IN IP4 172.20.10.3\r\ns=SIP Call\r\nc=IN IP4 172.20.10.3\r\nt=0 0\r\nm=audio 25048 RTP/AVP 0 8 11 98 97 102 103 104 105 106 101\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:11 L16/8000\r\na=rtpmap:98 iLBC/8000\r\na=fmtp:98 mode=20\r\na=rtpmap:97 iLBC/8000\r\na=fmtp:97 mode=30\r\na=rtpmap:102 SPEEX/8000\r\na=rtpmap:103 SPEEX/16000\r\na=rtpmap:104 SPEEX/32000\r\na=rtpmap:105 iSAC/16000\r\na=rtpmap:106 iSAC/32000\r\na=rtpmap:101 telephone-event/8000\r\na=ptime:30\r\n",
|
|
72
|
+
:variable_remote_media_ip => "172.20.10.3",
|
|
73
|
+
:variable_remote_media_port => "25048",
|
|
74
|
+
:variable_sip_audio_recv_pt => "0",
|
|
75
|
+
:variable_sip_use_codec_name => "PCMU",
|
|
76
|
+
:variable_sip_use_codec_rate => "8000",
|
|
77
|
+
:variable_sip_use_codec_ptime => "30",
|
|
78
|
+
:variable_read_codec => "PCMU",
|
|
79
|
+
:variable_read_rate => "8000",
|
|
80
|
+
:variable_write_codec => "PCMU",
|
|
81
|
+
:variable_write_rate => "8000",
|
|
82
|
+
:variable_endpoint_disposition => "RECEIVED",
|
|
83
|
+
:variable_call_uuid => "3f0e1e18-c056-11e1-b099-fffeda3ce54f",
|
|
84
|
+
:variable_open => "true",
|
|
85
|
+
:variable_rfc2822_date => "Wed, 27 Jun 2012 13:47:25 +0100",
|
|
86
|
+
:variable_export_vars => "RFC2822_DATE",
|
|
87
|
+
:variable_current_application => "park"
|
|
88
|
+
}
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
let :headers do
|
|
92
|
+
{
|
|
93
|
+
:x_variable_direction => "inbound",
|
|
94
|
+
:x_variable_uuid => "3f0e1e18-c056-11e1-b099-fffeda3ce54f",
|
|
95
|
+
:x_variable_session_id => "1",
|
|
96
|
+
:x_variable_sip_local_network_addr => "109.148.160.137",
|
|
97
|
+
:x_variable_sip_network_ip => "192.168.1.74",
|
|
98
|
+
:x_variable_sip_network_port => "59253",
|
|
99
|
+
:x_variable_sip_received_ip => "192.168.1.74",
|
|
100
|
+
:x_variable_sip_received_port => "59253",
|
|
101
|
+
:x_variable_sip_via_protocol => "udp",
|
|
102
|
+
:x_variable_sip_authorized => "true",
|
|
103
|
+
:x_variable_sip_number_alias => "1000",
|
|
104
|
+
:x_variable_sip_auth_username => "1000",
|
|
105
|
+
:x_variable_sip_auth_realm => "127.0.0.1",
|
|
106
|
+
:x_variable_number_alias => "1000",
|
|
107
|
+
:x_variable_user_name => "1000",
|
|
108
|
+
:x_variable_domain_name => "127.0.0.1",
|
|
109
|
+
:x_variable_record_stereo => "true",
|
|
110
|
+
:x_variable_default_gateway => "example.com",
|
|
111
|
+
:x_variable_default_areacode => "918",
|
|
112
|
+
:x_variable_transfer_fallback_extension => "operator",
|
|
113
|
+
:x_variable_toll_allow => "domestic,international,local",
|
|
114
|
+
:x_variable_accountcode => "1000",
|
|
115
|
+
:x_variable_user_context => "default",
|
|
116
|
+
:x_variable_effective_caller_id_name => "Extension 1000",
|
|
117
|
+
:x_variable_effective_caller_id_number => "1000",
|
|
118
|
+
:x_variable_outbound_caller_id_name => "FreeSWITCH",
|
|
119
|
+
:x_variable_outbound_caller_id_number => "0000000000",
|
|
120
|
+
:x_variable_callgroup => "techsupport",
|
|
121
|
+
:x_variable_sip_from_user => "1000",
|
|
122
|
+
:x_variable_sip_from_uri => "1000@127.0.0.1",
|
|
123
|
+
:x_variable_sip_from_host => "127.0.0.1",
|
|
124
|
+
:x_variable_sip_from_user_stripped => "1000",
|
|
125
|
+
:x_variable_sip_from_tag => "1248111553",
|
|
126
|
+
:x_variable_sofia_profile_name => "internal",
|
|
127
|
+
:x_variable_sip_full_via => "SIP/2.0/UDP 192.168.1.74:59253;rport=59253;branch=z9hG4bK2021947958",
|
|
128
|
+
:x_variable_sip_full_from => "<sip:1000@127.0.0.1>;tag=1248111553",
|
|
129
|
+
:x_variable_sip_full_to => "<sip:10@127.0.0.1>",
|
|
130
|
+
:x_variable_sip_req_user => "10",
|
|
131
|
+
:x_variable_sip_req_uri => "10@127.0.0.1",
|
|
132
|
+
:x_variable_sip_req_host => "127.0.0.1",
|
|
133
|
+
:x_variable_sip_to_user => "10",
|
|
134
|
+
:x_variable_sip_to_uri => "10@127.0.0.1",
|
|
135
|
+
:x_variable_sip_to_host => "127.0.0.1",
|
|
136
|
+
:x_variable_sip_contact_user => "1000",
|
|
137
|
+
:x_variable_sip_contact_port => "59253",
|
|
138
|
+
:x_variable_sip_contact_uri => "1000@192.168.1.74:59253",
|
|
139
|
+
:x_variable_sip_contact_host => "192.168.1.74",
|
|
140
|
+
:x_variable_channel_name => "sofia/internal/1000@127.0.0.1",
|
|
141
|
+
:x_variable_sip_call_id => "1251435211@127.0.0.1",
|
|
142
|
+
:x_variable_sip_user_agent => "YATE/4.1.0",
|
|
143
|
+
:x_variable_sip_via_host => "192.168.1.74",
|
|
144
|
+
:x_variable_sip_via_port => "59253",
|
|
145
|
+
:x_variable_sip_via_rport => "59253",
|
|
146
|
+
:x_variable_max_forwards => "20",
|
|
147
|
+
:x_variable_presence_id => "1000@127.0.0.1",
|
|
148
|
+
:x_variable_switch_r_sdp => "v=0\r\no=yate 1340801245 1340801245 IN IP4 172.20.10.3\r\ns=SIP Call\r\nc=IN IP4 172.20.10.3\r\nt=0 0\r\nm=audio 25048 RTP/AVP 0 8 11 98 97 102 103 104 105 106 101\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:11 L16/8000\r\na=rtpmap:98 iLBC/8000\r\na=fmtp:98 mode=20\r\na=rtpmap:97 iLBC/8000\r\na=fmtp:97 mode=30\r\na=rtpmap:102 SPEEX/8000\r\na=rtpmap:103 SPEEX/16000\r\na=rtpmap:104 SPEEX/32000\r\na=rtpmap:105 iSAC/16000\r\na=rtpmap:106 iSAC/32000\r\na=rtpmap:101 telephone-event/8000\r\na=ptime:30\r\n",
|
|
149
|
+
:x_variable_remote_media_ip => "172.20.10.3",
|
|
150
|
+
:x_variable_remote_media_port => "25048",
|
|
151
|
+
:x_variable_sip_audio_recv_pt => "0",
|
|
152
|
+
:x_variable_sip_use_codec_name => "PCMU",
|
|
153
|
+
:x_variable_sip_use_codec_rate => "8000",
|
|
154
|
+
:x_variable_sip_use_codec_ptime => "30",
|
|
155
|
+
:x_variable_read_codec => "PCMU",
|
|
156
|
+
:x_variable_read_rate => "8000",
|
|
157
|
+
:x_variable_write_codec => "PCMU",
|
|
158
|
+
:x_variable_write_rate => "8000",
|
|
159
|
+
:x_variable_endpoint_disposition => "RECEIVED",
|
|
160
|
+
:x_variable_call_uuid => "3f0e1e18-c056-11e1-b099-fffeda3ce54f",
|
|
161
|
+
:x_variable_open => "true",
|
|
162
|
+
:x_variable_rfc2822_date => "Wed, 27 Jun 2012 13:47:25 +0100",
|
|
163
|
+
:x_variable_export_vars => "RFC2822_DATE",
|
|
164
|
+
:x_variable_current_application => "park"
|
|
165
|
+
}
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
subject { Call.new id, translator, es_env, stream, media_engine, default_voice }
|
|
169
|
+
|
|
170
|
+
its(:id) { should be == id }
|
|
171
|
+
its(:translator) { should be translator }
|
|
172
|
+
its(:es_env) { should be == es_env }
|
|
173
|
+
its(:stream) { should be stream }
|
|
174
|
+
its(:media_engine) { should be media_engine }
|
|
175
|
+
|
|
176
|
+
describe '#register_component' do
|
|
177
|
+
it 'should make the component accessible by ID' do
|
|
178
|
+
component_id = 'abc123'
|
|
179
|
+
component = mock 'Translator::Freeswitch::Component', :id => component_id
|
|
180
|
+
subject.register_component component
|
|
181
|
+
subject.component_with_id(component_id).should be component
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
describe '#send_offer' do
|
|
186
|
+
it 'sends an offer to the translator' do
|
|
187
|
+
expected_offer = Punchblock::Event::Offer.new :target_call_id => subject.id,
|
|
188
|
+
:to => "10@127.0.0.1",
|
|
189
|
+
:from => "Extension 1000 <1000@127.0.0.1>",
|
|
190
|
+
:headers => headers
|
|
191
|
+
translator.expects(:handle_pb_event).with expected_offer
|
|
192
|
+
subject.send_offer
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
it 'should make the call identify as inbound' do
|
|
196
|
+
subject.send_offer
|
|
197
|
+
subject.direction.should be == :inbound
|
|
198
|
+
subject.inbound?.should be true
|
|
199
|
+
subject.outbound?.should be false
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
describe "#application" do
|
|
204
|
+
it "should execute a FS application on the current call" do
|
|
205
|
+
stream.expects(:application).once.with(id, 'appname', 'options')
|
|
206
|
+
subject.application 'appname', 'options'
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
describe "#sendmsg" do
|
|
211
|
+
it "should execute a FS sendmsg on the current call" do
|
|
212
|
+
stream.expects(:sendmsg).once.with(id, 'msg', :foo => 'bar')
|
|
213
|
+
subject.sendmsg 'msg', :foo => 'bar'
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
describe "#uuid_foo" do
|
|
218
|
+
it "should execute a FS uuid_* on the current call using bgapi" do
|
|
219
|
+
stream.expects(:bgapi).once.with("uuid_record #{id} blah.mp3")
|
|
220
|
+
subject.uuid_foo 'record', 'blah.mp3'
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
describe '#dial' do
|
|
225
|
+
let(:dial_command_options) { {} }
|
|
226
|
+
|
|
227
|
+
let(:to) { 'sofia/internal/1000' }
|
|
228
|
+
let(:from) { '1001' }
|
|
229
|
+
|
|
230
|
+
let :dial_command do
|
|
231
|
+
Punchblock::Command::Dial.new({:to => to, :from => from}.merge(dial_command_options))
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
before { dial_command.request! }
|
|
235
|
+
|
|
236
|
+
it 'sends an originate bgapi command' do
|
|
237
|
+
stream.expects(:bgapi).once.with "originate {return_ring_ready=true,origination_uuid=#{subject.id},origination_caller_id_number='#{from}'}#{to} &park()"
|
|
238
|
+
subject.dial dial_command
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
context 'with a name and channel in the from field' do
|
|
242
|
+
let(:from_name) { 'Jane Smith' }
|
|
243
|
+
let(:from_number) { '1001' }
|
|
244
|
+
let(:from) { "#{from_name} <#{from_number}>" }
|
|
245
|
+
|
|
246
|
+
it 'sends an originate bgapi command with the cid fields set correctly' do
|
|
247
|
+
stream.expects(:bgapi).once.with "originate {return_ring_ready=true,origination_uuid=#{subject.id},origination_caller_id_number='#{from_number}',origination_caller_id_name='#{from_name}'}#{to} &park()"
|
|
248
|
+
subject.dial dial_command
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
context 'with a timeout specified' do
|
|
253
|
+
let :dial_command_options do
|
|
254
|
+
{ :timeout => 10000 }
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
it 'includes the timeout in the originate command' do
|
|
258
|
+
stream.expects(:bgapi).once.with "originate {return_ring_ready=true,origination_uuid=#{subject.id},origination_caller_id_number='#{from}',originate_timeout=10}#{to} &park()"
|
|
259
|
+
subject.dial dial_command
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
it 'sends the call ID as a response to the Dial' do
|
|
264
|
+
subject.dial dial_command
|
|
265
|
+
dial_command.response
|
|
266
|
+
dial_command.target_call_id.should be == subject.id
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
it 'should make the call identify as outbound' do
|
|
270
|
+
subject.dial dial_command
|
|
271
|
+
subject.direction.should be == :outbound
|
|
272
|
+
subject.outbound?.should be true
|
|
273
|
+
subject.inbound?.should be false
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
describe '#handle_es_event' do
|
|
278
|
+
context 'with a CHANNEL_HANGUP event' do
|
|
279
|
+
let :es_event do
|
|
280
|
+
RubyFS::Event.new nil, :event_name => "CHANNEL_HANGUP",
|
|
281
|
+
:hangup_cause => cause,
|
|
282
|
+
:channel_state => "CS_HANGUP",
|
|
283
|
+
:channel_call_state => "HANGUP",
|
|
284
|
+
:channel_state_number => "10",
|
|
285
|
+
:unique_id => "756bdd8e-c064-11e1-b0ac-fffeda3ce54f",
|
|
286
|
+
:answer_state => "hangup",
|
|
287
|
+
:variable_sip_term_status => "487",
|
|
288
|
+
:variable_proto_specific_hangup_cause => "sip%3A487",
|
|
289
|
+
:variable_sip_term_cause => "487"
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
let(:cause) { 'ORIGINATOR_CANCEL' }
|
|
293
|
+
|
|
294
|
+
it "should cause the actor to be terminated" do
|
|
295
|
+
translator.expects(:handle_pb_event).once
|
|
296
|
+
subject.handle_es_event es_event
|
|
297
|
+
sleep 5.5
|
|
298
|
+
subject.should_not be_alive
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
it "de-registers the call from the translator" do
|
|
302
|
+
translator.stubs :handle_pb_event
|
|
303
|
+
translator.expects(:deregister_call).once.with(subject)
|
|
304
|
+
subject.handle_es_event es_event
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
it "should cause all components to send complete events before sending end event" do
|
|
308
|
+
ssml_doc = RubySpeech::SSML.draw { audio { 'foo.wav' } }
|
|
309
|
+
comp_command = Punchblock::Component::Output.new :ssml => ssml_doc
|
|
310
|
+
comp_command.request!
|
|
311
|
+
component = subject.execute_command comp_command
|
|
312
|
+
comp_command.response(0.1).should be_a Ref
|
|
313
|
+
|
|
314
|
+
expected_complete_event = Punchblock::Event::Complete.new :target_call_id => subject.id, :component_id => component.id
|
|
315
|
+
expected_complete_event.reason = Punchblock::Event::Complete::Hangup.new
|
|
316
|
+
expected_end_event = Punchblock::Event::End.new :reason => :hangup, :target_call_id => subject.id
|
|
317
|
+
|
|
318
|
+
end_sequence = sequence 'end events'
|
|
319
|
+
translator.expects(:handle_pb_event).with(expected_complete_event).once.in_sequence(end_sequence)
|
|
320
|
+
translator.expects(:handle_pb_event).with(expected_end_event).once.in_sequence(end_sequence)
|
|
321
|
+
subject.handle_es_event es_event
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
[
|
|
325
|
+
'NORMAL_CLEARING',
|
|
326
|
+
'ORIGINATOR_CANCEL',
|
|
327
|
+
'SYSTEM_SHUTDOWN',
|
|
328
|
+
'MANAGER_REQUEST',
|
|
329
|
+
'BLIND_TRANSFER',
|
|
330
|
+
'ATTENDED_TRANSFER',
|
|
331
|
+
'PICKED_OFF',
|
|
332
|
+
'NORMAL_UNSPECIFIED'
|
|
333
|
+
].each do |cause|
|
|
334
|
+
context "with a #{cause} cause" do
|
|
335
|
+
let(:cause) { cause }
|
|
336
|
+
|
|
337
|
+
it 'should send an end (hangup) event to the translator' do
|
|
338
|
+
expected_end_event = Punchblock::Event::End.new :reason => :hangup,
|
|
339
|
+
:target_call_id => subject.id
|
|
340
|
+
translator.expects(:handle_pb_event).with expected_end_event
|
|
341
|
+
subject.handle_es_event es_event
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
context "with a user busy cause" do
|
|
347
|
+
let(:cause) { 'USER_BUSY' }
|
|
348
|
+
|
|
349
|
+
it 'should send an end (busy) event to the translator' do
|
|
350
|
+
expected_end_event = Punchblock::Event::End.new :reason => :busy,
|
|
351
|
+
:target_call_id => subject.id
|
|
352
|
+
translator.expects(:handle_pb_event).with expected_end_event
|
|
353
|
+
subject.handle_es_event es_event
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
[
|
|
358
|
+
'NO_USER_RESPONSE',
|
|
359
|
+
'NO_ANSWER',
|
|
360
|
+
'SUBSCRIBER_ABSENT',
|
|
361
|
+
'ALLOTTED_TIMEOUT',
|
|
362
|
+
'MEDIA_TIMEOUT',
|
|
363
|
+
'PROGRESS_TIMEOUT'
|
|
364
|
+
].each do |cause|
|
|
365
|
+
context "with a #{cause} cause" do
|
|
366
|
+
let(:cause) { cause }
|
|
367
|
+
|
|
368
|
+
it 'should send an end (timeout) event to the translator' do
|
|
369
|
+
expected_end_event = Punchblock::Event::End.new :reason => :timeout,
|
|
370
|
+
:target_call_id => subject.id
|
|
371
|
+
translator.expects(:handle_pb_event).with expected_end_event
|
|
372
|
+
subject.handle_es_event es_event
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
[
|
|
378
|
+
'CALL_REJECTED',
|
|
379
|
+
'NUMBER_CHANGED',
|
|
380
|
+
'REDIRECTION_TO_NEW_DESTINATION',
|
|
381
|
+
'FACILITY_REJECTED',
|
|
382
|
+
'NORMAL_CIRCUIT_CONGESTION',
|
|
383
|
+
'SWITCH_CONGESTION',
|
|
384
|
+
'USER_NOT_REGISTERED',
|
|
385
|
+
'FACILITY_NOT_SUBSCRIBED',
|
|
386
|
+
'OUTGOING_CALL_BARRED',
|
|
387
|
+
'INCOMING_CALL_BARRED',
|
|
388
|
+
'BEARERCAPABILITY_NOTAUTH',
|
|
389
|
+
'BEARERCAPABILITY_NOTAVAIL',
|
|
390
|
+
'SERVICE_UNAVAILABLE',
|
|
391
|
+
'BEARERCAPABILITY_NOTIMPL',
|
|
392
|
+
'CHAN_NOT_IMPLEMENTED',
|
|
393
|
+
'FACILITY_NOT_IMPLEMENTED',
|
|
394
|
+
'SERVICE_NOT_IMPLEMENTED'
|
|
395
|
+
].each do |cause|
|
|
396
|
+
context "with a #{cause} cause" do
|
|
397
|
+
let(:cause) { cause }
|
|
398
|
+
|
|
399
|
+
it 'should send an end (reject) event to the translator' do
|
|
400
|
+
expected_end_event = Punchblock::Event::End.new :reason => :reject,
|
|
401
|
+
:target_call_id => subject.id
|
|
402
|
+
translator.expects(:handle_pb_event).with expected_end_event
|
|
403
|
+
subject.handle_es_event es_event
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
[
|
|
409
|
+
"UNSPECIFIED",
|
|
410
|
+
"UNALLOCATED_NUMBER",
|
|
411
|
+
"NO_ROUTE_TRANSIT_NET",
|
|
412
|
+
"NO_ROUTE_DESTINATION",
|
|
413
|
+
"CHANNEL_UNACCEPTABLE",
|
|
414
|
+
"CALL_AWARDED_DELIVERED",
|
|
415
|
+
"EXCHANGE_ROUTING_ERROR",
|
|
416
|
+
"DESTINATION_OUT_OF_ORDER",
|
|
417
|
+
"INVALID_NUMBER_FORMAT",
|
|
418
|
+
"RESPONSE_TO_STATUS_ENQUIRY",
|
|
419
|
+
"NETWORK_OUT_OF_ORDER",
|
|
420
|
+
"NORMAL_TEMPORARY_FAILURE",
|
|
421
|
+
"ACCESS_INFO_DISCARDED",
|
|
422
|
+
"REQUESTED_CHAN_UNAVAIL",
|
|
423
|
+
"PRE_EMPTED",
|
|
424
|
+
"INVALID_CALL_REFERENCE",
|
|
425
|
+
"INCOMPATIBLE_DESTINATION",
|
|
426
|
+
"INVALID_MSG_UNSPECIFIED",
|
|
427
|
+
"MESSAGE_TYPE_NONEXIST",
|
|
428
|
+
"WRONG_MESSAGE",
|
|
429
|
+
"IE_NONEXIST",
|
|
430
|
+
"INVALID_IE_CONTENTS",
|
|
431
|
+
"WRONG_CALL_STATE",
|
|
432
|
+
"RECOVERY_ON_TIMER_EXPIRE",
|
|
433
|
+
"MANDATORY_IE_LENGTH_ERROR",
|
|
434
|
+
"PROTOCOL_ERROR",
|
|
435
|
+
"INTERWORKING",
|
|
436
|
+
"CRASH",
|
|
437
|
+
"LOSE_RACE",
|
|
438
|
+
"USER_CHALLENGE"
|
|
439
|
+
].each do |cause|
|
|
440
|
+
context "with a #{cause} cause" do
|
|
441
|
+
let(:cause) { cause }
|
|
442
|
+
|
|
443
|
+
it 'should send an end (error) event to the translator' do
|
|
444
|
+
expected_end_event = Punchblock::Event::End.new :reason => :error,
|
|
445
|
+
:target_call_id => subject.id
|
|
446
|
+
translator.expects(:handle_pb_event).with expected_end_event
|
|
447
|
+
subject.handle_es_event es_event
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
context 'with an event for a known component' do
|
|
454
|
+
let(:mock_component_node) { mock 'Punchblock::Component::Output' }
|
|
455
|
+
let :component do
|
|
456
|
+
Component::Output.new mock_component_node, subject
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
let(:es_event) do
|
|
460
|
+
RubyFS::Event.new nil, :scope_variable_punchblock_component_id => component.id
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
before do
|
|
464
|
+
subject.register_component component
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
it 'should send the event to the component' do
|
|
468
|
+
component.expects(:handle_es_event).once.with es_event
|
|
469
|
+
subject.handle_es_event es_event
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
context 'with a CHANNEL_STATE event' do
|
|
474
|
+
let :es_event do
|
|
475
|
+
RubyFS::Event.new nil, {
|
|
476
|
+
:event_name => 'CHANNEL_STATE',
|
|
477
|
+
:channel_call_state => channel_call_state
|
|
478
|
+
}
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
context 'ringing' do
|
|
482
|
+
let(:channel_call_state) { 'RINGING' }
|
|
483
|
+
|
|
484
|
+
it 'should send a ringing event' do
|
|
485
|
+
expected_ringing = Punchblock::Event::Ringing.new
|
|
486
|
+
expected_ringing.target_call_id = subject.id
|
|
487
|
+
translator.expects(:handle_pb_event).with expected_ringing
|
|
488
|
+
subject.handle_es_event es_event
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
it '#answered? should return false' do
|
|
492
|
+
subject.handle_es_event es_event
|
|
493
|
+
subject.should_not be_answered
|
|
494
|
+
end
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
context 'something else' do
|
|
498
|
+
let(:channel_call_state) { 'FOO' }
|
|
499
|
+
|
|
500
|
+
it 'should not send a ringing event' do
|
|
501
|
+
translator.expects(:handle_pb_event).never
|
|
502
|
+
subject.handle_es_event es_event
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
it '#answered? should return false' do
|
|
506
|
+
subject.handle_es_event es_event
|
|
507
|
+
subject.should_not be_answered
|
|
508
|
+
end
|
|
509
|
+
end
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
context 'with a CHANNEL_ANSWER event' do
|
|
513
|
+
let :es_event do
|
|
514
|
+
RubyFS::Event.new nil, :event_name => 'CHANNEL_ANSWER'
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
it 'should send an answered event' do
|
|
518
|
+
expected_answered = Punchblock::Event::Answered.new
|
|
519
|
+
expected_answered.target_call_id = subject.id
|
|
520
|
+
translator.expects(:handle_pb_event).with expected_answered
|
|
521
|
+
subject.handle_es_event es_event
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
it '#answered? should be true' do
|
|
525
|
+
subject.handle_es_event es_event
|
|
526
|
+
subject.should be_answered
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
context 'with a handler registered for a matching event' do
|
|
531
|
+
let :es_event do
|
|
532
|
+
RubyFS::Event.new nil, :event_name => 'DTMF'
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
let(:response) { mock 'Response' }
|
|
536
|
+
|
|
537
|
+
it 'should execute the handler' do
|
|
538
|
+
response.expects(:call).once.with es_event
|
|
539
|
+
subject.register_handler :es, :event_name => 'DTMF' do |event|
|
|
540
|
+
response.call event
|
|
541
|
+
end
|
|
542
|
+
subject.handle_es_event es_event
|
|
543
|
+
end
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
context 'with a CHANNEL_BRIDGE event' do
|
|
547
|
+
let(:other_call_id) { Punchblock.new_uuid }
|
|
548
|
+
|
|
549
|
+
let :expected_joined do
|
|
550
|
+
Punchblock::Event::Joined.new.tap do |joined|
|
|
551
|
+
joined.target_call_id = subject.id
|
|
552
|
+
joined.call_id = other_call_id
|
|
553
|
+
end
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
context "where this is the joining call" do
|
|
557
|
+
let :bridge_event do
|
|
558
|
+
RubyFS::Event.new nil, {
|
|
559
|
+
:unique_id => id,
|
|
560
|
+
:event_name => 'CHANNEL_BRIDGE',
|
|
561
|
+
:other_leg_unique_id => other_call_id
|
|
562
|
+
}
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
it "should send a joined event with the correct call ID" do
|
|
566
|
+
translator.expects(:handle_pb_event).with expected_joined
|
|
567
|
+
subject.handle_es_event bridge_event
|
|
568
|
+
end
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
context "where this is the joined call" do
|
|
572
|
+
let :bridge_event do
|
|
573
|
+
RubyFS::Event.new nil, {
|
|
574
|
+
:unique_id => other_call_id,
|
|
575
|
+
:event_name => 'CHANNEL_BRIDGE',
|
|
576
|
+
:other_leg_unique_id => id
|
|
577
|
+
}
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
it "should send a joined event with the correct call ID" do
|
|
581
|
+
translator.expects(:handle_pb_event).with expected_joined
|
|
582
|
+
subject.handle_es_event bridge_event
|
|
583
|
+
end
|
|
584
|
+
end
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
context 'with a CHANNEL_UNBRIDGE event' do
|
|
588
|
+
let(:other_call_id) { Punchblock.new_uuid }
|
|
589
|
+
|
|
590
|
+
let :expected_unjoined do
|
|
591
|
+
Punchblock::Event::Unjoined.new.tap do |joined|
|
|
592
|
+
joined.target_call_id = subject.id
|
|
593
|
+
joined.call_id = other_call_id
|
|
594
|
+
end
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
context "where this is the unjoining call" do
|
|
598
|
+
let :unbridge_event do
|
|
599
|
+
RubyFS::Event.new nil, {
|
|
600
|
+
:unique_id => id,
|
|
601
|
+
:event_name => 'CHANNEL_UNBRIDGE',
|
|
602
|
+
:other_leg_unique_id => other_call_id
|
|
603
|
+
}
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
it "should send a unjoined event with the correct call ID" do
|
|
607
|
+
translator.expects(:handle_pb_event).with expected_unjoined
|
|
608
|
+
subject.handle_es_event unbridge_event
|
|
609
|
+
end
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
context "where this is the joined call" do
|
|
613
|
+
let :unbridge_event do
|
|
614
|
+
RubyFS::Event.new nil, {
|
|
615
|
+
:unique_id => other_call_id,
|
|
616
|
+
:event_name => 'CHANNEL_UNBRIDGE',
|
|
617
|
+
:other_leg_unique_id => id
|
|
618
|
+
}
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
it "should send a unjoined event with the correct call ID" do
|
|
622
|
+
translator.expects(:handle_pb_event).with expected_unjoined
|
|
623
|
+
subject.handle_es_event unbridge_event
|
|
624
|
+
end
|
|
625
|
+
end
|
|
626
|
+
end
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
describe '#execute_command' do
|
|
630
|
+
before do
|
|
631
|
+
command.request!
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
context 'with an accept command' do
|
|
635
|
+
let(:command) { Command::Accept.new }
|
|
636
|
+
|
|
637
|
+
it "should send a respond 180 command and set the command's response" do
|
|
638
|
+
subject.wrapped_object.expects(:application).once.with('respond', '180 Ringing').yields(true)
|
|
639
|
+
subject.execute_command command
|
|
640
|
+
command.response(0.5).should be true
|
|
641
|
+
end
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
context 'with an answer command' do
|
|
645
|
+
let(:command) { Command::Answer.new }
|
|
646
|
+
|
|
647
|
+
it "should execute the answer application and set the command's response" do
|
|
648
|
+
subject
|
|
649
|
+
Punchblock.expects(:new_uuid).once.returns 'abc123'
|
|
650
|
+
subject.wrapped_object.expects(:application).once.with('answer', "%[punchblock_command_id=abc123]")
|
|
651
|
+
subject.should_not be_answered
|
|
652
|
+
subject.execute_command command
|
|
653
|
+
subject.handle_es_event RubyFS::Event.new(nil, :event_name => 'CHANNEL_ANSWER', :scope_variable_punchblock_command_id => 'abc123')
|
|
654
|
+
command.response(0.5).should be true
|
|
655
|
+
subject.should be_answered
|
|
656
|
+
end
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
def expect_hangup_with_reason(reason)
|
|
660
|
+
subject.wrapped_object.expects(:sendmsg).once.with(:call_command => 'hangup', :hangup_cause => reason).yields(true)
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
context 'with a hangup command' do
|
|
664
|
+
let(:command) { Command::Hangup.new }
|
|
665
|
+
|
|
666
|
+
it "should send a hangup message and set the command's response" do
|
|
667
|
+
expect_hangup_with_reason 'NORMAL_CLEARING'
|
|
668
|
+
subject.execute_command command
|
|
669
|
+
command.response(0.5).should be true
|
|
670
|
+
end
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
context 'with a reject command' do
|
|
674
|
+
let(:command) { Command::Reject.new }
|
|
675
|
+
|
|
676
|
+
it "with a :busy reason should send a USER_BUSY hangup command and set the command's response" do
|
|
677
|
+
command.reason = :busy
|
|
678
|
+
expect_hangup_with_reason 'USER_BUSY'
|
|
679
|
+
subject.execute_command command
|
|
680
|
+
command.response(0.5).should be true
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
it "with a :decline reason should send a CALL_REJECTED hangup command and set the command's response" do
|
|
684
|
+
command.reason = :decline
|
|
685
|
+
expect_hangup_with_reason 'CALL_REJECTED'
|
|
686
|
+
subject.execute_command command
|
|
687
|
+
command.response(0.5).should be true
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
it "with an :error reason should send a NORMAL_TEMPORARY_FAILURE hangup command and set the command's response" do
|
|
691
|
+
command.reason = :error
|
|
692
|
+
expect_hangup_with_reason 'NORMAL_TEMPORARY_FAILURE'
|
|
693
|
+
subject.execute_command command
|
|
694
|
+
command.response(0.5).should be true
|
|
695
|
+
end
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
context 'with an Output component' do
|
|
699
|
+
let :command do
|
|
700
|
+
Punchblock::Component::Output.new
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
let(:mock_component) { mock 'Freeswitch::Component::Output', :id => 'foo' }
|
|
704
|
+
|
|
705
|
+
it 'should create an Output component and execute it asynchronously' do
|
|
706
|
+
Component::Output.expects(:new_link).once.with(command, subject).returns mock_component
|
|
707
|
+
mock_component.expects(:execute!).once
|
|
708
|
+
subject.execute_command command
|
|
709
|
+
subject.component_with_id('foo').should be mock_component
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
context 'with the media engine of :flite' do
|
|
713
|
+
let(:media_engine) { :flite }
|
|
714
|
+
|
|
715
|
+
it 'should create a FliteOutput component and execute it asynchronously using flite and the calls default voice' do
|
|
716
|
+
Component::FliteOutput.expects(:new_link).once.with(command, subject).returns mock_component
|
|
717
|
+
mock_component.expects(:execute!).once.with(media_engine, default_voice)
|
|
718
|
+
subject.execute_command command
|
|
719
|
+
subject.component_with_id('foo').should be mock_component
|
|
720
|
+
end
|
|
721
|
+
end
|
|
722
|
+
|
|
723
|
+
context 'with the media engine of :cepstral' do
|
|
724
|
+
let(:media_engine) { :cepstral }
|
|
725
|
+
|
|
726
|
+
it 'should create a TTSOutput component and execute it asynchronously using cepstral and the calls default voice' do
|
|
727
|
+
Component::TTSOutput.expects(:new_link).once.with(command, subject).returns mock_component
|
|
728
|
+
mock_component.expects(:execute!).once.with(media_engine, default_voice)
|
|
729
|
+
subject.execute_command command
|
|
730
|
+
subject.component_with_id('foo').should be mock_component
|
|
731
|
+
end
|
|
732
|
+
end
|
|
733
|
+
|
|
734
|
+
context 'with the media engine of :unimrcp' do
|
|
735
|
+
let(:media_engine) { :unimrcp }
|
|
736
|
+
|
|
737
|
+
it 'should create a TTSOutput component and execute it asynchronously using unimrcp and the calls default voice' do
|
|
738
|
+
Component::TTSOutput.expects(:new_link).once.with(command, subject).returns mock_component
|
|
739
|
+
mock_component.expects(:execute!).once.with(media_engine, default_voice)
|
|
740
|
+
subject.execute_command command
|
|
741
|
+
subject.component_with_id('foo').should be mock_component
|
|
742
|
+
end
|
|
743
|
+
end
|
|
744
|
+
end
|
|
745
|
+
|
|
746
|
+
context 'with an Input component' do
|
|
747
|
+
let :command do
|
|
748
|
+
Punchblock::Component::Input.new
|
|
749
|
+
end
|
|
750
|
+
|
|
751
|
+
let(:mock_component) { mock 'Freeswitch::Component::Input', :id => 'foo' }
|
|
752
|
+
|
|
753
|
+
it 'should create an Input component and execute it asynchronously' do
|
|
754
|
+
Component::Input.expects(:new_link).once.with(command, subject).returns mock_component
|
|
755
|
+
mock_component.expects(:execute!).once
|
|
756
|
+
subject.execute_command command
|
|
757
|
+
end
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
context 'with a Record component' do
|
|
761
|
+
let :command do
|
|
762
|
+
Punchblock::Component::Record.new
|
|
763
|
+
end
|
|
764
|
+
|
|
765
|
+
let(:mock_component) { mock 'Freeswitch::Component::Record', :id => 'foo' }
|
|
766
|
+
|
|
767
|
+
it 'should create a Record component and execute it asynchronously' do
|
|
768
|
+
Component::Record.expects(:new_link).once.with(command, subject).returns mock_component
|
|
769
|
+
mock_component.expects(:execute!).once
|
|
770
|
+
subject.execute_command command
|
|
771
|
+
end
|
|
772
|
+
end
|
|
773
|
+
|
|
774
|
+
context 'with a component command' do
|
|
775
|
+
let(:component_id) { 'foobar' }
|
|
776
|
+
|
|
777
|
+
let :command do
|
|
778
|
+
Punchblock::Component::Stop.new :component_id => component_id
|
|
779
|
+
end
|
|
780
|
+
|
|
781
|
+
let :mock_component do
|
|
782
|
+
mock 'Component', :id => component_id
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
context "for a known component ID" do
|
|
786
|
+
before { subject.register_component mock_component }
|
|
787
|
+
|
|
788
|
+
it 'should send the command to the component for execution' do
|
|
789
|
+
mock_component.expects(:execute_command).once
|
|
790
|
+
subject.execute_command command
|
|
791
|
+
end
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
context "for a component which began executing but crashed" do
|
|
795
|
+
let :component_command do
|
|
796
|
+
Punchblock::Component::Output.new :ssml => RubySpeech::SSML.draw
|
|
797
|
+
end
|
|
798
|
+
|
|
799
|
+
let(:comp_id) { component_command.response.id }
|
|
800
|
+
|
|
801
|
+
let(:subsequent_command) { Punchblock::Component::Stop.new :component_id => comp_id }
|
|
802
|
+
|
|
803
|
+
let :expected_event do
|
|
804
|
+
Punchblock::Event::Complete.new.tap do |e|
|
|
805
|
+
e.target_call_id = subject.id
|
|
806
|
+
e.component_id = comp_id
|
|
807
|
+
e.reason = Punchblock::Event::Complete::Error.new
|
|
808
|
+
end
|
|
809
|
+
end
|
|
810
|
+
|
|
811
|
+
before do
|
|
812
|
+
component_command.request!
|
|
813
|
+
subject.execute_command component_command
|
|
814
|
+
end
|
|
815
|
+
|
|
816
|
+
it 'sends an error in response to the command' do
|
|
817
|
+
component = subject.component_with_id comp_id
|
|
818
|
+
|
|
819
|
+
component.wrapped_object.define_singleton_method(:oops) do
|
|
820
|
+
raise 'Woops, I died'
|
|
821
|
+
end
|
|
822
|
+
|
|
823
|
+
translator.expects(:handle_pb_event).once.with expected_event
|
|
824
|
+
|
|
825
|
+
lambda { component.oops }.should raise_error(/Woops, I died/)
|
|
826
|
+
sleep 0.1
|
|
827
|
+
component.should_not be_alive
|
|
828
|
+
subject.component_with_id(comp_id).should be_nil
|
|
829
|
+
|
|
830
|
+
subsequent_command.request!
|
|
831
|
+
subject.execute_command subsequent_command
|
|
832
|
+
subsequent_command.response.should be == ProtocolError.new.setup(:item_not_found, "Could not find a component with ID #{comp_id} for call #{subject.id}", subject.id, comp_id)
|
|
833
|
+
end
|
|
834
|
+
end
|
|
835
|
+
|
|
836
|
+
context "for an unknown component ID" do
|
|
837
|
+
it 'sends an error in response to the command' do
|
|
838
|
+
subject.execute_command command
|
|
839
|
+
command.response.should be == ProtocolError.new.setup(:item_not_found, "Could not find a component with ID #{component_id} for call #{subject.id}", subject.id, component_id)
|
|
840
|
+
end
|
|
841
|
+
end
|
|
842
|
+
end
|
|
843
|
+
|
|
844
|
+
context 'with a command we do not understand' do
|
|
845
|
+
let :command do
|
|
846
|
+
Punchblock::Command::Mute.new
|
|
847
|
+
end
|
|
848
|
+
|
|
849
|
+
it 'sends an error in response to the command' do
|
|
850
|
+
subject.execute_command command
|
|
851
|
+
command.response.should be == ProtocolError.new.setup('command-not-acceptable', "Did not understand command for call #{subject.id}", subject.id)
|
|
852
|
+
end
|
|
853
|
+
end
|
|
854
|
+
|
|
855
|
+
context "with a join command" do
|
|
856
|
+
let(:other_call_id) { Punchblock.new_uuid }
|
|
857
|
+
|
|
858
|
+
let :command do
|
|
859
|
+
Punchblock::Command::Join.new :call_id => other_call_id
|
|
860
|
+
end
|
|
861
|
+
|
|
862
|
+
it "executes the proper uuid_bridge command" do
|
|
863
|
+
subject.wrapped_object.expects(:uuid_foo).once.with :bridge, other_call_id
|
|
864
|
+
subject.execute_command command
|
|
865
|
+
expect { command.response 1 }.to raise_exception(Timeout::Error)
|
|
866
|
+
end
|
|
867
|
+
|
|
868
|
+
context "subsequently receiving a CHANNEL_BRIDGE event" do
|
|
869
|
+
let :bridge_event do
|
|
870
|
+
RubyFS::Event.new nil, {
|
|
871
|
+
:event_name => 'CHANNEL_BRIDGE',
|
|
872
|
+
:other_leg_unique_id => other_call_id
|
|
873
|
+
}
|
|
874
|
+
end
|
|
875
|
+
|
|
876
|
+
before do
|
|
877
|
+
subject.execute_command command
|
|
878
|
+
end
|
|
879
|
+
|
|
880
|
+
it "should set the command response to true" do
|
|
881
|
+
subject.handle_es_event bridge_event
|
|
882
|
+
command.response.should be_true
|
|
883
|
+
end
|
|
884
|
+
end
|
|
885
|
+
end
|
|
886
|
+
|
|
887
|
+
context "with an unjoin command" do
|
|
888
|
+
let(:other_call_id) { Punchblock.new_uuid }
|
|
889
|
+
|
|
890
|
+
let :command do
|
|
891
|
+
Punchblock::Command::Unjoin.new :call_id => other_call_id
|
|
892
|
+
end
|
|
893
|
+
|
|
894
|
+
it "executes the unjoin via transfer to park" do
|
|
895
|
+
subject.wrapped_object.expects(:uuid_foo).once.with :transfer, '-both park inline'
|
|
896
|
+
subject.execute_command command
|
|
897
|
+
expect { command.response 1 }.to raise_exception(Timeout::Error)
|
|
898
|
+
end
|
|
899
|
+
|
|
900
|
+
context "subsequently receiving a CHANNEL_UNBRIDGE event" do
|
|
901
|
+
let :unbridge_event do
|
|
902
|
+
RubyFS::Event.new nil, {
|
|
903
|
+
:event_name => 'CHANNEL_UNBRIDGE',
|
|
904
|
+
:other_leg_unique_id => other_call_id
|
|
905
|
+
}
|
|
906
|
+
end
|
|
907
|
+
|
|
908
|
+
before do
|
|
909
|
+
subject.execute_command command
|
|
910
|
+
end
|
|
911
|
+
|
|
912
|
+
it "should set the command response to true" do
|
|
913
|
+
subject.handle_es_event unbridge_event
|
|
914
|
+
command.response.should be_true
|
|
915
|
+
end
|
|
916
|
+
end
|
|
917
|
+
end
|
|
918
|
+
end
|
|
919
|
+
end
|
|
920
|
+
end
|
|
921
|
+
end
|
|
922
|
+
end
|