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.
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