punchblock 0.7.1 → 0.7.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/CHANGELOG.md +12 -0
  2. data/lib/punchblock.rb +1 -0
  3. data/lib/punchblock/command/join.rb +6 -6
  4. data/lib/punchblock/command/unjoin.rb +6 -6
  5. data/lib/punchblock/command_node.rb +1 -0
  6. data/lib/punchblock/component/input.rb +24 -4
  7. data/lib/punchblock/component/output.rb +5 -1
  8. data/lib/punchblock/component/tropo/ask.rb +3 -1
  9. data/lib/punchblock/connection/xmpp.rb +28 -10
  10. data/lib/punchblock/event/joined.rb +6 -6
  11. data/lib/punchblock/event/unjoined.rb +6 -6
  12. data/lib/punchblock/media_container.rb +6 -5
  13. data/lib/punchblock/protocol_error.rb +5 -0
  14. data/lib/punchblock/rayo_node.rb +1 -1
  15. data/lib/punchblock/translator/asterisk.rb +3 -3
  16. data/lib/punchblock/translator/asterisk/call.rb +9 -3
  17. data/lib/punchblock/translator/asterisk/component.rb +35 -0
  18. data/lib/punchblock/translator/asterisk/component/asterisk.rb +1 -0
  19. data/lib/punchblock/translator/asterisk/component/asterisk/agi_command.rb +14 -23
  20. data/lib/punchblock/translator/asterisk/component/asterisk/ami_action.rb +5 -18
  21. data/lib/punchblock/translator/asterisk/component/asterisk/output.rb +94 -0
  22. data/lib/punchblock/version.rb +1 -1
  23. data/punchblock.gemspec +1 -0
  24. data/spec/punchblock/command/join_spec.rb +4 -4
  25. data/spec/punchblock/command/unjoin_spec.rb +4 -4
  26. data/spec/punchblock/component/input_spec.rb +28 -31
  27. data/spec/punchblock/component/output_spec.rb +23 -5
  28. data/spec/punchblock/component/tropo/ask_spec.rb +31 -34
  29. data/spec/punchblock/connection/xmpp_spec.rb +105 -3
  30. data/spec/punchblock/event/joined_spec.rb +4 -4
  31. data/spec/punchblock/event/unjoined_spec.rb +4 -4
  32. data/spec/punchblock/protocol_error_spec.rb +32 -1
  33. data/spec/punchblock/translator/asterisk/call_spec.rb +17 -3
  34. data/spec/punchblock/translator/asterisk/component/asterisk/agi_command_spec.rb +17 -0
  35. data/spec/punchblock/translator/asterisk/component/asterisk/output_spec.rb +489 -0
  36. data/spec/punchblock/translator/asterisk_spec.rb +14 -3
  37. metadata +53 -44
  38. data/assets/ozone/ask-1.0.xsd +0 -56
  39. data/assets/ozone/conference-1.0.xsd +0 -17
  40. data/assets/ozone/ozone-1.0.xsd +0 -127
  41. data/assets/ozone/say-1.0.xsd +0 -24
  42. data/assets/ozone/transfer-1.0.xsd +0 -32
@@ -31,9 +31,19 @@ module Punchblock
31
31
  its(:timeout) { should == 12000 }
32
32
  end
33
33
 
34
+ def grxml_doc(mode = :dtmf)
35
+ RubySpeech::GRXML.draw :mode => mode.to_s, :root => 'digits' do
36
+ rule id: 'digits' do
37
+ one_of do
38
+ 0.upto(1) { |d| item { d.to_s } }
39
+ end
40
+ end
41
+ end
42
+ end
43
+
34
44
  describe Ask::Choices do
35
45
  describe "when not passing a grammar" do
36
- subject { Ask::Choices.new :value => '[5 DIGITS]' }
46
+ subject { Ask::Choices.new :value => grxml_doc }
37
47
  its(:content_type) { should == 'application/grammar+grxml' }
38
48
  end
39
49
 
@@ -47,43 +57,30 @@ module Punchblock
47
57
  end
48
58
  end
49
59
 
50
- describe 'with a GRXML grammar' do
51
- subject { Ask::Choices.new :value => grxml, :content_type => 'application/grammar+grxml' }
52
-
53
- let :grxml do
54
- <<-GRXML
55
- <grammar xmlns="http://www.w3.org/2001/06/grammar" root="MAINRULE">
56
- <rule id="MAINRULE">
57
- <one-of>
58
- <item>
59
- <item repeat="0-1"> need a</item>
60
- <item repeat="0-1"> i need a</item>
61
- <one-of>
62
- <item> clue </item>
63
- </one-of>
64
- <tag> out.concept = "clue";</tag>
65
- </item>
66
- <item>
67
- <item repeat="0-1"> have an</item>
68
- <item repeat="0-1"> i have an</item>
69
- <one-of>
70
- <item> answer </item>
71
- </one-of>
72
- <tag> out.concept = "answer";</tag>
73
- </item>
74
- </one-of>
75
- </rule>
76
- </grammar>
77
- GRXML
78
- end
60
+ describe 'with a GRXML grammar' do
61
+ subject { Ask::Choices.new :value => grxml_doc, :content_type => 'application/grammar+grxml' }
79
62
 
