punchblock 0.7.1 → 0.7.2

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 (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