punchblock 1.9.4 → 2.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
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