80
- let(:expected_message) { "<![CDATA[ #{grxml} ]]>" }
63
+ its(:content_type) { should == 'application/grammar+grxml' }
81
64
 
82
- it "should wrap GRXML in CDATA" do
83
- subject.child.to_xml.should == expected_message.strip
84
- end
65
+ let(:expected_message) { "<![CDATA[ #{grxml_doc} ]]>" }
66
+
67
+ it "should wrap GRXML in CDATA" do
68
+ subject.child.to_xml.should == expected_message.strip
69
+ end
70
+
71
+ its(:value) { should == grxml_doc }
72
+
73
+ describe "comparison" do
74
+ let(:grammar2) { Ask::Choices.new :value => '<grammar xmlns="http://www.w3.org/2001/06/grammar" version="1.0" xml:lang="en-US" mode="dtmf" root="digits"><rule id="digits"><one-of><item>0</item><item>1</item></one-of></rule></grammar>' }
75
+ let(:grammar3) { Ask::Choices.new :value => grxml_doc }
76
+ let(:grammar4) { Ask::Choices.new :value => grxml_doc(:speech) }
77
+
78
+ it { should == grammar2 }
79
+ it { should == grammar3 }
80
+ it { should_not == grammar4 }
85
81
  end
86
82
  end
83
+ end
87
84
 
88
85
  describe "actions" do
89
86
  let(:mock_client) { mock 'Client' }
@@ -3,7 +3,8 @@ require 'spec_helper'
3
3
  module Punchblock
4
4
  module Connection
5
5
  describe XMPP do
6
- let(:connection) { XMPP.new :username => '1@call.rayo.net', :password => 1 }
6
+ let(:options) { { :root_domain => 'rayo.net' } }
7
+ let(:connection) { XMPP.new({:username => '1@app.rayo.net', :password => 1}.merge(options)) }
7
8
 
8
9
  let(:mock_event_handler) { stub_everything 'Event Handler' }
9
10
 
@@ -13,6 +14,48 @@ module Punchblock
13
14
 
14
15
  subject { connection }
15
16
 
17
+ describe "rayo domains" do
18
+ context "with no domains specified, and a JID of 1@app.rayo.net" do
19
+ let(:options) { { :username => '1@app.rayo.net' } }
20
+
21
+ its(:root_domain) { should == 'app.rayo.net' }
22
+ its(:calls_domain) { should == 'calls.app.rayo.net' }
23
+ its(:mixers_domain) { should == 'mixers.app.rayo.net' }
24
+ end
25
+
26
+ context "with only a rayo domain set" do
27
+ let(:options) { { :rayo_domain => 'rayo.org' } }
28
+
29
+ its(:root_domain) { should == 'rayo.org' }
30
+ its(:calls_domain) { should == 'calls.rayo.org' }
31
+ its(:mixers_domain) { should == 'mixers.rayo.org' }
32
+ end
33
+
34
+ context "with only a root domain set" do
35
+ let(:options) { { :root_domain => 'rayo.org' } }
36
+
37
+ its(:root_domain) { should == 'rayo.org' }
38
+ its(:calls_domain) { should == 'calls.rayo.org' }
39
+ its(:mixers_domain) { should == 'mixers.rayo.org' }
40
+ end
41
+
42
+ context "with a root domain and calls domain set" do
43
+ let(:options) { { :root_domain => 'rayo.org', :calls_domain => 'phone_calls.rayo.org' } }
44
+
45
+ its(:root_domain) { should == 'rayo.org' }
46
+ its(:calls_domain) { should == 'phone_calls.rayo.org' }
47
+ its(:mixers_domain) { should == 'mixers.rayo.org' }
48
+ end
49
+
50
+ context "with a root domain and mixers domain set" do
51
+ let(:options) { { :root_domain => 'rayo.org', :mixers_domain => 'conferences.rayo.org' } }
52
+
53
+ its(:root_domain) { should == 'rayo.org' }
54
+ its(:calls_domain) { should == 'calls.rayo.org' }
55
+ its(:mixers_domain) { should == 'conferences.rayo.org' }
56
+ end
57
+ end
58
+
16
59
  it 'should require a username and password to be passed in the options' do
17
60
  expect { XMPP.new :password => 1 }.to raise_error ArgumentError
18
61
  expect { XMPP.new :username => 1 }.to raise_error ArgumentError
@@ -84,7 +127,7 @@ module Punchblock
84
127
  it 'should send a "Chat" presence when ready' do
