punchblock 2.1.1 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -1
  3. data/CHANGELOG.md +7 -0
  4. data/lib/punchblock.rb +1 -1
  5. data/lib/punchblock/component.rb +2 -0
  6. data/lib/punchblock/component/input.rb +9 -1
  7. data/lib/punchblock/component/output.rb +1 -1
  8. data/lib/punchblock/component/receive_fax.rb +24 -0
  9. data/lib/punchblock/component/send_fax.rb +62 -0
  10. data/lib/punchblock/connection/asterisk.rb +1 -1
  11. data/lib/punchblock/event/complete.rb +15 -1
  12. data/lib/punchblock/translator/asterisk.rb +30 -15
  13. data/lib/punchblock/translator/asterisk/call.rb +13 -27
  14. data/lib/punchblock/translator/asterisk/component.rb +4 -7
  15. data/lib/punchblock/translator/asterisk/component/asterisk/agi_command.rb +1 -5
  16. data/lib/punchblock/translator/asterisk/component/composed_prompt.rb +8 -9
  17. data/lib/punchblock/translator/asterisk/component/input.rb +2 -3
  18. data/lib/punchblock/translator/asterisk/component/mrcp_prompt.rb +9 -9
  19. data/lib/punchblock/translator/asterisk/component/output.rb +134 -39
  20. data/lib/punchblock/translator/asterisk/component/record.rb +2 -3
  21. data/lib/punchblock/translator/asterisk/component/stop_by_redirect.rb +2 -3
  22. data/lib/punchblock/translator/dtmf_recognizer.rb +2 -4
  23. data/lib/punchblock/translator/freeswitch/component/abstract_output.rb +6 -1
  24. data/lib/punchblock/translator/freeswitch/component/flite_output.rb +1 -1
  25. data/lib/punchblock/translator/freeswitch/component/output.rb +12 -10
  26. data/lib/punchblock/translator/freeswitch/component/tts_output.rb +1 -1
  27. data/lib/punchblock/version.rb +1 -1
  28. data/spec/punchblock/component/input_spec.rb +91 -0
  29. data/spec/punchblock/component/output_spec.rb +1 -2
  30. data/spec/punchblock/component/receive_fax_spec.rb +111 -0
  31. data/spec/punchblock/component/send_fax_spec.rb +110 -0
  32. data/spec/punchblock/connection/asterisk_spec.rb +1 -1
  33. data/spec/punchblock/translator/asterisk/call_spec.rb +53 -79
  34. data/spec/punchblock/translator/asterisk/component/asterisk/agi_command_spec.rb +0 -3
  35. data/spec/punchblock/translator/asterisk/component/asterisk/ami_action_spec.rb +1 -1
  36. data/spec/punchblock/translator/asterisk/component/composed_prompt_spec.rb +2 -2
  37. data/spec/punchblock/translator/asterisk/component/input_spec.rb +6 -6
  38. data/spec/punchblock/translator/asterisk/component/mrcp_native_prompt_spec.rb +3 -3
  39. data/spec/punchblock/translator/asterisk/component/mrcp_prompt_spec.rb +9 -11
  40. data/spec/punchblock/translator/asterisk/component/output_spec.rb +902 -28
  41. data/spec/punchblock/translator/asterisk/component/stop_by_redirect_spec.rb +1 -1
  42. data/spec/punchblock/translator/asterisk/component_spec.rb +2 -9
  43. data/spec/punchblock/translator/asterisk_spec.rb +42 -94
  44. data/spec/punchblock/translator/freeswitch/component/flite_output_spec.rb +5 -5
  45. data/spec/punchblock/translator/freeswitch/component/output_spec.rb +7 -3
  46. data/spec/punchblock/translator/freeswitch/component/tts_output_spec.rb +17 -5
  47. metadata +67 -61
@@ -81,7 +81,6 @@ module Punchblock
81
81
  it 'should send the component node false' do
82
82
  subject.execute
83
83
  original_command.response(1).should be_false
84
- subject.should_not be_alive
85
84
  end
86
85
 
87
86
  context "which is 'No such channel'" do
@@ -90,7 +89,6 @@ module Punchblock
90
89
  it "should return an :item_not_found error for the command" do
91
90
  subject.execute
92
91
  original_command.response(0.5).should be == ProtocolError.new.setup(:item_not_found, "Could not find a call with ID #{mock_call.id}", mock_call.id)
93
- subject.should_not be_alive
94
92
  end
95
93
  end
96
94
 
@@ -100,7 +98,6 @@ module Punchblock
100
98
  it "should return an :item_not_found error for the command" do
101
99
  subject.execute
102
100
  original_command.response(0.5).should be == ProtocolError.new.setup(:item_not_found, "Could not find a call with ID #{mock_call.id}", mock_call.id)
103
- subject.should_not be_alive
104
101
  end
105
102
  end
106
103
  end
@@ -58,7 +58,7 @@ module Punchblock
58
58
  context 'for a non-causal action' do
59
59
  it 'should send a complete event to the component node' do
60
60
  ami_client.should_receive(:send_action).once.and_return response
61
- subject.wrapped_object.should_receive(:send_complete_event).once.with expected_complete_reason
61
+ subject.should_receive(:send_complete_event).once.with expected_complete_reason
62
62
  subject.execute
63
63
  end
64
64
  end
@@ -207,7 +207,7 @@ module Punchblock
207
207
  end
208
208
 
209
209
  it "sets the command response to true" do
210
- call.async.should_receive(:redirect_back).once
210
+ call.should_receive(:redirect_back).once
211
211
  subject.execute_command command
212
212
  command.response(0.1).should be == true
213
213
  end
@@ -219,7 +219,7 @@ module Punchblock
219
219
  source_uri: subject.id,
220
220
  target_call_id: call.id
221
221
 
222
- call.async.should_receive(:redirect_back)
222
+ call.should_receive(:redirect_back)
223
223
  subject.execute_command command
224
224
  original_command.should_not be_complete
225
225
  call.process_ami_event ami_event
@@ -96,7 +96,7 @@ module Punchblock
96
96
  end
97
97
 
98
98
  it "should not process further dtmf events" do
99
- subject.async.should_receive(:process_dtmf).never
99
+ subject.should_receive(:process_dtmf).never
100
100
  send_ami_events_for_dtmf 3
101
101
  end
102
102
  end
@@ -266,7 +266,7 @@ module Punchblock
266
266
  end
267
267
 
268
268
  it "should not process further dtmf events" do
269
- subject.async.should_receive(:process_dtmf).never
269
+ subject.should_receive(:process_dtmf).never
270
270
  send_ami_events_for_dtmf 3
271
271
  end
272
272
  end
@@ -331,7 +331,7 @@ module Punchblock
331
331
  let(:original_command_opts) { { :initial_timeout => -1 } }
332
332
 
333
333
  it "should not start a timer" do
334
- subject.wrapped_object.should_receive(:begin_initial_timer).never
334
+ subject.should_receive(:begin_initial_timer).never
335
335
  subject.execute
336
336
  end
337
337
  end
