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.
- data/CHANGELOG.md +12 -0
- data/lib/punchblock.rb +1 -0
- data/lib/punchblock/command/join.rb +6 -6
- data/lib/punchblock/command/unjoin.rb +6 -6
- data/lib/punchblock/command_node.rb +1 -0
- data/lib/punchblock/component/input.rb +24 -4
- data/lib/punchblock/component/output.rb +5 -1
- data/lib/punchblock/component/tropo/ask.rb +3 -1
- data/lib/punchblock/connection/xmpp.rb +28 -10
- data/lib/punchblock/event/joined.rb +6 -6
- data/lib/punchblock/event/unjoined.rb +6 -6
- data/lib/punchblock/media_container.rb +6 -5
- data/lib/punchblock/protocol_error.rb +5 -0
- data/lib/punchblock/rayo_node.rb +1 -1
- data/lib/punchblock/translator/asterisk.rb +3 -3
- data/lib/punchblock/translator/asterisk/call.rb +9 -3
- data/lib/punchblock/translator/asterisk/component.rb +35 -0
- data/lib/punchblock/translator/asterisk/component/asterisk.rb +1 -0
- data/lib/punchblock/translator/asterisk/component/asterisk/agi_command.rb +14 -23
- data/lib/punchblock/translator/asterisk/component/asterisk/ami_action.rb +5 -18
- data/lib/punchblock/translator/asterisk/component/asterisk/output.rb +94 -0
- data/lib/punchblock/version.rb +1 -1
- data/punchblock.gemspec +1 -0
- data/spec/punchblock/command/join_spec.rb +4 -4
- data/spec/punchblock/command/unjoin_spec.rb +4 -4
- data/spec/punchblock/component/input_spec.rb +28 -31
- data/spec/punchblock/component/output_spec.rb +23 -5
- data/spec/punchblock/component/tropo/ask_spec.rb +31 -34
- data/spec/punchblock/connection/xmpp_spec.rb +105 -3
- data/spec/punchblock/event/joined_spec.rb +4 -4
- data/spec/punchblock/event/unjoined_spec.rb +4 -4
- data/spec/punchblock/protocol_error_spec.rb +32 -1
- data/spec/punchblock/translator/asterisk/call_spec.rb +17 -3
- data/spec/punchblock/translator/asterisk/component/asterisk/agi_command_spec.rb +17 -0
- data/spec/punchblock/translator/asterisk/component/asterisk/output_spec.rb +489 -0
- data/spec/punchblock/translator/asterisk_spec.rb +14 -3
- metadata +53 -44
- data/assets/ozone/ask-1.0.xsd +0 -56
- data/assets/ozone/conference-1.0.xsd +0 -17
- data/assets/ozone/ozone-1.0.xsd +0 -127
- data/assets/ozone/say-1.0.xsd +0 -24
- 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 =>
|
|
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
|
-
|
|
51
|
-
|
|
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
|
-
|
|
63
|
+
its(:content_type) { should == 'application/grammar+grxml' }
|
|
81
64
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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(:
|
|
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 == '
|
|
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 == '
|
|
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-
|
|
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(:
|
|
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', :
|
|
25
|
+
subject { Joined.new :other_call_id => 'abc123', :mixer_name => 'blah' }
|
|
26
26
|
|
|
27
27
|
its(:other_call_id) { should == 'abc123' }
|
|
28
|
-
its(:
|
|
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-
|
|
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(:
|
|
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', :
|
|
25
|
+
subject { Unjoined.new :other_call_id => 'abc123', :mixer_name => 'blah' }
|
|
26
26
|
|
|
27
27
|
its(:other_call_id) { should == 'abc123' }
|
|
28
|
-
its(:
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|