85
128
  client = connection.send :client
86
129
  client.expects(:write).once.with do |stanza|
87
- stanza.to.should == 'call.rayo.net' &&
130
+ stanza.to.should == 'rayo.net' &&
88
131
  stanza.is_a?(Blather::Stanza::Presence::Status) &&
89
132
  stanza.chat?
90
133
  end
@@ -94,7 +137,7 @@ module Punchblock
94
137
  it 'should send a "Do Not Disturb" presence when not_ready' do
95
138
  client = connection.send :client
96
139
  client.expects(:write).once.with do |stanza|
97
- stanza.to.should == 'call.rayo.net' &&
140
+ stanza.to.should == 'rayo.net' &&
98
141
  stanza.is_a?(Blather::Stanza::Presence::Status) &&
99
142
  stanza.dnd?
100
143
  end
@@ -200,6 +243,65 @@ module Punchblock
200
243
  its(:name) { should == :item_not_found }
201
244
  its(:text) { should == 'Could not find call [id=f6d437f4-1e18-457b-99f8-b5d853f50347]' }
202
245
  end
246
+
247
+ describe "#prep_command_for_execution" do
248
+ let(:stanza) { subject.prep_command_for_execution command }
249
+
250
+ context "with a dial command" do
251
+ let(:command) { Command::Dial.new }
252
+ let(:expected_jid) { 'rayo.net' }
253
+
254
+ it "should use the correct JID" do
255
+ stanza = subject.prep_command_for_execution command
256
+ stanza.to.should == expected_jid
257
+ end
258
+ end
259
+
260
+ context "with a call command" do
261
+ let(:command) { Command::Answer.new.tap { |a| a.call_id = 'abc123' } }
262
+ let(:expected_jid) { 'abc123@calls.rayo.net' }
263
+
264
+ it "should use the correct JID" do
265
+ stanza.to.should == expected_jid
266
+ end
267
+ end
268
+
269
+ context "with a call component" do
270
+ let(:command) { Component::Output.new :call_id => 'abc123' }
271
+ let(:expected_jid) { 'abc123@calls.rayo.net' }
272
+
273
+ it "should use the correct JID" do
274
+ stanza.to.should == expected_jid
275
+ end
276
+ end
277
+
278
+ context "with a call component command" do
279
+ let(:command) { Component::Stop.new :call_id => 'abc123', :component_id => 'foobar' }
280
+ let(:expected_jid) { 'abc123@calls.rayo.net/foobar' }
281
+
282
+ it "should use the correct JID" do
283
+ stanza.to.should == expected_jid
284
+ end
285
+ end
286
+
287
+ context "with a mixer component" do
288
+ let(:command) { Component::Output.new :mixer_name => 'abc123' }
289
+ let(:expected_jid) { 'abc123@mixers.rayo.net' }
290
+
291
+ it "should use the correct JID" do
292
+ stanza.to.should == expected_jid
293
+ end
294
+ end
295
+
296
+ context "with a mixer component command" do
297
+ let(:command) { Component::Stop.new :mixer_name => 'abc123', :component_id => 'foobar' }
298
+ let(:expected_jid) { 'abc123@mixers.rayo.net/foobar' }
299
+
300
+ it "should use the correct JID" do
301
+ stanza.to.should == expected_jid
302
+ end
303
+ end
304
+ end
203
305
  end # describe XMPP
204
306
  end # XMPP
205
307
  end # Punchblock
@@ -8,7 +8,7 @@ module Punchblock
8
8
  end
9
9
 
10
10
  describe "from a stanza" do
11
- let(:stanza) { '<joined xmlns="urn:xmpp:rayo:1" call-id="b" mixer-id="m" />' }
11
+ let(:stanza) { '<joined xmlns="urn:xmpp:rayo:1" call-id="b" mixer-name="m" />' }
12
12
 
13
13
  subject { RayoNode.import parse_stanza(stanza).root, '9f00061', '1' }
14
14
 
@@ -17,15 +17,15 @@ module Punchblock
17
17
  it_should_behave_like 'event'
18
18
 
19
19
  its(:other_call_id) { should == 'b' }
20
- its(:mixer_id) { should == 'm' }
20
+ its(:mixer_name) { should == 'm' }
21
21
  its(:xmlns) { should == 'urn:xmpp:rayo:1' }
22
22
  end
23
23
 
24
24
  describe "when setting options in initializer" do
25
- subject { Joined.new :other_call_id => 'abc123', :mixer_id => 'blah' }
25
+ subject { Joined.new :other_call_id => 'abc123', :mixer_name => 'blah' }
26
26
 
27
27
  its(:other_call_id) { should == 'abc123' }
28
- its(:mixer_id) { should == 'blah' }
28
+ its(:mixer_name) { should == 'blah' }
29
29
  end
30
30
  end
