adhearsion 2.6.4 → 3.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 (242) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +6 -9
  3. data/CHANGELOG.md +22 -7
  4. data/Gemfile +2 -0
  5. data/README.markdown +4 -5
  6. data/Rakefile +1 -2
  7. data/adhearsion.gemspec +19 -8
  8. data/features/cli_create.feature +19 -3
  9. data/features/step_definitions/cli_steps.rb +0 -11
  10. data/features/support/env.rb +3 -4
  11. data/lib/adhearsion.rb +48 -27
  12. data/lib/adhearsion/call.rb +34 -50
  13. data/lib/adhearsion/call_controller.rb +6 -12
  14. data/lib/adhearsion/call_controller/dial.rb +15 -53
  15. data/lib/adhearsion/call_controller/input.rb +39 -162
  16. data/lib/adhearsion/call_controller/input/ask_grammar_builder.rb +44 -0
  17. data/lib/adhearsion/call_controller/input/menu_builder.rb +136 -0
  18. data/lib/adhearsion/call_controller/input/prompt_builder.rb +78 -0
  19. data/lib/adhearsion/call_controller/input/result.rb +46 -0
  20. data/lib/adhearsion/call_controller/output.rb +48 -67
  21. data/lib/adhearsion/call_controller/output/abstract_player.rb +3 -3
  22. data/lib/adhearsion/call_controller/output/async_player.rb +3 -3
  23. data/lib/adhearsion/call_controller/output/player.rb +1 -1
  24. data/lib/adhearsion/call_controller/record.rb +23 -8
  25. data/lib/adhearsion/calls.rb +1 -1
  26. data/lib/adhearsion/cli_commands/ahn_command.rb +2 -65
  27. data/lib/adhearsion/cli_commands/thor_errors.rb +0 -6
  28. data/lib/adhearsion/configuration.rb +91 -39
  29. data/lib/adhearsion/core_ext/blather/stanza.rb +41 -0
  30. data/lib/adhearsion/core_ext/blather/stanza/presence.rb +13 -0
  31. data/lib/adhearsion/error.rb +5 -0
  32. data/lib/adhearsion/event.rb +21 -0
  33. data/lib/adhearsion/event/active_speaker.rb +11 -0
  34. data/lib/adhearsion/event/answered.rb +11 -0
  35. data/lib/adhearsion/event/asterisk.rb +10 -0
  36. data/lib/adhearsion/event/asterisk/ami.rb +34 -0
  37. data/lib/adhearsion/event/complete.rb +75 -0
  38. data/lib/adhearsion/event/dtmf.rb +11 -0
  39. data/lib/adhearsion/event/end.rb +22 -0
  40. data/lib/adhearsion/event/input_timers_started.rb +9 -0
  41. data/lib/adhearsion/event/joined.rb +17 -0
  42. data/lib/adhearsion/event/offer.rb +14 -0
  43. data/lib/adhearsion/event/ringing.rb +11 -0
  44. data/lib/adhearsion/event/started_speaking.rb +13 -0
  45. data/lib/adhearsion/event/stopped_speaking.rb +13 -0
  46. data/lib/adhearsion/event/unjoined.rb +17 -0
  47. data/lib/adhearsion/events.rb +47 -66
  48. data/lib/adhearsion/foundation.rb +0 -1
  49. data/lib/adhearsion/foundation/object.rb +0 -5
  50. data/lib/adhearsion/generators/app/app_generator.rb +4 -1
  51. data/lib/adhearsion/generators/app/templates/Gemfile.erb +2 -10
  52. data/lib/adhearsion/generators/app/templates/adhearsion.erb +9 -9
  53. data/lib/adhearsion/generators/app/templates/config.ru +7 -0
  54. data/lib/adhearsion/generators/app/templates/en.yml +4 -0
  55. data/lib/adhearsion/generators/app/templates/events.erb +2 -2
  56. data/lib/adhearsion/generators/app/templates/hello_world.wav +0 -0
  57. data/lib/adhearsion/generators/app/templates/simon_game.rb +2 -1
  58. data/lib/adhearsion/generators/app/templates/simon_game_spec.rb +2 -2
  59. data/lib/adhearsion/has_headers.rb +34 -0
  60. data/lib/adhearsion/http_server.rb +37 -0
  61. data/lib/adhearsion/initializer.rb +19 -153
  62. data/lib/adhearsion/logging.rb +6 -25
  63. data/lib/adhearsion/outbound_call.rb +5 -5
  64. data/lib/adhearsion/plugin.rb +1 -0
  65. data/lib/adhearsion/protocol_error.rb +26 -0
  66. data/lib/adhearsion/rayo.rb +30 -0
  67. data/lib/adhearsion/rayo/client.rb +62 -0
  68. data/lib/adhearsion/rayo/client/component_registry.rb +33 -0
  69. data/lib/adhearsion/rayo/command.rb +21 -0
  70. data/lib/adhearsion/rayo/command/accept.rb +16 -0
  71. data/lib/adhearsion/rayo/command/answer.rb +16 -0
  72. data/lib/adhearsion/rayo/command/dial.rb +57 -0
  73. data/lib/adhearsion/rayo/command/hangup.rb +16 -0
  74. data/lib/adhearsion/rayo/command/join.rb +43 -0
  75. data/lib/adhearsion/rayo/command/mute.rb +13 -0
  76. data/lib/adhearsion/rayo/command/redirect.rb +23 -0
  77. data/lib/adhearsion/rayo/command/reject.rb +40 -0
  78. data/lib/adhearsion/rayo/command/unjoin.rb +24 -0
  79. data/lib/adhearsion/rayo/command/unmute.rb +13 -0
  80. data/lib/adhearsion/rayo/command_node.rb +47 -0
  81. data/lib/adhearsion/rayo/component.rb +21 -0
  82. data/lib/adhearsion/rayo/component/asterisk.rb +13 -0
  83. data/lib/adhearsion/rayo/component/asterisk/agi.rb +14 -0
  84. data/lib/adhearsion/rayo/component/asterisk/agi/command.rb +46 -0
  85. data/lib/adhearsion/rayo/component/asterisk/ami.rb +14 -0
  86. data/lib/adhearsion/rayo/component/asterisk/ami/action.rb +61 -0
  87. data/lib/adhearsion/rayo/component/component_node.rb +90 -0
  88. data/lib/adhearsion/rayo/component/input.rb +186 -0
  89. data/lib/adhearsion/rayo/component/output.rb +471 -0
  90. data/lib/adhearsion/rayo/component/prompt.rb +53 -0
  91. data/lib/adhearsion/rayo/component/receive_fax.rb +26 -0
  92. data/lib/adhearsion/rayo/component/record.rb +165 -0
  93. data/lib/adhearsion/rayo/component/send_fax.rb +64 -0
  94. data/lib/adhearsion/rayo/component/stop.rb +11 -0
  95. data/lib/adhearsion/rayo/connection.rb +12 -0
  96. data/lib/adhearsion/rayo/connection/asterisk.rb +74 -0
  97. data/lib/adhearsion/rayo/connection/connected.rb +22 -0
  98. data/lib/adhearsion/rayo/connection/generic_connection.rb +22 -0
  99. data/lib/adhearsion/rayo/connection/xmpp.rb +198 -0
  100. data/lib/adhearsion/rayo/disconnected_error.rb +22 -0
  101. data/lib/adhearsion/{punchblock_plugin → rayo}/initializer.rb +19 -19
  102. data/lib/adhearsion/rayo/rayo_node.rb +127 -0
  103. data/lib/adhearsion/rayo/ref.rb +57 -0
  104. data/lib/adhearsion/statistics.rb +1 -1
  105. data/lib/adhearsion/tasks.rb +1 -2
  106. data/lib/adhearsion/tasks/configuration.rb +1 -1
  107. data/lib/adhearsion/tasks/environment.rb +0 -2
  108. data/lib/adhearsion/tasks/i18n.rb +49 -0
  109. data/lib/adhearsion/translator.rb +11 -0
  110. data/lib/adhearsion/translator/asterisk.rb +234 -0
  111. data/lib/adhearsion/translator/asterisk/agi_app.rb +17 -0
  112. data/lib/adhearsion/translator/asterisk/agi_command.rb +45 -0
  113. data/lib/adhearsion/translator/asterisk/ami_error_converter.rb +20 -0
  114. data/lib/adhearsion/translator/asterisk/call.rb +416 -0
  115. data/lib/adhearsion/translator/asterisk/channel.rb +43 -0
  116. data/lib/adhearsion/translator/asterisk/component.rb +88 -0
  117. data/lib/adhearsion/translator/asterisk/component/asterisk.rb +15 -0
  118. data/lib/adhearsion/translator/asterisk/component/asterisk/agi_command.rb +42 -0
  119. data/lib/adhearsion/translator/asterisk/component/asterisk/ami_action.rb +68 -0
  120. data/lib/adhearsion/translator/asterisk/component/composed_prompt.rb +76 -0
  121. data/lib/adhearsion/translator/asterisk/component/dtmf_recognizer.rb +137 -0
  122. data/lib/adhearsion/translator/asterisk/component/input.rb +34 -0
  123. data/lib/adhearsion/translator/asterisk/component/input_component.rb +90 -0
  124. data/lib/adhearsion/translator/asterisk/component/mrcp_native_prompt.rb +71 -0
  125. data/lib/adhearsion/translator/asterisk/component/mrcp_prompt.rb +55 -0
  126. data/lib/adhearsion/translator/asterisk/component/mrcp_recog_prompt.rb +165 -0
  127. data/lib/adhearsion/translator/asterisk/component/output.rb +233 -0
  128. data/lib/adhearsion/translator/asterisk/component/record.rb +101 -0
  129. data/lib/adhearsion/translator/asterisk/component/stop_by_redirect.rb +30 -0
  130. data/lib/adhearsion/translator/asterisk/unimrcp_app.rb +28 -0
  131. data/lib/adhearsion/uri_list.rb +21 -0
  132. data/lib/adhearsion/version.rb +1 -1
  133. data/spec/adhearsion/call_controller/dial_spec.rb +79 -1420
  134. data/spec/adhearsion/call_controller/input_spec.rb +1141 -237
  135. data/spec/adhearsion/call_controller/output/async_player_spec.rb +10 -10
  136. data/spec/adhearsion/call_controller/output/player_spec.rb +8 -8
  137. data/spec/adhearsion/call_controller/output_spec.rb +162 -215
  138. data/spec/adhearsion/call_controller/record_spec.rb +15 -16
  139. data/spec/adhearsion/call_controller_spec.rb +23 -40
  140. data/spec/adhearsion/call_spec.rb +123 -129
  141. data/spec/adhearsion/calls_spec.rb +3 -3
  142. data/spec/adhearsion/configuration_spec.rb +94 -108
  143. data/spec/adhearsion/event/answered_spec.rb +50 -0
  144. data/spec/adhearsion/event/asterisk/Find Results +402 -0
  145. data/spec/adhearsion/event/asterisk/ami_spec.rb +81 -0
  146. data/spec/adhearsion/event/complete_spec.rb +176 -0
  147. data/spec/adhearsion/event/dtmf_spec.rb +35 -0
  148. data/spec/adhearsion/event/end_spec.rb +85 -0
  149. data/spec/adhearsion/event/input_timers_started_spec.rb +19 -0
  150. data/spec/adhearsion/event/joined_spec.rb +53 -0
  151. data/spec/adhearsion/event/offer_spec.rb +106 -0
  152. data/spec/adhearsion/event/ringing_spec.rb +50 -0
  153. data/spec/adhearsion/event/started_speaking_spec.rb +37 -0
  154. data/spec/adhearsion/event/stopped_speaking_spec.rb +37 -0
  155. data/spec/adhearsion/event/unjoined_spec.rb +48 -0
  156. data/spec/adhearsion/event/untitled +0 -0
  157. data/spec/adhearsion/events_spec.rb +19 -45
  158. data/spec/adhearsion/initializer_spec.rb +12 -184
  159. data/spec/adhearsion/logging_spec.rb +5 -20
  160. data/spec/adhearsion/outbound_call_spec.rb +13 -13
  161. data/spec/adhearsion/plugin_spec.rb +3 -4
  162. data/spec/adhearsion/protocol_error_spec.rb +91 -0
  163. data/spec/adhearsion/rayo/client/component_registry_spec.rb +26 -0
  164. data/spec/adhearsion/rayo/client_spec.rb +134 -0
  165. data/spec/adhearsion/rayo/command/accept_spec.rb +63 -0
  166. data/spec/adhearsion/rayo/command/answer_spec.rb +73 -0
  167. data/spec/adhearsion/rayo/command/dial_spec.rb +156 -0
  168. data/spec/adhearsion/rayo/command/hangup_spec.rb +63 -0
  169. data/spec/adhearsion/rayo/command/join_spec.rb +158 -0
  170. data/spec/adhearsion/rayo/command/mute_spec.rb +32 -0
  171. data/spec/adhearsion/rayo/command/redirect_spec.rb +89 -0
  172. data/spec/adhearsion/rayo/command/reject_spec.rb +117 -0
  173. data/spec/adhearsion/rayo/command/unjoin_spec.rb +82 -0
  174. data/spec/adhearsion/rayo/command/unmute_spec.rb +32 -0
  175. data/spec/adhearsion/rayo/command_node_spec.rb +101 -0
  176. data/spec/adhearsion/rayo/component/asterisk/agi/command_spec.rb +111 -0
  177. data/spec/adhearsion/rayo/component/asterisk/ami/action_spec.rb +173 -0
  178. data/spec/adhearsion/rayo/component/component_node_spec.rb +110 -0
  179. data/spec/adhearsion/rayo/component/input_spec.rb +715 -0
  180. data/spec/adhearsion/rayo/component/output_spec.rb +1030 -0
  181. data/spec/adhearsion/rayo/component/prompt_spec.rb +171 -0
  182. data/spec/adhearsion/rayo/component/receive_fax_spec.rb +136 -0
  183. data/spec/adhearsion/rayo/component/record_spec.rb +497 -0
  184. data/spec/adhearsion/rayo/component/send_fax_spec.rb +144 -0
  185. data/spec/adhearsion/rayo/connection/asterisk_spec.rb +118 -0
  186. data/spec/adhearsion/rayo/connection/xmpp_spec.rb +449 -0
  187. data/spec/adhearsion/rayo/initializer_spec.rb +353 -0
  188. data/spec/adhearsion/rayo/ref_spec.rb +168 -0
  189. data/spec/adhearsion/rayo_spec.rb +7 -0
  190. data/spec/adhearsion/router/route_spec.rb +1 -1
  191. data/spec/adhearsion/statistics_spec.rb +2 -5
  192. data/spec/adhearsion/translator/asterisk/call_spec.rb +2047 -0
  193. data/spec/adhearsion/translator/asterisk/component/asterisk/agi_command_spec.rb +256 -0
  194. data/spec/adhearsion/translator/asterisk/component/asterisk/ami_action_spec.rb +151 -0
  195. data/spec/adhearsion/translator/asterisk/component/composed_prompt_spec.rb +257 -0
  196. data/spec/adhearsion/translator/asterisk/component/input_spec.rb +571 -0
  197. data/spec/adhearsion/translator/asterisk/component/mrcp_native_prompt_spec.rb +774 -0
  198. data/spec/adhearsion/translator/asterisk/component/mrcp_prompt_spec.rb +1244 -0
  199. data/spec/adhearsion/translator/asterisk/component/output_spec.rb +1850 -0
  200. data/spec/adhearsion/translator/asterisk/component/record_spec.rb +426 -0
  201. data/spec/adhearsion/translator/asterisk/component/stop_by_redirect_spec.rb +62 -0
  202. data/spec/adhearsion/translator/asterisk/component_spec.rb +83 -0
  203. data/spec/adhearsion/translator/asterisk_spec.rb +685 -0
  204. data/spec/adhearsion/uri_list_spec.rb +88 -0
  205. data/spec/adhearsion_spec.rb +89 -14
  206. data/spec/fixtures/locale/en.yml +13 -0
  207. data/spec/fixtures/locale/it.yml +13 -0
  208. data/spec/spec_helper.rb +18 -5
  209. data/spec/support/call_controller_test_helpers.rb +3 -2
  210. data/spec/support/initializer_stubs.rb +3 -1
  211. data/spec/support/punchblock_examples.rb +65 -0
  212. data/spec/support/punchblock_mocks.rb +12 -0
  213. metadata +412 -70
  214. data/features/cli_daemon.feature +0 -20
  215. data/features/cli_restart.feature +0 -52
  216. data/features/cli_stop.feature +0 -50
  217. data/lib/adhearsion/call_controller/menu_dsl.rb +0 -21
  218. data/lib/adhearsion/call_controller/menu_dsl/array_match_calculator.rb +0 -26
  219. data/lib/adhearsion/call_controller/menu_dsl/calculated_match.rb +0 -43
  220. data/lib/adhearsion/call_controller/menu_dsl/calculated_match_collection.rb +0 -45
  221. data/lib/adhearsion/call_controller/menu_dsl/fixnum_match_calculator.rb +0 -11
  222. data/lib/adhearsion/call_controller/menu_dsl/match_calculator.rb +0 -40
  223. data/lib/adhearsion/call_controller/menu_dsl/menu.rb +0 -207
  224. data/lib/adhearsion/call_controller/menu_dsl/menu_builder.rb +0 -92
  225. data/lib/adhearsion/call_controller/menu_dsl/range_match_calculator.rb +0 -60
  226. data/lib/adhearsion/call_controller/menu_dsl/string_match_calculator.rb +0 -25
  227. data/lib/adhearsion/call_controller/utility.rb +0 -77
  228. data/lib/adhearsion/foundation/custom_daemonizer.rb +0 -52
  229. data/lib/adhearsion/punchblock_plugin.rb +0 -63
  230. data/scripts/cloc-1.64.pl +0 -10483
  231. data/spec/adhearsion/call_controller/menu_dsl/array_match_calculator_spec.rb +0 -76
  232. data/spec/adhearsion/call_controller/menu_dsl/calculated_match_collection_spec.rb +0 -60
  233. data/spec/adhearsion/call_controller/menu_dsl/calculated_match_spec.rb +0 -61
  234. data/spec/adhearsion/call_controller/menu_dsl/fixnum_match_calculator_spec.rb +0 -39
  235. data/spec/adhearsion/call_controller/menu_dsl/match_calculator_spec.rb +0 -17
  236. data/spec/adhearsion/call_controller/menu_dsl/menu_builder_spec.rb +0 -165
  237. data/spec/adhearsion/call_controller/menu_dsl/menu_spec.rb +0 -420
  238. data/spec/adhearsion/call_controller/menu_dsl/range_match_calculator_spec.rb +0 -32
  239. data/spec/adhearsion/call_controller/menu_dsl/string_match_calculator_spec.rb +0 -40
  240. data/spec/adhearsion/call_controller/utility_spec.rb +0 -90
  241. data/spec/adhearsion/punchblock_plugin/initializer_spec.rb +0 -356
  242. data/spec/adhearsion/punchblock_plugin_spec.rb +0 -59
