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