punchblock 2.1.1 → 2.2.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 (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