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
@@ -0,0 +1,285 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ module Punchblock
6
+ module Translator
7
+ class Freeswitch
8
+ module Component
9
+ describe TTSOutput do
10
+ let(:connection) do
11
+ mock_connection_with_event_handler do |event|
12
+ original_command.add_event event
13
+ end
14
+ end
15
+ let(:media_engine) { :flite }
16
+ let(:default_voice) { :hal }
17
+ let(:translator) { Punchblock::Translator::Freeswitch.new connection }
18
+ let(:mock_call) { Punchblock::Translator::Freeswitch::Call.new 'foo', translator }
19
+
20
+ let :original_command do
21
+ Punchblock::Component::Output.new command_options
22
+ end
23
+
24
+ let :ssml_doc do
25
+ RubySpeech::SSML.draw do
26
+ say_as(:interpret_as => :cardinal) { 'FOO' }
27
+ end
28
+ end
29
+
30
+ let :command_options do
31
+ { :ssml => ssml_doc }
32
+ end
33
+
34
+ def execute
35
+ subject.execute media_engine, default_voice
36
+ end
37
+
38
+ subject { described_class.new original_command, mock_call }
39
+
40
+ describe '#execute' do
41
+ before { original_command.request! }
42
+ def expect_playback(voice = default_voice)
43
+ subject.wrapped_object.expects(:application).once.with :speak, "#{media_engine}|#{voice}|#{ssml_doc}"
44
+ end
45
+
46
+ let :ssml_doc do
47
+ RubySpeech::SSML.draw do
48
+ audio :src => 'http://foo.com/bar.mp3'
49
+ end
50
+ end
51
+
52
+ let(:command_opts) { {} }
53
+
54
+ let :command_options do
55
+ { :ssml => ssml_doc }.merge(command_opts)
56
+ end
57
+
58
+ let :original_command do
59
+ Punchblock::Component::Output.new command_options
60
+ end
61
+
62
+ describe 'ssml' do
63
+ context 'unset' do
64
+ let(:command_opts) { { :ssml => nil } }
65
+ it "should return an error and not execute any actions" do
66
+ execute
67
+ error = ProtocolError.new.setup 'option error', 'An SSML document is required.'
68
+ original_command.response(0.1).should be == error
69
+ end
70
+ end
71
+
72
+ context 'with an SSML node' do
73
+ it 'should speak the document using the speak application' do
74
+ expect_playback
75
+ execute
76
+ end
77
+
78
+ it 'should send a complete event when the speak finishes' do
79
+ expect_playback.yields true
80
+ execute
81
+ subject.handle_es_event RubyFS::Event.new(nil, :event_name => "CHANNEL_EXECUTE_COMPLETE")
82
+ original_command.complete_event(0.1).reason.should be_a Punchblock::Component::Output::Complete::Success
83
+ end
84
+ end
85
+ end
86
+
87
+ describe 'start-offset' do
88
+ context 'unset' do
89
+ let(:command_opts) { { :start_offset => nil } }
90
+ it 'should not pass any options to Playback' do
91
+ expect_playback
92
+ execute
93
+ end
94
+ end
95
+
96
+ context 'set' do
97
+ let(:command_opts) { { :start_offset => 10 } }
98
+ it "should return an error and not execute any actions" do
99
+ execute
100
+ error = ProtocolError.new.setup 'option error', 'A start_offset value is unsupported.'
101
+ original_command.response(0.1).should be == error
102
+ end
103
+ end
104
+ end
105
+
106
+ describe 'start-paused' do
107
+ context 'false' do
108
+ let(:command_opts) { { :start_paused => false } }
109
+ it 'should not pass any options to Playback' do
110
+ expect_playback
111
+ execute
112
+ end
113
+ end
114
+
115
+ context 'true' do
116
+ let(:command_opts) { { :start_paused => true } }
117
+ it "should return an error and not execute any actions" do
118
+ execute
119
+ error = ProtocolError.new.setup 'option error', 'A start_paused value is unsupported.'
120
+ original_command.response(0.1).should be == error
121
+ end
122
+ end
123
+ end
124
+
125
+ describe 'repeat-interval' do
126
+ context 'unset' do
127
+ let(:command_opts) { { :repeat_interval => nil } }
128
+ it 'should not pass any options to Playback' do
129
+ expect_playback
130
+ execute
131
+ end
132
+ end
133
+
134
+ context 'set' do
135
+ let(:command_opts) { { :repeat_interval => 10 } }
136
+ it "should return an error and not execute any actions" do
137
+ execute
138
+ error = ProtocolError.new.setup 'option error', 'A repeat_interval value is unsupported.'
139
+ original_command.response(0.1).should be == error
140
+ end
141
+ end
142
+ end
143
+
144
+ describe 'repeat-times' do
145
+ context 'unset' do
146
+ let(:command_opts) { { :repeat_times => nil } }
147
+ it 'should not pass any options to Playback' do
148
+ expect_playback
149
+ execute
150
+ end
151
+ end
152
+
153
+ context 'set' do
154
+ let(:command_opts) { { :repeat_times => 2 } }
155
+ it "should return an error and not execute any actions" do
156
+ execute
157
+ error = ProtocolError.new.setup 'option error', 'A repeat_times value is unsupported.'
158
+ original_command.response(0.1).should be == error
159
+ end
160
+ end
161
+ end
162
+
163
+ describe 'max-time' do
164
+ context 'unset' do
165
+ let(:command_opts) { { :max_time => nil } }
166
+ it 'should not pass any options to Playback' do
167
+ expect_playback
168
+ execute
169
+ end
170
+ end
171
+
172
+ context 'set' do
173
+ let(:command_opts) { { :max_time => 30 } }
174
+ it "should return an error and not execute any actions" do
175
+ execute
176
+ error = ProtocolError.new.setup 'option error', 'A max_time value is unsupported.'
177
+ original_command.response(0.1).should be == error
178
+ end
179
+ end
180
+ end
181
+
182
+ describe 'voice' do
183
+ context 'unset' do
184
+ let(:command_opts) { { :voice => nil } }
185
+ it 'should use the default voice' do
186
+ expect_playback
187
+ execute
188
+ end
189
+ end
190
+
191
+ context 'set' do
192
+ let(:command_opts) { { :voice => 'alison' } }
193
+ it "should execute speak with the specified voice" do
194
+ expect_playback 'alison'
195
+ execute
196
+ end
197
+ end
198
+ end
199
+
200
+ describe 'interrupt_on' do
201
+ context "set to nil" do
202
+ let(:command_opts) { { :interrupt_on => nil } }
203
+ it "should not pass any digits to Playback" do
204
+ expect_playback
205
+ execute
206
+ end
207
+ end
208
+
209
+ context "set to :any" do
210
+ let(:command_opts) { { :interrupt_on => :any } }
211
+ it "should return an error and not execute any actions" do
212
+ execute
213
+ error = ProtocolError.new.setup 'option error', 'An interrupt-on value of any is unsupported.'
214
+ original_command.response(0.1).should be == error
215
+ end
216
+ end
217
+
218
+ context "set to :dtmf" do
219
+ let(:command_opts) { { :interrupt_on => :dtmf } }
220
+ it "should return an error and not execute any actions" do
221
+ execute
222
+ error = ProtocolError.new.setup 'option error', 'An interrupt-on value of dtmf is unsupported.'
223
+ original_command.response(0.1).should be == error
224
+ end
225
+ end
226
+
227
+ context "set to :speech" do
228
+ let(:command_opts) { { :interrupt_on => :speech } }
229
+ it "should return an error and not execute any actions" do
230
+ execute
231
+ error = ProtocolError.new.setup 'option error', 'An interrupt-on value of speech is unsupported.'
232
+ original_command.response(0.1).should be == error
233
+ end
234
+ end
235
+ end
236
+ end
237
+
238
+ describe "#execute_command" do
239
+ context "with a command it does not understand" do
240
+ let(:command) { Punchblock::Component::Output::Pause.new }
241
+
242
+ before { command.request! }
243
+
244
+ it "returns a ProtocolError response" do
245
+ subject.execute_command command
246
+ command.response(0.1).should be_a ProtocolError
247
+ end
248
+ end
249
+
250
+ context "with a Stop command" do
251
+ let(:command) { Punchblock::Component::Stop.new }
252
+ let(:reason) { original_command.complete_event(5).reason }
253
+
254
+ before do
255
+ command.request!
256
+ original_command.request!
257
+ original_command.execute!
258
+ end
259
+
260
+ it "sets the command response to true" do
261
+ subject.wrapped_object.expects(:application)
262
+ subject.execute_command command
263
+ command.response(0.1).should be == true
264
+ end
265
+
266
+ it "sends the correct complete event" do
267
+ subject.wrapped_object.expects(:application)
268
+ original_command.should_not be_complete
269
+ subject.execute_command command
270
+ reason.should be_a Punchblock::Event::Complete::Stop
271
+ original_command.should be_complete
272
+ end
273
+
274
+ it "breaks the current dialplan application" do
275
+ subject.wrapped_object.expects(:application).once.with 'break'
276
+ subject.execute_command command
277
+ end
278
+ end
279
+ end
280
+ end
281
+
282
+ end
283
+ end
284
+ end
285
+ end
@@ -0,0 +1,118 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ module Punchblock
6
+ module Translator
7
+ class Freeswitch
8
+ describe Component do
9
+
10
+ end
11
+
12
+ module Component
13
+ describe Component do
14
+ let(:connection) { Punchblock::Connection::Freeswitch.new }
15
+ let(:translator) { connection.translator }
16
+ let(:call) { Punchblock::Translator::Freeswitch::Call.new 'foo', translator }
17
+ let(:command) { Punchblock::Component::Input.new }
18
+
19
+ subject { Component.new command, call }
20
+
21
+ before { command.request! }
22
+
23
+ describe '#handle_es_event' do
24
+ context 'with a handler registered for a matching event' do
25
+ let :es_event do
26
+ RubyFS::Event.new nil, :event_name => 'CHANNEL_EXECUTE'
27
+ end
28
+
29
+ let(:response) { mock 'Response' }
30
+
31
+ it 'should execute the handler' do
32
+ response.expects(:call).once.with es_event
33
+ subject.register_handler :es, :event_name => 'CHANNEL_EXECUTE' do |event|
34
+ response.call event
35
+ end
36
+ subject.handle_es_event es_event
37
+ end
38
+ end
39
+ end
40
+
41
+ describe "#send_event" do
42
+ before { command.execute! }
43
+
44
+ let :event do
45
+ Punchblock::Event::Complete.new
46
+ end
47
+
48
+ let :expected_event do
49
+ Punchblock::Event::Complete.new.tap do |e|
50
+ e.target_call_id = call.id
51
+ e.component_id = subject.id
52
+ end
53
+ end
54
+
55
+ it "should send the event to the connection" do
56
+ connection.expects(:handle_event).once.with expected_event
57
+ subject.send_event event
58
+ end
59
+ end
60
+
61
+ describe "#send_complete_event" do
62
+ before { command.execute! }
63
+
64
+ let(:reason) { Punchblock::Event::Complete::Stop.new }
65
+ let :expected_event do
66
+ Punchblock::Event::Complete.new.tap do |c|
67
+ c.reason = Punchblock::Event::Complete::Stop.new
68
+ end
69
+ end
70
+
71
+ it "should send a complete event with the specified reason" do
72
+ subject.wrapped_object.expects(:send_event).once.with expected_event
73
+ subject.send_complete_event reason
74
+ end
75
+
76
+ it "should cause the actor to be shut down" do
77
+ subject.wrapped_object.stubs(:send_event).returns true
78
+ subject.send_complete_event reason
79
+ sleep 0.2
80
+ subject.should_not be_alive
81
+ end
82
+ end
83
+
84
+ describe "#call_ended" do
85
+ it "should send a complete event with the call hangup reason" do
86
+ subject.wrapped_object.expects(:send_complete_event).once.with Punchblock::Event::Complete::Hangup.new
87
+ subject.call_ended
88
+ end
89
+ end
90
+
91
+ describe "#application" do
92
+ it "should execute a FS application on the current call" do
93
+ call.expects(:application).once.with('appname', "%[punchblock_component_id=#{subject.id}]options")
94
+ subject.application 'appname', 'options'
95
+ end
96
+ end
97
+
98
+ describe '#execute_command' do
99
+ before do
100
+ component_command.request!
101
+ end
102
+
103
+ context 'with a command we do not understand' do
104
+ let :component_command do
105
+ Punchblock::Component::Stop.new :component_id => subject.id
106
+ end
107
+
108
+ it 'sends an error in response to the command' do
109
+ subject.execute_command component_command
110
+ component_command.response.should be == ProtocolError.new.setup('command-not-acceptable', "Did not understand command for component #{subject.id}", call.id, subject.id)
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,597 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ module Punchblock
6
+ module Translator
7
+ describe Freeswitch do
8
+ let(:connection) { mock 'Connection::Freeswitch' }
9
+ let(:media_engine) { :flite }
10
+ let(:default_voice) { :hal }
11
+
12
+ let(:translator) { described_class.new connection, media_engine, default_voice }
13
+ let(:stream) { mock 'RubyFS::Stream' }
14
+
15
+ before { connection.expects(:stream).times(0..1).returns stream }
16
+
17
+ subject { translator }
18
+
19
+ its(:connection) { should be connection }
20
+ its(:stream) { should be stream }
21
+
22
+ describe '#terminate' do
23
+ it "terminates all calls" do
24
+ call = described_class::Call.new 'foo', subject
25
+ subject.register_call call
26
+ subject.terminate
27
+ call.should_not be_alive
28
+ end
29
+ end
30
+
31
+ describe '#execute_command' do
32
+ describe 'with a call command' do
33
+ let(:command) { Command::Answer.new }
34
+ let(:call_id) { 'abc123' }
35
+
36
+ it 'executes the call command' do
37
+ subject.wrapped_object.expects(:execute_call_command).with do |c|
38
+ c.should be command
39
+ c.target_call_id.should be == call_id
40
+ end
41
+ subject.execute_command command, :call_id => call_id
42
+ end
43
+ end
44
+
45
+ describe 'with a global component command' do
46
+ let(:command) { Component::Stop.new }
47
+ let(:component_id) { '123abc' }
48
+
49
+ it 'executes the component command' do
50
+ subject.wrapped_object.expects(:execute_component_command).with do |c|
51
+ c.should be command
52
+ c.component_id.should be == component_id
53
+ end
54
+ subject.execute_command command, :component_id => component_id
55
+ end
56
+ end
57
+
58
+ describe 'with a global command' do
59
+ let(:command) { Command::Dial.new }
60
+
61
+ it 'executes the command directly' do
62
+ subject.wrapped_object.expects(:execute_global_command).with command
63
+ subject.execute_command command
64
+ end
65
+ end
66
+ end
67
+
68
+ describe '#register_call' do
69
+ let(:call_id) { 'abc123' }
70
+ let(:call) { described_class::Call.new call_id, subject }
71
+
72
+ before do
73
+ subject.register_call call
74
+ end
75
+
76
+ it 'should make the call accessible by ID' do
77
+ subject.call_with_id(call_id).should be call
78
+ end
79
+ end
80
+
81
+ describe '#deregister_call' do
82
+ let(:call_id) { 'abc123' }
83
+ let(:call) { described_class::Call.new call_id, subject }
84
+
85
+ before do
86
+ subject.register_call call
87
+ end
88
+
89
+ it 'should make the call inaccessible by ID' do
90
+ subject.call_with_id(call_id).should be call
91
+ subject.deregister_call call
92
+ subject.call_with_id(call_id).should be_nil
93
+ end
94
+ end
95
+
96
+ describe '#register_component' do
97
+ let(:component_id) { 'abc123' }
98
+ let(:component) { mock 'Foo', :id => component_id }
99
+
100
+ it 'should make the component accessible by ID' do
101
+ subject.register_component component
102
+ subject.component_with_id(component_id).should be component
103
+ end
104
+ end
105
+
106
+ describe '#execute_call_command' do
107
+ let(:call_id) { 'abc123' }
108
+ let(:command) { Command::Answer.new.tap { |c| c.target_call_id = call_id } }
109
+
110
+ context "with a known call ID" do
111
+ let(:call) { described_class::Call.new 'SIP/foo', subject }
112
+
113
+ before do
114
+ command.request!
115
+ call.stubs(:id).returns call_id
116
+ subject.register_call call
117
+ end
118
+
119
+ it 'sends the command to the call for execution' do
120
+ call.expects(:execute_command!).once.with command
121
+ subject.execute_call_command command
122
+ end
123
+ end
124
+
125
+ let :end_error_event do
126
+ Punchblock::Event::End.new.tap do |e|
127
+ e.target_call_id = call_id
128
+ e.reason = :error
129
+ end
130
+ end
131
+
132
+ context "for an outgoing call which began executing but crashed" do
133
+ let(:dial_command) { Command::Dial.new :to => 'SIP/1234', :from => 'abc123' }
134
+
135
+ let(:call_id) { dial_command.response.id }
136
+
137
+ before do
138
+ stream.stub_everything
139
+ subject.execute_command dial_command
140
+ end
141
+
142
+ it 'sends an error in response to the command' do
143
+ call = subject.call_with_id call_id
144
+
145
+ call.wrapped_object.define_singleton_method(:oops) do
146
+ raise 'Woops, I died'
147
+ end
148
+
149
+ connection.expects(:handle_event).once.with end_error_event
150
+
151
+ lambda { call.oops }.should raise_error(/Woops, I died/)
152
+ sleep 0.1
153
+ call.should_not be_alive
154
+ subject.call_with_id(call_id).should be_nil
155
+
156
+ command.request!
157
+ subject.execute_call_command command
158
+ command.response.should be == ProtocolError.new.setup(:item_not_found, "Could not find a call with ID #{call_id}", call_id)
159
+ end
160
+ end
161
+
162
+ context "for an incoming call which began executing but crashed" do
163
+ let :es_event do
164
+ RubyFS::Event.new nil, :event_name => 'CHANNEL_PARK', :unique_id => 'abc123'
165
+ end
166
+
167
+ let(:call) { subject.call_with_id('abc123') }
168
+ let(:call_id) { call.id }
169
+
170
+ before do
171
+ connection.expects(:handle_event).at_least(1)
172
+ subject.handle_es_event es_event
173
+ call_id
174
+ end
175
+
176
+ it 'sends an error in response to the command' do
177
+ call.wrapped_object.define_singleton_method(:oops) do
178
+ raise 'Woops, I died'
179
+ end
180
+
181
+ connection.expects(:handle_event).once.with end_error_event
182
+
183
+ lambda { call.oops }.should raise_error(/Woops, I died/)
184
+ sleep 0.1
185
+ call.should_not be_alive
186
+ subject.call_with_id(call_id).should be_nil
187
+
188
+ command.request!
189
+ subject.execute_call_command command
190
+ command.response.should be == ProtocolError.new.setup(:item_not_found, "Could not find a call with ID #{call_id}", call_id)
191
+ end
192
+ end
193
+
194
+ context "with an unknown call ID" do
195
+ it 'sends an error in response to the command' do
196
+ command.request!
197
+ subject.execute_call_command command
198
+ command.response.should be == ProtocolError.new.setup(:item_not_found, "Could not find a call with ID #{call_id}", call_id, nil)
199
+ end
200
+ end
201
+ end
202
+
203
+ describe '#execute_component_command' do
204
+ let(:component_id) { '123abc' }
205
+ let(:component) { mock 'Translator::Freeswitch::Component', :id => component_id }
206
+
207
+ let(:command) { Component::Stop.new.tap { |c| c.component_id = component_id } }
208
+
209
+ before do
210
+ command.request!
211
+ end
212
+
213
+ context 'with a known component ID' do
214
+ before do
215
+ subject.register_component component
216
+ end
217
+
218
+ it 'sends the command to the component for execution' do
219
+ component.expects(:execute_command!).once.with command
220
+ subject.execute_component_command command
221
+ end
222
+ end
223
+
224
+ context "with an unknown component ID" do
225
+ it 'sends an error in response to the command' do
226
+ subject.execute_component_command command
227
+ command.response.should be == ProtocolError.new.setup(:item_not_found, "Could not find a component with ID #{component_id}", nil, component_id)
228
+ end
229
+ end
230
+ end
231
+
232
+ describe '#execute_global_command' do
233
+ context 'with a Dial' do
234
+ let :command do
235
+ Command::Dial.new :to => '1234', :from => 'abc123'
236
+ end
237
+
238
+ let(:id) { Punchblock.new_uuid }
239
+
240
+ before do
241
+ id
242
+ Punchblock.expects(:new_uuid).once.returns id
243
+ command.request!
244
+ stream.stub_everything
245
+ end
246
+
247
+ it 'should be able to look up the call by ID' do
248
+ subject.execute_global_command command
249
+ call = subject.call_with_id id
250
+ call.should be_a Freeswitch::Call
251
+ call.translator.should be subject
252
+ call.stream.should be stream
253
+ call.media_engine.should be media_engine
254
+ call.default_voice.should be default_voice
255
+ end
256
+
257
+ it 'should instruct the call to send a dial' do
258
+ mock_call = stub_everything 'Freeswitch::Call'
259
+ Freeswitch::Call.expects(:new_link).once.returns mock_call
260
+ mock_call.expects(:dial!).once.with command
261
+ subject.execute_global_command command
262
+ end
263
+ end
264
+
265
+ context "with a command we don't understand" do
266
+ let :command do
267
+ Command::Answer.new
268
+ end
269
+
270
+ it 'sends an error in response to the command' do
271
+ subject.execute_command command
272
+ command.response.should be == ProtocolError.new.setup('command-not-acceptable', "Did not understand command")
273
+ end
274
+ end
275
+ end
276
+
277
+ describe '#handle_pb_event' do
278
+ it 'should forward the event to the connection' do
279
+ event = mock 'Punchblock::Event'
280
+ subject.connection.expects(:handle_event).once.with event
281
+ subject.handle_pb_event event
282
+ end
283
+ end
284
+
285
+ describe '#handle_es_event' do
286
+ before { subject.wrapped_object.stubs :handle_pb_event }
287
+
288
+ let(:unique_id) { "3f0e1e18-c056-11e1-b099-fffeda3ce54f" }
289
+
290
+ let :es_env do
291
+ {
292
+ :variable_direction => "inbound",
293
+ :variable_uuid => "3f0e1e18-c056-11e1-b099-fffeda3ce54f",
294
+ :variable_session_id => "1",
295
+ :variable_sip_local_network_addr => "109.148.160.137",
296
+ :variable_sip_network_ip => "192.168.1.74",
297
+ :variable_sip_network_port => "59253",
298
+ :variable_sip_received_ip => "192.168.1.74",
299
+ :variable_sip_received_port => "59253",
300
+ :variable_sip_via_protocol => "udp",
301
+ :variable_sip_authorized => "true",
302
+ :variable_sip_number_alias => "1000",
303
+ :variable_sip_auth_username => "1000",
304
+ :variable_sip_auth_realm => "127.0.0.1",
305
+ :variable_number_alias => "1000",
306
+ :variable_user_name => "1000",
307
+ :variable_domain_name => "127.0.0.1",
308
+ :variable_record_stereo => "true",
309
+ :variable_default_gateway => "example.com",
310
+ :variable_default_areacode => "918",
311
+ :variable_transfer_fallback_extension => "operator",
312
+ :variable_toll_allow => "domestic,international,local",
313
+ :variable_accountcode => "1000",
314
+ :variable_user_context => "default",
315
+ :variable_effective_caller_id_name => "Extension 1000",
316
+ :variable_effective_caller_id_number => "1000",
317
+ :variable_outbound_caller_id_name => "FreeSWITCH",
318
+ :variable_outbound_caller_id_number => "0000000000",
319
+ :variable_callgroup => "techsupport",
320
+ :variable_sip_from_user => "1000",
321
+ :variable_sip_from_uri => "1000@127.0.0.1",
322
+ :variable_sip_from_host => "127.0.0.1",
323
+ :variable_sip_from_user_stripped => "1000",
324
+ :variable_sip_from_tag => "1248111553",
325
+ :variable_sofia_profile_name => "internal",
326
+ :variable_sip_full_via => "SIP/2.0/UDP 192.168.1.74:59253;rport=59253;branch=z9hG4bK2021947958",
327
+ :variable_sip_full_from => "<sip:1000@127.0.0.1>;tag=1248111553",
328
+ :variable_sip_full_to => "<sip:10@127.0.0.1>",
329
+ :variable_sip_req_user => "10",
330
+ :variable_sip_req_uri => "10@127.0.0.1",
331
+ :variable_sip_req_host => "127.0.0.1",
332
+ :variable_sip_to_user => "10",
333
+ :variable_sip_to_uri => "10@127.0.0.1",
334
+ :variable_sip_to_host => "127.0.0.1",
335
+ :variable_sip_contact_user => "1000",
336
+ :variable_sip_contact_port => "59253",
337
+ :variable_sip_contact_uri => "1000@192.168.1.74:59253",
338
+ :variable_sip_contact_host => "192.168.1.74",
339
+ :variable_channel_name => "sofia/internal/1000@127.0.0.1",
340
+ :variable_sip_call_id => "1251435211@127.0.0.1",
341
+ :variable_sip_user_agent => "YATE/4.1.0",
342
+ :variable_sip_via_host => "192.168.1.74",
343
+ :variable_sip_via_port => "59253",
344
+ :variable_sip_via_rport => "59253",
345
+ :variable_max_forwards => "20",
346
+ :variable_presence_id => "1000@127.0.0.1",
347
+ :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",
348
+ :variable_remote_media_ip => "172.20.10.3",
349
+ :variable_remote_media_port => "25048",
350
+ :variable_sip_audio_recv_pt => "0",
351
+ :variable_sip_use_codec_name => "PCMU",
352
+ :variable_sip_use_codec_rate => "8000",
353
+ :variable_sip_use_codec_ptime => "30",
354
+ :variable_read_codec => "PCMU",
355
+ :variable_read_rate => "8000",
356
+ :variable_write_codec => "PCMU",
357
+ :variable_write_rate => "8000",
358
+ :variable_endpoint_disposition => "RECEIVED",
359
+ :variable_call_uuid => "3f0e1e18-c056-11e1-b099-fffeda3ce54f",
360
+ :variable_open => "true",
361
+ :variable_rfc2822_date => "Wed, 27 Jun 2012 13:47:25 +0100",
362
+ :variable_export_vars => "RFC2822_DATE",
363
+ :variable_current_application => "park"
364
+ }
365
+ end
366
+
367
+ let :es_content do
368
+ {
369
+ :event_name => "CHANNEL_PARK",
370
+ :core_uuid => "2ad09a34-c056-11e1-b095-fffeda3ce54f",
371
+ :freeswitch_hostname => "blmbp.home",
372
+ :freeswitch_switchname => "blmbp.home",
373
+ :freeswitch_ipv4 => "192.168.1.74",
374
+ :freeswitch_ipv6 => "%3A%3A1",
375
+ :event_date_local => "2012-06-27%2013%3A47%3A25",
376
+ :event_date_gmt => "Wed,%2027%20Jun%202012%2012%3A47%3A25%20GMT",
377
+ :event_date_timestamp => "1340801245553845",
378
+ :event_calling_file => "switch_ivr.c",
379
+ :event_calling_function => "switch_ivr_park",
380
+ :event_calling_line_number => "879",
381
+ :event_sequence => "485",
382
+ :channel_state => "CS_EXECUTE",
383
+ :channel_call_state => "RINGING",
384
+ :channel_state_number => "4",
385
+ :channel_name => "sofia/internal/1000%40127.0.0.1",
386
+ :unique_id => "3f0e1e18-c056-11e1-b099-fffeda3ce54f",
387
+ :call_direction => "inbound",
388
+ :presence_call_direction => "inbound",
389
+ :channel_hit_dialplan => "true",
390
+ :channel_presence_id => "1000%40127.0.0.1",
391
+ :channel_call_uuid => "3f0e1e18-c056-11e1-b099-fffeda3ce54f",
392
+ :answer_state => "ringing",
393
+ :channel_read_codec_name => "PCMU",
394
+ :channel_read_codec_rate => "8000",
395
+ :channel_read_codec_bit_rate => "64000",
396
+ :channel_write_codec_name => "PCMU",
397
+ :channel_write_codec_rate => "8000",
398
+ :channel_write_codec_bit_rate => "64000",
399
+ :caller_direction => "inbound",
400
+ :caller_username => "1000",
401
+ :caller_dialplan => "XML",
402
+ :caller_caller_id_name => "1000",
403
+ :caller_caller_id_number => "1000",
404
+ :caller_network_addr => "192.168.1.74",
405
+ :caller_ani => "1000",
406
+ :caller_destination_number => "10",
407
+ :caller_unique_id => "3f0e1e18-c056-11e1-b099-fffeda3ce54f",
408
+ :caller_source => "mod_sofia",
409
+ :caller_context => "default",
410
+ :caller_channel_name => "sofia/internal/1000%40127.0.0.1",
411
+ :caller_profile_index => "1",
412
+ :caller_profile_created_time => "1340801245532983",
413
+ :caller_channel_created_time => "1340801245532983",
414
+ :caller_channel_answered_time => "0",
415
+ :caller_channel_progress_time => "0",
416
+ :caller_channel_progress_media_time => "0",
417
+ :caller_channel_hangup_time => "0",
418
+ :caller_channel_transfer_time => "0",
419
+ :caller_screen_bit => "true",
420
+ :caller_privacy_hide_name => "false",
421
+ :caller_privacy_hide_number => "false"
422
+ }.merge es_env
423
+ end
424
+
425
+ let :es_event do
426
+ RubyFS::Event.new nil, es_content
427
+ end
428
+
429
+ it 'should be able to look up the call by ID' do
430
+ subject.handle_es_event es_event
431
+ call = subject.call_with_id unique_id
432
+ call.should be_a Freeswitch::Call
433
+ call.translator.should be subject
434
+ call.stream.should be stream
435
+ call.media_engine.should be media_engine
436
+ call.default_voice.should be default_voice
437
+ call.es_env.should be == {
438
+ :variable_direction => "inbound",
439
+ :variable_uuid => "3f0e1e18-c056-11e1-b099-fffeda3ce54f",
440
+ :variable_session_id => "1",
441
+ :variable_sip_local_network_addr => "109.148.160.137",
442
+ :variable_sip_network_ip => "192.168.1.74",
443
+ :variable_sip_network_port => "59253",
444
+ :variable_sip_received_ip => "192.168.1.74",
445
+ :variable_sip_received_port => "59253",
446
+ :variable_sip_via_protocol => "udp",
447
+ :variable_sip_authorized => "true",
448
+ :variable_sip_number_alias => "1000",
449
+ :variable_sip_auth_username => "1000",
450
+ :variable_sip_auth_realm => "127.0.0.1",
451
+ :variable_number_alias => "1000",
452
+ :variable_user_name => "1000",
453
+ :variable_domain_name => "127.0.0.1",
454
+ :variable_record_stereo => "true",
455
+ :variable_default_gateway => "example.com",
456
+ :variable_default_areacode => "918",
457
+ :variable_transfer_fallback_extension => "operator",
458
+ :variable_toll_allow => "domestic,international,local",
459
+ :variable_accountcode => "1000",
460
+ :variable_user_context => "default",
461
+ :variable_effective_caller_id_name => "Extension 1000",
462
+ :variable_effective_caller_id_number => "1000",
463
+ :variable_outbound_caller_id_name => "FreeSWITCH",
464
+ :variable_outbound_caller_id_number => "0000000000",
465
+ :variable_callgroup => "techsupport",
466
+ :variable_sip_from_user => "1000",
467
+ :variable_sip_from_uri => "1000@127.0.0.1",
468
+ :variable_sip_from_host => "127.0.0.1",
469
+ :variable_sip_from_user_stripped => "1000",
470
+ :variable_sip_from_tag => "1248111553",
471
+ :variable_sofia_profile_name => "internal",
472
+ :variable_sip_full_via => "SIP/2.0/UDP 192.168.1.74:59253;rport=59253;branch=z9hG4bK2021947958",
473
+ :variable_sip_full_from => "<sip:1000@127.0.0.1>;tag=1248111553",
474
+ :variable_sip_full_to => "<sip:10@127.0.0.1>",
475
+ :variable_sip_req_user => "10",
476
+ :variable_sip_req_uri => "10@127.0.0.1",
477
+ :variable_sip_req_host => "127.0.0.1",
478
+ :variable_sip_to_user => "10",
479
+ :variable_sip_to_uri => "10@127.0.0.1",
480
+ :variable_sip_to_host => "127.0.0.1",
481
+ :variable_sip_contact_user => "1000",
482
+ :variable_sip_contact_port => "59253",
483
+ :variable_sip_contact_uri => "1000@192.168.1.74:59253",
484
+ :variable_sip_contact_host => "192.168.1.74",
485
+ :variable_channel_name => "sofia/internal/1000@127.0.0.1",
486
+ :variable_sip_call_id => "1251435211@127.0.0.1",
487
+ :variable_sip_user_agent => "YATE/4.1.0",
488
+ :variable_sip_via_host => "192.168.1.74",
489
+ :variable_sip_via_port => "59253",
490
+ :variable_sip_via_rport => "59253",
491
+ :variable_max_forwards => "20",
492
+ :variable_presence_id => "1000@127.0.0.1",
493
+ :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",
494
+ :variable_remote_media_ip => "172.20.10.3",
495
+ :variable_remote_media_port => "25048",
496
+ :variable_sip_audio_recv_pt => "0",
497
+ :variable_sip_use_codec_name => "PCMU",
498
+ :variable_sip_use_codec_rate => "8000",
499
+ :variable_sip_use_codec_ptime => "30",
500
+ :variable_read_codec => "PCMU",
501
+ :variable_read_rate => "8000",
502
+ :variable_write_codec => "PCMU",
503
+ :variable_write_rate => "8000",
504
+ :variable_endpoint_disposition => "RECEIVED",
505
+ :variable_call_uuid => "3f0e1e18-c056-11e1-b099-fffeda3ce54f",
506
+ :variable_open => "true",
507
+ :variable_rfc2822_date => "Wed, 27 Jun 2012 13:47:25 +0100",
508
+ :variable_export_vars => "RFC2822_DATE",
509
+ :variable_current_application => "park"
510
+ }
511
+ end
512
+
513
+ describe "with a RubyFS::Stream::Connected" do
514
+ let(:es_event) { RubyFS::Stream::Connected.new }
515
+
516
+ it "should send a Punchblock::Connection::Connected event" do
517
+ subject.wrapped_object.expects(:handle_pb_event).once.with(Punchblock::Connection::Connected.new)
518
+ subject.handle_es_event es_event
519
+ end
520
+ end
521
+
522
+ describe "with a RubyFS::Stream::Disconnected" do
523
+ let(:es_event) { RubyFS::Stream::Disconnected.new }
524
+
525
+ it "should not raise an error" do
526
+ subject.handle_es_event es_event
527
+ end
528
+ end
529
+
530
+ describe 'with a CHANNEL_PARK event' do
531
+ it 'should instruct the call to send an offer' do
532
+ mock_call = stub_everything 'Freeswitch::Call'
533
+ Freeswitch::Call.expects(:new).once.returns mock_call
534
+ subject.wrapped_object.expects(:link)
535
+ mock_call.expects(:send_offer!).once
536
+ subject.handle_es_event es_event
537
+ end
538
+
539
+ context 'if a call already exists for a matching ID' do
540
+ let(:call) { Freeswitch::Call.new unique_id, subject }
541
+
542
+ before do
543
+ subject.register_call call
544
+ end
545
+
546
+ it "should not create a new call" do
547
+ Freeswitch::Call.expects(:new).never
548
+ subject.handle_es_event es_event
549
+ end
550
+ end
551
+ end
552
+
553
+ describe 'with an event with an Other-Leg-Unique-ID value' do
554
+ let(:call_a) { Freeswitch::Call.new Punchblock.new_uuid, subject }
555
+ let(:call_b) { Freeswitch::Call.new Punchblock.new_uuid, subject }
556
+
557
+ before do
558
+ subject.register_call call_a
559
+ subject.register_call call_b
560
+ end
561
+
562
+ let :es_event do
563
+ RubyFS::Event.new nil, {
564
+ :unique_id => call_a.id,
565
+ :other_leg_unique_id => call_b.id
566
+ }
567
+ end
568
+
569
+ it "is delivered to the bridging leg" do
570
+ call_a.expects(:handle_es_event!).once.with es_event
571
+ subject.handle_es_event es_event
572
+ end
573
+
574
+ it "is delivered to the other leg" do
575
+ call_b.expects(:handle_es_event!).once.with es_event
576
+ subject.handle_es_event es_event
577
+ end
578
+ end
579
+
580
+ describe 'with an ES event for a known ID' do
581
+ let :call do
582
+ Freeswitch::Call.new unique_id, subject
583
+ end
584
+
585
+ before do
586
+ subject.register_call call
587
+ end
588
+
589
+ it 'sends the ES event to the call' do
590
+ call.expects(:handle_es_event!).once.with es_event
591
+ subject.handle_es_event es_event
592
+ end
593
+ end
594
+ end
595
+ end
596
+ end
597
+ end