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
@@ -0,0 +1,646 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ module Punchblock
6
+ module Translator
7
+ class Asterisk
8
+ module Component
9
+ describe MRCPPrompt do
10
+ include HasMockCallbackConnection
11
+
12
+ let(:media_engine) { :unimrcp }
13
+ let(:ami_client) { double('AMI') }
14
+ let(:translator) { Punchblock::Translator::Asterisk.new ami_client, connection, media_engine }
15
+ let(:mock_call) { Punchblock::Translator::Asterisk::Call.new 'foo', translator, ami_client, connection }
16
+
17
+ let :ssml_doc do
18
+ RubySpeech::SSML.draw do
19
+ say_as(:interpret_as => :cardinal) { 'FOO' }
20
+ end
21
+ end
22
+
23
+ let :voice_grammar do
24
+ RubySpeech::GRXML.draw :mode => 'voice', :root => 'color' do
25
+ rule id: 'color' do
26
+ one_of do
27
+ item { 'red' }
28
+ item { 'blue' }
29
+ item { 'green' }
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ let :dtmf_grammar do
36
+ RubySpeech::GRXML.draw :mode => 'dtmf', :root => 'pin' do
37
+ rule id: 'digit' do
38
+ one_of do
39
+ 0.upto(9) { |d| item { d.to_s } }
40
+ end
41
+ end
42
+
43
+ rule id: 'pin', scope: 'public' do
44
+ item repeat: '2' do
45
+ ruleref uri: '#digit'
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ let(:grammar) { voice_grammar }
52
+
53
+ let(:output_command_opts) { {} }
54
+
55
+ let :output_command_options do
56
+ { render_document: {value: ssml_doc} }.merge(output_command_opts)
57
+ end
58
+
59
+ let(:input_command_opts) { {} }
60
+
61
+ let :input_command_options do
62
+ { grammar: {value: grammar} }.merge(input_command_opts)
63
+ end
64
+
65
+ let(:command_options) { {} }
66
+
67
+ let :output_command do
68
+ Punchblock::Component::Output.new output_command_options
69
+ end
70
+
71
+ let :input_command do
72
+ Punchblock::Component::Input.new input_command_options
73
+ end
74
+
75
+ let :original_command do
76
+ Punchblock::Component::Prompt.new output_command, input_command, command_options
77
+ end
78
+
79
+ let(:recog_status) { 'OK' }
80
+ let(:recog_completion_cause) { '000' }
81
+ let(:recog_result) { "%3C?xml%20version=%221.0%22?%3E%3Cresult%3E%0D%0A%3Cinterpretation%20grammar=%22session:grammar-0%22%20confidence=%220.43%22%3E%3Cinput%20mode=%22speech%22%3EHello%3C/input%3E%3Cinstance%3EHello%3C/instance%3E%3C/interpretation%3E%3C/result%3E" }
82
+
83
+ subject { described_class.new original_command, mock_call }
84
+
85
+ before do
86
+ original_command.request!
87
+ {
88
+ 'RECOG_STATUS' => recog_status,
89
+ 'RECOG_COMPLETION_CAUSE' => recog_completion_cause,
90
+ 'RECOG_RESULT' => recog_result
91
+ }.each do |var, val|
92
+ mock_call.stub(:channel_var).with(var).and_return val
93
+ end
94
+ end
95
+
96
+ context 'with an invalid recognizer' do
97
+ let(:input_command_opts) { { recognizer: 'foobar' } }
98
+
99
+ it "should return an error and not execute any actions" do
100
+ subject.execute
101
+ error = ProtocolError.new.setup 'option error', 'The recognizer foobar is unsupported.'
102
+ original_command.response(0.1).should be == error
103
+ end
104
+ end
105
+
106
+ [:asterisk].each do |recognizer|
107
+ context "with a recognizer #{recognizer.inspect}" do
108
+ let(:input_command_opts) { { recognizer: recognizer } }
109
+
110
+ it "should return an error and not execute any actions" do
111
+ subject.execute
112
+ error = ProtocolError.new.setup 'option error', "The recognizer #{recognizer} is unsupported."
113
+ original_command.response(0.1).should be == error
114
+ end
115
+ end
116
+ end
117
+
118
+ def expect_mrcpsynth_with_options(options)
119
+ expect_app_with_options 'MRCPSynth', options
120
+ end
121
+
122
+ def expect_synthandrecog_with_options(options)
123
+ expect_app_with_options 'SynthAndRecog', options
124
+ end
125
+
126
+ def expect_app_with_options(app, options)
127
+ mock_call.should_receive(:execute_agi_command).once.with do |*args|
128
+ args[0].should be == "EXEC #{app}"
129
+ args[1].should match options
130
+ end.and_return code: 200, result: 1
131
+ end
132
+
133
+ describe 'Output#document' do
134
+ context 'with multiple inline documents' do
135
+ let(:output_command_options) { { render_documents: [{value: ssml_doc}, {value: ssml_doc}] } }
136
+
137
+ it "should return a ref and execute SynthAndRecog" do
138
+ param = [[ssml_doc.to_doc.to_s, ssml_doc.to_doc.to_s].join(','), grammar.to_doc].map { |o| "\"#{o.to_s.squish.gsub('"', '\"')}\"" }.push('uer=1&b=1').join(',')
139
+ mock_call.should_receive(:execute_agi_command).once.with('EXEC SynthAndRecog', param).and_return code: 200, result: 1
140
+ subject.execute
141
+ original_command.response(0.1).should be_a Ref
142
+ end
143
+ end
144
+
145
+ context 'with multiple documents by URI' do
146
+ let(:output_command_options) { { render_documents: [{url: 'http://example.com/doc1.ssml'}, {url: 'http://example.com/doc2.ssml'}] } }
147
+
148
+ it "should return a ref and execute SynthAndRecog" do
149
+ param = [['http://example.com/doc1.ssml', 'http://example.com/doc2.ssml'].join(','), grammar.to_doc].map { |o| "\"#{o.to_s.squish.gsub('"', '\"')}\"" }.push('uer=1&b=1').join(',')
150
+ mock_call.should_receive(:execute_agi_command).once.with('EXEC SynthAndRecog', param).and_return code: 200, result: 1
151
+ subject.execute
152
+ original_command.response(0.1).should be_a Ref
153
+ end
154
+ end
155
+
156
+ context 'unset' do
157
+ let(:output_command_options) { {} }
158
+
159
+ it "should return an error and not execute any actions" do
160
+ subject.execute
161
+ error = ProtocolError.new.setup 'option error', 'An SSML document is required.'
162
+ original_command.response(0.1).should be == error
163
+ end
164
+ end
165
+ end
166
+
167
+ describe 'Output#renderer' do
168
+ [nil, :unimrcp].each do |renderer|
169
+ context renderer.to_s do
170
+ let(:output_command_opts) { { renderer: renderer } }
171
+
172
+ it "should return a ref and execute SynthAndRecog" do
173
+ param = [ssml_doc.to_doc, grammar.to_doc].map { |o| "\"#{o.to_s.squish.gsub('"', '\"')}\"" }.push('uer=1&b=1').join(',')
174
+ mock_call.should_receive(:execute_agi_command).once.with('EXEC SynthAndRecog', param).and_return code: 200, result: 1
175
+ subject.execute
176
+ original_command.response(0.1).should be_a Ref
177
+ end
178
+
179
+ context "when SynthAndRecog completes" do
180
+ context "with a match" do
181
+ let :expected_nlsml do
182
+ RubySpeech::NLSML.draw do
183
+ interpretation grammar: 'session:grammar-0', confidence: 0.43 do
184
+ input 'Hello', mode: :speech
185
+ instance 'Hello'
186
+ end
187
+ end
188
+ end
189
+
190
+ it 'should send a match complete event' do
191
+ expected_complete_reason = Punchblock::Component::Input::Complete::Match.new nlsml: expected_nlsml
192
+
193
+ mock_call.should_receive(:execute_agi_command).and_return code: 200, result: 1
194
+ subject.execute
195
+ original_command.complete_event(0.1).reason.should == expected_complete_reason
196
+ end
197
+ end
198
+
199
+ context "with a nomatch cause" do
200
+ let(:recog_completion_cause) { '001' }
201
+
202
+ it 'should send a nomatch complete event' do
203
+ expected_complete_reason = Punchblock::Component::Input::Complete::NoMatch.new
204
+ mock_call.should_receive(:execute_agi_command).and_return code: 200, result: 1
205
+ subject.execute
206
+ original_command.complete_event(0.1).reason.should == expected_complete_reason
207
+ end
208
+ end
209
+
210
+ context "with a noinput cause" do
211
+ let(:recog_completion_cause) { '002' }
212
+
213
+ it 'should send a nomatch complete event' do
214
+ expected_complete_reason = Punchblock::Component::Input::Complete::NoInput.new
215
+ mock_call.should_receive(:execute_agi_command).and_return code: 200, result: 1
216
+ subject.execute
217
+ original_command.complete_event(0.1).reason.should == expected_complete_reason
218
+ end
219
+ end
220
+
221
+ context "when the RECOG_STATUS variable is set to 'ERROR'" do
222
+ let(:recog_status) { 'ERROR' }
223
+
224
+ it "should send an error complete event" do
225
+ mock_call.should_receive(:execute_agi_command).and_return code: 200, result: 1
226
+ subject.execute
227
+ complete_reason = original_command.complete_event(0.1).reason
228
+ complete_reason.should be_a Punchblock::Event::Complete::Error
229
+ complete_reason.details.should == "Terminated due to UniMRCP error"
230
+ end
231
+ end
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
+ mock_call.should_receive(:execute_agi_command).and_raise error
238
+ subject.execute
239
+ complete_reason = original_command.complete_event(0.1).reason
240
+ complete_reason.should be_a Punchblock::Event::Complete::Error
241
+ complete_reason.details.should == "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
+ mock_call.should_receive(:execute_agi_command).and_raise error
249
+ subject.execute
250
+ complete_reason = original_command.complete_event(0.1).reason
251
+ complete_reason.should be_a Punchblock::Event::Complete::Hangup
252
+ end
253
+ end
254
+ end
255
+ end
256
+
257
+ [:foobar, :swift, :asterisk].each do |renderer|
258
+ context renderer do
259
+ let(:output_command_opts) { { renderer: renderer } }
260
+
261
+ it "should return an error and not execute any actions" do
262
+ subject.execute
263
+ error = ProtocolError.new.setup 'option error', "The renderer #{renderer} is unsupported."
264
+ original_command.response(0.1).should be == error
265
+ end
266
+ end
267
+ end
268
+ end
269
+
270
+ describe 'barge_in' do
271
+ context 'unset' do
272
+ let(:command_options) { { barge_in: nil } }
273
+
274
+ it 'should pass the b=1 option to SynthAndRecog' do
275
+ expect_synthandrecog_with_options(/b=1/)
276
+ subject.execute
277
+ end
278
+ end
279
+
280
+ context 'true' do
281
+ let(:command_options) { { barge_in: true } }
282
+
283
+ it 'should pass the b=1 option to SynthAndRecog' do
284
+ expect_synthandrecog_with_options(/b=1/)
285
+ subject.execute
286
+ end
287
+ end
288
+
289
+ context 'false' do
290
+ let(:command_options) { { barge_in: false } }
291
+
292
+ it 'should pass the b=0 option to SynthAndRecog' do
293
+ expect_synthandrecog_with_options(/b=0/)
294
+ subject.execute
295
+ end
296
+ end
297
+ end
298
+
299
+ describe 'Output#voice' do
300
+ context 'unset' do
301
+ let(:output_command_opts) { { voice: nil } }
302
+
303
+ it 'should not pass the vn option to SynthAndRecog' do
304
+ expect_synthandrecog_with_options(//)
305
+ subject.execute
306
+ end
307
+ end
308
+
309
+ context 'set' do
310
+ let(:output_command_opts) { { voice: 'alison' } }
311
+
312
+ it 'should pass the vn option to SynthAndRecog' do
313
+ expect_synthandrecog_with_options(/vn=alison/)
314
+ subject.execute
315
+ end
316
+ end
317
+ end
318
+
319
+ describe 'Output#start-offset' do
320
+ context 'unset' do
321
+ let(:output_command_opts) { { start_offset: nil } }
322
+ it 'should not pass any options to SynthAndRecog' do
323
+ expect_synthandrecog_with_options(//)
324
+ subject.execute
325
+ end
326
+ end
327
+
328
+ context 'set' do
329
+ let(:output_command_opts) { { start_offset: 10 } }
330
+ it "should return an error and not execute any actions" do
331
+ subject.execute
332
+ error = ProtocolError.new.setup 'option error', 'A start_offset value is unsupported on Asterisk.'
333
+ original_command.response(0.1).should be == error
334
+ end
335
+ end
336
+ end
337
+
338
+ describe 'Output#start-paused' do
339
+ context 'false' do
340
+ let(:output_command_opts) { { start_paused: false } }
341
+ it 'should not pass any options to SynthAndRecog' do
342
+ expect_synthandrecog_with_options(//)
343
+ subject.execute
344
+ end
345
+ end
346
+
347
+ context 'true' do
348
+ let(:output_command_opts) { { start_paused: true } }
349
+ it "should return an error and not execute any actions" do
350
+ subject.execute
351
+ error = ProtocolError.new.setup 'option error', 'A start_paused value is unsupported on Asterisk.'
352
+ original_command.response(0.1).should be == error
353
+ end
354
+ end
355
+ end
356
+
357
+ describe 'Output#repeat-interval' do
358
+ context 'unset' do
359
+ let(:output_command_opts) { { repeat_interval: nil } }
360
+ it 'should not pass any options to SynthAndRecog' do
361
+ expect_synthandrecog_with_options(//)
362
+ subject.execute
363
+ end
364
+ end
365
+
366
+ context 'set' do
367
+ let(:output_command_opts) { { repeat_interval: 10 } }
368
+ it "should return an error and not execute any actions" do
369
+ subject.execute
370
+ error = ProtocolError.new.setup 'option error', 'A repeat_interval value is unsupported on Asterisk.'
371
+ original_command.response(0.1).should be == error
372
+ end
373
+ end
374
+ end
375
+
376
+ describe 'Output#repeat-times' do
377
+ context 'unset' do
378
+ let(:output_command_opts) { { repeat_times: nil } }
379
+ it 'should not pass any options to SynthAndRecog' do
380
+ expect_synthandrecog_with_options(//)
381
+ subject.execute
382
+ end
383
+ end
384
+
385
+ context 'set' do
386
+ let(:output_command_opts) { { repeat_times: 2 } }
387
+ it "should return an error and not execute any actions" do
388
+ subject.execute
389
+ error = ProtocolError.new.setup 'option error', 'A repeat_times value is unsupported on Asterisk.'
390
+ original_command.response(0.1).should be == error
391
+ end
392
+ end
393
+ end
394
+
395
+ describe 'Output#max-time' do
396
+ context 'unset' do
397
+ let(:output_command_opts) { { max_time: nil } }
398
+ it 'should not pass any options to SynthAndRecog' do
399
+ expect_synthandrecog_with_options(//)
400
+ subject.execute
401
+ end
402
+ end
403
+
404
+ context 'set' do
405
+ let(:output_command_opts) { { max_time: 30 } }
406
+ it "should return an error and not execute any actions" do
407
+ subject.execute
408
+ error = ProtocolError.new.setup 'option error', 'A max_time value is unsupported on Asterisk.'
409
+ original_command.response(0.1).should be == error
410
+ end
411
+ end
412
+ end
413
+
414
+ describe 'Output#interrupt_on' do
415
+ context 'unset' do
416
+ let(:output_command_opts) { { interrupt_on: nil } }
417
+ it 'should not pass any options to SynthAndRecog' do
418
+ expect_synthandrecog_with_options(//)
419
+ subject.execute
420
+ end
421
+ end
422
+
423
+ context 'set' do
424
+ let(:output_command_opts) { { interrupt_on: :dtmf } }
425
+ it "should return an error and not execute any actions" do
426
+ subject.execute
427
+ error = ProtocolError.new.setup 'option error', 'A interrupt_on value is unsupported on Asterisk.'
428
+ original_command.response(0.1).should be == error
429
+ end
430
+ end
431
+ end
432
+
433
+ describe 'Output#grammar' do
434
+ context 'with multiple inline grammars' do
435
+ let(:input_command_options) { { grammars: [{value: voice_grammar}, {value: dtmf_grammar}] } }
436
+
437
+ it "should return a ref and execute SynthAndRecog" do
438
+ param = [ssml_doc.to_doc, [voice_grammar.to_doc.to_s, dtmf_grammar.to_doc.to_s].join(',')].map { |o| "\"#{o.to_s.squish.gsub('"', '\"')}\"" }.push('uer=1&b=1').join(',')
439
+ mock_call.should_receive(:execute_agi_command).once.with('EXEC SynthAndRecog', param).and_return code: 200, result: 1
440
+ subject.execute
441
+ original_command.response(0.1).should be_a Ref
442
+ end
443
+ end
444
+
445
+ context 'with multiple grammars by URI' do
446
+ let(:input_command_options) { { grammars: [{url: 'http://example.com/grammar1.grxml'}, {url: 'http://example.com/grammar2.grxml'}] } }
447
+
448
+ it "should return a ref and execute SynthAndRecog" do
449
+ param = [ssml_doc.to_doc, ['http://example.com/grammar1.grxml', 'http://example.com/grammar2.grxml'].join(',')].map { |o| "\"#{o.to_s.squish.gsub('"', '\"')}\"" }.push('uer=1&b=1').join(',')
450
+ mock_call.should_receive(:execute_agi_command).once.with('EXEC SynthAndRecog', param).and_return code: 200, result: 1
451
+ subject.execute
452
+ original_command.response(0.1).should be_a Ref
453
+ end
454
+ end
455
+
456
+ context 'unset' do
457
+ let(:input_command_options) { {} }
458
+
459
+ it "should return an error and not execute any actions" do
460
+ subject.execute
461
+ error = ProtocolError.new.setup 'option error', 'A grammar is required.'
462
+ original_command.response(0.1).should be == error
463
+ end
464
+ end
465
+ end
466
+
467
+ describe 'Input#initial-timeout' do
468
+ context 'a positive number' do
469
+ let(:input_command_opts) { { initial_timeout: 1000 } }
470
+
471
+ it 'should pass the nit option to SynthAndRecog' do
472
+ expect_synthandrecog_with_options(/nit=1000/)
473
+ subject.execute
474
+ end
475
+ end
476
+
477
+ context '-1' do
478
+ let(:input_command_opts) { { initial_timeout: -1 } }
479
+
480
+ it 'should not pass any options to SynthAndRecog' do
481
+ expect_synthandrecog_with_options(//)
482
+ subject.execute
483
+ end
484
+ end
485
+
486
+ context 'unset' do
487
+ let(:input_command_opts) { { initial_timeout: nil } }
488
+
489
+ it 'should not pass any options to SynthAndRecog' do
490
+ expect_synthandrecog_with_options(//)
491
+ subject.execute
492
+ end
493
+ end
494
+
495
+ context 'a negative number other than -1' do
496
+ let(:input_command_opts) { { initial_timeout: -1000 } }
497
+
498
+ it "should return an error and not execute any actions" do
499
+ subject.execute
500
+ error = ProtocolError.new.setup 'option error', 'An initial-timeout value must be -1 or a positive integer.'
501
+ original_command.response(0.1).should be == error
502
+ end
503
+ end
504
+ end
505
+
506
+ describe 'Input#inter-digit-timeout' do
507
+ context 'a positive number' do
508
+ let(:input_command_opts) { { inter_digit_timeout: 1000 } }
509
+
510
+ it 'should pass the dit option to SynthAndRecog' do
511
+ expect_synthandrecog_with_options(/dit=1000/)
512
+ subject.execute
513
+ end
514
+ end
515
+
516
+ context '-1' do
517
+ let(:input_command_opts) { { inter_digit_timeout: -1 } }
518
+
519
+ it 'should not pass any options to SynthAndRecog' do
520
+ expect_synthandrecog_with_options(//)
521
+ subject.execute
522
+ end
523
+ end
524
+
525
+ context 'unset' do
526
+ let(:input_command_opts) { { inter_digit_timeout: nil } }
527
+
528
+ it 'should not pass any options to SynthAndRecog' do
529
+ expect_synthandrecog_with_options(//)
530
+ subject.execute
531
+ end
532
+ end
533
+
534
+ context 'a negative number other than -1' do
535
+ let(:input_command_opts) { { inter_digit_timeout: -1000 } }
536
+
537
+ it "should return an error and not execute any actions" do
538
+ subject.execute
539
+ error = ProtocolError.new.setup 'option error', 'An inter-digit-timeout value must be -1 or a positive integer.'
540
+ original_command.response(0.1).should be == error
541
+ end
542
+ end
543
+ end
544
+
545
+ describe 'Input#mode' do
546
+ pending
547
+ end
548
+
549
+ describe 'Input#terminator' do
550
+ context 'a string' do
551
+ let(:input_command_opts) { { terminator: '#' } }
552
+
553
+ it 'should pass the dttc option to SynthAndRecog' do
554
+ expect_synthandrecog_with_options(/dttc=#/)
555
+ subject.execute
556
+ end
557
+ end
558
+
559
+ context 'unset' do
560
+ let(:input_command_opts) { { terminator: nil } }
561
+
562
+ it 'should not pass any options to SynthAndRecog' do
563
+ expect_synthandrecog_with_options(//)
564
+ subject.execute
565
+ end
566
+ end
567
+ end
568
+
569
+ describe 'Input#recognizer' do
570
+ pending
571
+ end
572
+
573
+ describe 'Input#sensitivity' do
574
+ pending
575
+ end
576
+
577
+ describe 'Input#min-confidence' do
578
+ pending
579
+ end
580
+
581
+ describe 'Input#max-silence' do
582
+ pending
583
+ end
584
+
585
+ describe 'Input#match-content-type' do
586
+ pending
587
+ end
588
+
589
+ describe 'Input#language' do
590
+ pending
591
+ end
592
+
593
+ describe "#execute_command" do
594
+ context "with a command it does not understand" do
595
+ let(:command) { Punchblock::Component::Output::Pause.new }
596
+
597
+ before { command.request! }
598
+ it "returns a ProtocolError response" do
599
+ subject.execute_command command
600
+ command.response(0.1).should be_a ProtocolError
601
+ end
602
+ end
603
+
604
+ context "with a Stop command" do
605
+ let(:command) { Punchblock::Component::Stop.new }
606
+ let(:reason) { original_command.complete_event(5).reason }
607
+ let(:channel) { "SIP/1234-00000000" }
608
+ let :ami_event do
609
+ RubyAMI::Event.new 'AsyncAGI',
610
+ 'SubEvent' => "Start",
611
+ 'Channel' => channel,
612
+ '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"
613
+ end
614
+
615
+ before do
616
+ command.request!
617
+ original_command.execute!
618
+ end
619
+
620
+ it "sets the command response to true" do
621
+ mock_call.async.should_receive(:redirect_back)
622
+ subject.execute_command command
623
+ command.response(0.1).should be == true
624
+ end
625
+
626
+ it "sends the correct complete event" do
627
+ mock_call.async.should_receive(:redirect_back)
628
+ subject.execute_command command
629
+ original_command.should_not be_complete
630
+ mock_call.process_ami_event ami_event
631
+ reason.should be_a Punchblock::Event::Complete::Stop
632
+ original_command.should be_complete
633
+ end
634
+
635
+ it "redirects the call by unjoining it" do
636
+ mock_call.async.should_receive(:redirect_back)
637
+ subject.execute_command command
638
+ end
639
+ end
640
+ end
641
+
642
+ end
643
+ end
644
+ end
645
+ end
646
+ end