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,369 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ module Punchblock
6
+ module Translator
7
+ class Freeswitch
8
+ module Component
9
+ describe Output 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(:translator) { Punchblock::Translator::Freeswitch.new connection }
16
+ let(:mock_call) { Punchblock::Translator::Freeswitch::Call.new 'foo', translator }
17
+
18
+ let :original_command do
19
+ Punchblock::Component::Output.new command_options
20
+ end
21
+
22
+ let :ssml_doc do
23
+ RubySpeech::SSML.draw do
24
+ say_as(:interpret_as => :cardinal) { 'FOO' }
25
+ end
26
+ end
27
+
28
+ let :command_options do
29
+ { :ssml => ssml_doc }
30
+ end
31
+
32
+ subject { Output.new original_command, mock_call }
33
+
34
+ describe '#execute' do
35
+ before { original_command.request! }
36
+ def expect_playback(filename = audio_filename)
37
+ subject.wrapped_object.expects(:application).once.with 'playback', "file_string://#{filename}"
38
+ end
39
+
40
+ let(:audio_filename) { 'http://foo.com/bar.mp3' }
41
+
42
+ let :ssml_doc do
43
+ RubySpeech::SSML.draw do
44
+ audio :src => audio_filename
45
+ end
46
+ end
47
+
48
+ let(:command_opts) { {} }
49
+
50
+ let :command_options do
51
+ { :ssml => ssml_doc }.merge(command_opts)
52
+ end
53
+
54
+ let :original_command do
55
+ Punchblock::Component::Output.new command_options
56
+ end
57
+
58
+ describe 'ssml' do
59
+ context 'unset' do
60
+ let(:command_opts) { { :ssml => nil } }
61
+ it "should return an error and not execute any actions" do
62
+ subject.execute
63
+ error = ProtocolError.new.setup 'option error', 'An SSML document is required.'
64
+ original_command.response(0.1).should be == error
65
+ end
66
+ end
67
+
68
+ context 'with a string (not SSML)' do
69
+ let :command_options do
70
+ { :text => 'Foo Bar' }
71
+ end
72
+
73
+ it "should return an unrenderable document error" do
74
+ subject.execute
75
+ error = ProtocolError.new.setup 'unrenderable document error', 'The provided document could not be rendered.'
76
+ original_command.response(0.1).should be == error
77
+ end
78
+
79
+ context 'with a single text node without spaces' do
80
+ let(:audio_filename) { 'http://foo.com/bar.mp3' }
81
+ let :command_options do
82
+ {
83
+ :ssml => RubySpeech::SSML.draw { string audio_filename }
84
+ }
85
+ end
86
+
87
+ it 'should playback the audio file using the playback application' do
88
+ expect_playback
89
+ subject.execute
90
+ end
91
+ end
92
+ end
93
+
94
+ context 'with a single audio SSML node' do
95
+ let(:audio_filename) { 'http://foo.com/bar.mp3' }
96
+ let :command_options do
97
+ {
98
+ :ssml => RubySpeech::SSML.draw { audio :src => audio_filename }
99
+ }
100
+ end
101
+
102
+ it 'should playback the audio file using the playback application' do
103
+ expect_playback
104
+ subject.execute
105
+ end
106
+
107
+ it 'should send a complete event when the file finishes playback' do
108
+ expect_playback.yields true
109
+ subject.execute
110
+ subject.handle_es_event RubyFS::Event.new(nil, :event_name => "CHANNEL_EXECUTE_COMPLETE", :application_response => 'FILE PLAYED')
111
+ original_command.complete_event(0.1).reason.should be_a Punchblock::Component::Output::Complete::Success
112
+ end
113
+
114
+ context "when playback returns an error" do
115
+ let(:fs_event) { RubyFS::Event.new(nil, :event_name => "CHANNEL_EXECUTE_COMPLETE", :application_response => "PLAYBACK ERROR") }
116
+ let(:complete_reason) { original_command.complete_event(0.1).reason }
117
+
118
+ it "sends a complete event with an error reason" do
119
+ expect_playback.yields true
120
+ subject.execute
121
+ subject.handle_es_event fs_event
122
+ complete_reason.should be_a Punchblock::Event::Complete::Error
123
+ complete_reason.details.should == 'Engine error: PLAYBACK ERROR'
124
+ end
125
+ end
126
+ end
127
+
128
+ context 'with multiple audio SSML nodes' do
129
+ let(:audio_filename1) { 'http://foo.com/bar.mp3' }
130
+ let(:audio_filename2) { 'http://foo.com/baz.mp3' }
131
+ let :command_options do
132
+ {
133
+ :ssml => RubySpeech::SSML.draw do
134
+ audio :src => audio_filename1
135
+ audio :src => audio_filename2
136
+ end
137
+ }
138
+ end
139
+
140
+ it 'should playback all audio files using playback' do
141
+ expect_playback [audio_filename1, audio_filename2].join('!')
142
+ subject.execute
143
+ end
144
+
145
+ it 'should send a complete event when the files finish playback' do
146
+ expect_playback([audio_filename1, audio_filename2].join('!')).yields true
147
+ subject.execute
148
+ subject.handle_es_event RubyFS::Event.new(nil, :event_name => "CHANNEL_EXECUTE_COMPLETE", :application_response => "FILE PLAYED")
149
+ original_command.complete_event(0.1).reason.should be_a Punchblock::Component::Output::Complete::Success
150
+ end
151
+ end
152
+
153
+ context "with an SSML document containing elements other than <audio/>" do
154
+ let :command_options do
155
+ {
156
+ :ssml => RubySpeech::SSML.draw do
157
+ string "Foo Bar"
158
+ end
159
+ }
160
+ end
161
+
162
+ it "should return an unrenderable document error" do
163
+ subject.execute
164
+ error = ProtocolError.new.setup 'unrenderable document error', 'The provided document could not be rendered.'
165
+ original_command.response(0.1).should be == error
166
+ end
167
+ end
168
+ end
169
+
170
+ describe 'start-offset' do
171
+ context 'unset' do
172
+ let(:command_opts) { { :start_offset => nil } }
173
+ it 'should not pass any options to Playback' do
174
+ expect_playback
175
+ subject.execute
176
+ end
177
+ end
178
+
179
+ context 'set' do
180
+ let(:command_opts) { { :start_offset => 10 } }
181
+ it "should return an error and not execute any actions" do
182
+ subject.execute
183
+ error = ProtocolError.new.setup 'option error', 'A start_offset value is unsupported.'
184
+ original_command.response(0.1).should be == error
185
+ end
186
+ end
187
+ end
188
+
189
+ describe 'start-paused' do
190
+ context 'false' do
191
+ let(:command_opts) { { :start_paused => false } }
192
+ it 'should not pass any options to Playback' do
193
+ expect_playback
194
+ subject.execute
195
+ end
196
+ end
197
+
198
+ context 'true' do
199
+ let(:command_opts) { { :start_paused => true } }
200
+ it "should return an error and not execute any actions" do
201
+ subject.execute
202
+ error = ProtocolError.new.setup 'option error', 'A start_paused value is unsupported.'
203
+ original_command.response(0.1).should be == error
204
+ end
205
+ end
206
+ end
207
+
208
+ describe 'repeat-interval' do
209
+ context 'unset' do
210
+ let(:command_opts) { { :repeat_interval => nil } }
211
+ it 'should not pass any options to Playback' do
212
+ expect_playback
213
+ subject.execute
214
+ end
215
+ end
216
+
217
+ context 'set' do
218
+ let(:command_opts) { { :repeat_interval => 10 } }
219
+ it "should return an error and not execute any actions" do
220
+ subject.execute
221
+ error = ProtocolError.new.setup 'option error', 'A repeat_interval value is unsupported.'
222
+ original_command.response(0.1).should be == error
223
+ end
224
+ end
225
+ end
226
+
227
+ describe 'repeat-times' do
228
+ context 'unset' do
229
+ let(:command_opts) { { :repeat_times => nil } }
230
+ it 'should not pass any options to Playback' do
231
+ expect_playback
232
+ subject.execute
233
+ end
234
+ end
235
+
236
+ context 'set' do
237
+ let(:command_opts) { { :repeat_times => 2 } }
238
+ it "should return an error and not execute any actions" do
239
+ subject.execute
240
+ error = ProtocolError.new.setup 'option error', 'A repeat_times value is unsupported.'
241
+ original_command.response(0.1).should be == error
242
+ end
243
+ end
244
+ end
245
+
246
+ describe 'max-time' do
247
+ context 'unset' do
248
+ let(:command_opts) { { :max_time => nil } }
249
+ it 'should not pass any options to Playback' do
250
+ expect_playback
251
+ subject.execute
252
+ end
253
+ end
254
+
255
+ context 'set' do
256
+ let(:command_opts) { { :max_time => 30 } }
257
+ it "should return an error and not execute any actions" do
258
+ subject.execute
259
+ error = ProtocolError.new.setup 'option error', 'A max_time value is unsupported.'
260
+ original_command.response(0.1).should be == error
261
+ end
262
+ end
263
+ end
264
+
265
+ describe 'voice' do
266
+ context 'unset' do
267
+ let(:command_opts) { { :voice => nil } }
268
+ it 'should not pass the v option to Playback' do
269
+ expect_playback
270
+ subject.execute
271
+ end
272
+ end
273
+
274
+ context 'set' do
275
+ let(:command_opts) { { :voice => 'alison' } }
276
+ it "should return an error and not execute any actions" do
277
+ subject.execute
278
+ error = ProtocolError.new.setup 'option error', 'A voice value is unsupported.'
279
+ original_command.response(0.1).should be == error
280
+ end
281
+ end
282
+ end
283
+
284
+ describe 'interrupt_on' do
285
+ context "set to nil" do
286
+ let(:command_opts) { { :interrupt_on => nil } }
287
+ it "should not pass any digits to Playback" do
288
+ expect_playback
289
+ subject.execute
290
+ end
291
+ end
292
+
293
+ context "set to :any" do
294
+ let(:command_opts) { { :interrupt_on => :any } }
295
+ it "should return an error and not execute any actions" do
296
+ subject.execute
297
+ error = ProtocolError.new.setup 'option error', 'An interrupt-on value of any is unsupported.'
298
+ original_command.response(0.1).should be == error
299
+ end
300
+ end
301
+
302
+ context "set to :dtmf" do
303
+ let(:command_opts) { { :interrupt_on => :dtmf } }
304
+ it "should return an error and not execute any actions" do
305
+ subject.execute
306
+ error = ProtocolError.new.setup 'option error', 'An interrupt-on value of dtmf is unsupported.'
307
+ original_command.response(0.1).should be == error
308
+ end
309
+ end
310
+
311
+ context "set to :speech" do
312
+ let(:command_opts) { { :interrupt_on => :speech } }
313
+ it "should return an error and not execute any actions" do
314
+ subject.execute
315
+ error = ProtocolError.new.setup 'option error', 'An interrupt-on value of speech is unsupported.'
316
+ original_command.response(0.1).should be == error
317
+ end
318
+ end
319
+ end
320
+ end
321
+
322
+ describe "#execute_command" do
323
+ context "with a command it does not understand" do
324
+ let(:command) { Punchblock::Component::Output::Pause.new }
325
+
326
+ before { command.request! }
327
+
328
+ it "returns a ProtocolError response" do
329
+ subject.execute_command command
330
+ command.response(0.1).should be_a ProtocolError
331
+ end
332
+ end
333
+
334
+ context "with a Stop command" do
335
+ let(:command) { Punchblock::Component::Stop.new }
336
+ let(:reason) { original_command.complete_event(5).reason }
337
+
338
+ before do
339
+ command.request!
340
+ original_command.request!
341
+ original_command.execute!
342
+ end
343
+
344
+ it "sets the command response to true" do
345
+ subject.wrapped_object.expects(:application)
346
+ subject.execute_command command
347
+ command.response(0.1).should be == true
348
+ end
349
+
350
+ it "sends the correct complete event" do
351
+ subject.wrapped_object.expects(:application)
352
+ original_command.should_not be_complete
353
+ subject.execute_command command
354
+ reason.should be_a Punchblock::Event::Complete::Stop
355
+ original_command.should be_complete
356
+ end
357
+
358
+ it "breaks the current dialplan application" do
359
+ subject.wrapped_object.expects(:application).once.with 'break'
360
+ subject.execute_command command
361
+ end
362
+ end
363
+ end
364
+ end
365
+
366
+ end
367
+ end
368
+ end
369
+ end
@@ -0,0 +1,373 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ module Punchblock
6
+ module Translator
7
+ class Freeswitch
8
+ module Component
9
+ describe Record 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(:id) { Punchblock.new_uuid }
16
+ let(:translator) { Punchblock::Translator::Freeswitch.new connection }
17
+ let(:mock_stream) { mock('RubyFS::Stream') }
18
+ let(:mock_call) { Punchblock::Translator::Freeswitch::Call.new id, translator, nil, mock_stream }
19
+
20
+ let :original_command do
21
+ Punchblock::Component::Record.new command_options
22
+ end
23
+
24
+ let :command_options do
25
+ {}
26
+ end
27
+
28
+ before { mock_stream.stub_everything }
29
+
30
+ subject { Record.new original_command, mock_call }
31
+
32
+ let(:filename) { "#{Record::RECORDING_BASE_PATH}/#{subject.id}.wav" }
33
+
34
+ describe '#execute' do
35
+ let(:reason) { original_command.complete_event(5).reason }
36
+ let(:recording) { original_command.complete_event(5).recording }
37
+
38
+ before { original_command.request! }
39
+
40
+ it "sets command response to a reference to the component" do
41
+ subject.execute
42
+ original_command.response(0.1).should be_a Ref
43
+ original_command.component_id.should be == subject.id
44
+ end
45
+
46
+ it "starts a recording via uuid_record, using the component ID as the filename" do
47
+ mock_call.expects(:uuid_foo).once.with(:record, "start #{filename}")
48
+ subject.execute
49
+ end
50
+
51
+ it "sends a success complete event when the recording ends" do
52
+ full_filename = "file://#{filename}"
53
+ subject.execute
54
+ record_stop_event = RubyFS::Event.new nil, {
55
+ :event_name => 'RECORD_STOP',
56
+ :record_file_path => filename
57
+ }
58
+ mock_call.handle_es_event record_stop_event
59
+ reason.should be_a Punchblock::Component::Record::Complete::Success
60
+ recording.uri.should be == full_filename
61
+ original_command.should be_complete
62
+ end
63
+
64
+ describe 'start_paused' do
65
+ context "set to nil" do
66
+ let(:command_options) { { :start_paused => nil } }
67
+ it "should execute normally" do
68
+ mock_call.expects(:uuid_foo).once
69
+ subject.execute
70
+ original_command.response(0.1).should be_a Ref
71
+ end
72
+ end
73
+
74
+ context "set to false" do
75
+ let(:command_options) { { :start_paused => false } }
76
+ it "should execute normally" do
77
+ mock_call.expects(:uuid_foo).once
78
+ subject.execute
79
+ original_command.response(0.1).should be_a Ref
80
+ end
81
+ end
82
+
83
+ context "set to true" do
84
+ let(:command_options) { { :start_paused => true } }
85
+ it "should return an error and not execute any actions" do
86
+ mock_call.expects(:uuid_foo).never
87
+ subject.execute
88
+ error = ProtocolError.new.setup 'option error', 'A start-paused value of true is unsupported.'
89
+ original_command.response(0.1).should be == error
90
+ end
91
+ end
92
+ end
93
+
94
+ describe 'initial_timeout' do
95
+ context "set to nil" do
96
+ let(:command_options) { { :initial_timeout => nil } }
97
+ it "should execute normally" do
98
+ mock_call.expects(:uuid_foo).once
99
+ subject.execute
100
+ original_command.response(0.1).should be_a Ref
101
+ end
102
+ end
103
+
104
+ context "set to -1" do
105
+ let(:command_options) { { :initial_timeout => -1 } }
106
+ it "should execute normally" do
107
+ mock_call.expects(:uuid_foo).once
108
+ subject.execute
109
+ original_command.response(0.1).should be_a Ref
110
+ end
111
+ end
112
+
113
+ context "set to a positive number" do
114
+ let(:command_options) { { :initial_timeout => 10 } }
115
+ it "should return an error and not execute any actions" do
116
+ mock_call.expects(:uuid_foo).never
117
+ subject.execute
118
+ error = ProtocolError.new.setup 'option error', 'An initial-timeout value is unsupported.'
119
+ original_command.response(0.1).should be == error
120
+ end
121
+ end
122
+ end
123
+
124
+ describe 'final_timeout' do
125
+ context "set to nil" do
126
+ let(:command_options) { { :final_timeout => nil } }
127
+ it "should execute normally" do
128
+ mock_call.expects(:uuid_foo).once
129
+ subject.execute
130
+ original_command.response(0.1).should be_a Ref
131
+ end
132
+ end
133
+
134
+ context "set to -1" do
135
+ let(:command_options) { { :final_timeout => -1 } }
136
+ it "should execute normally" do
137
+ mock_call.expects(:uuid_foo).once
138
+ subject.execute
139
+ original_command.response(0.1).should be_a Ref
140
+ end
141
+ end
142
+
143
+ context "set to a positive number" do
144
+ let(:command_options) { { :final_timeout => 10 } }
145
+ it "should return an error and not execute any actions" do
146
+ mock_call.expects(:send_agi_action!).never
147
+ subject.execute
148
+ error = ProtocolError.new.setup 'option error', 'A final-timeout value is unsupported.'
149
+ original_command.response(0.1).should be == error
150
+ end
151
+ end
152
+ end
153
+
154
+ describe 'format' do
155
+ context "set to nil" do
156
+ let(:command_options) { { :format => nil } }
157
+ it "should execute as 'wav'" do
158
+ mock_call.expects(:uuid_foo).once.with(:record, regexp_matches(/.wav/))
159
+ subject.execute
160
+ original_command.response(0.1).should be_a Ref
161
+ end
162
+
163
+ it "provides the correct filename in the recording" do
164
+ mock_call.expects(:uuid_foo)
165
+ subject.execute
166
+ record_stop_event = RubyFS::Event.new nil, {
167
+ :event_name => 'RECORD_STOP',
168
+ :record_file_path => "#{Record::RECORDING_BASE_PATH}/#{subject.id}.wav"
169
+ }
170
+ mock_call.handle_es_event record_stop_event
171
+ recording.uri.should match(/.*\.wav$/)
172
+ end
173
+ end
174
+
175
+ context "set to 'mp3'" do
176
+ let(:command_options) { { :format => 'mp3' } }
177
+ it "should execute as 'mp3'" do
178
+ mock_call.expects(:uuid_foo).once.with(:record, regexp_matches(/.mp3/))
179
+ subject.execute
180
+ original_command.response(0.1).should be_a Ref
181
+ end
182
+
183
+ it "provides the correct filename in the recording" do
184
+ mock_call.expects(:uuid_foo)
185
+ subject.execute
186
+ record_stop_event = RubyFS::Event.new nil, {
187
+ :event_name => 'RECORD_STOP',
188
+ :record_file_path => "#{Record::RECORDING_BASE_PATH}/#{subject.id}.mp3"
189
+ }
190
+ mock_call.handle_es_event record_stop_event
191
+ recording.uri.should match(/.*\.mp3$/)
192
+ end
193
+ end
194
+ end
195
+
196
+ describe 'start_beep' do
197
+ context "set to nil" do
198
+ let(:command_options) { { :start_beep => nil } }
199
+ it "should execute normally" do
200
+ mock_call.expects(:uuid_foo).once
201
+ subject.execute
202
+ original_command.response(0.1).should be_a Ref
203
+ end
204
+ end
205
+
206
+ context "set to false" do
207
+ let(:command_options) { { :start_beep => false } }
208
+ it "should execute normally" do
209
+ mock_call.expects(:uuid_foo).once
210
+ subject.execute
211
+ original_command.response(0.1).should be_a Ref
212
+ end
213
+ end
214
+
215
+ context "set to true" do
216
+ let(:command_options) { { :start_beep => true } }
217
+
218
+ it "should return an error and not execute any actions" do
219
+ mock_call.expects(:uuid_foo).never
220
+ subject.execute
221
+ error = ProtocolError.new.setup 'option error', 'A start-beep value of true is unsupported.'
222
+ original_command.response(0.1).should be == error
223
+ end
224
+ end
225
+ end
226
+
227
+ describe 'max_duration' do
228
+ context "set to nil" do
229
+ let(:command_options) { { :max_duration => nil } }
230
+ it "should execute normally" do
231
+ mock_call.expects(:uuid_foo).once.with(:record, regexp_matches(/.wav$/))
232
+ subject.execute
233
+ original_command.response(0.1).should be_a Ref
234
+ end
235
+ end
236
+
237
+ context "set to -1" do
238
+ let(:command_options) { { :max_duration => -1 } }
239
+ it "should execute normally" do
240
+ mock_call.expects(:uuid_foo).once.with(:record, regexp_matches(/.wav$/))
241
+ subject.execute
242
+ original_command.response(0.1).should be_a Ref
243
+ end
244
+ end
245
+
246
+ context 'a negative number other than -1' do
247
+ let(:command_options) { { :max_duration => -1000 } }
248
+
249
+ it "should return an error and not execute any actions" do
250
+ subject.execute
251
+ error = ProtocolError.new.setup 'option error', 'A max-duration value that is negative (and not -1) is invalid.'
252
+ original_command.response(0.1).should be == error
253
+ end
254
+ end
255
+
256
+ context 'a positive number' do
257
+ let(:reason) { original_command.complete_event(5).reason }
258
+ let(:recording) { original_command.complete_event(5).recording }
259
+ let(:command_options) { { :max_duration => 1000 } }
260
+
261
+ it "executes the recording with a time limit" do
262
+ mock_call.expects(:uuid_foo).once.with(:record, regexp_matches(/.wav 1$/))
263
+ subject.execute
264
+ original_command.response(0.1).should be_a Ref
265
+ end
266
+ end
267
+ end
268
+ end
269
+
270
+ describe "#execute_command" do
271
+ let(:reason) { original_command.complete_event(5).reason }
272
+ let(:recording) { original_command.complete_event(5).recording }
273
+
274
+ context "with a command it does not understand" do
275
+ let(:command) { Punchblock::Component::Output::Pause.new }
276
+
277
+ before { command.request! }
278
+ it "returns a ProtocolError response" do
279
+ subject.execute_command command
280
+ command.response(0.1).should be_a ProtocolError
281
+ end
282
+ end
283
+
284
+ context "with a Stop command" do
285
+ let(:command) { Punchblock::Component::Stop.new }
286
+
287
+ before do
288
+ mock_call.expects :uuid_foo
289
+ command.request!
290
+ original_command.request!
291
+ subject.execute
292
+ end
293
+
294
+ let :send_stop_event do
295
+ record_stop_event = RubyFS::Event.new nil, {
296
+ :event_name => 'RECORD_STOP',
297
+ :record_file_path => filename
298
+ }
299
+ mock_call.handle_es_event record_stop_event
300
+ end
301
+
302
+ it "sets the command response to true" do
303
+ mock_call.expects :uuid_foo
304
+ subject.execute_command command
305
+ send_stop_event
306
+ command.response(0.1).should be == true
307
+ end
308
+
309
+ it "executes a uuid_record stop command" do
310
+ mock_call.expects(:uuid_foo).with(:record, "stop #{filename}")
311
+ subject.execute_command command
312
+ end
313
+
314
+ it "sends the correct complete event" do
315
+ mock_call.expects(:uuid_foo).with(:record, "stop #{filename}")
316
+ subject.execute_command command
317
+ send_stop_event
318
+ reason.should be_a Punchblock::Event::Complete::Stop
319
+ recording.uri.should be == "file://#{filename}"
320
+ original_command.should be_complete
321
+ end
322
+ end
323
+
324
+ context "with a Pause command" do
325
+ let(:command) { Punchblock::Component::Record::Pause.new }
326
+
327
+ before do
328
+ pending
329
+ mock_call.expects :uuid_foo
330
+ command.request!
331
+ original_command.request!
332
+ subject.execute
333
+ end
334
+
335
+ it "sets the command response to true" do
336
+ subject.execute_command command
337
+ command.response(0.1).should be == true
338
+ end
339
+
340
+ it "pauses the recording via AMI" do
341
+ mock_call.expects(:uuid_foo).once.with('PauseMonitor', 'Channel' => channel)
342
+ subject.execute_command command
343
+ end
344
+ end
345
+
346
+ context "with a Resume command" do
347
+ let(:command) { Punchblock::Component::Record::Resume.new }
348
+
349
+ before do
350
+ pending
351
+ mock_call.expects :uuid_foo
352
+ command.request!
353
+ original_command.request!
354
+ subject.execute
355
+ end
356
+
357
+ it "sets the command response to true" do
358
+ subject.execute_command command
359
+ command.response(0.1).should be == true
360
+ end
361
+
362
+ it "resumes the recording via AMI" do
363
+ mock_call.expects(:uuid_foo).once.with('ResumeMonitor', 'Channel' => channel)
364
+ subject.execute_command command
365
+ end
366
+ end
367
+ end
368
+
369
+ end
370
+ end
371
+ end
372
+ end
373
+ end