punchblock 1.9.4 → 2.0.0.beta1

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 (142) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +1 -2
  4. data/CHANGELOG.md +17 -0
  5. data/Gemfile +1 -0
  6. data/Guardfile +4 -0
  7. data/README.markdown +6 -0
  8. data/Rakefile +16 -0
  9. data/benchmarks/ami_event_name_comparison.rb +14 -0
  10. data/benchmarks/channel.rb +27 -0
  11. data/lib/punchblock/client.rb +2 -6
  12. data/lib/punchblock/command/accept.rb +3 -24
  13. data/lib/punchblock/command/answer.rb +3 -24
  14. data/lib/punchblock/command/dial.rb +24 -76
  15. data/lib/punchblock/command/hangup.rb +3 -19
  16. data/lib/punchblock/command/join.rb +21 -70
  17. data/lib/punchblock/command/mute.rb +3 -3
  18. data/lib/punchblock/command/redirect.rb +6 -39
  19. data/lib/punchblock/command/reject.rb +14 -54
  20. data/lib/punchblock/command/unjoin.rb +8 -40
  21. data/lib/punchblock/command/unmute.rb +3 -3
  22. data/lib/punchblock/command_node.rb +0 -17
  23. data/lib/punchblock/component/asterisk/agi/command.rb +20 -127
  24. data/lib/punchblock/component/asterisk/ami/action.rb +30 -117
  25. data/lib/punchblock/component/component_node.rb +1 -1
  26. data/lib/punchblock/component/input.rb +89 -268
  27. data/lib/punchblock/component/output.rb +106 -154
  28. data/lib/punchblock/component/prompt.rb +51 -0
  29. data/lib/punchblock/component/record.rb +41 -130
  30. data/lib/punchblock/component.rb +1 -0
  31. data/lib/punchblock/connection/asterisk.rb +31 -4
  32. data/lib/punchblock/connection/xmpp.rb +6 -14
  33. data/lib/punchblock/core_ext/blather/stanza.rb +1 -1
  34. data/lib/punchblock/event/active_speaker.rb +2 -10
  35. data/lib/punchblock/event/answered.rb +3 -3
  36. data/lib/punchblock/event/asterisk/ami/event.rb +15 -47
  37. data/lib/punchblock/event/complete.rb +26 -48
  38. data/lib/punchblock/event/dtmf.rb +3 -13
  39. data/lib/punchblock/event/end.rb +10 -11
  40. data/lib/punchblock/event/joined.rb +5 -25
  41. data/lib/punchblock/event/offer.rb +4 -25
  42. data/lib/punchblock/event/ringing.rb +3 -3
  43. data/lib/punchblock/event/unjoined.rb +5 -25
  44. data/lib/punchblock/event.rb +0 -10
  45. data/lib/punchblock/has_headers.rb +20 -26
  46. data/lib/punchblock/rayo_node.rb +46 -23
  47. data/lib/punchblock/ref.rb +39 -18
  48. data/lib/punchblock/translator/asterisk/agi_app.rb +15 -0
  49. data/lib/punchblock/translator/asterisk/agi_command.rb +3 -1
  50. data/lib/punchblock/translator/asterisk/ami_error_converter.rb +20 -0
  51. data/lib/punchblock/translator/asterisk/call.rb +60 -39
  52. data/lib/punchblock/translator/asterisk/channel.rb +41 -0
  53. data/lib/punchblock/translator/asterisk/component/asterisk/agi_command.rb +4 -1
  54. data/lib/punchblock/translator/asterisk/component/asterisk/ami_action.rb +4 -4
  55. data/lib/punchblock/translator/asterisk/component/composed_prompt.rb +62 -0
  56. data/lib/punchblock/translator/asterisk/component/input.rb +1 -0
  57. data/lib/punchblock/translator/asterisk/component/mrcp_native_prompt.rb +56 -0
  58. data/lib/punchblock/translator/asterisk/component/mrcp_prompt.rb +53 -0
  59. data/lib/punchblock/translator/asterisk/component/mrcp_recog_prompt.rb +99 -0
  60. data/lib/punchblock/translator/asterisk/component/output.rb +30 -22
  61. data/lib/punchblock/translator/asterisk/component/record.rb +8 -6
  62. data/lib/punchblock/translator/asterisk/component.rb +6 -5
  63. data/lib/punchblock/translator/asterisk/unimrcp_app.rb +26 -0
  64. data/lib/punchblock/translator/asterisk.rb +24 -28
  65. data/lib/punchblock/translator/dtmf_recognizer.rb +39 -20
  66. data/lib/punchblock/translator/freeswitch/call.rb +15 -14
  67. data/lib/punchblock/translator/freeswitch/component/abstract_output.rb +5 -4
  68. data/lib/punchblock/translator/freeswitch/component/flite_output.rb +1 -1
  69. data/lib/punchblock/translator/freeswitch/component/input.rb +5 -0
  70. data/lib/punchblock/translator/freeswitch/component/output.rb +2 -2
  71. data/lib/punchblock/translator/freeswitch/component/record.rb +19 -13
  72. data/lib/punchblock/translator/freeswitch/component/tts_output.rb +2 -2
  73. data/lib/punchblock/translator/freeswitch/component.rb +2 -5
  74. data/lib/punchblock/translator/freeswitch.rb +2 -2
  75. data/lib/punchblock/translator/input_component.rb +33 -13
  76. data/lib/punchblock/uri_list.rb +21 -0
  77. data/lib/punchblock/version.rb +1 -1
  78. data/lib/punchblock.rb +4 -3
  79. data/punchblock.gemspec +7 -3
  80. data/spec/punchblock/client/component_registry_spec.rb +1 -1
  81. data/spec/punchblock/client_spec.rb +10 -26
  82. data/spec/punchblock/command/accept_spec.rb +41 -7
  83. data/spec/punchblock/command/answer_spec.rb +51 -7
  84. data/spec/punchblock/command/dial_spec.rb +56 -14
  85. data/spec/punchblock/command/hangup_spec.rb +41 -7
  86. data/spec/punchblock/command/join_spec.rb +53 -11
  87. data/spec/punchblock/command/mute_spec.rb +19 -4
  88. data/spec/punchblock/command/redirect_spec.rb +40 -10
  89. data/spec/punchblock/command/reject_spec.rb +43 -11
  90. data/spec/punchblock/command/unjoin_spec.rb +40 -9
  91. data/spec/punchblock/command/unmute_spec.rb +19 -4
  92. data/spec/punchblock/command_node_spec.rb +0 -4
  93. data/spec/punchblock/component/asterisk/agi/command_spec.rb +16 -39
  94. data/spec/punchblock/component/asterisk/ami/action_spec.rb +50 -53
  95. data/spec/punchblock/component/component_node_spec.rb +3 -5
  96. data/spec/punchblock/component/input_spec.rb +194 -61
  97. data/spec/punchblock/component/output_spec.rb +194 -62
  98. data/spec/punchblock/component/prompt_spec.rb +132 -0
  99. data/spec/punchblock/component/record_spec.rb +70 -32
  100. data/spec/punchblock/connection/asterisk_spec.rb +17 -3
  101. data/spec/punchblock/connection/freeswitch_spec.rb +4 -4
  102. data/spec/punchblock/connection/xmpp_spec.rb +20 -38
  103. data/spec/punchblock/event/answered_spec.rb +12 -10
  104. data/spec/punchblock/event/asterisk/ami/event_spec.rb +27 -22
  105. data/spec/punchblock/event/complete_spec.rb +15 -19
  106. data/spec/punchblock/event/dtmf_spec.rb +5 -6
  107. data/spec/punchblock/event/end_spec.rb +20 -10
  108. data/spec/punchblock/event/joined_spec.rb +8 -7
  109. data/spec/punchblock/event/offer_spec.rb +41 -12
  110. data/spec/punchblock/event/ringing_spec.rb +12 -10
  111. data/spec/punchblock/event/started_speaking_spec.rb +5 -6
  112. data/spec/punchblock/event/stopped_speaking_spec.rb +5 -6
  113. data/spec/punchblock/event/unjoined_spec.rb +7 -7
  114. data/spec/punchblock/ref_spec.rb +86 -9
  115. data/spec/punchblock/translator/asterisk/call_spec.rb +317 -154
  116. data/spec/punchblock/translator/asterisk/component/asterisk/agi_command_spec.rb +28 -5
  117. data/spec/punchblock/translator/asterisk/component/asterisk/ami_action_spec.rb +15 -13
  118. data/spec/punchblock/translator/asterisk/component/composed_prompt_spec.rb +237 -0
  119. data/spec/punchblock/translator/asterisk/component/input_spec.rb +171 -14
  120. data/spec/punchblock/translator/asterisk/component/mrcp_native_prompt_spec.rb +652 -0
  121. data/spec/punchblock/translator/asterisk/component/mrcp_prompt_spec.rb +646 -0
  122. data/spec/punchblock/translator/asterisk/component/output_spec.rb +127 -77
  123. data/spec/punchblock/translator/asterisk/component/record_spec.rb +17 -8
  124. data/spec/punchblock/translator/asterisk/component/stop_by_redirect_spec.rb +2 -2
  125. data/spec/punchblock/translator/asterisk/component_spec.rb +3 -7
  126. data/spec/punchblock/translator/asterisk_spec.rb +20 -24
  127. data/spec/punchblock/translator/freeswitch/call_spec.rb +103 -99
  128. data/spec/punchblock/translator/freeswitch/component/flite_output_spec.rb +17 -8
  129. data/spec/punchblock/translator/freeswitch/component/input_spec.rb +26 -14
  130. data/spec/punchblock/translator/freeswitch/component/output_spec.rb +30 -52
  131. data/spec/punchblock/translator/freeswitch/component/record_spec.rb +23 -19
  132. data/spec/punchblock/translator/freeswitch/component/tts_output_spec.rb +18 -8
  133. data/spec/punchblock/translator/freeswitch/component_spec.rb +4 -8
  134. data/spec/punchblock/translator/freeswitch_spec.rb +11 -14
  135. data/spec/punchblock/uri_list_spec.rb +49 -0
  136. data/spec/punchblock_spec.rb +11 -1
  137. data/spec/spec_helper.rb +7 -11
  138. data/spec/support/mock_connection_with_event_handler.rb +1 -1
  139. metadata +104 -24
  140. data/lib/punchblock/header.rb +0 -9
  141. data/lib/punchblock/key_value_pair_node.rb +0 -51
  142. data/spec/punchblock/header_spec.rb +0 -11