@@ -0,0 +1,1850 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ module Adhearsion
6
+ module Translator
7
+ class Asterisk
8
+ module Component
9
+ describe Output do
10
+ include HasMockCallbackConnection
11
+
12
+ let(:renderer) { nil }
13
+ let(:ami_client) { double('AMI') }
14
+ let(:translator) { Translator::Asterisk.new ami_client, connection }
15
+ let(:mock_call) { Translator::Asterisk::Call.new 'foo', translator, ami_client, connection }
16
+
17
+ let :original_command do
18
+ Adhearsion::Rayo::Component::Output.new command_options
19
+ end
20
+
21
+ let :ssml_doc do
22
+ RubySpeech::SSML.draw do
23
+ say_as(:interpret_as => :cardinal) { 'FOO' }
24
+ end
25
+ end
26
+
27
+ let(:command_opts) { {} }
28
+
29
+ let :command_options do
30
+ { :render_document => {:value => ssml_doc}, renderer: renderer }
31
+ end
32
+
33
+ let(:ast13mode) { false }
34
+
35
+ subject { Output.new original_command, mock_call }
36
+
37
+ def expect_answered(value = true)
38
+ allow(mock_call).to receive(:answered?).and_return(value)
39
+ end
40
+
41
+ def expect_mrcpsynth_with_options(options)
42
+ expect(mock_call).to receive(:execute_agi_command).once { |*args|
43
+ expect(args[0]).to eq('EXEC MRCPSynth')
44
+ expect(args[1]).to match options
45
+ }.and_return code: 200, result: 1
46
+ end
47
+
48
+ describe '#execute' do
49
+ before { original_command.request! }
50
+
51
+ context 'with an invalid renderer' do
52
+ let(:renderer) { 'foobar' }
53
+
54
+ it "should return an error and not execute any actions" do
55
+ subject.execute
56
+ error = Adhearsion::ProtocolError.new.setup 'option error', 'The renderer foobar is unsupported.'
57
+ expect(original_command.response(0.1)).to eq(error)
58
+ end
59
+ end
60
+
61
+ context 'with a renderer of :swift' do
62
+ let(:renderer) { 'swift' }
63
+
64
+ let(:audio_filename) { 'http://foo.com/bar.mp3' }
65
+
66
+ let :ssml_doc do
67
+ RubySpeech::SSML.draw do
68
+ audio :src => audio_filename
69
+ say_as(:interpret_as => :cardinal) { 'FOO' }
70
+ end
71
+ end
72
+
73
+ let :command_options do
74
+ { :render_document => {:value => ssml_doc}, renderer: renderer }.merge(command_opts)
75
+ end
76
+
77
+ def ssml_with_options(prefix = '', postfix = '')
78
+ base_doc = ssml_doc.to_s.squish.gsub(/["\\]/) { |m| "\\#{m}" }
79
+ prefix + base_doc + postfix
80
+ end
81
+
82
+ before { expect_answered }
83
+
84
+ it "should execute Swift" do
85
+ expect(mock_call).to receive(:execute_agi_command).once.with 'EXEC Swift', ssml_with_options
86
+ subject.execute
87
+ end
88
+
89
+ it 'should send a complete event when Swift completes' do
90
+ expect(mock_call).to receive(:execute_agi_command).and_return code: 200, result: 1
91
+ subject.execute
92
+ expect(original_command.complete_event(0.1).reason).to be_a Adhearsion::Rayo::Component::Output::Complete::Finish
93
+ end
94
+
95
+ context "when we get a RubyAMI Error" do
96
+ it "should send an error complete event" do
97
+ error = RubyAMI::Error.new.tap { |e| e.message = 'FooBar' }
98
+ expect(mock_call).to receive(:execute_agi_command).and_raise error
99
+ subject.execute
100
+ complete_reason = original_command.complete_event(0.1).reason
101
+ expect(complete_reason).to be_a Adhearsion::Event::Complete::Error
102
+ expect(complete_reason.details).to eq("Terminated due to AMI error 'FooBar'")
103
+ end
104
+ end
105
+
106
+ context "when the channel is gone" do
107
+ it "should send an error complete event" do
108
+ error = ChannelGoneError.new 'FooBar'
109
+ expect(mock_call).to receive(:execute_agi_command).and_raise error
110
+ subject.execute
111
+ complete_reason = original_command.complete_event(0.1).reason
112
+ expect(complete_reason).to be_a Adhearsion::Event::Complete::Hangup
113
+ end
114
+ end
115
+
116
+ context "when the call is not answered" do
117
+ before { expect_answered false }
118
+
119
+ it "should send progress" do
120
+ expect(mock_call).to receive(:send_progress)
121
+ expect(mock_call).to receive(:execute_agi_command).and_return code: 200, result: 1
122
+ subject.execute
123
+ end
124
+ end
125
+
126
+ describe 'interrupt_on' do
127
+ context "set to nil" do
128
+ let(:command_opts) { { :interrupt_on => nil } }
129
+ it "should not add interrupt arguments" do
130
+ expect(mock_call).to receive(:execute_agi_command).once.with('EXEC Swift', ssml_with_options).and_return code: 200, result: 1
131
+ subject.execute
132
+ end
133
+ end
134
+
135
+ context "set to :any" do
136
+ let(:command_opts) { { :interrupt_on => :any } }
137
+ it "should add the interrupt options to the argument" do
138
+ expect(mock_call).to receive(:execute_agi_command).once.with('EXEC Swift', ssml_with_options('', '|1|1')).and_return code: 200, result: 1
139
+ subject.execute
140
+ end
141
+ end
142
+
143
+ context "set to :dtmf" do
144
+ let(:command_opts) { { :interrupt_on => :dtmf } }
145
+ it "should add the interrupt options to the argument" do
146
+ expect(mock_call).to receive(:execute_agi_command).once.with('EXEC Swift', ssml_with_options('', '|1|1')).and_return code: 200, result: 1
147
+ subject.execute
148
+ end
149
+ end
150
+
151
+ context "set to :voice" do
152
+ let(:command_opts) { { :interrupt_on => :voice } }
153
+ it "should return an error and not execute any actions" do
154
+ subject.execute
155
+ error = Adhearsion::ProtocolError.new.setup 'option error', 'An interrupt-on value of speech is unsupported.'
156
+ expect(original_command.response(0.1)).to eq(error)
157
+ end
158
+ end
159
+ end
160
+
161
+ describe 'voice' do
162
+ context "set to nil" do
163
+ let(:command_opts) { { :voice => nil } }
164
+ it "should not add a voice at the beginning of the argument" do
165
+ expect(mock_call).to receive(:execute_agi_command).once.with('EXEC Swift', ssml_with_options).and_return code: 200, result: 1
166
+ subject.execute
167
+ end
168
+ end
169
+
170
+ context "set to Leonard" do
171
+ let(:command_opts) { { :voice => "Leonard" } }
172
+ it "should add a voice at the beginning of the argument" do
173
+ expect(mock_call).to receive(:execute_agi_command).once.with('EXEC Swift', ssml_with_options('Leonard^', '')).and_return code: 200, result: 1
174
+ subject.execute
175
+ end
176
+ end
177
+
178
+ end
179
+
180
+ describe "with multiple documents" do
181
+ let :first_ssml_doc do
182
+ RubySpeech::SSML.draw do
183
+ audio :src => audio_filename
184
+ end
185
+ end
186
+ let :second_ssml_doc do
187
+ RubySpeech::SSML.draw do
188
+ say_as(:interpret_as => :cardinal) { 'FOO' }
189
+ end
190
+ end
191
+ let(:command_opts) { { render_documents: [{value: first_ssml_doc}, {value: second_ssml_doc}] } }
192
+
193
+ it "executes Swift with a concatenated version of the documents" do
194
+ expect(mock_call).to receive(:execute_agi_command).once.with 'EXEC Swift', ssml_with_options
195
+ subject.execute
196
+ end
197
+ end
198
+ end
199
+
200
+ context 'with a renderer of :unimrcp' do
201
+ let(:renderer) { :unimrcp }
202
+
203
+ let(:audio_filename) { 'http://foo.com/bar.mp3' }
204
+
205
+ let :ssml_doc do
206
+ RubySpeech::SSML.draw do
207
+ audio :src => audio_filename
208
+ say_as(:interpret_as => :cardinal) { 'FOO' }
209
+ end
210
+ end
211
+
212
+ let(:command_opts) { {} }
213
+
214
+ let :command_options do
215
+ { :render_document => {:value => ssml_doc}, renderer: renderer }.merge(command_opts)
216
+ end
217
+
218
+ let(:synthstatus) { 'OK' }
219
+ before { allow(mock_call).to receive(:channel_var).with('SYNTHSTATUS').and_return synthstatus }
220
+
221
+ before { expect_answered }
222
+
223
+ it "should execute MRCPSynth" do
224
+ expect(mock_call).to receive(:execute_agi_command).once.with('EXEC MRCPSynth', ["\"#{ssml_doc.to_s.squish.gsub('"', '\"')}\"", ''].join(',')).and_return code: 200, result: 1
225
+ subject.execute
226
+ end
227
+
228
+ it 'should send a complete event when MRCPSynth completes' do
229
+ expect(mock_call).to receive(:execute_agi_command).and_return code: 200, result: 1
230
+ subject.execute
231
+ expect(original_command.complete_event(0.1).reason).to be_a Adhearsion::Rayo::Component::Output::Complete::Finish
232
+ end
233
+
234
+ context "when we get a RubyAMI Error" do
235
+ it "should send an error complete event" do
236
+ error = RubyAMI::Error.new.tap { |e| e.message = 'FooBar' }
237
+ expect(mock_call).to receive(:execute_agi_command).and_raise error
238
+ subject.execute
239
+ complete_reason = original_command.complete_event(0.1).reason
240
+ expect(complete_reason).to be_a Adhearsion::Event::Complete::Error
241
+ expect(complete_reason.details).to eq("Terminated due to AMI error 'FooBar'")
242
+ end
243
+ end
244
+
245
+ context "when the channel is gone" do
246
+ it "should send an error complete event" do
247
+ error = ChannelGoneError.new 'FooBar'
248
+ expect(mock_call).to receive(:execute_agi_command).and_raise error
249
+ subject.execute
250
+ complete_reason = original_command.complete_event(0.1).reason
251
+ expect(complete_reason).to be_a Adhearsion::Event::Complete::Hangup
252
+ end
253
+ end
254
+
255
+ context "when the call is not answered" do
256
+ before { expect_answered false }
257
+
258
+ it "should send progress" do
259
+ expect(mock_call).to receive(:send_progress)
260
+ expect(mock_call).to receive(:execute_agi_command).and_return code: 200, result: 1
261
+ subject.execute
262
+ end
263
+ end
264
+
265
+ context "when the SYNTHSTATUS variable is set to 'ERROR'" do
266
+ let(:synthstatus) { 'ERROR' }
267
+
268
+ it "should send an error complete event" do
269
+ expect(mock_call).to receive(:execute_agi_command).and_return code: 200, result: 1
270
+ subject.execute
271
+ complete_reason = original_command.complete_event(0.1).reason
272
+ expect(complete_reason).to be_a Adhearsion::Event::Complete::Error
273
+ expect(complete_reason.details).to eq("Terminated due to UniMRCP error")
274
+ end
275
+ end
276
+
277
+ describe 'document' do
278
+ context 'unset' do
279
+ let(:ssml_doc) { nil }
280
+ it "should return an error and not execute any actions" do
281
+ subject.execute
282
+ error = Adhearsion::ProtocolError.new.setup 'option error', 'An SSML document is required.'
283
+ expect(original_command.response(0.1)).to eq(error)
284
+ end
285
+ end
286
+
287
+ context 'with multiple documents' do
288
+ let(:command_opts) { { :render_documents => [{:value => ssml_doc}, {:value => ssml_doc}] } }
289
+
290
+ it "should execute MRCPSynth once with each document" do
291
+ param = ["\"#{ssml_doc.to_s.squish.gsub('"', '\"')}\"", ''].join(',')
292
+ expect(mock_call).to receive(:execute_agi_command).once.with('EXEC MRCPSynth', param).and_return code: 200, result: 1
293
+ expect(mock_call).to receive(:execute_agi_command).once.with('EXEC MRCPSynth', param).and_return code: 200, result: 1
294
+ subject.execute
295
+ end
296
+
297
+ it 'should not execute further output after a stop command' do
298
+ expect(mock_call).to receive(:execute_agi_command).once.ordered do
299
+ sleep 0.5
300
+ end
301
+ latch = CountDownLatch.new 1
302
+ expect(original_command).to receive(:add_event).once { |e|
303
+ expect(e.reason).to be_a Adhearsion::Rayo::Component::Output::Complete::Finish
304
+ latch.countdown!
305
+ }
306
+ Celluloid::Future.new { subject.execute }
307
+ sleep 0.2
308
+ expect(mock_call).to receive(:redirect_back).ordered
309
+ stop_command = Adhearsion::Rayo::Component::Stop.new
310
+ stop_command.request!
311
+ subject.execute_command stop_command
312
+ expect(latch.wait(2)).to be_truthy
313
+ end
314
+ end
315
+ end
316
+
317
+ describe 'start-offset' do
318
+ context 'unset' do
319
+ let(:command_opts) { { :start_offset => nil } }
320
+ it 'should not pass any options to MRCPSynth' do
321
+ expect_mrcpsynth_with_options(//)
322
+ subject.execute
323
+ end
324
+ end
325
+
326
+ context 'set' do
327
+ let(:command_opts) { { :start_offset => 10 } }
328
+ it "should return an error and not execute any actions" do
329
+ subject.execute
330
+ error = Adhearsion::ProtocolError.new.setup 'option error', 'A start_offset value is unsupported on Asterisk.'
331
+ expect(original_command.response(0.1)).to eq(error)
332
+ end
333
+ end
334
+ end
335
+
336
+ describe 'start-paused' do
337
+ context 'false' do
338
+ let(:command_opts) { { :start_paused => false } }
339
+ it 'should not pass any options to MRCPSynth' do
340
+ expect_mrcpsynth_with_options(//)
341
+ subject.execute
342
+ end
343
+ end
344
+
345
+ context 'true' do
346
+ let(:command_opts) { { :start_paused => true } }
347
+ it "should return an error and not execute any actions" do
348
+ subject.execute
349
+ error = Adhearsion::ProtocolError.new.setup 'option error', 'A start_paused value is unsupported on Asterisk.'
350
+ expect(original_command.response(0.1)).to eq(error)
351
+ end
352
+ end
353
+ end
354
+
355
+ describe 'repeat-interval' do
356
+ context 'unset' do
357
+ let(:command_opts) { { :repeat_interval => nil } }
358
+ it 'should not pass any options to MRCPSynth' do
359
+ expect_mrcpsynth_with_options(//)
360
+ subject.execute
361
+ end
362
+ end
363
+
364
+ context 'set' do
365
+ let(:command_opts) { { :repeat_interval => 10 } }
366
+ it "should return an error and not execute any actions" do
367
+ subject.execute
368
+ error = Adhearsion::ProtocolError.new.setup 'option error', 'A repeat_interval value is unsupported on Asterisk.'
369
+ expect(original_command.response(0.1)).to eq(error)
370
+ end
371
+ end
372
+ end
373
+
374
+ describe 'repeat-times' do
375
+ context 'unset' do
376
+ let(:command_opts) { { :repeat_times => nil } }
377
+ it 'should not pass any options to MRCPSynth' do
378
+ expect_mrcpsynth_with_options(//)
379
+ subject.execute
380
+ end
381
+ end
382
+
383
+ context 'set' do
384
+ let(:command_opts) { { :repeat_times => 2 } }
385
+
386
+ it "should render the specified number of times" do
387
+ 2.times { expect_mrcpsynth_with_options(//) }
388
+ subject.execute
389
+ expect(original_command.complete_event(0.1).reason).to be_a Adhearsion::Rayo::Component::Output::Complete::Finish
390
+ end
391
+
392
+ context 'to 0' do
393
+ let(:command_opts) { { :repeat_times => 0 } }
394
+
395
+ it "should render 10,000 the specified number of times" do
396
+ expect_answered
397
+ 1000.times { expect_mrcpsynth_with_options(//) }
398
+ subject.execute
399
+ expect(original_command.complete_event(0.1).reason).to be_a Adhearsion::Rayo::Component::Output::Complete::Finish
400
+ end
401
+ end
402
+
403
+ it 'should not execute further output after a stop command' do
404
+ expect(mock_call).to receive(:execute_agi_command).once.ordered do
405
+ sleep 0.2
406
+ end
407
+ latch = CountDownLatch.new 1
408
+ expect(original_command).to receive(:add_event).once { |e|
409
+ expect(e.reason).to be_a Adhearsion::Rayo::Component::Output::Complete::Finish
410
+ latch.countdown!
411
+ }
412
+ Celluloid::Future.new { subject.execute }
413
+ sleep 0.1
414
+ expect(mock_call).to receive(:redirect_back).ordered
415
+ stop_command = Adhearsion::Rayo::Component::Stop.new
416
+ stop_command.request!
417
+ subject.execute_command stop_command
418
+ expect(latch.wait(2)).to be_truthy
419
+ end
420
+ end
421
+ end
422
+
423
+ describe 'max-time' do
424
+ context 'unset' do
425
+ let(:command_opts) { { :max_time => nil } }
426
+ it 'should not pass any options to MRCPSynth' do
427
+ expect_mrcpsynth_with_options(//)
428
+ subject.execute
429
+ end
430
+ end
431
+
432
+ context 'set' do
433
+ let(:command_opts) { { :max_time => 30 } }
434
+ it "should return an error and not execute any actions" do
435
+ subject.execute
436
+ error = Adhearsion::ProtocolError.new.setup 'option error', 'A max_time value is unsupported on Asterisk.'
437
+ expect(original_command.response(0.1)).to eq(error)
438
+ end
439
+ end
440
+ end
441
+
442
+ describe 'voice' do
443
+ context 'unset' do
444
+ let(:command_opts) { { :voice => nil } }
445
+ it 'should not pass the v option to MRCPSynth' do
446
+ expect_mrcpsynth_with_options(//)
447
+ subject.execute
448
+ end
449
+ end
450
+
451
+ context 'set' do
452
+ let(:command_opts) { { :voice => 'alison' } }
453
+ it 'should pass the v option to MRCPSynth' do
454
+ expect_mrcpsynth_with_options(/v=alison/)
455
+ subject.execute
456
+ end
457
+ end
458
+ end
459
+
460
+ describe 'interrupt_on' do
461
+ context "set to nil" do
462
+ let(:command_opts) { { :interrupt_on => nil } }
463
+ it "should not pass the i option to MRCPSynth" do
464
+ expect_mrcpsynth_with_options(//)
465
+ subject.execute
466
+ end
467
+ end
468
+
469
+ context "set to :any" do
470
+ let(:command_opts) { { :interrupt_on => :any } }
471
+ it "should pass the i option to MRCPSynth" do
472
+ expect_mrcpsynth_with_options(/i=any/)
473
+ subject.execute
474
+ end
475
+ end
476
+
477
+ context "set to :dtmf" do
478
+ let(:command_opts) { { :interrupt_on => :dtmf } }
479
+ it "should pass the i option to MRCPSynth" do
480
+ expect_mrcpsynth_with_options(/i=any/)
481
+ subject.execute
482
+ end
483
+ end
484
+
485
+ context "set to :voice" do
486
+ let(:command_opts) { { :interrupt_on => :voice } }
487
+ it "should return an error and not execute any actions" do
488
+ subject.execute
489
+ error = Adhearsion::ProtocolError.new.setup 'option error', 'An interrupt-on value of speech is unsupported.'
490
+ expect(original_command.response(0.1)).to eq(error)
491
+ end
492
+ end
493
+ end
494
+ end
495
+
496
+ [:asterisk, nil].each do |renderer|
497
+ context "with a renderer of #{renderer.inspect}" do
498
+ def expect_playback(filename = audio_filename)
499
+ expect(mock_call).to receive(:execute_agi_command).once.with('EXEC Playback', filename).and_return code: 200
500
+ end
501
+
502
+ def expect_playback_noanswer
503
+ expect(mock_call).to receive(:execute_agi_command).once.with('EXEC Playback', audio_filename + ',noanswer').and_return code: 200
504
+ end
505
+
506
+ let(:audio_filename) { 'tt-monkeys' }
507
+
508
+ let :ssml_doc do
509
+ RubySpeech::SSML.draw do
510
+ audio :src => audio_filename
511
+ end
512
+ end
513
+
514
+ let(:command_opts) { {} }
515
+
516
+ let :command_options do
517
+ { :render_document => {:value => ssml_doc}, renderer: renderer }.merge(command_opts)
518
+ end
519
+
520
+ let :original_command do
521
+ Adhearsion::Rayo::Component::Output.new command_options
522
+ end
523
+
524
+ let(:playbackstatus) { 'SUCCESS' }
525
+ before { allow(mock_call).to receive(:channel_var).with('PLAYBACKSTATUS').and_return playbackstatus }
526
+
527
+ describe 'ssml' do
528
+ context 'unset' do
529
+ let(:ssml_doc) { nil }
530
+ it "should return an error and not execute any actions" do
531
+ subject.execute
532
+ error = Adhearsion::ProtocolError.new.setup 'option error', 'An SSML document is required.'
533
+ expect(original_command.response(0.1)).to eq(error)
534
+ end
535
+ end
536
+
537
+ context 'with a single audio SSML node' do
538
+ let(:audio_filename) { 'tt-monkeys' }
539
+ let :ssml_doc do
540
+ RubySpeech::SSML.draw { audio :src => audio_filename }
541
+ end
542
+
543
+ it 'should playback the audio file using Playback' do
544
+ expect_answered
545
+ expect_playback
546
+ subject.execute
547
+ end
548
+
549
+ it 'should send a complete event when the file finishes playback' do
550
+ def mock_call.answered?
551
+ true
552
+ end
553
+ expect_playback
554
+ subject.execute
555
+ expect(original_command.complete_event(0.1).reason).to be_a Adhearsion::Rayo::Component::Output::Complete::Finish
556
+ end
557
+
558
+ context "when the audio filename is prefixed by file://" do
559
+ let(:audio_filename) { 'file://tt-monkeys' }
560
+
561
+ it 'should playback the audio file using Playback' do
562
+ expect_answered
563
+ expect_playback 'tt-monkeys'
564
+ subject.execute
565
+ end
566
+ end
567
+
568
+ context "when the audio filename has an extension" do
569
+ let(:audio_filename) { 'tt-monkeys.wav' }
570
+
571
+ it 'should playback the audio file using Playback' do
572
+ expect_answered
573
+ expect_playback 'tt-monkeys'
574
+ subject.execute
575
+ end
576
+
577
+ context "when there are other dots in the filename" do
578
+ let(:audio_filename) { 'blue.tt-monkeys.wav' }
579
+
580
+ it 'should playback the audio file using Playback' do
581
+ expect_answered
582
+ expect_playback 'blue.tt-monkeys'
583
+ subject.execute
584
+ end
585
+
586
+ context "and no file extension" do
587
+ let(:audio_filename) { '/var/lib/gems/1.9.1/gems/myapp-1.0.0/prompts/greeting' }
588
+
589
+ it 'should playback the audio file using Playback' do
590
+ expect_answered
591
+ expect_playback '/var/lib/gems/1.9.1/gems/myapp-1.0.0/prompts/greeting'
592
+ subject.execute
593
+ end
594
+ end
595
+ end
596
+ end
597
+
598
+ context "when we get a RubyAMI Error" do
599
+ it "should send an error complete event" do
600
+ expect_answered
601
+ error = RubyAMI::Error.new.tap { |e| e.message = 'FooBar' }
602
+ expect(mock_call).to receive(:execute_agi_command).and_raise error
603
+ subject.execute
604
+ complete_reason = original_command.complete_event(0.1).reason
605
+ expect(complete_reason).to be_a Adhearsion::Event::Complete::Error
606
+ expect(complete_reason.details).to eq("Terminated due to AMI error 'FooBar'")
607
+ end
608
+ end
609
+
610
+ context "when the channel is gone" do
611
+ it "should send an error complete event" do
612
+ expect_answered
613
+ error = ChannelGoneError.new 'FooBar'
614
+ expect(mock_call).to receive(:execute_agi_command).and_raise error
615
+ subject.execute
616
+ complete_reason = original_command.complete_event(0.1).reason
617
+ expect(complete_reason).to be_a Adhearsion::Event::Complete::Hangup
618
+ end
619
+ end
620
+
621
+ context "when the PLAYBACKSTATUS variable is set to 'FAILED'" do
622
+ let(:playbackstatus) { 'FAILED' }
623
+
624
+ it "should send an error complete event" do
625
+ expect_answered
626
+ expect(mock_call).to receive(:execute_agi_command).and_return code: 200, result: 1
627
+ subject.execute
628
+ complete_reason = original_command.complete_event(0.1).reason
629
+ expect(complete_reason).to be_a Adhearsion::Event::Complete::Error
630
+ expect(complete_reason.details).to eq("Terminated due to playback error")
631
+ end
632
+ end
633
+ end
634
+
635
+ context 'with a single text node without spaces' do
636
+ let(:audio_filename) { 'tt-monkeys' }
637
+ let :ssml_doc do
638
+ RubySpeech::SSML.draw { string audio_filename }
639
+ end
640
+
641
+ it 'should playback the audio file using Playback' do
642
+ expect_answered
643
+ expect_playback
644
+ subject.execute
645
+ end
646
+
647
+ it 'should send a complete event when the file finishes playback' do
648
+ expect_answered
649
+ expect_playback
650
+ subject.execute
651
+ expect(original_command.complete_event(0.1).reason).to be_a Adhearsion::Rayo::Component::Output::Complete::Finish
652
+ end
653
+
654
+ context "when we get a RubyAMI Error" do
655
+ it "should send an error complete event" do
656
+ expect_answered
657
+ error = RubyAMI::Error.new.tap { |e| e.message = 'FooBar' }
658
+ expect(mock_call).to receive(:execute_agi_command).and_raise error
659
+ subject.execute
660
+ complete_reason = original_command.complete_event(0.1).reason
661
+ expect(complete_reason).to be_a Adhearsion::Event::Complete::Error
662
+ expect(complete_reason.details).to eq("Terminated due to AMI error 'FooBar'")
663
+ end
664
+ end
665
+
666
+ context "with early media playback" do
667
+ it "should play the file with Playback" do
668
+ expect_answered false
669
+ expect_playback_noanswer
670
+ expect(mock_call).to receive(:send_progress)
671
+ subject.execute
672
+ end
673
+
674
+ context "with interrupt_on set to something that is not nil" do
675
+ let(:audio_filename) { 'tt-monkeys' }
676
+ let :command_options do
677
+ {
678
+ :render_document => {
679
+ :value => RubySpeech::SSML.draw { string audio_filename },
680
+ },
681
+ :interrupt_on => :any
682
+ }
683
+ end
684
+ it "should return an error when the output is interruptible and it is early media" do
685
+ expect_answered false
686
+ error = Adhearsion::ProtocolError.new.setup 'option error', 'Interrupt digits are not allowed with early media.'
687
+ subject.execute
688
+ expect(original_command.response(0.1)).to eq(error)
689
+ end
690
+ end
691
+ end
692
+ end
693
+
694
+ context 'with multiple audio SSML nodes' do
695
+ let(:audio_filename1) { 'foo' }
696
+ let(:audio_filename2) { 'bar' }
697
+ let :ssml_doc do
698
+ RubySpeech::SSML.draw do
699
+ audio :src => audio_filename1
700
+ audio :src => audio_filename2
701
+ end
702
+ end
703
+
704
+ it 'should playback all audio files using Playback' do
705
+ latch = CountDownLatch.new 2
706
+ expect_playback [audio_filename1, audio_filename2].join('&')
707
+ expect_answered
708
+ subject.execute
709
+ latch.wait 2
710
+ sleep 2
711
+ end
712
+
713
+ it 'should send a complete event after the final file has finished playback' do
714
+ expect_answered
715
+ expect_playback [audio_filename1, audio_filename2].join('&')
716
+ latch = CountDownLatch.new 1
717
+ expect(original_command).to receive(:add_event).once { |e|
718
+ expect(e.reason).to be_a Adhearsion::Rayo::Component::Output::Complete::Finish
719
+ latch.countdown!
720
+ }
721
+ subject.execute
722
+ expect(latch.wait(2)).to be_truthy
723
+ end
724
+ end
725
+
726
+ context "with an SSML document containing elements other than <audio/>" do
727
+ let :ssml_doc do
728
+ RubySpeech::SSML.draw do
729
+ string "Foo Bar"
730
+ end
731
+ end
732
+
733
+ it "should return an unrenderable document error" do
734
+ subject.execute
735
+ error = Adhearsion::ProtocolError.new.setup 'unrenderable document error', 'The provided document could not be rendered. See http://adhearsion.com/docs/common_problems#unrenderable-document-error for details.'
736
+ expect(original_command.response(0.1)).to eq(error)
737
+ end
738
+ end
739
+
740
+ context 'with multiple documents' do
741
+ let(:command_opts) { { render_documents: [{value: ssml_doc}, {value: ssml_doc}] } }
742
+
743
+ it "should render each document in turn using a Playback per document" do
744
+ expect_answered
745
+ 2.times { expect_playback }
746
+ subject.execute
747
+ expect(original_command.complete_event(0.1).reason).to be_a Adhearsion::Rayo::Component::Output::Complete::Finish
748
+ end
749
+
750
+ it 'should not execute further output after a stop command' do
751
+ expect_answered
752
+ expect(mock_call).to receive(:execute_agi_command).once.ordered do
753
+ sleep 0.2
754
+ end
755
+ latch = CountDownLatch.new 1
756
+ expect(original_command).to receive(:add_event).once { |e|
757
+ expect(e.reason).to be_a Adhearsion::Rayo::Component::Output::Complete::Finish
758
+ latch.countdown!
759
+ }
760
+ Celluloid::Future.new { subject.execute }
761
+ sleep 0.1
762
+ expect(mock_call).to receive(:redirect_back).ordered
763
+ stop_command = Adhearsion::Rayo::Component::Stop.new
764
+ stop_command.request!
765
+ subject.execute_command stop_command
766
+ expect(latch.wait(2)).to be_truthy
767
+ end
768
+
769
+ context "when the PLAYBACKSTATUS variable is set to 'FAILED'" do
770
+ let(:playbackstatus) { 'FAILED' }
771
+
772
+ it "should terminate playback and send an error complete event" do
773
+ expect_answered
774
+ expect(mock_call).to receive(:execute_agi_command).once.and_return code: 200, result: 1
775
+ subject.execute
776
+ complete_reason = original_command.complete_event(0.1).reason
777
+ expect(complete_reason).to be_a Adhearsion::Event::Complete::Error
778
+ expect(complete_reason.details).to eq("Terminated due to playback error")
779
+ end
780
+ end
781
+ end
782
+ end
783
+
784
+ describe 'start-offset' do
785
+ context 'unset' do
786
+ let(:command_opts) { { :start_offset => nil } }
787
+ it 'should not pass any options to Playback' do
788
+ expect_answered
789
+ expect_playback
790
+ subject.execute
791
+ end
792
+ end
793
+
794
+ context 'set' do
795
+ let(:command_opts) { { :start_offset => 10 } }
796
+ it "should return an error and not execute any actions" do
797
+ subject.execute
798
+ error = Adhearsion::ProtocolError.new.setup 'option error', 'A start_offset value is unsupported on Asterisk.'
799
+ expect(original_command.response(0.1)).to eq(error)
800
+ end
801
+ end
802
+ end
803
+
804
+ describe 'start-paused' do
805
+ context 'false' do
806
+ let(:command_opts) { { :start_paused => false } }
807
+ it 'should not pass any options to Playback' do
808
+ expect_answered
809
+ expect_playback
810
+ subject.execute
811
+ end
812
+ end
813
+
814
+ context 'true' do
815
+ let(:command_opts) { { :start_paused => true } }
816
+ it "should return an error and not execute any actions" do
817
+ subject.execute
818
+ error = Adhearsion::ProtocolError.new.setup 'option error', 'A start_paused value is unsupported on Asterisk.'
819
+ expect(original_command.response(0.1)).to eq(error)
820
+ end
821
+ end
822
+ end
823
+
824
+ describe 'repeat-interval' do
825
+ context 'unset' do
826
+ let(:command_opts) { { :repeat_interval => nil } }
827
+ it 'should not pass any options to Playback' do
828
+ expect_answered
829
+ expect_playback
830
+ subject.execute
831
+ end
832
+ end
833
+
834
+ context 'set' do
835
+ let(:command_opts) { { :repeat_interval => 10 } }
836
+ it "should return an error and not execute any actions" do
837
+ subject.execute
838
+ error = Adhearsion::ProtocolError.new.setup 'option error', 'A repeat_interval value is unsupported on Asterisk.'
839
+ expect(original_command.response(0.1)).to eq(error)
840
+ end
841
+ end
842
+ end
843
+
844
+ describe 'repeat-times' do
845
+ context 'unset' do
846
+ let(:command_opts) { { :repeat_times => nil } }
847
+ it 'should not pass any options to Playback' do
848
+ expect_answered
849
+ expect_playback
850
+ subject.execute
851
+ end
852
+ end
853
+
854
+ context 'set' do
855
+ let(:command_opts) { { :repeat_times => 2 } }
856
+
857
+ it "should render the specified number of times" do
858
+ expect_answered
859
+ 2.times { expect_playback }
860
+ subject.execute
861
+ expect(original_command.complete_event(0.1).reason).to be_a Adhearsion::Rayo::Component::Output::Complete::Finish
862
+ end
863
+
864
+ context 'to 0' do
865
+ let(:command_opts) { { :repeat_times => 0 } }
866
+
867
+ it "should render 10,000 the specified number of times" do
868
+ expect_answered
869
+ 1000.times { expect_playback }
870
+ subject.execute
871
+ expect(original_command.complete_event(0.1).reason).to be_a Adhearsion::Rayo::Component::Output::Complete::Finish
872
+ end
873
+ end
874
+
875
+ it 'should not execute further output after a stop command' do
876
+ expect_answered
877
+ expect(mock_call).to receive(:execute_agi_command).once.ordered do
878
+ sleep 0.2
879
+ end
880
+ latch = CountDownLatch.new 1
881
+ expect(original_command).to receive(:add_event).once { |e|
882
+ expect(e.reason).to be_a Adhearsion::Rayo::Component::Output::Complete::Finish
883
+ latch.countdown!
884
+ }
885
+ Celluloid::Future.new { subject.execute }
886
+ sleep 0.1
887
+ expect(mock_call).to receive(:redirect_back).ordered
888
+ stop_command = Adhearsion::Rayo::Component::Stop.new
889
+ stop_command.request!
890
+ subject.execute_command stop_command
891
+ expect(latch.wait(2)).to be_truthy
892
+ end
893
+ end
894
+ end
895
+
896
+ describe 'max-time' do
897
+ context 'unset' do
898
+ let(:command_opts) { { :max_time => nil } }
899
+ it 'should not pass any options to Playback' do
900
+ expect_answered
901
+ expect_playback
902
+ subject.execute
903
+ end
904
+ end
905
+
906
+ context 'set' do
907
+ let(:command_opts) { { :max_time => 30 } }
908
+ it "should return an error and not execute any actions" do
909
+ subject.execute
910
+ error = Adhearsion::ProtocolError.new.setup 'option error', 'A max_time value is unsupported on Asterisk.'
911
+ expect(original_command.response(0.1)).to eq(error)
912
+ end
913
+ end
914
+ end
915
+
916
+ describe 'voice' do
917
+ context 'unset' do
918
+ let(:command_opts) { { :voice => nil } }
919
+ it 'should not pass the v option to Playback' do
920
+ expect_answered
921
+ expect_playback
922
+ subject.execute
923
+ end
924
+ end
925
+
926
+ context 'set' do
927
+ let(:command_opts) { { :voice => 'alison' } }
928
+ it "should return an error and not execute any actions" do
929
+ subject.execute
930
+ error = Adhearsion::ProtocolError.new.setup 'option error', 'A voice value is unsupported on Asterisk.'
931
+ expect(original_command.response(0.1)).to eq(error)
932
+ end
933
+ end
934
+ end
935
+
936
+ describe 'interrupt_on' do
937
+ def ami_event_for_dtmf(digit, position)
938
+ if ast13mode
939
+ RubyAMI::Event.new 'DTMF' + (position == :start ? 'Begin' : '') + (position == :end ? 'End' : ''),
940
+ 'Digit' => digit.to_s
941
+ else
942
+ RubyAMI::Event.new 'DTMF',
943
+ 'Digit' => digit.to_s,
944
+ 'Start' => position == :start ? 'Yes' : 'No',
945
+ 'End' => position == :end ? 'Yes' : 'No'
946
+ end
947
+ end
948
+
949
+ def send_ami_events_for_dtmf(digit)
950
+ mock_call.process_ami_event ami_event_for_dtmf(digit, :start)
951
+ mock_call.process_ami_event ami_event_for_dtmf(digit, :end)
952
+ end
953
+
954
+ let(:reason) { original_command.complete_event(5).reason }
955
+ let(:channel) { "SIP/1234-00000000" }
956
+ let :ami_event do
957
+ RubyAMI::Event.new 'AsyncAGI',
958
+ 'SubEvent' => "Start",
959
+ 'Channel' => channel,
960
+ '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"
961
+ end
962
+
963
+ context "set to nil" do
964
+ let(:command_opts) { { :interrupt_on => nil } }
965
+ it "does not redirect the call" do
966
+ expect_answered
967
+ expect_playback
968
+ expect(mock_call).to receive(:redirect_back).never
969
+ subject.execute
970
+ expect(original_command.response(0.1)).to be_a Adhearsion::Rayo::Ref
971
+ send_ami_events_for_dtmf 1
972
+ end
973
+ end
974
+
975
+ context "set to :any" do
976
+ let(:command_opts) { { :interrupt_on => :any } }
977
+
978
+ before do
979
+ expect_answered
980
+ expect(mock_call).to receive(:execute_agi_command).once.with('EXEC Playback', audio_filename)
981
+ expect(subject).to receive(:send_finish).and_return nil
982
+ end
983
+
984
+ context "when a DTMF digit is received" do
985
+ it "sends the correct complete event" do
986
+ expect(mock_call).to receive :redirect_back
987
+ subject.execute
988
+ expect(original_command.response(0.1)).to be_a Adhearsion::Rayo::Ref
989
+ expect(original_command).not_to be_complete
990
+ send_ami_events_for_dtmf 1
991
+ mock_call.process_ami_event ami_event
992
+ sleep 0.2
993
+ expect(original_command).to be_complete
994
+ expect(reason).to be_a Adhearsion::Rayo::Component::Output::Complete::Finish
995
+ end
996
+
997
+ it "redirects the call back to async AGI" do
998
+ expect(mock_call).to receive(:redirect_back).once
999
+ subject.execute
1000
+ expect(original_command.response(0.1)).to be_a Adhearsion::Rayo::Ref
1001
+ send_ami_events_for_dtmf 1
1002
+ end
1003
+ end
1004
+ end
1005
+
1006
+ context "set to :dtmf" do
1007
+ let(:command_opts) { { :interrupt_on => :dtmf } }
1008
+
1009
+ before do
1010
+ expect_answered
1011
+ expect(mock_call).to receive(:execute_agi_command).once.with('EXEC Playback', audio_filename)
1012
+ expect(subject).to receive(:send_finish).and_return nil
1013
+ end
1014
+
1015
+ context "when a DTMF digit is received" do
1016
+ it "sends the correct complete event" do
1017
+ expect(mock_call).to receive :redirect_back
1018
+ subject.execute
1019
+ expect(original_command.response(0.1)).to be_a Adhearsion::Rayo::Ref
1020
+ expect(original_command).not_to be_complete
1021
+ send_ami_events_for_dtmf 1
1022
+ mock_call.process_ami_event ami_event
1023
+ sleep 0.2
1024
+ expect(original_command).to be_complete
1025
+ expect(reason).to be_a Adhearsion::Rayo::Component::Output::Complete::Finish
1026
+ end
1027
+
1028
+ it "redirects the call back to async AGI" do
1029
+ expect(mock_call).to receive(:redirect_back).once
1030
+ subject.execute
1031
+ expect(original_command.response(0.1)).to be_a Adhearsion::Rayo::Ref
1032
+ send_ami_events_for_dtmf 1
1033
+ end
1034
+ end
1035
+ end
1036
+
1037
+ context "set to :voice" do
1038
+ let(:command_opts) { { :interrupt_on => :voice } }
1039
+ it "should return an error and not execute any actions" do
1040
+ subject.execute
1041
+ error = Adhearsion::ProtocolError.new.setup 'option error', 'An interrupt-on value of speech is unsupported.'
1042
+ expect(original_command.response(0.1)).to eq(error)
1043
+ end
1044
+ end
1045
+ end
1046
+ end
1047
+ end
1048
+
1049
+ context "with a renderer of :native_or_unimrcp" do
1050
+ def expect_playback(filename = audio_filename)
1051
+ expect(mock_call).to receive(:execute_agi_command).ordered.once.with('EXEC Playback', filename).and_return code: 200
1052
+ end
1053
+
1054
+ def expect_playback_noanswer
1055
+ expect(mock_call).to receive(:execute_agi_command).once.with('EXEC Playback', audio_filename + ',noanswer').and_return code: 200
1056
+ end
1057
+
1058
+ def expect_mrcpsynth(doc = ssml_doc)
1059
+ expect(mock_call).to receive(:execute_agi_command).ordered.once.with('EXEC MRCPSynth', ["\"#{doc.to_s.squish.gsub('"', '\"')}\"", ''].join(',')).and_return code: 200, result: 1
1060
+ end
1061
+
1062
+ let(:audio_filename) { 'tt-monkeys' }
1063
+
1064
+ let :ssml_doc do
1065
+ RubySpeech::SSML.draw do
1066
+ audio :src => audio_filename do
1067
+ string "Foobar"
1068
+ end
1069
+ end
1070
+ end
1071
+
1072
+ let(:command_opts) { {} }
1073
+
1074
+ let :command_options do
1075
+ { :render_document => {:value => ssml_doc}, renderer: :native_or_unimrcp }.merge(command_opts)
1076
+ end
1077
+
1078
+ let :original_command do
1079
+ Adhearsion::Rayo::Component::Output.new command_options
1080
+ end
1081
+
1082
+ let(:playbackstatus) { 'SUCCESS' }
1083
+ before { allow(mock_call).to receive(:channel_var).with('PLAYBACKSTATUS').and_return playbackstatus }
1084
+
1085
+ describe 'ssml' do
1086
+ context 'unset' do
1087
+ let(:ssml_doc) { nil }
1088
+ it "should return an error and not execute any actions" do
1089
+ subject.execute
1090
+ error = Adhearsion::ProtocolError.new.setup 'option error', 'An SSML document is required.'
1091
+ expect(original_command.response(0.1)).to eq(error)
1092
+ end
1093
+ end
1094
+
1095
+ context 'with a single audio SSML node' do
1096
+ let(:audio_filename) { 'tt-monkeys' }
1097
+ let :ssml_doc do
1098
+ RubySpeech::SSML.draw language: 'pt-BR' do
1099
+ audio :src => audio_filename do
1100
+ voice name: 'frank' do
1101
+ string "Hello world"
1102
+ end
1103
+ end
1104
+ end
1105
+ end
1106
+
1107
+ it 'should playback the audio file using Playback' do
1108
+ expect_answered
1109
+ expect_playback
1110
+ subject.execute
1111
+ end
1112
+
1113
+ it 'should send a complete event when the file finishes playback' do
1114
+ def mock_call.answered?
1115
+ true
1116
+ end
1117
+ expect_playback
1118
+ subject.execute
1119
+ expect(original_command.complete_event(0.1).reason).to be_a Adhearsion::Rayo::Component::Output::Complete::Finish
1120
+ end
1121
+
1122
+ context "when the audio filename is prefixed by file://" do
1123
+ let(:audio_filename) { 'file://tt-monkeys' }
1124
+
1125
+ it 'should playback the audio file using Playback' do
1126
+ expect_answered
1127
+ expect_playback 'tt-monkeys'
1128
+ subject.execute
1129
+ end
1130
+ end
1131
+
1132
+ context "when the audio filename has an extension" do
1133
+ let(:audio_filename) { 'tt-monkeys.wav' }
1134
+
1135
+ it 'should playback the audio file using Playback' do
1136
+ expect_answered
1137
+ expect_playback 'tt-monkeys'
1138
+ subject.execute
1139
+ end
1140
+
1141
+ context "when there are other dots in the filename" do
1142
+ let(:audio_filename) { 'blue.tt-monkeys.wav' }
1143
+
1144
+ it 'should playback the audio file using Playback' do
1145
+ expect_answered
1146
+ expect_playback 'blue.tt-monkeys'
1147
+ subject.execute
1148
+ end
1149
+ end
1150
+ end
1151
+
1152
+ context "when we get a RubyAMI Error" do
1153
+ it "should send an error complete event" do
1154
+ expect_answered
1155
+ error = RubyAMI::Error.new.tap { |e| e.message = 'FooBar' }
1156
+ expect(mock_call).to receive(:execute_agi_command).and_raise error
1157
+ subject.execute
1158
+ complete_reason = original_command.complete_event(0.1).reason
1159
+ expect(complete_reason).to be_a Adhearsion::Event::Complete::Error
1160
+ expect(complete_reason.details).to eq("Terminated due to AMI error 'FooBar'")
1161
+ end
1162
+ end
1163
+
1164
+ context "when the channel is gone" do
1165
+ it "should send an error complete event" do
1166
+ expect_answered
1167
+ error = ChannelGoneError.new 'FooBar'
1168
+ expect(mock_call).to receive(:execute_agi_command).and_raise error
1169
+ subject.execute
1170
+ complete_reason = original_command.complete_event(0.1).reason
1171
+ expect(complete_reason).to be_a Adhearsion::Event::Complete::Hangup
1172
+ end
1173
+ end
1174
+
1175
+ context "when the PLAYBACKSTATUS variable is set to 'FAILED'" do
1176
+ let(:playbackstatus) { 'FAILED' }
1177
+
1178
+ let(:synthstatus) { 'SUCCESS' }
1179
+ before { allow(mock_call).to receive(:channel_var).with('SYNTHSTATUS').and_return synthstatus }
1180
+
1181
+ let :fallback_doc do
1182
+ RubySpeech::SSML.draw language: 'pt-BR' do
1183
+ voice name: 'frank' do
1184
+ string "Hello world"
1185
+ end
1186
+ end
1187
+ end
1188
+
1189
+ it "should attempt to render the children of the audio tag via MRCP and then send a complete event" do
1190
+ expect_answered
1191
+ expect_playback
1192
+ expect_mrcpsynth fallback_doc
1193
+ subject.execute
1194
+ expect(original_command.complete_event(0.1).reason).to be_a Adhearsion::Rayo::Component::Output::Complete::Finish
1195
+ end
1196
+
1197
+ context "and the SYNTHSTATUS variable is set to 'ERROR'" do
1198
+ let(:synthstatus) { 'ERROR' }
1199
+
1200
+ it "should send an error complete event" do
1201
+ expect_answered
1202
+ expect_playback
1203
+ expect_mrcpsynth fallback_doc
1204
+ subject.execute
1205
+ complete_reason = original_command.complete_event(0.1).reason
1206
+ expect(complete_reason).to be_a Adhearsion::Event::Complete::Error
1207
+ expect(complete_reason.details).to eq("Terminated due to UniMRCP error")
1208
+ end
1209
+ end
1210
+ end
1211
+ end
1212
+
1213
+ context 'with a single text node without spaces' do
1214
+ let(:audio_filename) { 'tt-monkeys' }
1215
+ let :ssml_doc do
1216
+ RubySpeech::SSML.draw { string audio_filename }
1217
+ end
1218
+
1219
+ it 'should playback the audio file using Playback' do
1220
+ expect_answered
1221
+ expect_playback
1222
+ subject.execute
1223
+ end
1224
+
1225
+ it 'should send a complete event when the file finishes playback' do
1226
+ expect_answered
1227
+ expect_playback
1228
+ subject.execute
1229
+ expect(original_command.complete_event(0.1).reason).to be_a Adhearsion::Rayo::Component::Output::Complete::Finish
1230
+ end
1231
+
1232
+ context "when we get a RubyAMI Error" do
1233
+ it "should send an error complete event" do
1234
+ expect_answered
1235
+ error = RubyAMI::Error.new.tap { |e| e.message = 'FooBar' }
1236
+ expect(mock_call).to receive(:execute_agi_command).and_raise error
1237
+ subject.execute
1238
+ complete_reason = original_command.complete_event(0.1).reason
1239
+ expect(complete_reason).to be_a Adhearsion::Event::Complete::Error
1240
+ expect(complete_reason.details).to eq("Terminated due to AMI error 'FooBar'")
1241
+ end
1242
+ end
1243
+
1244
+ context "with early media playback" do
1245
+ it "should play the file with Playback" do
1246
+ expect_answered false
1247
+ expect_playback_noanswer
1248
+ expect(mock_call).to receive(:send_progress)
1249
+ subject.execute
1250
+ end
1251
+
1252
+ context "with interrupt_on set to something that is not nil" do
1253
+ let(:audio_filename) { 'tt-monkeys' }
1254
+ let :command_options do
1255
+ {
1256
+ :render_document => {
1257
+ :value => RubySpeech::SSML.draw { string audio_filename },
1258
+ },
1259
+ :interrupt_on => :any
1260
+ }
1261
+ end
1262
+ it "should return an error when the output is interruptible and it is early media" do
1263
+ expect_answered false
1264
+ error = Adhearsion::ProtocolError.new.setup 'option error', 'Interrupt digits are not allowed with early media.'
1265
+ subject.execute
1266
+ expect(original_command.response(0.1)).to eq(error)
1267
+ end
1268
+ end
1269
+ end
1270
+ end
1271
+
1272
+ context 'with multiple audio SSML nodes' do
1273
+ let(:audio_filename1) { 'foo' }
1274
+ let(:audio_filename2) { 'bar' }
1275
+ let(:audio_filename3) { 'baz' }
1276
+ let :ssml_doc do
1277
+ RubySpeech::SSML.draw do
1278
+ audio :src => audio_filename1 do
1279
+ string "Fallback 1"
1280
+ end
1281
+ audio :src => audio_filename2 do
1282
+ string "Fallback 2"
1283
+ end
1284
+ audio :src => audio_filename3 do
1285
+ string "Fallback 3"
1286
+ end
1287
+ end
1288
+ end
1289
+
1290
+ it 'should playback all audio files using Playback' do
1291
+ latch = CountDownLatch.new 2
1292
+ expect_playback audio_filename1
1293
+ expect_playback audio_filename2
1294
+ expect_playback audio_filename3
1295
+ expect_answered
1296
+ subject.execute
1297
+ latch.wait 2
1298
+ sleep 2
1299
+ end
1300
+
1301
+ it 'should send a complete event after the final file has finished playback' do
1302
+ expect_answered
1303
+ expect_playback audio_filename1
1304
+ expect_playback audio_filename2
1305
+ expect_playback audio_filename3
1306
+ latch = CountDownLatch.new 1
1307
+ expect(original_command).to receive(:add_event).once { |e|
1308
+ expect(e.reason).to be_a Adhearsion::Rayo::Component::Output::Complete::Finish
1309
+ latch.countdown!
1310
+ }
1311
+ subject.execute
1312
+ expect(latch.wait(2)).to be_truthy
1313
+ end
1314
+
1315
+ it 'should not execute further output after a stop command' do
1316
+ expect_answered
1317
+ expect(mock_call).to receive(:execute_agi_command).once.ordered do
1318
+ sleep 0.2
1319
+ end
1320
+ latch = CountDownLatch.new 1
1321
+ expect(original_command).to receive(:add_event).once { |e|
1322
+ expect(e.reason).to be_a Adhearsion::Rayo::Component::Output::Complete::Finish
1323
+ latch.countdown!
1324
+ }
1325
+ Celluloid::Future.new { subject.execute }
1326
+ sleep 0.1
1327
+ expect(mock_call).to receive(:redirect_back).ordered
1328
+ stop_command = Adhearsion::Rayo::Component::Stop.new
1329
+ stop_command.request!
1330
+ subject.execute_command stop_command
1331
+ expect(latch.wait(2)).to be_truthy
1332
+ end
1333
+
1334
+ context "when the PLAYBACKSTATUS variable is set to 'FAILED'" do
1335
+ let(:synthstatus) { 'SUCCESS' }
1336
+ before { allow(mock_call).to receive(:channel_var).with('PLAYBACKSTATUS').and_return 'SUCCESS', 'FAILED', 'SUCCESS' }
1337
+ before { allow(mock_call).to receive(:channel_var).with('SYNTHSTATUS').and_return synthstatus }
1338
+
1339
+ let :fallback_doc do
1340
+ RubySpeech::SSML.draw do
1341
+ string "Fallback 2"
1342
+ end
1343
+ end
1344
+
1345
+ it "should attempt to render the document via MRCP and then send a complete event" do
1346
+ expect_answered
1347
+ expect_playback audio_filename1
1348
+ expect_playback audio_filename2
1349
+ expect_mrcpsynth fallback_doc
1350
+ expect_playback audio_filename3
1351
+ subject.execute
1352
+ expect(original_command.complete_event(0.1).reason).to be_a Adhearsion::Rayo::Component::Output::Complete::Finish
1353
+ end
1354
+
1355
+ context "and the SYNTHSTATUS variable is set to 'ERROR'" do
1356
+ let(:synthstatus) { 'ERROR' }
1357
+
1358
+ it "should terminate playback and send an error complete event" do
1359
+ expect_answered
1360
+ expect_playback audio_filename1
1361
+ expect_playback audio_filename2
1362
+ expect_mrcpsynth fallback_doc
1363
+ subject.execute
1364
+ complete_reason = original_command.complete_event(0.1).reason
1365
+ expect(complete_reason).to be_a Adhearsion::Event::Complete::Error
1366
+ expect(complete_reason.details).to eq("Terminated due to UniMRCP error")
1367
+ end
1368
+ end
1369
+ end
1370
+ end
1371
+
1372
+ context "with an SSML document containing top-level elements other than <audio/>" do
1373
+ let :ssml_doc do
1374
+ RubySpeech::SSML.draw do
1375
+ voice name: 'Paul' do
1376
+ string "Foo Bar"
1377
+ end
1378
+ end
1379
+ end
1380
+
1381
+ before { allow(mock_call).to receive(:channel_var).with('SYNTHSTATUS').and_return 'SUCCESS' }
1382
+
1383
+ it "should attempt to render the document via MRCP and then send a complete event" do
1384
+ expect_answered
1385
+ expect_mrcpsynth ssml_doc
1386
+ subject.execute
1387
+ expect(original_command.complete_event(0.1).reason).to be_a Adhearsion::Rayo::Component::Output::Complete::Finish
1388
+ end
1389
+ end
1390
+
1391
+ context "with mixed TTS and audio tags" do
1392
+ let :ssml_doc do
1393
+ RubySpeech::SSML.draw do
1394
+ voice name: 'Paul' do
1395
+ string "Foo Bar"
1396
+ end
1397
+ audio src: 'tt-monkeys'
1398
+ voice name: 'Frank' do
1399
+ string "Doo Dah"
1400
+ end
1401
+ string 'tt-weasels'
1402
+ end
1403
+ end
1404
+
1405
+ let :first_doc do
1406
+ RubySpeech::SSML.draw do
1407
+ voice name: 'Paul' do
1408
+ string "Foo Bar"
1409
+ end
1410
+ end
1411
+ end
1412
+
1413
+ let :second_doc do
1414
+ RubySpeech::SSML.draw do
1415
+ voice name: 'Frank' do
1416
+ string "Doo Dah"
1417
+ end
1418
+ end
1419
+ end
1420
+
1421
+ before { allow(mock_call).to receive(:channel_var).with('SYNTHSTATUS').and_return 'SUCCESS' }
1422
+
1423
+ it "should attempt to render the document via MRCP and then send a complete event" do
1424
+ expect_answered
1425
+ expect_mrcpsynth first_doc
1426
+ expect_playback 'tt-monkeys'
1427
+ expect_mrcpsynth second_doc
1428
+ expect_playback 'tt-weasels'
1429
+ subject.execute
1430
+ expect(original_command.complete_event(0.1).reason).to be_a Adhearsion::Rayo::Component::Output::Complete::Finish
1431
+ end
1432
+ end
1433
+
1434
+ context 'with multiple documents' do
1435
+ let :second_ssml_doc do
1436
+ RubySpeech::SSML.draw do
1437
+ audio :src => 'two.wav' do
1438
+ string "Bazzz"
1439
+ end
1440
+ end
1441
+ end
1442
+
1443
+ let :third_ssml_doc do
1444
+ RubySpeech::SSML.draw do
1445
+ audio :src => 'three.wav' do
1446
+ string "Barrrr"
1447
+ end
1448
+ end
1449
+ end
1450
+
1451
+ let(:command_opts) { { render_documents: [{value: ssml_doc}, {value: second_ssml_doc}, {value: third_ssml_doc}] } }
1452
+
1453
+ it "should render each document in turn using a Playback per document" do
1454
+ expect_answered
1455
+ expect_playback
1456
+ expect_playback 'two'
1457
+ expect_playback 'three'
1458
+ subject.execute
1459
+ expect(original_command.complete_event(0.1).reason).to be_a Adhearsion::Rayo::Component::Output::Complete::Finish
1460
+ end
1461
+
1462
+ it 'should not execute further output after a stop command' do
1463
+ expect_answered
1464
+ expect(mock_call).to receive(:execute_agi_command).once.ordered do
1465
+ sleep 0.2
1466
+ end
1467
+ latch = CountDownLatch.new 1
1468
+ expect(original_command).to receive(:add_event).once { |e|
1469
+ expect(e.reason).to be_a Adhearsion::Rayo::Component::Output::Complete::Finish
1470
+ latch.countdown!
1471
+ }
1472
+ Celluloid::Future.new { subject.execute }
1473
+ sleep 0.1
1474
+ expect(mock_call).to receive(:redirect_back).ordered
1475
+ stop_command = Adhearsion::Rayo::Component::Stop.new
1476
+ stop_command.request!
1477
+ subject.execute_command stop_command
1478
+ expect(latch.wait(2)).to be_truthy
1479
+ end
1480
+
1481
+ context "when the PLAYBACKSTATUS variable is set to 'FAILED'" do
1482
+ let(:synthstatus) { 'SUCCESS' }
1483
+ before { allow(mock_call).to receive(:channel_var).with('PLAYBACKSTATUS').and_return 'SUCCESS', 'FAILED', 'SUCCESS' }
1484
+ before { allow(mock_call).to receive(:channel_var).with('SYNTHSTATUS').and_return synthstatus }
1485
+
1486
+ let :fallback_doc do
1487
+ RubySpeech::SSML.draw do
1488
+ string "Bazzz"
1489
+ end
1490
+ end
1491
+
1492
+ it "should attempt to render the document via MRCP and then send a complete event" do
1493
+ expect_answered
1494
+ expect_playback
1495
+ expect_playback 'two'
1496
+ expect_mrcpsynth fallback_doc
1497
+ expect_playback 'three'
1498
+ subject.execute
1499
+ expect(original_command.complete_event(0.1).reason).to be_a Adhearsion::Rayo::Component::Output::Complete::Finish
1500
+ end
1501
+
1502
+ context "and the SYNTHSTATUS variable is set to 'ERROR'" do
1503
+ let(:synthstatus) { 'ERROR' }
1504
+
1505
+ it "should terminate playback and send an error complete event" do
1506
+ expect_answered
1507
+ expect_playback
1508
+ expect_playback 'two'
1509
+ expect_mrcpsynth fallback_doc
1510
+ subject.execute
1511
+ complete_reason = original_command.complete_event(0.1).reason
1512
+ expect(complete_reason).to be_a Adhearsion::Event::Complete::Error
1513
+ expect(complete_reason.details).to eq("Terminated due to UniMRCP error")
1514
+ end
1515
+ end
1516
+ end
1517
+ end
1518
+ end
1519
+
1520
+ describe 'start-offset' do
1521
+ context 'unset' do
1522
+ let(:command_opts) { { :start_offset => nil } }
1523
+ it 'should not pass any options to Playback' do
1524
+ expect_answered
1525
+ expect_playback
1526
+ subject.execute
1527
+ end
1528
+ end
1529
+
1530
+ context 'set' do
1531
+ let(:command_opts) { { :start_offset => 10 } }
1532
+ it "should return an error and not execute any actions" do
1533
+ subject.execute
1534
+ error = Adhearsion::ProtocolError.new.setup 'option error', 'A start_offset value is unsupported on Asterisk.'
1535
+ expect(original_command.response(0.1)).to eq(error)
1536
+ end
1537
+ end
1538
+ end
1539
+
1540
+ describe 'start-paused' do
1541
+ context 'false' do
1542
+ let(:command_opts) { { :start_paused => false } }
1543
+ it 'should not pass any options to Playback' do
1544
+ expect_answered
1545
+ expect_playback
1546
+ subject.execute
1547
+ end
1548
+ end
1549
+
1550
+ context 'true' do
1551
+ let(:command_opts) { { :start_paused => true } }
1552
+ it "should return an error and not execute any actions" do
1553
+ subject.execute
1554
+ error = Adhearsion::ProtocolError.new.setup 'option error', 'A start_paused value is unsupported on Asterisk.'
1555
+ expect(original_command.response(0.1)).to eq(error)
1556
+ end
1557
+ end
1558
+ end
1559
+
1560
+ describe 'repeat-interval' do
1561
+ context 'unset' do
1562
+ let(:command_opts) { { :repeat_interval => nil } }
1563
+ it 'should not pass any options to Playback' do
1564
+ expect_answered
1565
+ expect_playback
1566
+ subject.execute
1567
+ end
1568
+ end
1569
+
1570
+ context 'set' do
1571
+ let(:command_opts) { { :repeat_interval => 10 } }
1572
+ it "should return an error and not execute any actions" do
1573
+ subject.execute
1574
+ error = Adhearsion::ProtocolError.new.setup 'option error', 'A repeat_interval value is unsupported on Asterisk.'
1575
+ expect(original_command.response(0.1)).to eq(error)
1576
+ end
1577
+ end
1578
+ end
1579
+
1580
+ describe 'repeat-times' do
1581
+ context 'unset' do
1582
+ let(:command_opts) { { :repeat_times => nil } }
1583
+ it 'should not pass any options to Playback' do
1584
+ expect_answered
1585
+ expect_playback
1586
+ subject.execute
1587
+ end
1588
+ end
1589
+
1590
+ context 'set' do
1591
+ let(:command_opts) { { :repeat_times => 2 } }
1592
+
1593
+ it "should render the specified number of times" do
1594
+ expect_answered
1595
+ 2.times { expect_playback }
1596
+ subject.execute
1597
+ expect(original_command.complete_event(0.1).reason).to be_a Adhearsion::Rayo::Component::Output::Complete::Finish
1598
+ end
1599
+
1600
+ context 'to 0' do
1601
+ let(:command_opts) { { :repeat_times => 0 } }
1602
+
1603
+ it "should render 10,000 the specified number of times" do
1604
+ expect_answered
1605
+ 1000.times { expect_playback }
1606
+ subject.execute
1607
+ expect(original_command.complete_event(0.1).reason).to be_a Adhearsion::Rayo::Component::Output::Complete::Finish
1608
+ end
1609
+ end
1610
+
1611
+ it 'should not execute further output after a stop command' do
1612
+ expect_answered
1613
+ expect(mock_call).to receive(:execute_agi_command).once.ordered do
1614
+ sleep 0.2
1615
+ end
1616
+ latch = CountDownLatch.new 1
1617
+ expect(original_command).to receive(:add_event).once { |e|
1618
+ expect(e.reason).to be_a Adhearsion::Rayo::Component::Output::Complete::Finish
1619
+ latch.countdown!
1620
+ }
1621
+ Celluloid::Future.new { subject.execute }
1622
+ sleep 0.1
1623
+ expect(mock_call).to receive(:redirect_back).ordered
1624
+ stop_command = Adhearsion::Rayo::Component::Stop.new
1625
+ stop_command.request!
1626
+ subject.execute_command stop_command
1627
+ expect(latch.wait(2)).to be_truthy
1628
+ end
1629
+ end
1630
+ end
1631
+
1632
+ describe 'max-time' do
1633
+ context 'unset' do
1634
+ let(:command_opts) { { :max_time => nil } }
1635
+ it 'should not pass any options to Playback' do
1636
+ expect_answered
1637
+ expect_playback
1638
+ subject.execute
1639
+ end
1640
+ end
1641
+
1642
+ context 'set' do
1643
+ let(:command_opts) { { :max_time => 30 } }
1644
+ it "should return an error and not execute any actions" do
1645
+ subject.execute
1646
+ error = Adhearsion::ProtocolError.new.setup 'option error', 'A max_time value is unsupported on Asterisk.'
1647
+ expect(original_command.response(0.1)).to eq(error)
1648
+ end
1649
+ end
1650
+ end
1651
+
1652
+ describe 'voice' do
1653
+ context 'unset' do
1654
+ let(:command_opts) { { :voice => nil } }
1655
+ it 'should not pass the v option to Playback' do
1656
+ expect_answered
1657
+ expect_playback
1658
+ subject.execute
1659
+ end
1660
+ end
1661
+
1662
+ context 'set' do
1663
+ let(:command_opts) { { :voice => 'alison' } }
1664
+ it "should return an error and not execute any actions" do
1665
+ subject.execute
1666
+ error = Adhearsion::ProtocolError.new.setup 'option error', 'A voice value is unsupported on Asterisk.'
1667
+ expect(original_command.response(0.1)).to eq(error)
1668
+ end
1669
+ end
1670
+ end
1671
+
1672
+ describe 'interrupt_on' do
1673
+ def ami_event_for_dtmf(digit, position)
1674
+ if ast13mode
1675
+ RubyAMI::Event.new 'DTMF' + (position == :start ? 'Begin' : '') + (position == :end ? 'End' : ''),
1676
+ 'Digit' => digit.to_s
1677
+ else
1678
+ RubyAMI::Event.new 'DTMF',
1679
+ 'Digit' => digit.to_s,
1680
+ 'Start' => position == :start ? 'Yes' : 'No',
1681
+ 'End' => position == :end ? 'Yes' : 'No'
1682
+ end
1683
+ end
1684
+
1685
+ def send_ami_events_for_dtmf(digit)
1686
+ mock_call.process_ami_event ami_event_for_dtmf(digit, :start)
1687
+ mock_call.process_ami_event ami_event_for_dtmf(digit, :end)
1688
+ end
1689
+
1690
+ let(:reason) { original_command.complete_event(5).reason }
1691
+ let(:channel) { "SIP/1234-00000000" }
1692
+ let :ami_event do
1693
+ RubyAMI::Event.new 'AsyncAGI',
1694
+ 'SubEvent' => "Start",
1695
+ 'Channel' => channel,
1696
+ '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"
1697
+ end
1698
+
1699
+ context "set to nil" do
1700
+ let(:command_opts) { { :interrupt_on => nil } }
1701
+ it "does not redirect the call" do
1702
+ expect_answered
1703
+ expect_playback
1704
+ expect(mock_call).to receive(:redirect_back).never
1705
+ subject.execute
1706
+ expect(original_command.response(0.1)).to be_a Adhearsion::Rayo::Ref
1707
+ send_ami_events_for_dtmf 1
1708
+ end
1709
+ end
1710
+
1711
+ context "set to :any" do
1712
+ let(:command_opts) { { :interrupt_on => :any } }
1713
+
1714
+ before do
1715
+ expect_answered
1716
+ expect(mock_call).to receive(:execute_agi_command).once.with('EXEC Playback', audio_filename)
1717
+ expect(subject).to receive(:send_finish).and_return nil
1718
+ end
1719
+
1720
+ context "when a DTMF digit is received" do
1721
+ it "sends the correct complete event" do
1722
+ expect(mock_call).to receive :redirect_back
1723
+ subject.execute
1724
+ expect(original_command.response(0.1)).to be_a Adhearsion::Rayo::Ref
1725
+ expect(original_command).not_to be_complete
1726
+ send_ami_events_for_dtmf 1
1727
+ mock_call.process_ami_event ami_event
1728
+ sleep 0.2
1729
+ expect(original_command).to be_complete
1730
+ expect(reason).to be_a Adhearsion::Rayo::Component::Output::Complete::Finish
1731
+ end
1732
+
1733
+ it "redirects the call back to async AGI" do
1734
+ expect(mock_call).to receive(:redirect_back).once
1735
+ subject.execute
1736
+ expect(original_command.response(0.1)).to be_a Adhearsion::Rayo::Ref
1737
+ send_ami_events_for_dtmf 1
1738
+ end
1739
+ end
1740
+ end
1741
+
1742
+ context "set to :dtmf" do
1743
+ let(:command_opts) { { :interrupt_on => :dtmf } }
1744
+
1745
+ before do
1746
+ expect_answered
1747
+ expect(mock_call).to receive(:execute_agi_command).once.with('EXEC Playback', audio_filename)
1748
+ expect(subject).to receive(:send_finish).and_return nil
1749
+ end
1750
+
1751
+ def send_correct_complete_event
1752
+ expect(mock_call).to receive :redirect_back
1753
+ subject.execute
1754
+ expect(original_command.response(0.1)).to be_a Adhearsion::Rayo::Ref
1755
+ expect(original_command).not_to be_complete
1756
+ send_ami_events_for_dtmf 1
1757
+ mock_call.process_ami_event ami_event
1758
+ sleep 0.2
1759
+ expect(original_command).to be_complete
1760
+ expect(reason).to be_a Adhearsion::Rayo::Component::Output::Complete::Finish
1761
+ end
1762
+
1763
+ context "when a DTMF digit is received" do
1764
+ it "sends the correct complete event" do
1765
+ send_correct_complete_event
1766
+ end
1767
+
1768
+ it "redirects the call back to async AGI" do
1769
+ expect(mock_call).to receive(:redirect_back).once
1770
+ subject.execute
1771
+ expect(original_command.response(0.1)).to be_a Adhearsion::Rayo::Ref
1772
+ send_ami_events_for_dtmf 1
1773
+ end
1774
+
1775
+ context 'with an Asterisk 13 DTMFEnd event' do
1776
+ let(:ast13mode) { true }
1777
+ it "sends the correct complete event" do
1778
+ send_correct_complete_event
1779
+ end
1780
+ end
1781
+ end
1782
+ end
1783
+
1784
+ context "set to :voice" do
1785
+ let(:command_opts) { { :interrupt_on => :voice } }
1786
+ it "should return an error and not execute any actions" do
1787
+ subject.execute
1788
+ error = Adhearsion::ProtocolError.new.setup 'option error', 'An interrupt-on value of speech is unsupported.'
1789
+ expect(original_command.response(0.1)).to eq(error)
1790
+ end
1791
+ end
1792
+ end
1793
+ end
1794
+ end
1795
+
1796
+ describe "#execute_command" do
1797
+ context "with a command it does not understand" do
1798
+ let(:command) { Adhearsion::Rayo::Component::Output::Pause.new }
1799
+
1800
+ before { command.request! }
1801
+ it "returns a Adhearsion::ProtocolError response" do
1802
+ subject.execute_command command
1803
+ expect(command.response(0.1)).to be_a Adhearsion::ProtocolError
1804
+ end
1805
+ end
1806
+
1807
+ context "with a Stop command" do
1808
+ let(:command) { Adhearsion::Rayo::Component::Stop.new }
1809
+ let(:reason) { original_command.complete_event(5).reason }
1810
+ let(:channel) { "SIP/1234-00000000" }
1811
+ let :ami_event do
1812
+ RubyAMI::Event.new 'AsyncAGI',
1813
+ 'SubEvent' => "Start",
1814
+ 'Channel' => channel,
1815
+ '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"
1816
+ end
1817
+
1818
+ before do
1819
+ command.request!
1820
+ original_command.request!
1821
+ original_command.execute!
1822
+ end
1823
+
1824
+ it "sets the command response to true" do
1825
+ expect(mock_call).to receive(:redirect_back)
1826
+ subject.execute_command command
1827
+ expect(command.response(0.1)).to eq(true)
1828
+ end
1829
+
1830
+ it "sends the correct complete event" do
1831
+ expect(mock_call).to receive(:redirect_back)
1832
+ subject.execute_command command
1833
+ expect(original_command).not_to be_complete
1834
+ mock_call.process_ami_event ami_event
1835
+ expect(reason).to be_a Adhearsion::Event::Complete::Stop
1836
+ expect(original_command).to be_complete
1837
+ end
1838
+
1839
+ it "redirects the call by unjoining it" do
1840
+ expect(mock_call).to receive(:redirect_back)
1841
+ subject.execute_command command
1842
+ end
1843
+ end
1844
+ end
1845
+
1846
+ end
1847
+ end
1848
+ end
1849
+ end
1850
+ end