punchblock 1.3.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/CHANGELOG.md +5 -0
  2. data/lib/punchblock.rb +1 -1
  3. data/lib/punchblock/connection.rb +1 -0
  4. data/lib/punchblock/connection/asterisk.rb +0 -1
  5. data/lib/punchblock/connection/freeswitch.rb +49 -0
  6. data/lib/punchblock/event/offer.rb +1 -1
  7. data/lib/punchblock/translator.rb +5 -0
  8. data/lib/punchblock/translator/asterisk.rb +16 -28
  9. data/lib/punchblock/translator/asterisk/call.rb +4 -21
  10. data/lib/punchblock/translator/asterisk/component.rb +0 -5
  11. data/lib/punchblock/translator/asterisk/component/asterisk/agi_command.rb +0 -3
  12. data/lib/punchblock/translator/asterisk/component/asterisk/ami_action.rb +0 -1
  13. data/lib/punchblock/translator/asterisk/component/input.rb +7 -97
  14. data/lib/punchblock/translator/asterisk/component/output.rb +0 -4
  15. data/lib/punchblock/translator/asterisk/component/record.rb +0 -2
  16. data/lib/punchblock/translator/freeswitch.rb +153 -0
  17. data/lib/punchblock/translator/freeswitch/call.rb +265 -0
  18. data/lib/punchblock/translator/freeswitch/component.rb +92 -0
  19. data/lib/punchblock/translator/freeswitch/component/abstract_output.rb +57 -0
  20. data/lib/punchblock/translator/freeswitch/component/flite_output.rb +17 -0
  21. data/lib/punchblock/translator/freeswitch/component/input.rb +29 -0
  22. data/lib/punchblock/translator/freeswitch/component/output.rb +56 -0
  23. data/lib/punchblock/translator/freeswitch/component/record.rb +79 -0
  24. data/lib/punchblock/translator/freeswitch/component/tts_output.rb +26 -0
  25. data/lib/punchblock/translator/input_component.rb +108 -0
  26. data/lib/punchblock/version.rb +1 -1
  27. data/punchblock.gemspec +3 -2
  28. data/spec/punchblock/connection/freeswitch_spec.rb +90 -0
  29. data/spec/punchblock/translator/asterisk/call_spec.rb +23 -2
  30. data/spec/punchblock/translator/asterisk/component/input_spec.rb +3 -3
  31. data/spec/punchblock/translator/asterisk_spec.rb +1 -1
  32. data/spec/punchblock/translator/freeswitch/call_spec.rb +922 -0
  33. data/spec/punchblock/translator/freeswitch/component/flite_output_spec.rb +279 -0
  34. data/spec/punchblock/translator/freeswitch/component/input_spec.rb +312 -0
  35. data/spec/punchblock/translator/freeswitch/component/output_spec.rb +369 -0
  36. data/spec/punchblock/translator/freeswitch/component/record_spec.rb +373 -0
  37. data/spec/punchblock/translator/freeswitch/component/tts_output_spec.rb +285 -0
  38. data/spec/punchblock/translator/freeswitch/component_spec.rb +118 -0
  39. data/spec/punchblock/translator/freeswitch_spec.rb +597 -0
  40. data/spec/punchblock_spec.rb +11 -0
  41. data/spec/spec_helper.rb +1 -0
  42. metadata +52 -7
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module Punchblock
4
- VERSION = "1.3.0"
4
+ VERSION = "1.4.0"
5
5
  end
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.0"]
31
- s.add_runtime_dependency %q<celluloid>, [">= 0.10.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 <sip:5678>',
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 => 'SIP/1234', :from => 'sip:foo@bar.com'}.merge(dial_command_options))
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 on Asterisk.'
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 on Asterisk.'
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 on Asterisk.'
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.wrapped_object.should be_a Asterisk::Call
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