@@ -340,7 +340,7 @@ module Punchblock
340
340
  let(:original_command_opts) { { :initial_timeout => nil } }
341
341
 
342
342
  it "should not start a timer" do
343
- subject.wrapped_object.should_receive(:begin_initial_timer).never
343
+ subject.should_receive(:begin_initial_timer).never
344
344
  subject.execute
345
345
  end
346
346
  end
@@ -450,7 +450,7 @@ module Punchblock
450
450
  let(:original_command_opts) { { :inter_digit_timeout => -1 } }
451
451
 
452
452
  it "should not start a timer" do
453
- subject.wrapped_object.should_receive(:begin_inter_digit_timer).never
453
+ subject.should_receive(:begin_inter_digit_timer).never
454
454
  subject.execute
455
455
  end
456
456
  end
@@ -459,7 +459,7 @@ module Punchblock
459
459
  let(:original_command_opts) { { :inter_digit_timeout => nil } }
460
460
 
461
461
  it "should not start a timer" do
462
- subject.wrapped_object.should_receive(:begin_inter_digit_timer).never
462
+ subject.should_receive(:begin_inter_digit_timer).never
463
463
  subject.execute
464
464
  end
465
465
  end
@@ -623,13 +623,13 @@ module Punchblock
623
623
  end
624
624
 
625
625
  it "sets the command response to true" do
626
- mock_call.async.should_receive(:redirect_back)
626
+ mock_call.should_receive(:redirect_back)
627
627
  subject.execute_command command
628
628
  command.response(0.1).should be == true
629
629
  end
630
630
 
631
631
  it "sends the correct complete event" do
632
- mock_call.async.should_receive(:redirect_back)
632
+ mock_call.should_receive(:redirect_back)
633
633
  subject.execute_command command
634
634
  original_command.should_not be_complete
635
635
  mock_call.process_ami_event ami_event
@@ -638,7 +638,7 @@ module Punchblock
638
638
  end
639
639
 
640
640
  it "redirects the call by unjoining it" do
641
- mock_call.async.should_receive(:redirect_back)
641
+ mock_call.should_receive(:redirect_back)
642
642
  subject.execute_command command
643
643
  end
644
644
  end
@@ -133,22 +133,20 @@ module Punchblock
133
133
  context 'with multiple inline documents' do
134
134
  let(:output_command_options) { { render_documents: [{value: ssml_doc}, {value: ssml_doc}] } }
135
135
 
136
- it "should return a ref and execute SynthAndRecog" do
137
- param = [[ssml_doc.to_doc.to_s, ssml_doc.to_doc.to_s].join(','), grammar.to_doc].map { |o| "\"#{o.to_s.squish.gsub('"', '\"')}\"" }.push('uer=1&b=1').join(',')
138
- mock_call.should_receive(:execute_agi_command).once.with('EXEC SynthAndRecog', param).and_return code: 200, result: 1
136
+ it "should return an error and not execute any actions" do
139
137
  subject.execute
140
- original_command.response(0.1).should be_a Ref
138
+ error = ProtocolError.new.setup 'option error', 'Only one document is allowed.'
139
+ original_command.response(0.1).should be == error
141
140
  end
142
141
  end
143
142
 
144
143
  context 'with multiple documents by URI' do
145
144
  let(:output_command_options) { { render_documents: [{url: 'http://example.com/doc1.ssml'}, {url: 'http://example.com/doc2.ssml'}] } }
146
145
 
147
- it "should return a ref and execute SynthAndRecog" do
148
- param = [['http://example.com/doc1.ssml', 'http://example.com/doc2.ssml'].join(','), grammar.to_doc].map { |o| "\"#{o.to_s.squish.gsub('"', '\"')}\"" }.push('uer=1&b=1').join(',')
149
- mock_call.should_receive(:execute_agi_command).once.with('EXEC SynthAndRecog', param).and_return code: 200, result: 1
146
+ it "should return an error and not execute any actions" do
150
147
  subject.execute
151
- original_command.response(0.1).should be_a Ref
148
+ error = ProtocolError.new.setup 'option error', 'Only one document is allowed.'
149
+ original_command.response(0.1).should be == error
152
150
  end
153
151
  end
154
152
 
@@ -617,13 +615,13 @@ module Punchblock
617
615
  end
618
616
 
619
617
  it "sets the command response to true" do
620
- mock_call.async.should_receive(:redirect_back)
618
+ mock_call.should_receive(:redirect_back)
621
619
  subject.execute_command command
622
620
  command.response(0.1).should be == true
623
621
  end
624
622
 
625
623
  it "sends the correct complete event" do
626
- mock_call.async.should_receive(:redirect_back)
624
+ mock_call.should_receive(:redirect_back)
627
625
  subject.execute_command command
628
626
  original_command.should_not be_complete
629
627
  mock_call.process_ami_event ami_event
@@ -632,7 +630,7 @@ module Punchblock
632
630
  end
633
631
 
634
632
  it "redirects the call by unjoining it" do
635
- mock_call.async.should_receive(:redirect_back)
633
+ mock_call.should_receive(:redirect_back)
636
634
  subject.execute_command command
637
635
  end
638
636
  end
@@ -36,6 +36,13 @@ module Punchblock
36
36
  mock_call.stub(:answered?).and_return(value)
37
37
  end
38
38
 
39
+ def expect_mrcpsynth_with_options(options)
40
+ mock_call.should_receive(:execute_agi_command).once.with do |*args|
41
+ args[0].should be == 'EXEC MRCPSynth'
42
+ args[1].should match options
43
+ end.and_return code: 200, result: 1
44
+ end
45
+
39
46
  describe '#execute' do
40
47
  before { original_command.request! }
41
48
 
@@ -167,6 +174,25 @@ module Punchblock
167
174
  end
168
175
 
169
176
  end
177
+
178
+ describe "with multiple documents" do
179
+ let :first_ssml_doc do
180
+ RubySpeech::SSML.draw do
181
+ audio :src => audio_filename
182
+ end
183
+ end
184
+ let :second_ssml_doc do
185
+ RubySpeech::SSML.draw do
186
+ say_as(:interpret_as => :cardinal) { 'FOO' }
187
+ end
188
+ end
189
+ let(:command_opts) { { render_documents: [{value: first_ssml_doc}, {value: second_ssml_doc}] } }
190
+
191
+ it "executes Swift with a concatenated version of the documents" do
192
+ mock_call.should_receive(:execute_agi_command).once.with 'EXEC Swift', ssml_with_options
193
+ subject.execute
194
+ end
195
+ end
170
196
  end
171
197
 
172
198
  context 'with a renderer of :unimrcp' do
@@ -190,13 +216,6 @@ module Punchblock
190
216
  let(:synthstatus) { 'OK' }
191
217
  before { mock_call.stub(:channel_var).with('SYNTHSTATUS').and_return synthstatus }
192
218
 
193
- def expect_mrcpsynth_with_options(options)
194
- mock_call.should_receive(:execute_agi_command).once.with do |*args|
195
- args[0].should be == 'EXEC MRCPSynth'
196
- args[1].should match options
197
- end.and_return code: 200, result: 1
198
- end
199
-
200
219
  before { expect_answered }