@@ -7,8 +7,8 @@ module Punchblock
7
7
  class Asterisk
8
8
  describe Call do
9
9
  let(:channel) { 'SIP/foo' }
10
- let(:ami_client) { stub('AMI Client').as_null_object }
11
- let(:connection) { stub('connection').as_null_object }
10
+ let(:ami_client) { double('AMI Client').as_null_object }
11
+ let(:connection) { double('connection').as_null_object }
12
12
  let(:translator) { Asterisk.new ami_client, connection }
13
13
  let(:agi_env) do
14
14
  {
@@ -37,26 +37,26 @@ module Punchblock
37
37
 
38
38
  let :sip_headers do
39
39
  {
40
- :x_agi_request => 'async',
41
- :x_agi_channel => 'SIP/1234-00000000',
42
- :x_agi_language => 'en',
43
- :x_agi_type => 'SIP',
44
- :x_agi_uniqueid => '1320835995.0',
45
- :x_agi_version => '1.8.4.1',
46
- :x_agi_callerid => '5678',
47
- :x_agi_calleridname => 'Jane Smith',
48
- :x_agi_callingpres => '0',
49
- :x_agi_callingani2 => '0',
50
- :x_agi_callington => '0',
51
- :x_agi_callingtns => '0',
52
- :x_agi_dnid => 'unknown',
53
- :x_agi_rdnis => 'unknown',
54
- :x_agi_context => 'default',
55
- :x_agi_extension => '1000',
56
- :x_agi_priority => '1',
57
- :x_agi_enhanced => '0.0',
58
- :x_agi_accountcode => '',
59
- :x_agi_threadid => '4366221312'
40
+ 'X-agi_request' => 'async',
41
+ 'X-agi_channel' => 'SIP/1234-00000000',
42
+ 'X-agi_language' => 'en',
43
+ 'X-agi_type' => 'SIP',
44
+ 'X-agi_uniqueid' => '1320835995.0',
45
+ 'X-agi_version' => '1.8.4.1',
46
+ 'X-agi_callerid' => '5678',
47
+ 'X-agi_calleridname' => 'Jane Smith',
48
+ 'X-agi_callingpres' => '0',
49
+ 'X-agi_callingani2' => '0',
50
+ 'X-agi_callington' => '0',
51
+ 'X-agi_callingtns' => '0',
52
+ 'X-agi_dnid' => 'unknown',
53
+ 'X-agi_rdnis' => 'unknown',
54
+ 'X-agi_context' => 'default',
55
+ 'X-agi_extension' => '1000',
56
+ 'X-agi_priority' => '1',
57
+ 'X-agi_enhanced' => '0.0',
58
+ 'X-agi_accountcode' => '',
59
+ 'X-agi_threadid' => '4366221312'
60
60
  }
61
61
  end
62
62
 
@@ -80,7 +80,7 @@ module Punchblock
80
80
  describe '#register_component' do
81
81
  it 'should make the component accessible by ID' do
82
82
  component_id = 'abc123'
83
- component = mock 'Translator::Asterisk::Component', :id => component_id
83
+ component = double 'Translator::Asterisk::Component', :id => component_id
84
84
  subject.register_component component
85
85
  subject.component_with_id(component_id).should be component
86
86
  end
@@ -296,25 +296,24 @@ module Punchblock
296
296
  it "should cause the actor to be terminated" do
297
297
  translator.should_receive(:handle_pb_event).twice
298
298
  subject.process_ami_event ami_event
299
- sleep 5.5
300
299
  subject.should_not be_alive
301
300
  end
302
301
 
303
302
  it "de-registers the call from the translator" do
304
303
  translator.stub :handle_pb_event
305
- translator.should_receive(:deregister_call).once.with(subject)
304
+ translator.should_receive(:deregister_call).once.with(subject.id, subject.channel)
306
305
  subject.process_ami_event ami_event
307
306
  end
308
307
 
309
308
  it "should cause all components to send complete events before sending end event" do
310
309
  subject.stub :send_progress
311
- comp_command = Punchblock::Component::Input.new :grammar => {:value => '<grammar/>'}, :mode => :dtmf
310
+ comp_command = Punchblock::Component::Input.new :grammar => {:value => RubySpeech::GRXML.draw(root: 'foo') { rule id: 'foo' }}, :mode => :dtmf
312
311
  comp_command.request!
313
312
  component = subject.execute_command comp_command
314
313
  comp_command.response(0.1).should be_a Ref
315
314
  expected_complete_event = Punchblock::Event::Complete.new :target_call_id => subject.id, :component_id => component.id
316
315
  expected_complete_event.reason = Punchblock::Event::Complete::Hangup.new
317
- expected_end_event = Punchblock::Event::End.new :reason => :hangup, :target_call_id => subject.id
316
+ expected_end_event = Punchblock::Event::End.new :reason => :hangup, platform_code: cause, :target_call_id => subject.id
318
317
 
319
318
  translator.should_receive(:handle_pb_event).with(expected_complete_event).once.ordered
320
319
  translator.should_receive(:handle_pb_event).with(expected_end_event).once.ordered
@@ -322,18 +321,38 @@ module Punchblock
322
321
  end
323
322
 
324
323
  it "should not allow commands to be executed while components are shutting down" do
324
+ call_id = subject.id
325
+
325
326
  subject.stub :send_progress
326
- comp_command = Punchblock::Component::Input.new :grammar => {:value => '<grammar/>'}, :mode => :dtmf
327
+ comp_command = Punchblock::Component::Input.new :grammar => {:value => RubySpeech::GRXML.draw(root: 'foo') { rule id: 'foo' }}, :mode => :dtmf
327
328
  comp_command.request!
328
329
  component = subject.execute_command comp_command
329
330
  comp_command.response(0.1).should be_a Ref
330
331
 
331
332
  subject.async.process_ami_event ami_event
332
333
 
333
- comp_command = Punchblock::Component::Input.new :grammar => {:value => '<grammar/>'}, :mode => :dtmf
334
+ comp_command = Punchblock::Component::Input.new :grammar => {:value => '<grammar root="foo"><rule id="foo"/></grammar>'}, :mode => :dtmf
334
335
  comp_command.request!
335
336
  subject.execute_command comp_command
336
- comp_command.response(0.1).should == ProtocolError.new.setup(:item_not_found, "Could not find a call with ID #{subject.id}", subject.id)
337
+ comp_command.response(0.1).should == ProtocolError.new.setup(:item_not_found, "Could not find a call with ID #{call_id}", call_id)
338
+ end
339
+
340
+ context "after processing a hangup command" do
341
+ let(:command) { Command::Hangup.new }
342
+
343
+ before do
344
+ command.request!
345
+ subject.execute_command command
346
+ end
347
+
348
+ it 'should send an end (hangup_command) event to the translator' do
349
+ expected_end_event = Punchblock::Event::End.new :reason => :hangup_command,
350
+ platform_code: cause,
351
+ :target_call_id => subject.id
352
+ translator.should_receive(:handle_pb_event).with expected_end_event
353
+
354
+ subject.process_ami_event ami_event
355
+ end
337
356
  end
338
357
 
339
358
  context "with an undefined cause" do
@@ -342,6 +361,7 @@ module Punchblock
342
361
 
343
362
  it 'should send an end (hangup) event to the translator' do
344
363
  expected_end_event = Punchblock::Event::End.new :reason => :hangup,
364
+ platform_code: cause,
345
365
  :target_call_id => subject.id
346
366
  translator.should_receive(:handle_pb_event).with expected_end_event
347
367
  subject.process_ami_event ami_event
@@ -354,6 +374,7 @@ module Punchblock
354
374
 
355
375
  it 'should send an end (hangup) event to the translator' do
356
376
  expected_end_event = Punchblock::Event::End.new :reason => :hangup,
377
+ platform_code: cause,
357
378
  :target_call_id => subject.id
358
379
  translator.should_receive(:handle_pb_event).with expected_end_event
359
380
  subject.process_ami_event ami_event
@@ -366,6 +387,7 @@ module Punchblock
366
387
 
367
388
  it 'should send an end (busy) event to the translator' do
368
389
  expected_end_event = Punchblock::Event::End.new :reason => :busy,
390
+ platform_code: cause,
369
391
  :target_call_id => subject.id
370
392
  translator.should_receive(:handle_pb_event).with expected_end_event
371
393
  subject.process_ami_event ami_event
@@ -382,6 +404,7 @@ module Punchblock
382
404
 
383
405
  it 'should send an end (timeout) event to the translator' do
384
406
  expected_end_event = Punchblock::Event::End.new :reason => :timeout,
407
+ platform_code: cause,
385
408
  :target_call_id => subject.id
386
409
  translator.should_receive(:handle_pb_event).with expected_end_event
387
410
  subject.process_ami_event ami_event
@@ -400,6 +423,7 @@ module Punchblock
400
423
 
401
424
  it 'should send an end (reject) event to the translator' do
402
425
  expected_end_event = Punchblock::Event::End.new :reason => :reject,
426
+ platform_code: cause,
403
427
  :target_call_id => subject.id
404
428
  translator.should_receive(:handle_pb_event).with expected_end_event
405
429
  subject.process_ami_event ami_event
@@ -452,6 +476,7 @@ module Punchblock
452
476
 
453
477
  it 'should send an end (error) event to the translator' do
454
478
  expected_end_event = Punchblock::Event::End.new :reason => :error,
479
+ platform_code: cause,
455
480
  :target_call_id => subject.id
456
481
  translator.should_receive(:handle_pb_event).with expected_end_event
457
482
  subject.process_ami_event ami_event
@@ -461,7 +486,7 @@ module Punchblock
461
486
  end
462
487
 
463
488
  context 'with an event for a known AGI command component' do
464
- let(:mock_component_node) { mock 'Punchblock::Component::Asterisk::AGI::Command', :name => 'EXEC ANSWER', :params_array => [] }
489
+ let(:mock_component_node) { Punchblock::Component::Asterisk::AGI::Command.new name: 'EXEC ANSWER', params: [] }
465
490
  let :component do
466
491
  Component::Asterisk::AGICommand.new mock_component_node, subject
467
492
  end
@@ -592,7 +617,7 @@ module Punchblock
592
617
  'Channel' => "SIP/1234-00000000"
593
618
  end
594
619
 
595
- let(:response) { mock 'Response' }
620
+ let(:response) { double 'Response' }
596
621
 
597
622
  it 'should execute the handler' do
598
623
  response.should_receive(:call).once.with ami_event
@@ -621,7 +646,7 @@ module Punchblock
621
646
 
622
647
  let(:other_call_id) { other_call.id }
623
648
  let :command do
624
- Punchblock::Command::Join.new :call_id => other_call_id
649
+ Punchblock::Command::Join.new call_uri: other_call_id
625
650
  end
626
651
 
627
652
  before do
@@ -702,10 +727,8 @@ module Punchblock
702
727
  let(:state) { 'Link' }
703
728
 
704
729
  let :expected_joined do
705
- Punchblock::Event::Joined.new.tap do |joined|
706
- joined.target_call_id = subject.id
707
- joined.call_id = other_call_id
708
- end
730
+ Punchblock::Event::Joined.new target_call_id: subject.id,
731
+ call_uri: other_call_id
709
732
  end
710
733
 
711
734
  it 'sends the Joined event when the call is the first channel' do
@@ -723,10 +746,8 @@ module Punchblock
723
746
  let(:state) { 'Unlink' }
724
747
 
725
748
  let :expected_unjoined do
726
- Punchblock::Event::Unjoined.new.tap do |joined|
727
- joined.target_call_id = subject.id
728
- joined.call_id = other_call_id
729
- end
749
+ Punchblock::Event::Unjoined.new target_call_id: subject.id,
750
+ call_uri: other_call_id
730
751
  end
731
752
 
732
753
  it 'sends the Unjoined event when the call is the first channel' do
@@ -777,10 +798,8 @@ module Punchblock
777
798
  end
778
799
 
779
800
  let :expected_unjoined do
780
- Punchblock::Event::Unjoined.new.tap do |joined|
781
- joined.target_call_id = subject.id
782
- joined.call_id = other_call_id
783
- end
801
+ Punchblock::Event::Unjoined.new target_call_id: subject.id,
802
+ call_uri: other_call_id
784
803
  end
785
804
 
786
805
  it 'sends the Unjoined event when the call is the first channel' do
@@ -821,14 +840,14 @@ module Punchblock
821
840
  end
822
841
 
823
842
  let :expected_pb_event do
824
- Event::Asterisk::AMI::Event.new :name => 'Foo',
825
- :attributes => { :channel => channel,
826
- :uniqueid => "1320842458.8",
827
- :calleridnum => "5678",
828
- :calleridname => "Jane Smith",
829
- :cause => "0",
830
- :'cause-txt' => "Unknown"},
831
- :target_call_id => subject.id
843
+ Event::Asterisk::AMI::Event.new name: 'Foo',
844
+ headers: { 'Channel' => channel,
845
+ 'Uniqueid' => "1320842458.8",
846
+ 'Calleridnum' => "5678",
847
+ 'Calleridname' => "Jane Smith",
848
+ 'Cause' => "0",
849
+ 'Cause-txt' => "Unknown"},
850
+ target_call_id: subject.id
832
851
  end
833
852
 
834
853
  it 'sends the AMI event to the connection as a PB event' do
@@ -863,8 +882,8 @@ module Punchblock
863
882
  command.response(0.5).should be == ProtocolError.new.setup('error', message, subject.id)
864
883
  end
865
884
 
866
- context "with message 'No such channel'" do
867
- let(:message) { 'No such channel' }
885
+ context "because the channel is gone" do
886
+ let(:error) { ChannelGoneError }
868
887
 
869
888
  it "should return an :item_not_found event for the call" do
870
889
  subject.execute_command command
@@ -884,9 +903,9 @@ module Punchblock
884
903
  command.response(0.5).should be true
885
904
  end
886
905
 
887
- it "with a :decline reason should send an EXEC Busy AGI command and set the command's response" do
906
+ it "with a :decline reason should send a Hangup AMI command (cause 21) and set the command's response" do
888
907
  command.reason = :decline
889
- subject.wrapped_object.should_receive(:execute_agi_command).with('EXEC Busy').and_return code: 200
908
+ ami_client.should_receive(:send_action).once.with('Hangup', 'Channel' => channel, 'Cause' => 21).and_return RubyAMI::Response.new
890
909
  subject.execute_command command
891
910
  command.response(0.5).should be true
892
911
  end
@@ -909,8 +928,8 @@ module Punchblock
909
928
  command.response(0.5).should be == ProtocolError.new.setup('error', message, subject.id)
910
929
  end
911
930
 
912
- context "with message 'No such channel'" do
913
- let(:message) { 'No such channel' }
931
+ context "because the channel is gone" do
932
+ let(:error) { ChannelGoneError }
914
933
 
915
934
  it "should return an :item_not_found event for the call" do
916
935
  subject.execute_command command
@@ -935,7 +954,7 @@ module Punchblock
935
954
  subject.should be_answered
936
955
  end
937
956
 
938
- context "when the AMI commannd raises an error" do
957
+ context "when the AMI command raises an error" do
939
958
  let(:message) { 'Some error' }
940
959
  let(:error) { RubyAMI::Error.new.tap { |e| e.message = message } }
941
960
 
@@ -951,8 +970,8 @@ module Punchblock
951
970
  subject.should_not be_answered
952
971
  end
953
972
 
954
- context "with message 'No such channel'" do
955
- let(:message) { 'No such channel' }
973
+ context "because the channel is gone" do
974
+ let(:error) { ChannelGoneError }
956
975
 
957
976
  it "should return an :item_not_found event for the call" do
958
977
  subject.execute_command command
@@ -982,7 +1001,7 @@ module Punchblock
982
1001
  command.response(0.5).should be == ProtocolError.new.setup('error', message, subject.id)
983
1002
  end
984
1003
 
985
- context "with message 'No such channel'" do
1004
+ context "which is 'No such channel'" do
986
1005
  let(:message) { 'No such channel' }
987
1006
 
988
1007
  it "should return an :item_not_found event for the call" do
@@ -990,6 +1009,140 @@ module Punchblock
990
1009
  command.response(0.5).should be == ProtocolError.new.setup(:item_not_found, "Could not find a call with ID #{subject.id}", subject.id)
991
1010
  end
992
1011
  end
1012
+
1013
+ context "which is 'Channel SIP/nosuchchannel does not exist.'" do
1014
+ let(:message) { 'Channel SIP/nosuchchannel does not exist.' }
1015
+
1016
+ it "should return an :item_not_found event for the call" do
1017
+ subject.execute_command command
1018
+ command.response(0.5).should be == ProtocolError.new.setup(:item_not_found, "Could not find a call with ID #{subject.id}", subject.id)
1019
+ end
1020
+ end
1021
+ end
1022
+ end
1023
+
1024
+ context "with a join command" do
1025
+ let(:other_call_id) { "abc123" }
1026
+ let(:other_channel) { 'SIP/bar' }
1027
+ let(:other_translator) { double('Translator::Asterisk').as_null_object }
1028
+
1029
+ let :other_call do
1030
+ Call.new other_channel, other_translator, ami_client, connection
1031
+ end
1032
+
1033
+ let :command do
1034
+ Punchblock::Command::Join.new call_uri: other_call_id
1035
+ end
1036
+
1037
+ before { translator.should_receive(:call_with_id).with(other_call_id).and_return(other_call) }
1038
+
1039
+ it "executes the proper dialplan Bridge application" do
1040
+ subject.wrapped_object.should_receive(:execute_agi_command).with('EXEC Bridge', other_channel).and_return code: 200
1041
+ subject.execute_command command
1042
+ end
1043
+
1044
+ context "when the AMI command raises an error" do
1045
+ let(:message) { 'Some error' }
1046
+ let(:error) { RubyAMI::Error.new.tap { |e| e.message = message } }
1047
+
1048
+ before { subject.wrapped_object.should_receive(:execute_agi_command).and_raise error }
1049
+
1050
+ it "should return an error with the message" do
1051
+ subject.execute_command command
1052
+ command.response(0.5).should be == ProtocolError.new.setup('error', message, subject.id)
1053
+ end
1054
+
1055
+ it "should not be answered" do
1056
+ subject.execute_command command
1057
+ subject.should_not be_answered
1058
+ end
1059
+
1060
+ context "because the channel is gone" do
1061
+ let(:error) { ChannelGoneError }
1062
+
1063
+ it "should return an :item_not_found event for the call" do
1064
+ subject.execute_command command
1065
+ command.response(0.5).should be == ProtocolError.new.setup(:item_not_found, "Could not find a call with ID #{subject.id}", subject.id)
1066
+ end
1067
+ end
1068
+ end
1069
+ end
1070
+
1071
+ context "with an unjoin command" do
1072
+ let(:other_call_id) { "abc123" }
1073
+ let(:other_channel) { 'SIP/bar' }
1074
+
1075
+ let :other_call do
1076
+ Call.new other_channel, translator, ami_client, connection
1077
+ end
1078
+
1079
+ let :command do
1080
+ Punchblock::Command::Unjoin.new call_uri: other_call_id
1081
+ end
1082
+
1083
+ it "executes the unjoin through redirection" do
1084
+ translator.should_receive(:call_with_id).with(other_call_id).and_return(nil)
1085
+
1086
+ ami_client.should_receive(:send_action).once.with("Redirect",
1087
+ 'Channel' => channel,
1088
+ 'Exten' => Punchblock::Translator::Asterisk::REDIRECT_EXTENSION,
1089
+ 'Priority' => Punchblock::Translator::Asterisk::REDIRECT_PRIORITY,
1090
+ 'Context' => Punchblock::Translator::Asterisk::REDIRECT_CONTEXT,
1091
+ ).and_return RubyAMI::Response.new
1092
+
1093
+ subject.execute_command command
1094
+
1095
+ command.response(1).should be_true
1096
+ end
1097
+
1098
+ it "executes the unjoin through redirection, on the subject call and the other call" do
1099
+ translator.should_receive(:call_with_id).with(other_call_id).and_return(other_call)
1100
+
1101
+ ami_client.should_receive(:send_action).once.with("Redirect",
1102
+ 'Channel' => channel,
1103
+ 'Exten' => Punchblock::Translator::Asterisk::REDIRECT_EXTENSION,
1104
+ 'Priority' => Punchblock::Translator::Asterisk::REDIRECT_PRIORITY,
1105
+ 'Context' => Punchblock::Translator::Asterisk::REDIRECT_CONTEXT,
1106
+ 'ExtraChannel' => other_channel,
1107
+ 'ExtraExten' => Punchblock::Translator::Asterisk::REDIRECT_EXTENSION,
1108
+ 'ExtraPriority' => Punchblock::Translator::Asterisk::REDIRECT_PRIORITY,
1109
+ 'ExtraContext' => Punchblock::Translator::Asterisk::REDIRECT_CONTEXT
1110
+ ).and_return RubyAMI::Response.new
1111
+
1112
+ subject.execute_command command
1113
+ end
1114
+
1115
+ context "when the AMI commannd raises an error" do
1116
+ let(:message) { 'Some error' }
1117
+ let(:error) { RubyAMI::Error.new.tap { |e| e.message = message } }
1118
+
1119
+ before do
1120
+ translator.should_receive(:call_with_id).with(other_call_id).and_return(nil)
1121
+ ami_client.should_receive(:send_action).and_raise error
1122
+ end
1123
+
1124
+ it "should return an error with the message" do
1125
+ subject.execute_command command
1126
+ command.response(0.5).should be == ProtocolError.new.setup('error', message, subject.id)
1127
+ end
1128
+
1129
+ context "which is 'No such channel'" do
1130
+ let(:message) { 'No such channel' }
1131
+
1132
+ it "should return an :item_not_found event for the call" do
1133
+ subject.execute_command command
1134
+ command.response(0.5).should be == ProtocolError.new.setup(:item_not_found, "Could not find a call with ID #{subject.id}", subject.id)
1135
+ end
1136
+ end
1137
+
1138
+ context "which is 'Channel SIP/nosuchchannel does not exist.'" do
1139
+ let(:message) { 'Channel SIP/nosuchchannel does not exist.' }
1140
+
1141
+ it "should return an :item_not_found event for the call" do
1142
+ subject.execute_command command
1143
+ command.response(0.5).should be == ProtocolError.new.setup(:item_not_found, "Could not find a call with ID #{subject.id}", subject.id)
1144
+ end
1145
+ end
993
1146
  end
994
1147
  end
995
1148
 
@@ -1035,6 +1188,81 @@ module Punchblock
1035
1188
  end
1036
1189
  end
1037
1190
 
1191
+ context 'with a Prompt component' do
1192
+ def grxml_doc(mode = :dtmf)
1193
+ RubySpeech::GRXML.draw :mode => mode.to_s, :root => 'digits' do
1194
+ rule id: 'digits' do
1195
+ one_of do
1196
+ 0.upto(1) { |d| item { d.to_s } }
1197
+ end
1198
+ end
1199
+ end
1200
+ end
1201
+
1202
+ let :command do
1203
+ Punchblock::Component::Prompt.new(
1204
+ {
1205
+ render_document: {
1206
+ content_type: 'text/uri-list',
1207
+ value: ['http://example.com/hello.mp3']
1208
+ },
1209
+ renderer: renderer
1210
+ },
1211
+ {
1212
+ grammar: {
1213
+ value: grxml_doc,
1214
+ content_type: 'application/srgs+xml'
1215
+ },
1216
+ recognizer: recognizer
1217
+ })
1218
+ end
1219
+
1220
+ let(:mock_action) { Translator::Asterisk::Component::MRCPPrompt.new(command, subject) }
1221
+
1222
+ context "when the recognizer is unimrcp and the renderer is unimrcp" do
1223
+ let(:recognizer) { :unimrcp }
1224
+ let(:renderer) { :unimrcp }
1225
+
1226
+ it 'should create an MRCPPrompt component and execute it asynchronously' do
1227
+ Component::MRCPPrompt.should_receive(:new_link).once.with(command, subject).and_return mock_action
1228
+ mock_action.async.should_receive(:execute).once
1229
+ subject.execute_command command
1230
+ end
1231
+ end
1232
+
1233
+ context "when the recognizer is unimrcp and the renderer is asterisk" do
1234
+ let(:recognizer) { :unimrcp }
1235
+ let(:renderer) { :asterisk }
1236
+
1237
+ it 'should create an MRCPPrompt component and execute it asynchronously' do
1238
+ Component::MRCPNativePrompt.should_receive(:new_link).once.with(command, subject).and_return mock_action
1239
+ mock_action.async.should_receive(:execute).once
1240
+ subject.execute_command command
1241
+ end
1242
+ end
1243
+
1244
+ context "when the recognizer is unimrcp and the renderer is something we can't compose with unimrcp" do
1245
+ let(:recognizer) { :unimrcp }
1246
+ let(:renderer) { :swift }
1247
+
1248
+ it 'should return an error' do
1249
+ subject.execute_command command
1250
+ command.response(0.5).should be == ProtocolError.new.setup(:invalid_command, "Invalid recognizer/renderer combination", subject.id)
1251
+ end
1252
+ end
1253
+
1254
+ context "when the recognizer is something other than unimrcp" do
1255
+ let(:recognizer) { :asterisk }
1256
+ let(:renderer) { :unimrcp }
1257
+
1258
+ it 'should create a ComposedPrompt component and execute it asynchronously' do
1259
+ Component::ComposedPrompt.should_receive(:new_link).once.with(command, subject).and_return mock_action
1260
+ mock_action.async.should_receive(:execute).once
1261
+ subject.execute_command command
1262
+ end
1263
+ end
1264
+ end
1265
+
1038
1266
  context 'with a Record component' do
1039
1267
  let :command do
1040
1268
  Punchblock::Component::Record.new
@@ -1057,7 +1285,7 @@ module Punchblock
1057
1285
  end
1058
1286
 
1059
1287
  let :mock_component do
1060
- mock 'Component', :id => component_id
1288
+ double 'Component', :id => component_id
1061
1289
  end
1062
1290
 
1063
1291
  context "for a known component ID" do
@@ -1074,16 +1302,14 @@ module Punchblock
1074
1302
  Punchblock::Component::Asterisk::AGI::Command.new :name => 'Wait'
1075
1303
  end
1076
1304
 
1077
- let(:comp_id) { component_command.response.id }
1305
+ let(:comp_id) { component_command.response.component_id }
1078
1306
 
1079
1307
  let(:subsequent_command) { Punchblock::Component::Stop.new :component_id => comp_id }
1080
1308
 
1081
1309
  let :expected_event do
1082
- Punchblock::Event::Complete.new.tap do |e|
1083
- e.target_call_id = subject.id
1084
- e.component_id = comp_id
1085
- e.reason = Punchblock::Event::Complete::Error.new
1086
- end
1310
+ Punchblock::Event::Complete.new target_call_id: subject.id,
1311
+ component_id: comp_id,
1312
+ reason: Punchblock::Event::Complete::Error.new
1087
1313
  end
1088
1314
 
1089
1315
  before do
@@ -1141,89 +1367,6 @@ module Punchblock
1141
1367
  command.response.should be == ProtocolError.new.setup('command-not-acceptable', "Did not understand command for call #{subject.id}", subject.id)
1142
1368
  end
1143
1369
  end
1144
-
1145
- context "with a join command" do
1146
- let(:other_call_id) { "abc123" }
1147
- let(:other_channel) { 'SIP/bar' }
1148
- let(:other_translator) { stub('Translator::Asterisk').as_null_object }
1149
-
1150
- let :other_call do
1151
- Call.new other_channel, other_translator, ami_client, connection
1152
- end
1153
-
1154
- let :command do
1155
- Punchblock::Command::Join.new :call_id => other_call_id
1156
- end
1157
-
1158
- it "executes the proper dialplan Bridge application" do
1159
- subject.wrapped_object.should_receive(:execute_agi_command).with('EXEC Bridge', other_channel).and_return code: 200
1160
- translator.should_receive(:call_with_id).with(other_call_id).and_return(other_call)
1161
- subject.execute_command command
1162
- end
1163
- end
1164
-
1165
- context "with an unjoin command" do
1166
- let(:other_call_id) { "abc123" }
1167
- let(:other_channel) { 'SIP/bar' }
1168
-
1169
- let :other_call do
1170
- Call.new other_channel, translator, ami_client, connection
1171
- end
1172
-
1173
- let :command do
1174
- Punchblock::Command::Unjoin.new :call_id => other_call_id
1175
- end
1176
-
1177
- it "executes the unjoin through redirection" do
1178
- translator.should_receive(:call_with_id).with(other_call_id).and_return(nil)
1179
-
1180
- ami_client.should_receive(:send_action).once.with("Redirect",
1181
- 'Channel' => channel,
1182
- 'Exten' => Punchblock::Translator::Asterisk::REDIRECT_EXTENSION,
1183
- 'Priority' => Punchblock::Translator::Asterisk::REDIRECT_PRIORITY,
1184
- 'Context' => Punchblock::Translator::Asterisk::REDIRECT_CONTEXT,
1185
- ).and_return RubyAMI::Response.new
1186
-
1187
- subject.execute_command command
1188
-
1189
- command.response(1).should be_true
1190
- end
1191
-
1192
- it "executes the unjoin through redirection, on the subject call and the other call" do
1193
- translator.should_receive(:call_with_id).with(other_call_id).and_return(other_call)
1194
-
1195
- ami_client.should_receive(:send_action).once.with("Redirect",
1196
- 'Channel' => channel,
1197
- 'Exten' => Punchblock::Translator::Asterisk::REDIRECT_EXTENSION,
1198
- 'Priority' => Punchblock::Translator::Asterisk::REDIRECT_PRIORITY,
1199
- 'Context' => Punchblock::Translator::Asterisk::REDIRECT_CONTEXT,
1200
- 'ExtraChannel' => other_channel,
1201
- 'ExtraExten' => Punchblock::Translator::Asterisk::REDIRECT_EXTENSION,
1202
- 'ExtraPriority' => Punchblock::Translator::Asterisk::REDIRECT_PRIORITY,
1203
- 'ExtraContext' => Punchblock::Translator::Asterisk::REDIRECT_CONTEXT
1204
- ).and_return RubyAMI::Response.new
1205
-
1206
- subject.execute_command command
1207
- end
1208
-
1209
- it "handles redirect errors" do
1210
- translator.should_receive(:call_with_id).with(other_call_id).and_return(nil)
1211
-
1212
- error = RubyAMI::Error.new.tap { |e| e.message = 'FooBar' }
1213
-
1214
- ami_client.should_receive(:send_action).once.with("Redirect",
1215
- 'Channel' => channel,
1216
- 'Exten' => Punchblock::Translator::Asterisk::REDIRECT_EXTENSION,
1217
- 'Priority' => Punchblock::Translator::Asterisk::REDIRECT_PRIORITY,
1218
- 'Context' => Punchblock::Translator::Asterisk::REDIRECT_CONTEXT,
1219
- ).and_raise error
1220
-
1221
- subject.execute_command command
1222
- response = command.response(1)
1223
- response.should be_a ProtocolError
1224
- response.text.should == 'FooBar'
1225
- end
1226
- end
1227
1370
  end#execute_command
1228
1371
 
1229
1372
  describe '#execute_agi_command' do
@@ -1251,14 +1394,34 @@ module Punchblock
1251
1394
  end
1252
1395
 
1253
1396
  context 'with an error' do
1397
+ let(:message) { 'Action failed' }
1398
+
1254
1399
  let :error do
1255
- RubyAMI::Error.new.tap { |e| e.message = 'Action failed' }
1400
+ RubyAMI::Error.new.tap { |e| e.message = message }
1256
1401
  end
1257
1402
 
1258
1403
  it 'should raise the error' do
1259
1404
  ami_client.should_receive(:send_action).once.and_raise error
1260
1405
  expect { subject.execute_agi_command 'EXEC ANSWER' }.to raise_error(RubyAMI::Error, 'Action failed')
1261
1406
  end
1407
+
1408
+ context "which is 'No such channel'" do
1409
+ let(:message) { 'No such channel' }
1410
+
1411
+ it 'should raise ChannelGoneError' do
1412
+ ami_client.should_receive(:send_action).once.and_raise error
1413
+ expect { subject.execute_agi_command 'EXEC ANSWER' }.to raise_error(ChannelGoneError, message)
1414
+ end
1415
+ end
1416
+
1417
+ context "which is 'Channel SIP/nosuchchannel does not exist.'" do
1418
+ let(:message) { 'Channel SIP/nosuchchannel does not exist.' }
1419
+
1420
+ it 'should raise ChannelGoneError' do
1421
+ ami_client.should_receive(:send_action).once.and_raise error
1422
+ expect { subject.execute_agi_command 'EXEC ANSWER' }.to raise_error(ChannelGoneError, message)
1423
+ end
1424
+ end
1262
1425
  end
1263
1426
 
1264
1427
  describe 'when receiving an AsyncAGI event' do