punchblock 1.3.0 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|