201
220
 
202
221
  it "should execute MRCPSynth" do
@@ -265,10 +284,30 @@ module Punchblock
265
284
 
266
285
  context 'with multiple documents' do
267
286
  let(:command_opts) { { :render_documents => [{:value => ssml_doc}, {:value => ssml_doc}] } }
268
- it "should return an error and not execute any actions" do
287
+
288
+ it "should execute MRCPSynth once with each document" do
289
+ param = ["\"#{ssml_doc.to_s.squish.gsub('"', '\"')}\"", ''].join(',')
290
+ mock_call.should_receive(:execute_agi_command).once.with('EXEC MRCPSynth', param).and_return code: 200, result: 1
291
+ mock_call.should_receive(:execute_agi_command).once.with('EXEC MRCPSynth', param).and_return code: 200, result: 1
269
292
  subject.execute
270
- error = ProtocolError.new.setup 'option error', 'Only a single document is supported.'
271
- original_command.response(0.1).should be == error
293
+ end
294
+
295
+ it 'should not execute further output after a stop command' do
296
+ mock_call.should_receive(:execute_agi_command).once.ordered.and_return do
297
+ sleep 0.5
298
+ end
299
+ latch = CountDownLatch.new 1
300
+ original_command.should_receive(:add_event).once.with do |e|
301
+ e.reason.should be_a Punchblock::Component::Output::Complete::Finish
302
+ latch.countdown!
303
+ end
304
+ Celluloid::Future.new { subject.execute }
305
+ sleep 0.2
306
+ mock_call.should_receive(:redirect_back).ordered
307
+ stop_command = Punchblock::Component::Stop.new
308
+ stop_command.request!
309
+ subject.execute_command stop_command
310
+ latch.wait(2).should be_true
272
311
  end
273
312
  end
274
313
  end
@@ -341,10 +380,40 @@ module Punchblock
341
380
 
342
381
  context 'set' do
343
382
  let(:command_opts) { { :repeat_times => 2 } }