31
31
  end
@@ -8,7 +8,7 @@ module Punchblock
8
8
  end
9
9
 
10
10
  describe "from a stanza" do
11
- let(:stanza) { '<unjoined xmlns="urn:xmpp:rayo:1" call-id="b" mixer-id="m" />' }
11
+ let(:stanza) { '<unjoined xmlns="urn:xmpp:rayo:1" call-id="b" mixer-name="m" />' }
12
12
 
13
13
  subject { RayoNode.import parse_stanza(stanza).root, '9f00061', '1' }
14
14
 
@@ -17,15 +17,15 @@ module Punchblock
17
17
  it_should_behave_like 'event'
18
18
 
19
19
  its(:other_call_id) { should == 'b' }
20
- its(:mixer_id) { should == 'm' }
20
+ its(:mixer_name) { should == 'm' }
21
21
  its(:xmlns) { should == 'urn:xmpp:rayo:1' }
22
22
  end
23
23
 
24
24
  describe "when setting options in initializer" do
25
- subject { Unjoined.new :other_call_id => 'abc123', :mixer_id => 'blah' }
25
+ subject { Unjoined.new :other_call_id => 'abc123', :mixer_name => 'blah' }
26
26
 
27
27
  its(:other_call_id) { should == 'abc123' }
28
- its(:mixer_id) { should == 'blah' }
28
+ its(:mixer_name) { should == 'blah' }
29
29
  end
30
30
  end
31
31
  end
@@ -2,8 +2,39 @@ require 'spec_helper'
2
2
 
3
3
  module Punchblock
4
4
  describe ProtocolError do
5
- subject { ProtocolError.new :item_not_found, 'Could not find call [id=f6d437f4-1e18-457b-99f8-b5d853f50347]', 'f6d437f4-1e18-457b-99f8-b5d853f50347', 'abc123' }
5
+ let(:name) { :item_not_found }
6
+ let(:text) { 'Could not find call [id=f6d437f4-1e18-457b-99f8-b5d853f50347]' }
7
+ let(:call_id) { 'f6d437f4-1e18-457b-99f8-b5d853f50347' }
8
+ let(:component_id) { 'abc123' }
9
+ subject { ProtocolError.new name, text, call_id, component_id }
6
10
 
7
11
  its(:inspect) { should == '#<Punchblock::ProtocolError: name=:item_not_found text="Could not find call [id=f6d437f4-1e18-457b-99f8-b5d853f50347]" call_id="f6d437f4-1e18-457b-99f8-b5d853f50347" component_id="abc123">' }
12
+
13
+ describe "comparison" do
14
+ context "with the same name, text, call ID and component ID" do
15
+ let(:comparison) { ProtocolError.new name, text, call_id, component_id }
16
+ it { should == comparison }
17
+ end
18
+
19
+ context "with a different name" do
20
+ let(:comparison) { ProtocolError.new :foo, text, call_id, component_id }
21
+ it { should_not == comparison }
22
+ end
23
+
24
+ context "with a different text" do
25
+ let(:comparison) { ProtocolError.new name, 'foo', call_id, component_id }
26
+ it { should_not == comparison }
27
+ end
28
+
29
+ context "with a different call ID" do
30
+ let(:comparison) { ProtocolError.new name, text, 'foo', component_id }
31
+ it { should_not == comparison }
32
+ end
33
+
34
+ context "with a different component ID" do
35
+ let(:comparison) { ProtocolError.new name, text, call_id, 'foo' }
36
+ it { should_not == comparison }
37
+ end
38
+ end
8
39
  end
9
40
  end
@@ -106,7 +106,7 @@ module Punchblock
106
106
  end
107
107
 
108
108
  context 'with an event for a known AGI command component' do
109
- let(:mock_component_node) { mock 'Punchblock::Component::Asterisk::AGI::Command', :name => 'EXEC ANSWER' }
109
+ let(:mock_component_node) { mock 'Punchblock::Component::Asterisk::AGI::Command', :name => 'EXEC ANSWER', :params_array => [] }
110
110
  let :component do
111
111
  Component::Asterisk::AGICommand.new mock_component_node, subject.translator
112
112
  end
@@ -183,20 +183,34 @@ module Punchblock
183
183
  end
184
184
  end
185
185
 
186
- context 'with a component' do
186
+ context 'with an AGI command component' do
187
187
  let :command do
188
188
  Punchblock::Component::Asterisk::AGI::Command.new :name => 'Answer'
189
189
  end
190
190
 
191
191
  let(:mock_action) { mock 'Component::Asterisk::AGI::Command', :id => 'foo' }
192
192
 
193
- it 'should create a component actor and execute it asynchronously' do
193
+ it 'should create an AGI command component actor and execute it asynchronously' do
194
194
  Component::Asterisk::AGICommand.expects(:new).once.with(command, subject).returns mock_action