344
- it "should return an error and not execute any actions" do
383
+
384
+ it "should render the specified number of times" do
385
+ 2.times { expect_mrcpsynth_with_options(//) }
345
386
  subject.execute
346
- error = ProtocolError.new.setup 'option error', 'A repeat_times value is unsupported on Asterisk.'
347
- original_command.response(0.1).should be == error
387
+ original_command.complete_event(0.1).reason.should be_a Punchblock::Component::Output::Complete::Finish
388
+ end
389
+
390
+ context 'to 0' do
391
+ let(:command_opts) { { :repeat_times => 0 } }
392
+
393
+ it "should render 10,000 the specified number of times" do
394
+ expect_answered
395
+ 1000.times { expect_mrcpsynth_with_options(//) }
396
+ subject.execute
397
+ original_command.complete_event(0.1).reason.should be_a Punchblock::Component::Output::Complete::Finish
398
+ end
399
+ end
400
+
401
+ it 'should not execute further output after a stop command' do
402
+ mock_call.should_receive(:execute_agi_command).once.ordered.and_return do
403
+ sleep 0.2
404
+ end
405
+ latch = CountDownLatch.new 1
406
+ original_command.should_receive(:add_event).once.with do |e|
407
+ e.reason.should be_a Punchblock::Component::Output::Complete::Finish
408
+ latch.countdown!
409
+ end
410
+ Celluloid::Future.new { subject.execute }
411
+ sleep 0.1
412
+ mock_call.should_receive(:redirect_back).ordered
413
+ stop_command = Punchblock::Component::Stop.new
414
+ stop_command.request!
415
+ subject.execute_command stop_command
416
+ latch.wait(2).should be_true
348
417
  end
349
418
  end
350
419
  end
@@ -655,6 +724,49 @@ module Punchblock
655
724
  original_command.response(0.1).should be == error
656
725
  end
657
726
  end
727
+
728
+ context 'with multiple documents' do
729
+ let(:command_opts) { { render_documents: [{value: ssml_doc}, {value: ssml_doc}] } }
730
+
731
+ it "should render each document in turn using a Playback per document" do
732
+ expect_answered
733
+ 2.times { expect_playback }
734
+ subject.execute
735
+ original_command.complete_event(0.1).reason.should be_a Punchblock::Component::Output::Complete::Finish
736
+ end
737
+
738
+ it 'should not execute further output after a stop command' do
739
+ expect_answered
740
+ mock_call.should_receive(:execute_agi_command).once.ordered.and_return do
741
+ sleep 0.2
742
+ end
743
+ latch = CountDownLatch.new 1
744
+ original_command.should_receive(:add_event).once.with do |e|
745
+ e.reason.should be_a Punchblock::Component::Output::Complete::Finish
746
+ latch.countdown!
747
+ end
748
+ Celluloid::Future.new { subject.execute }
749
+ sleep 0.1
750
+ mock_call.should_receive(:redirect_back).ordered
751
+ stop_command = Punchblock::Component::Stop.new
752
+ stop_command.request!
753
+ subject.execute_command stop_command
754
+ latch.wait(2).should be_true
755
+ end
756
+
757
+ context "when the PLAYBACKSTATUS variable is set to 'FAILED'" do
758
+ let(:playbackstatus) { 'FAILED' }
759
+
760
+ it "should terminate playback and send an error complete event" do
761
+ expect_answered
762
+ mock_call.should_receive(:execute_agi_command).once.and_return code: 200, result: 1
763
+ subject.execute
764
+ complete_reason = original_command.complete_event(0.1).reason
765
+ complete_reason.should be_a Punchblock::Event::Complete::Error
766
+ complete_reason.details.should == "Terminated due to playback error"
767
+ end
768
+ end
769
+ end
658
770
  end
659
771
 
660
772
  describe 'start-offset' do
@@ -729,10 +841,42 @@ module Punchblock
729
841
 
730
842
  context 'set' do
731
843
  let(:command_opts) { { :repeat_times => 2 } }
732
- it "should return an error and not execute any actions" do
844
+
845
+ it "should render the specified number of times" do
846
+ expect_answered
847
+ 2.times { expect_playback }
733
848
  subject.execute
734
- error = ProtocolError.new.setup 'option error', 'A repeat_times value is unsupported on Asterisk.'
735
- original_command.response(0.1).should be == error
849
+ original_command.complete_event(0.1).reason.should be_a Punchblock::Component::Output::Complete::Finish
850
+ end
851
+
852
+ context 'to 0' do
853
+ let(:command_opts) { { :repeat_times => 0 } }
854
+
855
+ it "should render 10,000 the specified number of times" do
856
+ expect_answered
857
+ 1000.times { expect_playback }
858
+ subject.execute
859
+ original_command.complete_event(0.1).reason.should be_a Punchblock::Component::Output::Complete::Finish
860
+ end
861
+ end
862
+
863
+ it 'should not execute further output after a stop command' do
864
+ expect_answered
865
+ mock_call.should_receive(:execute_agi_command).once.ordered.and_return do
866
+ sleep 0.2
867
+ end
868
+ latch = CountDownLatch.new 1
869
+ original_command.should_receive(:add_event).once.with do |e|
870
+ e.reason.should be_a Punchblock::Component::Output::Complete::Finish
871
+ latch.countdown!
872
+ end
873
+ Celluloid::Future.new { subject.execute }
874
+ sleep 0.1
875
+ mock_call.should_receive(:redirect_back).ordered
876
+ stop_command = Punchblock::Component::Stop.new
877
+ stop_command.request!
878
+ subject.execute_command stop_command
879
+ latch.wait(2).should be_true
736
880
  end
737
881
  end
738
882
  end
@@ -804,7 +948,7 @@ module Punchblock
804
948
  it "does not redirect the call" do
805
949
  expect_answered
806
950
  expect_playback
807
- mock_call.async.should_receive(:redirect_back).never
951
+ mock_call.should_receive(:redirect_back).never
808
952
  subject.execute
809
953
  original_command.response(0.1).should be_a Ref
810
954
  send_ami_events_for_dtmf 1
@@ -817,24 +961,24 @@ module Punchblock
817
961
  before do
818
962
  expect_answered
819
963
  mock_call.should_receive(:execute_agi_command).once.with('EXEC Playback', audio_filename)
820
- subject.wrapped_object.should_receive(:send_finish).and_return nil
964
+ subject.should_receive(:send_finish).and_return nil
821
965
  end
822
966
 
823
967
  context "when a DTMF digit is received" do
824
968
  it "sends the correct complete event" do
825
- mock_call.async.should_receive :redirect_back
969
+ mock_call.should_receive :redirect_back
826
970
  subject.execute
827
971
  original_command.response(0.1).should be_a Ref
828
972
  original_command.should_not be_complete
829
973
  send_ami_events_for_dtmf 1
830
- mock_call.async.process_ami_event ami_event
974
+ mock_call.process_ami_event ami_event
831
975
  sleep 0.2
832
976
  original_command.should be_complete
833
977
  reason.should be_a Punchblock::Component::Output::Complete::Finish
834
978
  end
835
979
 
836
980
  it "redirects the call back to async AGI" do
837
- mock_call.async.should_receive(:redirect_back).once
981
+ mock_call.should_receive(:redirect_back).once
838
982
  subject.execute
839
983
  original_command.response(0.1).should be_a Ref
840
984
  send_ami_events_for_dtmf 1
@@ -848,24 +992,24 @@ module Punchblock
848
992
  before do
849
993
  expect_answered
850
994
  mock_call.should_receive(:execute_agi_command).once.with('EXEC Playback', audio_filename)
851
- subject.wrapped_object.should_receive(:send_finish).and_return nil
995
+ subject.should_receive(:send_finish).and_return nil
852
996
  end
853
997
 
854
998
  context "when a DTMF digit is received" do
855
999
  it "sends the correct complete event" do
856
- mock_call.async.should_receive :redirect_back
1000
+ mock_call.should_receive :redirect_back
857
1001
  subject.execute
858
1002
  original_command.response(0.1).should be_a Ref
859
1003
  original_command.should_not be_complete
860
1004
  send_ami_events_for_dtmf 1
861
- mock_call.async.process_ami_event ami_event
1005
+ mock_call.process_ami_event ami_event
862
1006
  sleep 0.2
863
1007
  original_command.should be_complete
864
1008
  reason.should be_a Punchblock::Component::Output::Complete::Finish
865
1009
  end
866
1010
 
867
1011
  it "redirects the call back to async AGI" do
868
- mock_call.async.should_receive(:redirect_back).once
1012
+ mock_call.should_receive(:redirect_back).once
869
1013
  subject.execute
870
1014
  original_command.response(0.1).should be_a Ref
871
1015
  send_ami_events_for_dtmf 1
@@ -884,6 +1028,736 @@ module Punchblock
884
1028
  end
885
1029
  end
886
1030
  end
1031
+
1032
+ context "with a renderer of :native_or_unimrcp" do
1033
+ def expect_playback(filename = audio_filename)
1034
+ mock_call.should_receive(:execute_agi_command).ordered.once.with('EXEC Playback', filename).and_return code: 200
1035
+ end
1036
+
1037
+ def expect_playback_noanswer
1038
+ mock_call.should_receive(:execute_agi_command).once.with('EXEC Playback', audio_filename + ',noanswer').and_return code: 200
1039
+ end
1040
+
1041
+ def expect_mrcpsynth(doc = ssml_doc)
1042
+ mock_call.should_receive(:execute_agi_command).ordered.once.with('EXEC MRCPSynth', ["\"#{doc.to_s.squish.gsub('"', '\"')}\"", ''].join(',')).and_return code: 200, result: 1
1043
+ end
1044
+
1045
+ let(:audio_filename) { 'tt-monkeys' }
1046
+
1047
+ let :ssml_doc do
1048
+ RubySpeech::SSML.draw do
1049
+ audio :src => audio_filename do
1050
+ string "Foobar"
1051
+ end
1052
+ end
1053
+ end
1054
+
1055
+ let(:command_opts) { {} }
1056
+
1057
+ let :command_options do
1058
+ { :render_document => {:value => ssml_doc}, renderer: :native_or_unimrcp }.merge(command_opts)
1059
+ end
1060
+
1061
+ let :original_command do
1062
+ Punchblock::Component::Output.new command_options
1063
+ end
1064
+
1065
+ let(:playbackstatus) { 'SUCCESS' }
1066
+ before { mock_call.stub(:channel_var).with('PLAYBACKSTATUS').and_return playbackstatus }
1067
+
1068
+ describe 'ssml' do
1069
+ context 'unset' do
1070
+ let(:ssml_doc) { nil }
1071
+ it "should return an error and not execute any actions" do
1072
+ subject.execute
1073
+ error = ProtocolError.new.setup 'option error', 'An SSML document is required.'
1074
+ original_command.response(0.1).should be == error
1075
+ end
1076
+ end
1077
+
1078
+ context 'with a single audio SSML node' do
1079
+ let(:audio_filename) { 'tt-monkeys' }
1080
+ let :ssml_doc do
1081
+ RubySpeech::SSML.draw language: 'pt-BR' do
1082
+ audio :src => audio_filename do
1083
+ voice name: 'frank' do
1084
+ string "Hello world"
1085
+ end
1086
+ end
1087
+ end
1088
+ end
1089
+
1090
+ it 'should playback the audio file using Playback' do
1091
+ expect_answered
1092
+ expect_playback
1093
+ subject.execute
1094
+ end
1095
+
1096
+ it 'should send a complete event when the file finishes playback' do
1097
+ def mock_call.answered?
1098
+ true
1099
+ end
1100
+ expect_playback
1101
+ subject.execute
1102
+ original_command.complete_event(0.1).reason.should be_a Punchblock::Component::Output::Complete::Finish
1103
+ end
1104
+
1105
+ context "when the audio filename is prefixed by file://" do
1106
+ let(:audio_filename) { 'file://tt-monkeys' }
1107
+
1108
+ it 'should playback the audio file using Playback' do
1109
+ expect_answered
1110
+ expect_playback 'tt-monkeys'
1111
+ subject.execute
1112
+ end
1113
+ end
1114
+
1115
+ context "when the audio filename has an extension" do
1116
+ let(:audio_filename) { 'tt-monkeys.wav' }
1117
+
1118
+ it 'should playback the audio file using Playback' do
1119
+ expect_answered
1120
+ expect_playback 'tt-monkeys'
1121
+ subject.execute
1122
+ end
1123
+
1124
+ context "when there are other dots in the filename" do
1125
+ let(:audio_filename) { 'blue.tt-monkeys.wav' }
1126
+
1127
+ it 'should playback the audio file using Playback' do
1128
+ expect_answered
1129
+ expect_playback 'blue.tt-monkeys'
1130
+ subject.execute
1131
+ end
1132
+ end
1133
+ end
1134
+
1135
+ context "when we get a RubyAMI Error" do
1136
+ it "should send an error complete event" do
1137
+ expect_answered
1138
+ error = RubyAMI::Error.new.tap { |e| e.message = 'FooBar' }
1139
+ mock_call.should_receive(:execute_agi_command).and_raise error
1140
+ subject.execute
1141
+ complete_reason = original_command.complete_event(0.1).reason
1142
+ complete_reason.should be_a Punchblock::Event::Complete::Error
1143
+ complete_reason.details.should == "Terminated due to AMI error 'FooBar'"
1144
+ end
1145
+ end
1146
+
1147
+ context "when the channel is gone" do
1148
+ it "should send an error complete event" do
1149
+ expect_answered
1150
+ error = ChannelGoneError.new 'FooBar'
1151
+ mock_call.should_receive(:execute_agi_command).and_raise error
1152
+ subject.execute
1153
+ complete_reason = original_command.complete_event(0.1).reason
1154
+ complete_reason.should be_a Punchblock::Event::Complete::Hangup
1155
+ end
1156
+ end
1157
+
1158
+ context "when the PLAYBACKSTATUS variable is set to 'FAILED'" do
1159
+ let(:playbackstatus) { 'FAILED' }
1160
+
1161
+ let(:synthstatus) { 'SUCCESS' }
1162
+ before { mock_call.stub(:channel_var).with('SYNTHSTATUS').and_return synthstatus }
1163
+
1164
+ let :fallback_doc do
1165
+ RubySpeech::SSML.draw language: 'pt-BR' do
1166
+ voice name: 'frank' do
1167
+ string "Hello world"
1168
+ end
1169
+ end
1170
+ end
1171
+
1172
+ it "should attempt to render the children of the audio tag via MRCP and then send a complete event" do
1173
+ expect_answered
1174
+ expect_playback
1175
+ expect_mrcpsynth fallback_doc
1176
+ subject.execute
1177
+ original_command.complete_event(0.1).reason.should be_a Punchblock::Component::Output::Complete::Finish
1178
+ end
1179
+
1180
+ context "and the SYNTHSTATUS variable is set to 'ERROR'" do
1181
+ let(:synthstatus) { 'ERROR' }
1182
+
1183
+ it "should send an error complete event" do
1184
+ expect_answered
1185
+ expect_playback
1186
+ expect_mrcpsynth fallback_doc
1187
+ subject.execute
1188
+ complete_reason = original_command.complete_event(0.1).reason
1189
+ complete_reason.should be_a Punchblock::Event::Complete::Error
1190
+ complete_reason.details.should == "Terminated due to UniMRCP error"
1191
+ end
1192
+ end
1193
+ end
1194
+ end
1195
+
1196
+ context 'with a single text node without spaces' do
1197
+ let(:audio_filename) { 'tt-monkeys' }
1198
+ let :ssml_doc do
1199
+ RubySpeech::SSML.draw { string audio_filename }
1200
+ end
1201
+
1202
+ it 'should playback the audio file using Playback' do
1203
+ expect_answered
1204
+ expect_playback
1205
+ subject.execute
1206
+ end
1207
+
1208
+ it 'should send a complete event when the file finishes playback' do
1209
+ expect_answered
1210
+ expect_playback
1211
+ subject.execute
1212
+ original_command.complete_event(0.1).reason.should be_a Punchblock::Component::Output::Complete::Finish
1213
+ end
1214
+
1215
+ context "when we get a RubyAMI Error" do
1216
+ it "should send an error complete event" do
1217
+ expect_answered
1218
+ error = RubyAMI::Error.new.tap { |e| e.message = 'FooBar' }
1219
+ mock_call.should_receive(:execute_agi_command).and_raise error
1220
+ subject.execute
1221
+ complete_reason = original_command.complete_event(0.1).reason
1222
+ complete_reason.should be_a Punchblock::Event::Complete::Error
1223
+ complete_reason.details.should == "Terminated due to AMI error 'FooBar'"
1224
+ end
1225
+ end
1226
+
1227
+ context "with early media playback" do
1228
+ it "should play the file with Playback" do
1229
+ expect_answered false
1230
+ expect_playback_noanswer
1231
+ mock_call.should_receive(:send_progress)
1232
+ subject.execute
1233
+ end
1234
+
1235
+ context "with interrupt_on set to something that is not nil" do
1236
+ let(:audio_filename) { 'tt-monkeys' }
1237
+ let :command_options do
1238
+ {
1239
+ :render_document => {
1240
+ :value => RubySpeech::SSML.draw { string audio_filename },
1241
+ },
1242
+ :interrupt_on => :any
1243
+ }
1244
+ end
1245
+ it "should return an error when the output is interruptible and it is early media" do
1246
+ expect_answered false
1247
+ error = ProtocolError.new.setup 'option error', 'Interrupt digits are not allowed with early media.'
1248
+ subject.execute
1249
+ original_command.response(0.1).should be == error
1250
+ end
1251
+ end
1252
+ end
1253
+ end
1254
+
1255
+ context 'with multiple audio SSML nodes' do
1256
+ let(:audio_filename1) { 'foo' }
1257
+ let(:audio_filename2) { 'bar' }
1258
+ let(:audio_filename3) { 'baz' }
1259
+ let :ssml_doc do
1260
+ RubySpeech::SSML.draw do
1261
+ audio :src => audio_filename1 do
1262
+ string "Fallback 1"
1263
+ end
1264
+ audio :src => audio_filename2 do
1265
+ string "Fallback 2"
1266
+ end
1267
+ audio :src => audio_filename3 do
1268
+ string "Fallback 3"
1269
+ end
1270
+ end
1271
+ end
1272
+
1273
+ it 'should playback all audio files using Playback' do
1274
+ latch = CountDownLatch.new 2
1275
+ expect_playback audio_filename1
1276
+ expect_playback audio_filename2
1277
+ expect_playback audio_filename3
1278
+ expect_answered
1279
+ subject.execute
1280
+ latch.wait 2
1281
+ sleep 2
1282
+ end
1283
+
1284
+ it 'should send a complete event after the final file has finished playback' do
1285
+ expect_answered
1286
+ expect_playback audio_filename1
1287
+ expect_playback audio_filename2
1288
+ expect_playback audio_filename3
1289
+ latch = CountDownLatch.new 1
1290
+ original_command.should_receive(:add_event).once.with do |e|
1291
+ e.reason.should be_a Punchblock::Component::Output::Complete::Finish
1292
+ latch.countdown!
1293
+ end
1294
+ subject.execute
1295
+ latch.wait(2).should be_true
1296
+ end
1297
+
1298
+ it 'should not execute further output after a stop command' do
1299
+ expect_answered
1300
+ mock_call.should_receive(:execute_agi_command).once.ordered.and_return do
1301
+ sleep 0.2
1302
+ end
1303
+ latch = CountDownLatch.new 1
1304
+ original_command.should_receive(:add_event).once.with do |e|
1305
+ e.reason.should be_a Punchblock::Component::Output::Complete::Finish
1306
+ latch.countdown!
1307
+ end
1308
+ Celluloid::Future.new { subject.execute }
1309
+ sleep 0.1
1310
+ mock_call.should_receive(:redirect_back).ordered
1311
+ stop_command = Punchblock::Component::Stop.new
1312
+ stop_command.request!
1313
+ subject.execute_command stop_command
1314
+ latch.wait(2).should be_true
1315
+ end
1316
+
1317
+ context "when the PLAYBACKSTATUS variable is set to 'FAILED'" do
1318
+ let(:synthstatus) { 'SUCCESS' }
1319
+ before { mock_call.stub(:channel_var).with('PLAYBACKSTATUS').and_return 'SUCCESS', 'FAILED', 'SUCCESS' }
1320
+ before { mock_call.stub(:channel_var).with('SYNTHSTATUS').and_return synthstatus }
1321
+
1322
+ let :fallback_doc do
1323
+ RubySpeech::SSML.draw do
1324
+ string "Fallback 2"
1325
+ end
1326
+ end
1327
+
1328
+ it "should attempt to render the document via MRCP and then send a complete event" do
1329
+ expect_answered
1330
+ expect_playback audio_filename1
1331
+ expect_playback audio_filename2
1332
+ expect_mrcpsynth fallback_doc
1333
+ expect_playback audio_filename3
1334
+ subject.execute
1335
+ original_command.complete_event(0.1).reason.should be_a Punchblock::Component::Output::Complete::Finish
1336
+ end
1337
+
1338
+ context "and the SYNTHSTATUS variable is set to 'ERROR'" do
1339
+ let(:synthstatus) { 'ERROR' }
1340
+
1341
+ it "should terminate playback and send an error complete event" do
1342
+ expect_answered
1343
+ expect_playback audio_filename1
1344
+ expect_playback audio_filename2
1345
+ expect_mrcpsynth fallback_doc
1346
+ subject.execute
1347
+ complete_reason = original_command.complete_event(0.1).reason
1348
+ complete_reason.should be_a Punchblock::Event::Complete::Error
1349
+ complete_reason.details.should == "Terminated due to UniMRCP error"
1350
+ end
1351
+ end
1352
+ end
1353
+ end
1354
+
1355
+ context "with an SSML document containing top-level elements other than <audio/>" do
1356
+ let :ssml_doc do
1357
+ RubySpeech::SSML.draw do
1358
+ voice name: 'Paul' do
1359
+ string "Foo Bar"
1360
+ end
1361
+ end
1362
+ end
1363
+
1364
+ before { mock_call.stub(:channel_var).with('SYNTHSTATUS').and_return 'SUCCESS' }
1365
+
1366
+ it "should attempt to render the document via MRCP and then send a complete event" do
1367
+ expect_answered
1368
+ expect_mrcpsynth ssml_doc
1369
+ subject.execute
1370
+ original_command.complete_event(0.1).reason.should be_a Punchblock::Component::Output::Complete::Finish
1371
+ end
1372
+ end
1373
+
1374
+ context "with mixed TTS and audio tags" do
1375
+ let :ssml_doc do
1376
+ RubySpeech::SSML.draw do
1377
+ voice name: 'Paul' do
1378
+ string "Foo Bar"
1379
+ end
1380
+ audio src: 'tt-monkeys'
1381
+ voice name: 'Frank' do
1382
+ string "Doo Dah"
1383
+ end
1384
+ string 'tt-weasels'
1385
+ end
1386
+ end
1387
+
1388
+ let :first_doc do
1389
+ RubySpeech::SSML.draw do
1390
+ voice name: 'Paul' do
1391
+ string "Foo Bar"
1392
+ end
1393
+ end
1394
+ end
1395
+
1396
+ let :second_doc do
1397
+ RubySpeech::SSML.draw do
1398
+ voice name: 'Frank' do
1399
+ string "Doo Dah"
1400
+ end
1401
+ end
1402
+ end
1403
+
1404
+ before { mock_call.stub(:channel_var).with('SYNTHSTATUS').and_return 'SUCCESS' }
1405
+
1406
+ it "should attempt to render the document via MRCP and then send a complete event" do
1407
+ expect_answered
1408
+ expect_mrcpsynth first_doc
1409
+ expect_playback 'tt-monkeys'
1410
+ expect_mrcpsynth second_doc
1411
+ expect_playback 'tt-weasels'
1412
+ subject.execute
1413
+ original_command.complete_event(0.1).reason.should be_a Punchblock::Component::Output::Complete::Finish
1414
+ end
1415
+ end
1416
+
1417
+ context 'with multiple documents' do
1418
+ let :second_ssml_doc do
1419
+ RubySpeech::SSML.draw do
1420
+ audio :src => 'two.wav' do
1421
+ string "Bazzz"
1422
+ end
1423
+ end
1424
+ end
1425
+
1426
+ let :third_ssml_doc do
1427
+ RubySpeech::SSML.draw do
1428
+ audio :src => 'three.wav' do
1429
+ string "Barrrr"
1430
+ end
1431
+ end
1432
+ end
1433
+
1434
+ let(:command_opts) { { render_documents: [{value: ssml_doc}, {value: second_ssml_doc}, {value: third_ssml_doc}] } }
1435
+
1436
+ it "should render each document in turn using a Playback per document" do
1437
+ expect_answered
1438
+ expect_playback
1439
+ expect_playback 'two'
1440
+ expect_playback 'three'
1441
+ subject.execute
1442
+ original_command.complete_event(0.1).reason.should be_a Punchblock::Component::Output::Complete::Finish
1443
+ end
1444
+
1445
+ it 'should not execute further output after a stop command' do
1446
+ expect_answered
1447
+ mock_call.should_receive(:execute_agi_command).once.ordered.and_return do
1448
+ sleep 0.2
1449
+ end
1450
+ latch = CountDownLatch.new 1
1451
+ original_command.should_receive(:add_event).once.with do |e|
1452
+ e.reason.should be_a Punchblock::Component::Output::Complete::Finish
1453
+ latch.countdown!
1454
+ end
1455
+ Celluloid::Future.new { subject.execute }
1456
+ sleep 0.1
1457
+ mock_call.should_receive(:redirect_back).ordered
1458
+ stop_command = Punchblock::Component::Stop.new
1459
+ stop_command.request!
1460
+ subject.execute_command stop_command
1461
+ latch.wait(2).should be_true
1462
+ end
1463
+
1464
+ context "when the PLAYBACKSTATUS variable is set to 'FAILED'" do
1465
+ let(:synthstatus) { 'SUCCESS' }
1466
+ before { mock_call.stub(:channel_var).with('PLAYBACKSTATUS').and_return 'SUCCESS', 'FAILED', 'SUCCESS' }
1467
+ before { mock_call.stub(:channel_var).with('SYNTHSTATUS').and_return synthstatus }
1468
+
1469
+ let :fallback_doc do
1470
+ RubySpeech::SSML.draw do
1471
+ string "Bazzz"
1472
+ end
1473
+ end
1474
+
1475
+ it "should attempt to render the document via MRCP and then send a complete event" do
1476
+ expect_answered
1477
+ expect_playback
1478
+ expect_playback 'two'
1479
+ expect_mrcpsynth fallback_doc
1480
+ expect_playback 'three'
1481
+ subject.execute
1482
+ original_command.complete_event(0.1).reason.should be_a Punchblock::Component::Output::Complete::Finish
1483
+ end
1484
+
1485
+ context "and the SYNTHSTATUS variable is set to 'ERROR'" do
1486
+ let(:synthstatus) { 'ERROR' }
1487
+
1488
+ it "should terminate playback and send an error complete event" do
1489
+ expect_answered
1490
+ expect_playback
1491
+ expect_playback 'two'
1492
+ expect_mrcpsynth fallback_doc
1493
+ subject.execute
1494
+ complete_reason = original_command.complete_event(0.1).reason
1495
+ complete_reason.should be_a Punchblock::Event::Complete::Error
1496
+ complete_reason.details.should == "Terminated due to UniMRCP error"
1497
+ end
1498
+ end
1499
+ end
1500
+ end
1501
+ end
1502
+
1503
+ describe 'start-offset' do
1504
+ context 'unset' do
1505
+ let(:command_opts) { { :start_offset => nil } }
1506
+ it 'should not pass any options to Playback' do
1507
+ expect_answered
1508
+ expect_playback
1509
+ subject.execute
1510
+ end
1511
+ end
1512
+
1513
+ context 'set' do
1514
+ let(:command_opts) { { :start_offset => 10 } }
1515
+ it "should return an error and not execute any actions" do
1516
+ subject.execute
1517
+ error = ProtocolError.new.setup 'option error', 'A start_offset value is unsupported on Asterisk.'
1518
+ original_command.response(0.1).should be == error
1519
+ end
1520
+ end
1521
+ end
1522
+
1523
+ describe 'start-paused' do
1524
+ context 'false' do
1525
+ let(:command_opts) { { :start_paused => false } }
1526
+ it 'should not pass any options to Playback' do
1527
+ expect_answered
1528
+ expect_playback
1529
+ subject.execute
1530
+ end
1531
+ end
1532
+
1533
+ context 'true' do
1534
+ let(:command_opts) { { :start_paused => true } }
1535
+ it "should return an error and not execute any actions" do
1536
+ subject.execute
1537
+ error = ProtocolError.new.setup 'option error', 'A start_paused value is unsupported on Asterisk.'
1538
+ original_command.response(0.1).should be == error
1539
+ end
1540
+ end
1541
+ end
1542
+
1543
+ describe 'repeat-interval' do
1544
+ context 'unset' do
1545
+ let(:command_opts) { { :repeat_interval => nil } }
1546
+ it 'should not pass any options to Playback' do
1547
+ expect_answered
1548
+ expect_playback
1549
+ subject.execute
1550
+ end
1551
+ end
1552
+
1553
+ context 'set' do
1554
+ let(:command_opts) { { :repeat_interval => 10 } }
1555
+ it "should return an error and not execute any actions" do
1556
+ subject.execute
1557
+ error = ProtocolError.new.setup 'option error', 'A repeat_interval value is unsupported on Asterisk.'
1558
+ original_command.response(0.1).should be == error
1559
+ end
1560
+ end
1561
+ end
1562
+
1563
+ describe 'repeat-times' do
1564
+ context 'unset' do
1565
+ let(:command_opts) { { :repeat_times => nil } }
1566
+ it 'should not pass any options to Playback' do
1567
+ expect_answered
1568
+ expect_playback
1569
+ subject.execute
1570
+ end
1571
+ end
1572
+
1573
+ context 'set' do
1574
+ let(:command_opts) { { :repeat_times => 2 } }
1575
+
1576
+ it "should render the specified number of times" do
1577
+ expect_answered
1578
+ 2.times { expect_playback }
1579
+ subject.execute
1580
+ original_command.complete_event(0.1).reason.should be_a Punchblock::Component::Output::Complete::Finish
1581
+ end
1582
+
1583
+ context 'to 0' do
1584
+ let(:command_opts) { { :repeat_times => 0 } }
1585
+
1586
+ it "should render 10,000 the specified number of times" do
1587
+ expect_answered
1588
+ 1000.times { expect_playback }
1589
+ subject.execute
1590
+ original_command.complete_event(0.1).reason.should be_a Punchblock::Component::Output::Complete::Finish
1591
+ end
1592
+ end
1593
+
1594
+ it 'should not execute further output after a stop command' do
1595
+ expect_answered
1596
+ mock_call.should_receive(:execute_agi_command).once.ordered.and_return do
1597
+ sleep 0.2
1598
+ end
1599
+ latch = CountDownLatch.new 1
1600
+ original_command.should_receive(:add_event).once.with do |e|
1601
+ e.reason.should be_a Punchblock::Component::Output::Complete::Finish
1602
+ latch.countdown!
1603
+ end
1604
+ Celluloid::Future.new { subject.execute }
1605
+ sleep 0.1
1606
+ mock_call.should_receive(:redirect_back).ordered
1607
+ stop_command = Punchblock::Component::Stop.new
1608
+ stop_command.request!
1609
+ subject.execute_command stop_command
1610
+ latch.wait(2).should be_true
1611
+ end
1612
+ end
1613
+ end
1614
+
1615
+ describe 'max-time' do
1616
+ context 'unset' do
1617
+ let(:command_opts) { { :max_time => nil } }
1618
+ it 'should not pass any options to Playback' do
1619
+ expect_answered
1620
+ expect_playback
1621
+ subject.execute
1622
+ end
1623
+ end
1624
+
1625
+ context 'set' do
1626
+ let(:command_opts) { { :max_time => 30 } }
1627
+ it "should return an error and not execute any actions" do
1628
+ subject.execute
1629
+ error = ProtocolError.new.setup 'option error', 'A max_time value is unsupported on Asterisk.'
1630
+ original_command.response(0.1).should be == error
1631
+ end
1632
+ end
1633
+ end
1634
+
1635
+ describe 'voice' do
1636
+ context 'unset' do
1637
+ let(:command_opts) { { :voice => nil } }
1638
+ it 'should not pass the v option to Playback' do
1639
+ expect_answered
1640
+ expect_playback
1641
+ subject.execute
1642
+ end
1643
+ end
1644
+
1645
+ context 'set' do
1646
+ let(:command_opts) { { :voice => 'alison' } }
1647
+ it "should return an error and not execute any actions" do
1648
+ subject.execute
1649
+ error = ProtocolError.new.setup 'option error', 'A voice value is unsupported on Asterisk.'
1650
+ original_command.response(0.1).should be == error
1651
+ end
1652
+ end
1653
+ end
1654
+
1655
+ describe 'interrupt_on' do
1656
+ def ami_event_for_dtmf(digit, position)
1657
+ RubyAMI::Event.new 'DTMF',
1658
+ 'Digit' => digit.to_s,
1659
+ 'Start' => position == :start ? 'Yes' : 'No',
1660
+ 'End' => position == :end ? 'Yes' : 'No'
1661
+ end
1662
+
1663
+ def send_ami_events_for_dtmf(digit)
1664
+ mock_call.process_ami_event ami_event_for_dtmf(digit, :start)
1665
+ mock_call.process_ami_event ami_event_for_dtmf(digit, :end)
1666
+ end
1667
+
1668
+ let(:reason) { original_command.complete_event(5).reason }
1669
+ let(:channel) { "SIP/1234-00000000" }
1670
+ let :ami_event do
1671
+ RubyAMI::Event.new 'AsyncAGI',
1672
+ 'SubEvent' => "Start",
1673
+ 'Channel' => channel,
1674
+ 'Env' => "agi_request%3A%20async%0Aagi_channel%3A%20SIP%2F1234-00000000%0Aagi_language%3A%20en%0Aagi_type%3A%20SIP%0Aagi_uniqueid%3A%201320835995.0%0Aagi_version%3A%201.8.4.1%0Aagi_callerid%3A%205678%0Aagi_calleridname%3A%20Jane%20Smith%0Aagi_callingpres%3A%200%0Aagi_callingani2%3A%200%0Aagi_callington%3A%200%0Aagi_callingtns%3A%200%0Aagi_dnid%3A%201000%0Aagi_rdnis%3A%20unknown%0Aagi_context%3A%20default%0Aagi_extension%3A%201000%0Aagi_priority%3A%201%0Aagi_enhanced%3A%200.0%0Aagi_accountcode%3A%20%0Aagi_threadid%3A%204366221312%0A%0A"
1675
+ end
1676
+
1677
+ context "set to nil" do
1678
+ let(:command_opts) { { :interrupt_on => nil } }
1679
+ it "does not redirect the call" do
1680
+ expect_answered
1681
+ expect_playback
1682
+ mock_call.should_receive(:redirect_back).never
1683
+ subject.execute
1684
+ original_command.response(0.1).should be_a Ref
1685
+ send_ami_events_for_dtmf 1
1686
+ end
1687
+ end
1688
+
1689
+ context "set to :any" do
1690
+ let(:command_opts) { { :interrupt_on => :any } }
1691
+
1692
+ before do
1693
+ expect_answered
1694
+ mock_call.should_receive(:execute_agi_command).once.with('EXEC Playback', audio_filename)
1695
+ subject.should_receive(:send_finish).and_return nil
1696
+ end
1697
+
1698
+ context "when a DTMF digit is received" do
1699
+ it "sends the correct complete event" do
1700
+ mock_call.should_receive :redirect_back
1701
+ subject.execute
1702
+ original_command.response(0.1).should be_a Ref
1703
+ original_command.should_not be_complete
1704
+ send_ami_events_for_dtmf 1
1705
+ mock_call.process_ami_event ami_event
1706
+ sleep 0.2
1707
+ original_command.should be_complete
1708
+ reason.should be_a Punchblock::Component::Output::Complete::Finish
1709
+ end
1710
+
1711
+ it "redirects the call back to async AGI" do
1712
+ mock_call.should_receive(:redirect_back).once
1713
+ subject.execute
1714
+ original_command.response(0.1).should be_a Ref
1715
+ send_ami_events_for_dtmf 1
1716
+ end
1717
+ end
1718
+ end
1719
+
1720
+ context "set to :dtmf" do
1721
+ let(:command_opts) { { :interrupt_on => :dtmf } }
1722
+
1723
+ before do
1724
+ expect_answered
1725
+ mock_call.should_receive(:execute_agi_command).once.with('EXEC Playback', audio_filename)
1726
+ subject.should_receive(:send_finish).and_return nil
1727
+ end
1728
+
1729
+ context "when a DTMF digit is received" do
1730
+ it "sends the correct complete event" do
1731
+ mock_call.should_receive :redirect_back
1732
+ subject.execute
1733
+ original_command.response(0.1).should be_a Ref
1734
+ original_command.should_not be_complete
1735
+ send_ami_events_for_dtmf 1
1736
+ mock_call.process_ami_event ami_event
1737
+ sleep 0.2
1738
+ original_command.should be_complete
1739
+ reason.should be_a Punchblock::Component::Output::Complete::Finish
1740
+ end
1741
+
1742
+ it "redirects the call back to async AGI" do
1743
+ mock_call.should_receive(:redirect_back).once
1744
+ subject.execute
1745
+ original_command.response(0.1).should be_a Ref
1746
+ send_ami_events_for_dtmf 1
1747
+ end
1748
+ end
1749
+ end
1750
+
1751
+ context "set to :voice" do
1752
+ let(:command_opts) { { :interrupt_on => :voice } }
1753
+ it "should return an error and not execute any actions" do
1754
+ subject.execute
1755
+ error = ProtocolError.new.setup 'option error', 'An interrupt-on value of speech is unsupported.'
1756
+ original_command.response(0.1).should be == error
1757
+ end
1758
+ end
1759
+ end
1760
+ end
887
1761
  end
888
1762
 
889
1763
  describe "#execute_command" do
@@ -915,13 +1789,13 @@ module Punchblock
915
1789
  end
916
1790
 
917
1791
  it "sets the command response to true" do
918
- mock_call.async.should_receive(:redirect_back)
1792
+ mock_call.should_receive(:redirect_back)
919
1793
  subject.execute_command command
920
1794
  command.response(0.1).should be == true
921
1795
  end
922
1796
 
923
1797
  it "sends the correct complete event" do
924
- mock_call.async.should_receive(:redirect_back)
1798
+ mock_call.should_receive(:redirect_back)
925
1799
  subject.execute_command command
926
1800
  original_command.should_not be_complete
927
1801
  mock_call.process_ami_event ami_event
@@ -930,7 +1804,7 @@ module Punchblock
930
1804
  end
931
1805
 
932
1806
  it "redirects the call by unjoining it" do
933
- mock_call.async.should_receive(:redirect_back)
1807
+ mock_call.should_receive(:redirect_back)
934
1808
  subject.execute_command command
935
1809
  end
936
1810
  end