195
195
  mock_action.expects(:execute!).once
196
196
  subject.execute_command command
197
197
  end
198
198
  end
199
199
 
200
+ context 'with an Output component' do
201
+ let :command do
202
+ Punchblock::Component::Output.new
203
+ end
204
+
205
+ let(:mock_action) { mock 'Component::Asterisk::Output', :id => 'foo' }
206
+
207
+ it 'should create an AGI command component actor and execute it asynchronously' do
208
+ Component::Asterisk::Output.expects(:new).once.with(command, subject).returns mock_action
209
+ mock_action.expects(:execute!).once
210
+ subject.execute_command command
211
+ end
212
+ end
213
+
200
214
  context 'with a component command' do
201
215
  let(:component_id) { 'foobar' }
202
216
 
@@ -27,6 +27,23 @@ module Punchblock
27
27
  mock_call.expects(:send_ami_action!).once.with(expected_action).returns(expected_action)
28
28
  subject.execute
29
29
  end
30
+
31
+ context 'with some parameters' do
32
+ let(:params) { [1000, 'foo'] }
33
+
34
+ let :expected_action do
35
+ RubyAMI::Action.new 'AGI', 'Channel' => channel, 'Command' => 'WAIT FOR DIGIT "1000" "foo"', 'CommandID' => component_id
36
+ end
37
+
38
+ let :command do
39
+ Punchblock::Component::Asterisk::AGI::Command.new :name => 'WAIT FOR DIGIT', :params => params
40
+ end
41
+
42
+ it 'should send the appropriate RubyAMI::Action' do
43
+ mock_call.expects(:send_ami_action!).once.with(expected_action).returns(expected_action)
44
+ subject.execute
45
+ end
46
+ end
30
47
  end
31
48
 
32
49
  context 'when the AMI action completes' do
@@ -0,0 +1,489 @@
1
+ require 'spec_helper'
2
+
3
+ module Punchblock
4
+ module Translator
5
+ class Asterisk
6
+ module Component
7
+ module Asterisk
8
+ describe Output do
9
+ let(:media_engine) { nil }
10
+ let(:translator) { Punchblock::Translator::Asterisk.new mock('AMI'), mock('Client'), media_engine }
11
+ let(:mock_call) { mock 'Call', :translator => translator }
12
+ let(:command_options) { nil }
13
+
14
+ let :command do
15
+ Punchblock::Component::Output.new command_options
16
+ end
17
+
18
+ let :ssml_doc do
19
+ RubySpeech::SSML.draw do
20
+ say_as(:interpret_as => :cardinal) { 'FOO' }
21
+ end
22
+ end
23
+
24
+ let :command_options do
25
+ { :ssml => ssml_doc }
26
+ end
27
+
28
+ subject { Output.new command, mock_call }
29
+
30
+ describe '#execute' do
31
+ before { command.request! }
32
+
33
+ context 'with a media engine of :unimrcp' do
34
+ let(:media_engine) { :unimrcp }
35
+
36
+ let(:audio_filename) { 'http://foo.com/bar.mp3' }
37
+
38
+ let :ssml_doc do
39
+ RubySpeech::SSML.draw do
40
+ audio :src => audio_filename
41
+ say_as(:interpret_as => :cardinal) { 'FOO' }
42
+ end
43
+ end
44
+
45
+ let(:command_opts) { {} }
46
+
47
+ let :command_options do
48
+ { :ssml => ssml_doc }.merge(command_opts)
49
+ end
50
+
51
+ def expect_mrcpsynth_with_options(options)
52
+ mock_call.expects(:send_agi_action!).once.with do |*args|
53
+ args[0].should == 'EXEC MRCPSynth'
54
+ args[2].should match options
55
+ end
56
+ end
57
+
58
+ it "should execute MRCPSynth" do
59
+ mock_call.expects(:send_agi_action!).once.with 'EXEC MRCPSynth', ssml_doc.to_s.squish.gsub(/["\\]/) { |m| "\\#{m}" }, ''
60
+ subject.execute
61
+ end
62
+
63
+ it 'should send a complete event when MRCPSynth completes' do
64
+ def mock_call.send_agi_action!(*args, &block)
65
+ block.call Punchblock::Component::Asterisk::AGI::Command::Complete::Success.new(:code => 200, :result => 1)
66
+ end
67
+ subject.execute
68
+ command.complete_event(0.1).reason.should be_a Punchblock::Component::Output::Complete::Success
69
+ end
70
+
71
+ describe 'ssml' do
72
+ context 'unset' do
73
+ let(:command_opts) { { :ssml => nil } }
74
+ it "should return an error and not execute any actions" do
75
+ subject.execute
76
+ error = ProtocolError.new 'option error', 'An SSML document is required.'
77
+ command.response(0.1).should == error
78
+ end
79
+ end
80
+ end
81
+
82
+ describe 'start-offset' do
83
+ context 'unset' do
84
+ let(:command_opts) { { :start_offset => nil } }
85
+ it 'should not pass any options to MRCPSynth' do
86
+ expect_mrcpsynth_with_options(//)
87
+ subject.execute
88
+ end
89
+ end
90
+
91
+ context 'set' do
92
+ let(:command_opts) { { :start_offset => 10 } }
93
+ it "should return an error and not execute any actions" do
94
+ subject.execute
95
+ error = ProtocolError.new 'option error', 'A start_offset value is unsupported on Asterisk.'
96
+ command.response(0.1).should == error
97
+ end
98
+ end
99
+ end
100
+
101
+ describe 'start-paused' do
102
+ context 'false' do
103
+ let(:command_opts) { { :start_paused => false } }
104
+ it 'should not pass any options to MRCPSynth' do
105
+ expect_mrcpsynth_with_options(//)
106
+ subject.execute
107
+ end
108
+ end
109
+
110
+ context 'true' do
111
+ let(:command_opts) { { :start_paused => true } }
112
+ it "should return an error and not execute any actions" do
113
+ subject.execute
114
+ error = ProtocolError.new 'option error', 'A start_paused value is unsupported on Asterisk.'
115
+ command.response(0.1).should == error
116
+ end
117
+ end
118
+ end
119
+
120
+ describe 'repeat-interval' do
121
+ context 'unset' do
122
+ let(:command_opts) { { :repeat_interval => nil } }
123
+ it 'should not pass any options to MRCPSynth' do
124
+ expect_mrcpsynth_with_options(//)
125
+ subject.execute
126
+ end
127
+ end
128
+
129
+ context 'set' do
130
+ let(:command_opts) { { :repeat_interval => 10 } }
131
+ it "should return an error and not execute any actions" do
132
+ subject.execute
133
+ error = ProtocolError.new 'option error', 'A repeat_interval value is unsupported on Asterisk.'
134
+ command.response(0.1).should == error
135
+ end
136
+ end
137
+ end
138
+
139
+ describe 'repeat-times' do
140
+ context 'unset' do
141
+ let(:command_opts) { { :repeat_times => nil } }
142
+ it 'should not pass any options to MRCPSynth' do
143
+ expect_mrcpsynth_with_options(//)
144
+ subject.execute
145
+ end
146
+ end
147
+
148
+ context 'set' do
149
+ let(:command_opts) { { :repeat_times => 2 } }
150
+ it "should return an error and not execute any actions" do
151
+ subject.execute
152
+ error = ProtocolError.new 'option error', 'A repeat_times value is unsupported on Asterisk.'
153
+ command.response(0.1).should == error
154
+ end
155
+ end
156
+ end
157
+
158
+ describe 'max-time' do
159
+ context 'unset' do
160
+ let(:command_opts) { { :max_time => nil } }
161
+ it 'should not pass any options to MRCPSynth' do
162
+ expect_mrcpsynth_with_options(//)
163
+ subject.execute
164
+ end
165
+ end
166
+
167
+ context 'set' do
168
+ let(:command_opts) { { :max_time => 30 } }
169
+ it "should return an error and not execute any actions" do
170
+ subject.execute
171
+ error = ProtocolError.new 'option error', 'A max_time value is unsupported on Asterisk.'
172
+ command.response(0.1).should == error
173
+ end
174
+ end
175
+ end
176
+
177
+ describe 'voice' do
178
+ context 'unset' do
179
+ let(:command_opts) { { :voice => nil } }
180
+ it 'should not pass the v option to MRCPSynth' do
181
+ expect_mrcpsynth_with_options(//)
182
+ subject.execute
183
+ end
184
+ end
185
+
186
+ context 'set' do
187
+ let(:command_opts) { { :voice => 'alison' } }
188
+ it 'should pass the v option to MRCPSynth' do
189
+ expect_mrcpsynth_with_options(/v=alison/)
190
+ subject.execute
191
+ end
192
+ end
193
+ end
194
+
195
+ describe 'interrupt_on' do
196
+ context "set to nil" do
197
+ let(:command_opts) { { :interrupt_on => nil } }
198
+ it "should not pass the i option to MRCPSynth" do
199
+ expect_mrcpsynth_with_options(//)
200
+ subject.execute
201
+ end
202
+ end
203
+
204
+ context "set to :any" do
205
+ let(:command_opts) { { :interrupt_on => :any } }
206
+ it "should pass the i option to MRCPSynth" do
207
+ expect_mrcpsynth_with_options(/i=any/)
208
+ subject.execute
209
+ end
210
+ end
211
+
212
+ context "set to :dtmf" do
213
+ let(:command_opts) { { :interrupt_on => :dtmf } }
214
+ it "should pass the i option to MRCPSynth" do
215
+ expect_mrcpsynth_with_options(/i=any/)
216
+ subject.execute
217
+ end
218
+ end
219
+
220
+ context "set to :speech" do
221
+ let(:command_opts) { { :interrupt_on => :speech } }
222
+ it "should return an error and not execute any actions" do
223
+ subject.execute
224
+ error = ProtocolError.new 'option error', 'An interrupt-on value of speech is unsupported.'
225
+ command.response(0.1).should == error
226
+ end
227
+ end
228
+ end
229
+ end
230
+
231
+ context 'with a media engine of :asterisk' do
232
+ let(:media_engine) { :asterisk }
233
+
234
+ def expect_stream_file_with_options(options = nil)
235
+ mock_call.expects(:send_agi_action!).once.with 'STREAM FILE', audio_filename, options do |*args|
236
+ args[2].should == options
237
+ subject.continue!
238
+ true
239
+ end
240
+ end
241
+
242
+ let(:audio_filename) { 'http://foo.com/bar.mp3' }
243
+
244
+ let :ssml_doc do
245
+ RubySpeech::SSML.draw do
246
+ audio :src => audio_filename
247
+ say_as(:interpret_as => :cardinal) { 'FOO' }
248
+ end
249
+ end
250
+
251
+ let(:command_opts) { {} }
252
+
253
+ let :command_options do
254
+ { :ssml => ssml_doc }.merge(command_opts)
255
+ end
256
+
257
+ let :command do
258
+ Punchblock::Component::Output.new command_options
259
+ end
260
+
261
+ describe 'ssml' do
262
+ context 'unset' do
263
+ let(:command_opts) { { :ssml => nil } }
264
+ it "should return an error and not execute any actions" do
265
+ subject.execute
266
+ error = ProtocolError.new 'option error', 'An SSML document is required.'
267
+ command.response(0.1).should == error
268
+ end
269
+ end
270
+
271
+ context 'with a single audio SSML node' do
272
+ let(:audio_filename) { 'http://foo.com/bar.mp3' }
273
+ let :command_options do
274
+ {
275
+ :ssml => RubySpeech::SSML.draw { audio :src => audio_filename }
276
+ }
277
+ end
278
+
279
+ it 'should playback the audio file using STREAM FILE' do
280
+ expect_stream_file_with_options
281
+ subject.execute
282
+ end
283
+
284
+ it 'should send a complete event when the file finishes playback' do
285
+ def mock_call.send_agi_action!(*args, &block)
286
+ block.call Punchblock::Component::Asterisk::AGI::Command::Complete::Success.new(:code => 200, :result => 1)
287
+ end
288
+ subject.execute
289
+ command.complete_event(0.1).reason.should be_a Punchblock::Component::Output::Complete::Success
290
+ end
291
+ end
292
+
293
+ context 'with multiple audio SSML nodes' do
294
+ let(:audio_filename1) { 'http://foo.com/bar.mp3' }
295
+ let(:audio_filename2) { 'http://foo.com/baz.mp3' }
296
+ let :command_options do
297
+ {
298
+ :ssml => RubySpeech::SSML.draw do
299
+ audio :src => audio_filename1
300
+ audio :src => audio_filename2
301
+ end
302
+ }
303
+ end
304
+
305
+ it 'should playback each audio file using STREAM FILE' do
306
+ latch = CountDownLatch.new 2
307
+ mock_call.expects(:send_agi_action!).once.with 'STREAM FILE', audio_filename1, nil do
308
+ subject.continue
309
+ true
310
+ latch.countdown!
311
+ end
312
+ mock_call.expects(:send_agi_action!).once.with 'STREAM FILE', audio_filename2, nil do
313
+ subject.continue
314
+ true
315
+ latch.countdown!
316
+ end
317
+ subject.execute
318
+ latch.wait 2
319
+ sleep 0.1
320
+ end
321
+
322
+ it 'should send a complete event after the final file has finished playback' do
323
+ def mock_call.send_agi_action!(*args, &block)
324
+ block.call Punchblock::Component::Asterisk::AGI::Command::Complete::Success.new(:code => 200, :result => 1)
325
+ end
326
+ command.expects(:add_event).once.with do |e|
327
+ e.reason.should be_a Punchblock::Component::Output::Complete::Success
328
+ end
329
+ subject.execute
330
+ end
331
+ end
332
+ end
333
+
334
+ describe 'start-offset' do
335
+ context 'unset' do
336
+ let(:command_opts) { { :start_offset => nil } }
337
+ it 'should not pass any options to STREAM FILE' do
338
+ expect_stream_file_with_options
339
+ subject.execute
340
+ end
341
+ end
342
+
343
+ context 'set' do
344
+ let(:command_opts) { { :start_offset => 10 } }
345
+ it "should return an error and not execute any actions" do
346
+ subject.execute
347
+ error = ProtocolError.new 'option error', 'A start_offset value is unsupported on Asterisk.'
348
+ command.response(0.1).should == error
349
+ end
350
+ end
351
+ end
352
+
353
+ describe 'start-paused' do
354
+ context 'false' do
355
+ let(:command_opts) { { :start_paused => false } }
356
+ it 'should not pass any options to STREAM FILE' do
357
+ expect_stream_file_with_options
358
+ subject.execute
359
+ end
360
+ end
361
+
362
+ context 'true' do
363
+ let(:command_opts) { { :start_paused => true } }
364
+ it "should return an error and not execute any actions" do
365
+ subject.execute
366
+ error = ProtocolError.new 'option error', 'A start_paused value is unsupported on Asterisk.'
367
+ command.response(0.1).should == error
368
+ end
369
+ end
370
+ end
371
+
372
+ describe 'repeat-interval' do
373
+ context 'unset' do
374
+ let(:command_opts) { { :repeat_interval => nil } }
375
+ it 'should not pass any options to STREAM FILE' do
376
+ expect_stream_file_with_options
377
+ subject.execute
378
+ end
379
+ end
380
+
381
+ context 'set' do
382
+ let(:command_opts) { { :repeat_interval => 10 } }
383
+ it "should return an error and not execute any actions" do
384
+ subject.execute
385
+ error = ProtocolError.new 'option error', 'A repeat_interval value is unsupported on Asterisk.'
386
+ command.response(0.1).should == error
387
+ end
388
+ end
389
+ end
390
+
391
+ describe 'repeat-times' do
392
+ context 'unset' do
393
+ let(:command_opts) { { :repeat_times => nil } }
394
+ it 'should not pass any options to STREAM FILE' do
395
+ expect_stream_file_with_options
396
+ subject.execute
397
+ end
398
+ end
399
+
400
+ context 'set' do
401
+ let(:command_opts) { { :repeat_times => 2 } }
402
+ it "should return an error and not execute any actions" do
403
+ subject.execute
404
+ error = ProtocolError.new 'option error', 'A repeat_times value is unsupported on Asterisk.'
405
+ command.response(0.1).should == error
406
+ end
407
+ end
408
+ end
409
+
410
+ describe 'max-time' do
411
+ context 'unset' do
412
+ let(:command_opts) { { :max_time => nil } }
413
+ it 'should not pass any options to STREAM FILE' do
414
+ expect_stream_file_with_options
415
+ subject.execute
416
+ end
417
+ end
418
+
419
+ context 'set' do
420
+ let(:command_opts) { { :max_time => 30 } }
421
+ it "should return an error and not execute any actions" do
422
+ subject.execute
423
+ error = ProtocolError.new 'option error', 'A max_time value is unsupported on Asterisk.'
424
+ command.response(0.1).should == error
425
+ end
426
+ end
427
+ end
428
+
429
+ describe 'voice' do
430
+ context 'unset' do
431
+ let(:command_opts) { { :voice => nil } }
432
+ it 'should not pass the v option to STREAM FILE' do
433
+ expect_stream_file_with_options
434
+ subject.execute
435
+ end
436
+ end
437
+
438
+ context 'set' do
439
+ let(:command_opts) { { :voice => 'alison' } }
440
+ it "should return an error and not execute any actions" do
441
+ subject.execute
442
+ error = ProtocolError.new 'option error', 'A voice value is unsupported on Asterisk.'
443
+ command.response(0.1).should == error
444
+ end
445
+ end
446
+ end
447
+
448
+ describe 'interrupt_on' do
449
+ context "set to nil" do
450
+ let(:command_opts) { { :interrupt_on => nil } }
451
+ it "should not pass any digits to STREAM FILE" do
452
+ expect_stream_file_with_options
453
+ subject.execute
454
+ end
455
+ end
456
+
457
+ context "set to :any" do
458
+ let(:command_opts) { { :interrupt_on => :any } }
459
+ it "should pass all digits to STREAM FILE" do
460
+ expect_stream_file_with_options '0123456789*#'
461
+ subject.execute
462
+ end
463
+ end
464
+
465
+ context "set to :dtmf" do
466
+ let(:command_opts) { { :interrupt_on => :dtmf } }
467
+ it "should pass all digits to STREAM FILE" do
468
+ expect_stream_file_with_options '0123456789*#'
469
+ subject.execute
470
+ end
471
+ end
472
+
473
+ context "set to :speech" do
474
+ let(:command_opts) { { :interrupt_on => :speech } }
475
+ it "should return an error and not execute any actions" do
476
+ subject.execute
477
+ error = ProtocolError.new 'option error', 'An interrupt-on value of speech is unsupported.'
478
+ command.response(0.1).should == error
479
+ end
480
+ end
481
+ end
482
+ end
483
+ end
484
+ end
485
+ end
486
+ end
487
+ end
488
+ end
489
+ end