punchblock 1.8.0 → 1.8.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +12 -0
  3. data/Gemfile +1 -1
  4. data/lib/punchblock/component/component_node.rb +1 -1
  5. data/lib/punchblock/component/stop.rb +2 -2
  6. data/lib/punchblock/connection/asterisk.rb +3 -3
  7. data/lib/punchblock/connection/freeswitch.rb +2 -2
  8. data/lib/punchblock/connection/xmpp.rb +1 -1
  9. data/lib/punchblock/translator.rb +1 -0
  10. data/lib/punchblock/translator/asterisk.rb +10 -9
  11. data/lib/punchblock/translator/asterisk/call.rb +15 -7
  12. data/lib/punchblock/translator/asterisk/component/asterisk/ami_action.rb +1 -1
  13. data/lib/punchblock/translator/asterisk/component/input.rb +3 -4
  14. data/lib/punchblock/translator/asterisk/component/output.rb +6 -6
  15. data/lib/punchblock/translator/asterisk/component/record.rb +7 -7
  16. data/lib/punchblock/translator/asterisk/component/stop_by_redirect.rb +3 -3
  17. data/lib/punchblock/translator/dtmf_recognizer.rb +87 -0
  18. data/lib/punchblock/translator/freeswitch.rb +9 -7
  19. data/lib/punchblock/translator/freeswitch/call.rb +6 -4
  20. data/lib/punchblock/translator/freeswitch/component/input.rb +1 -2
  21. data/lib/punchblock/translator/freeswitch/component/output.rb +1 -2
  22. data/lib/punchblock/translator/freeswitch/component/tts_output.rb +1 -1
  23. data/lib/punchblock/translator/input_component.rb +17 -64
  24. data/lib/punchblock/version.rb +1 -1
  25. data/punchblock.gemspec +1 -1
  26. data/spec/punchblock/component/input_spec.rb +1 -1
  27. data/spec/punchblock/component/output_spec.rb +33 -3
  28. data/spec/punchblock/component/record_spec.rb +1 -1
  29. data/spec/punchblock/connection/asterisk_spec.rb +3 -3
  30. data/spec/punchblock/connection/freeswitch_spec.rb +3 -2
  31. data/spec/punchblock/connection/xmpp_spec.rb +18 -6
  32. data/spec/punchblock/translator/asterisk/call_spec.rb +83 -31
  33. data/spec/punchblock/translator/asterisk/component/input_spec.rb +1 -1
  34. data/spec/punchblock/translator/asterisk/component/output_spec.rb +32 -27
  35. data/spec/punchblock/translator/asterisk/component/record_spec.rb +47 -43
  36. data/spec/punchblock/translator/asterisk/component/stop_by_redirect_spec.rb +2 -2
  37. data/spec/punchblock/translator/asterisk_spec.rb +28 -26
  38. data/spec/punchblock/translator/freeswitch/call_spec.rb +24 -15
  39. data/spec/punchblock/translator/freeswitch/component/input_spec.rb +1 -1
  40. data/spec/punchblock/translator/freeswitch/component/output_spec.rb +2 -3
  41. data/spec/punchblock/translator/freeswitch/component/record_spec.rb +1 -1
  42. data/spec/punchblock/translator/freeswitch_spec.rb +75 -18
  43. data/spec/spec_helper.rb +1 -1
  44. metadata +24 -74
@@ -19,6 +19,8 @@ module Punchblock
19
19
 
20
20
  trap_exit :actor_died
21
21
 
22
+ finalizer :finalize
23
+
22
24
  def initialize(connection, media_engine = nil, default_voice = nil)
23
25
  @connection, @media_engine, @default_voice = connection, media_engine, default_voice
24
26
  @calls, @components = {}, {}
@@ -60,18 +62,18 @@ module Punchblock
60
62
  call = Call.new event[:unique_id], current_actor, event.content.select { |k,v| k.to_s =~ /variable/ }, stream, @media_engine, @default_voice
61
63
  link call
62
64
  register_call call
63
- call.send_offer!
65
+ call.async.send_offer
64
66
  end
65
67
 
66
- register_handler :es, [:has_key?, :other_leg_unique_id] => true do |event|
68
+ register_handler :es, :event_name => ['CHANNEL_BRIDGE', 'CHANNEL_UNBRIDGE'], [:has_key?, :other_leg_unique_id] => true do |event|
67
69
  call = call_with_id event[:other_leg_unique_id]
68
- call.handle_es_event! event if call
70
+ call.async.handle_es_event event if call
69
71
  throw :pass
70
72
  end
71
73
 
72
74
  register_handler :es, lambda { |event| es_event_known_call? event } do |event|
73
75
  call = call_with_id event[:unique_id]
74
- call.handle_es_event! event
76
+ call.async.handle_es_event event
75
77
  end
76
78
  end
77
79
 
@@ -113,7 +115,7 @@ module Punchblock
113
115
 
114
116
  def execute_call_command(command)
115
117
  if call = call_with_id(command.target_call_id)
116
- call.execute_command! command
118
+ call.async.execute_command command
117
119
  else
118
120
  command.response = ProtocolError.new.setup :item_not_found, "Could not find a call with ID #{command.target_call_id}", command.target_call_id
119
121
  end
@@ -121,7 +123,7 @@ module Punchblock
121
123
 
122
124
  def execute_component_command(command)
123
125
  if (component = component_with_id(command.component_id))
124
- component.execute_command! command
126
+ component.async.execute_command command
125
127
  else
126
128
  command.response = ProtocolError.new.setup :item_not_found, "Could not find a component with ID #{command.component_id}", command.target_call_id, command.component_id
127
129
  end
@@ -132,7 +134,7 @@ module Punchblock
132
134
  when Punchblock::Command::Dial
133
135
  call = Call.new_link Punchblock.new_uuid, current_actor, nil, stream, @media_engine, @default_voice
134
136
  register_call call
135
- call.dial! command
137
+ call.async.dial command
136
138
  else
137
139
  command.response = ProtocolError.new.setup 'command-not-acceptable', "Did not understand command"
138
140
  end
@@ -128,9 +128,11 @@ module Punchblock
128
128
  @direction = :outbound
129
129
 
130
130
  cid_number, cid_name = dial_command.from, nil
131
- dial_command.from.match(/(?<cid_name>.*)<(?<cid_number>.*)>/) do |m|
132
- cid_name = m[:cid_name].strip
133
- cid_number = m[:cid_number]
131
+ if dial_command.from
132
+ dial_command.from.match(/(?<cid_name>.*)<(?<cid_number>.*)>/) do |m|
133
+ cid_name = m[:cid_name].strip
134
+ cid_number = m[:cid_number]
135
+ end
134
136
  end
135
137
 
136
138
  options = {
@@ -246,7 +248,7 @@ module Punchblock
246
248
  def execute_component(type, command, *execute_args)
247
249
  type.new_link(command, current_actor).tap do |component|
248
250
  register_component component
249
- component.execute!(*execute_args)
251
+ component.async.execute(*execute_args)
250
252
  end
251
253
  end
252
254
 
@@ -11,10 +11,9 @@ module Punchblock
11
11
  private
12
12
 
13
13
  def register_dtmf_event_handler
14
- component = current_actor
15
14
  call.register_handler :es, :event_name => 'DTMF' do |event|
16
15
  safe_from_dead_actors do
17
- component.process_dtmf! event[:dtmf_digit]
16
+ @recognizer << event[:dtmf_digit]
18
17
  end
19
18
  end
20
19
  end
@@ -9,7 +9,6 @@ module Punchblock
9
9
 
10
10
  def validate
11
11
  super
12
- raise OptionError, "A voice value is unsupported." if @component_node.voice
13
12
  filenames
14
13
  end
15
14
 
@@ -36,7 +35,7 @@ module Punchblock
36
35
  def playback(path)
37
36
  op = current_actor
38
37
  register_handler :es, :event_name => 'CHANNEL_EXECUTE_COMPLETE' do |event|
39
- op.send_complete_event! complete_reason_for_event(event)
38
+ op.async.send_complete_event complete_reason_for_event(event)
40
39
  end
41
40
  application 'playback', path
42
41
  end
@@ -10,7 +10,7 @@ module Punchblock
10
10
  def do_output(engine, default_voice = nil)
11
11
  op = current_actor
12
12
  register_handler :es, :event_name => 'CHANNEL_EXECUTE_COMPLETE' do |event|
13
- op.send_complete_event! success_reason
13
+ op.async.send_complete_event success_reason
14
14
  end
15
15
  voice = @component_node.voice || default_voice || 'kal'
16
16
  application :speak, [engine, voice, document].join('|')
@@ -3,19 +3,16 @@
3
3
  module Punchblock
4
4
  module Translator
5
5
  module InputComponent
6
- def setup
7
- @buffer = ""
8
- @initial_timeout = @component_node.initial_timeout || -1
9
- @inter_digit_timeout = @component_node.inter_digit_timeout || -1
10
- end
11
-
12
6
  def execute
13
7
  validate
14
- send_ref
15
8
 
16
- @grammar = prepare_grammar
9
+ component = current_actor
10
+ @recognizer = DTMFRecognizer.new current_actor,
11
+ @component_node.grammar.value,
12
+ (@component_node.initial_timeout || -1),
13
+ (@component_node.inter_digit_timeout || -1)
17
14
 
18
- begin_initial_timer @initial_timeout/1000 unless @initial_timeout == -1
15
+ send_ref
19
16
 
20
17
  @dtmf_handler_id = register_dtmf_event_handler
21
18
  rescue OptionError => e
@@ -23,16 +20,7 @@ module Punchblock
23
20
  end
24
21
 
25
22
  def process_dtmf(digit)
26
- @buffer << digit
27
- cancel_initial_timer
28
- case (match = @grammar.match @buffer.dup)
29
- when RubySpeech::GRXML::Match
30
- complete success_reason(match)
31
- when RubySpeech::GRXML::NoMatch
32
- complete Punchblock::Component::Input::Complete::NoMatch.new
33
- when RubySpeech::GRXML::PotentialMatch
34
- reset_inter_digit_timer
35
- end
23
+ @recognizer << digit
36
24
  end
37
25
 
38
26
  def execute_command(command)
@@ -45,62 +33,27 @@ module Punchblock
45
33
  end
46
34
  end
47
35
 
48
- private
49
-
50
- def validate
51
- raise OptionError, 'A grammar document is required.' unless @component_node.grammar
52
- raise OptionError, 'A mode value other than DTMF is unsupported.' unless @component_node.mode == :dtmf
53
- raise OptionError, 'An initial timeout value that is negative (and not -1) is invalid.' unless @initial_timeout >= -1
54
- raise OptionError, 'An inter-digit timeout value that is negative (and not -1) is invalid.' unless @inter_digit_timeout >= -1
55
- end
56
-
57
- def prepare_grammar
58
- @component_node.grammar.value.clone.tap do |grammar|
59
- grammar.inline!
60
- grammar.tokenize!
61
- grammar.normalize_whitespace
62
- end
63
- end
64
-
65
- def begin_initial_timer(timeout)
66
- @initial_timer = after timeout do
67
- complete Punchblock::Component::Input::Complete::NoInput.new
68
- end
36
+ def match(mode, confidence, utterance, interpretation)
37
+ complete Punchblock::Component::Input::Complete::Success.new(:mode => mode, :confidence => confidence, :utterance => utterance, :interpretation => interpretation)
69
38
  end
70
39
 
71
- def cancel_initial_timer
72
- return unless instance_variable_defined?(:@initial_timer) && @initial_timer
73
- @initial_timer.cancel
74
- @initial_timer = nil
40
+ def nomatch
41
+ complete Punchblock::Component::Input::Complete::NoMatch.new
75
42
  end
76
43
 
77
- def reset_inter_digit_timer
78
- return if @inter_digit_timeout == -1
79
- @inter_digit_timer ||= begin
80
- after @inter_digit_timeout/1000 do
81
- complete Punchblock::Component::Input::Complete::NoMatch.new
82
- end
83
- end
84
- @inter_digit_timer.reset
44
+ def noinput
45
+ complete Punchblock::Component::Input::Complete::NoInput.new
85
46
  end
86
47
 
87
- def cancel_inter_digit_timer
88
- return unless instance_variable_defined?(:@inter_digit_timer) && @inter_digit_timer
89
- @inter_digit_timer.cancel
90
- @inter_digit_timer = nil
91
- end
48
+ private
92
49
 
93
- def success_reason(match)
94
- Punchblock::Component::Input::Complete::Success.new :mode => match.mode,
95
- :confidence => match.confidence,
96
- :utterance => match.utterance,
97
- :interpretation => match.interpretation
50
+ def validate
51
+ raise OptionError, 'A grammar document is required.' unless @component_node.grammar
52
+ raise OptionError, 'A mode value other than DTMF is unsupported.' unless @component_node.mode == :dtmf
98
53
  end
99
54
 
100
55
  def complete(reason)
101
56
  unregister_dtmf_event_handler
102
- cancel_initial_timer
103
- cancel_inter_digit_timer
104
57
  send_complete_event reason
105
58
  end
106
59
  end
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module Punchblock
4
- VERSION = "1.8.0"
4
+ VERSION = "1.8.1"
5
5
  end
data/punchblock.gemspec CHANGED
@@ -29,7 +29,7 @@ Gem::Specification.new do |s|
29
29
  s.add_runtime_dependency %q<state_machine>, ["~> 1.0"]
30
30
  s.add_runtime_dependency %q<future-resource>, ["~> 1.0"]
31
31
  s.add_runtime_dependency %q<has-guarded-handlers>, ["~> 1.5"]
32
- s.add_runtime_dependency %q<celluloid>, ["~> 0.12", ">= 0.12.4"]
32
+ s.add_runtime_dependency %q<celluloid>, ["~> 0.13"]
33
33
  s.add_runtime_dependency %q<ruby_ami>, ["~> 1.2", ">= 1.2.1"]
34
34
  s.add_runtime_dependency %q<ruby_fs>, ["~> 1.0"]
35
35
  s.add_runtime_dependency %q<ruby_speech>, ["~> 1.0"]
@@ -150,7 +150,7 @@ module Punchblock
150
150
  describe '#stop_action' do
151
151
  subject { command.stop_action }
152
152
 
153
- its(:to_xml) { should be == '<stop xmlns="urn:xmpp:rayo:1"/>' }
153
+ its(:to_xml) { should be == '<stop xmlns="urn:xmpp:rayo:ext:1"/>' }
154
154
  its(:component_id) { should be == 'abc123' }
155
155
  its(:target_call_id) { should be == '123abc' }
156
156
  end
@@ -70,6 +70,36 @@ module Punchblock
70
70
  its(:voice) { should be == 'allison' }
71
71
  its(:renderer) { should be == 'swift' }
72
72
  its(:text) { should be == 'Hello world' }
73
+
74
+ context "with SSML" do
75
+ let :stanza do
76
+ <<-MESSAGE
77
+ <output xmlns='urn:xmpp:rayo:output:1'
78
+ interrupt-on='speech'
79
+ start-offset='2000'
80
+ start-paused='false'
81
+ repeat-interval='2000'
82
+ repeat-times='10'
83
+ max-time='30000'
84
+ voice='allison'
85
+ renderer='swift'>
86
+ <speak version="1.0"
87
+ xmlns="http://www.w3.org/2001/10/synthesis"
88
+ xml:lang="en-US">
89
+ <say-as interpret-as="ordinal">100</say-as>
90
+ </speak>
91
+ </output>
92
+ MESSAGE
93
+ end
94
+
95
+ def ssml_doc(mode = :ordinal)
96
+ RubySpeech::SSML.draw do
97
+ say_as(:interpret_as => mode) { string '100' }
98
+ end
99
+ end
100
+
101
+ its(:ssml) { should be == ssml_doc }
102
+ end
73
103
  end
74
104
 
75
105
  describe "for text" do
@@ -82,7 +112,7 @@ module Punchblock
82
112
  describe "for SSML" do
83
113
  def ssml_doc(mode = :ordinal)
84
114
  RubySpeech::SSML.draw do
85
- say_as(:interpret_as => mode) { 100 }
115
+ say_as(:interpret_as => mode) { string '100' }
86
116
  end
87
117
  end
88
118
 
@@ -93,7 +123,7 @@ module Punchblock
93
123
  its(:ssml) { should be == ssml_doc }
94
124
 
95
125
  describe "comparison" do
96
- let(:output2) { Output.new :ssml => '<speak xmlns="http://www.w3.org/2001/10/synthesis" version="1.0" xml:lang="en-US"><say-as interpret-as="ordinal"/></speak>', :voice => 'kate' }
126
+ let(:output2) { Output.new :ssml => '<speak xmlns="http://www.w3.org/2001/10/synthesis" version="1.0" xml:lang="en-US"><say-as interpret-as="ordinal">100</say-as></speak>', :voice => 'kate' }
97
127
  let(:output3) { Output.new :ssml => ssml_doc, :voice => 'kate' }
98
128
  let(:output4) { Output.new :ssml => ssml_doc(:normal), :voice => 'kate' }
99
129
 
@@ -204,7 +234,7 @@ module Punchblock
204
234
  describe '#stop_action' do
205
235
  subject { command.stop_action }
206
236
 
207
- its(:to_xml) { should be == '<stop xmlns="urn:xmpp:rayo:1"/>' }
237
+ its(:to_xml) { should be == '<stop xmlns="urn:xmpp:rayo:ext:1"/>' }
208
238
  its(:component_id) { should be == 'abc123' }
209
239
  its(:target_call_id) { should be == '123abc' }
210
240
  end
@@ -209,7 +209,7 @@ module Punchblock
209
209
  describe '#stop_action' do
210
210
  subject { command.stop_action }
211
211
 
212
- its(:to_xml) { should be == '<stop xmlns="urn:xmpp:rayo:1"/>' }
212
+ its(:to_xml) { should be == '<stop xmlns="urn:xmpp:rayo:ext:1"/>' }
213
213
  its(:component_id) { should be == 'abc123' }
214
214
  its(:target_call_id) { should be == '123abc' }
215
215
  end
@@ -49,14 +49,14 @@ module Punchblock
49
49
  end
50
50
 
51
51
  it 'shuts down the translator' do
52
- subject.translator.should_receive(:shutdown!).once
52
+ subject.translator.async.should_receive(:shutdown).once
53
53
  subject.stop
54
54
  end
55
55
  end
56
56
 
57
57
  it 'sends events from RubyAMI to the translator' do
58
58
  event = mock 'RubyAMI::Event'
59
- subject.translator.should_receive(:handle_ami_event!).once.with event
59
+ subject.translator.async.should_receive(:handle_ami_event).once.with event
60
60
  subject.ami_client.handle_event event
61
61
  end
62
62
 
@@ -64,7 +64,7 @@ module Punchblock
64
64
  it 'sends a command to the translator' do
65
65
  command = mock 'Command'
66
66
  options = {:foo => :bar}
67
- subject.translator.should_receive(:execute_command!).once.with command, options
67
+ subject.translator.async.should_receive(:execute_command).once.with command, options
68
68
  subject.write command, options
69
69
  end
70
70
  end
@@ -63,7 +63,8 @@ module Punchblock
63
63
 
64
64
  it 'sends events from RubyFS to the translator' do
65
65
  event = mock 'RubyFS::Event'
66
- subject.translator.should_receive(:handle_es_event!).once.with event
66
+ subject.translator.async.should_receive(:handle_es_event).once.with event
67
+ subject.translator.async.should_receive(:handle_es_event).once.with RubyFS::Stream::Disconnected.new
67
68
  subject.stream.fire_event event
68
69
  end
69
70
 
@@ -71,7 +72,7 @@ module Punchblock
71
72
  it 'sends a command to the translator' do
72
73
  command = mock 'Command'
73
74
  options = {:foo => :bar}
74
- subject.translator.should_receive(:execute_command!).once.with command, options
75
+ subject.translator.async.should_receive(:execute_command).once.with command, options
75
76
  subject.write command, options
76
77
  end
77
78
  end
@@ -64,10 +64,11 @@ module Punchblock
64
64
  end
65
65
 
66
66
  it 'should properly set the Blather logger' do
67
+ old_logger = Punchblock.logger
67
68
  Punchblock.logger = :foo
68
69
  XMPP.new :username => '1@call.rayo.net', :password => 1
69
70
  Blather.logger.should be :foo
70
- Punchblock.reset_logger
71
+ Punchblock.logger = old_logger
71
72
  end
72
73
 
73
74
  it "looking up original command by command ID" do
@@ -232,17 +233,28 @@ module Punchblock
232
233
  let(:example_error) { import_stanza error_xml }
233
234
  let(:cmd) { Component::Output.new }
234
235
 
235
- before(:all) do
236
+ before do
236
237
  cmd.request!
237
238
  connection.__send__ :handle_error, example_error, cmd
238
239
  end
239
240
 
240
241
  subject { cmd.response }
241
242
 
242
- its(:call_id) { should be == call_id }
243
- its(:component_id) { should be == component_id }
244
- its(:name) { should be == :item_not_found }
245
- its(:text) { should be == 'Could not find call [id=f6d437f4-1e18-457b-99f8-b5d853f50347]' }
243
+ it "should have the correct call ID" do
244
+ subject.call_id.should be == call_id
245
+ end
246
+
247
+ it "should have the correct component ID" do
248
+ subject.component_id.should be == component_id
249
+ end
250
+
251
+ it "should have the correct name" do
252
+ subject.name.should be == :item_not_found
253
+ end
254
+
255
+ it "should have the correct text" do
256
+ subject.text.should be == 'Could not find call [id=f6d437f4-1e18-457b-99f8-b5d853f50347]'
257
+ end
246
258
  end
247
259
 
248
260
  describe "#prep_command_for_execution" do
@@ -7,7 +7,7 @@ module Punchblock
7
7
  class Asterisk
8
8
  describe Call do
9
9
  let(:channel) { 'SIP/foo' }
10
- let(:translator) { stub('Translator::Asterisk').as_null_object }
10
+ let(:translator) { Asterisk.new stub('AMI Client').as_null_object, stub('connection').as_null_object }
11
11
  let(:agi_env) do
12
12
  {
13
13
  :agi_request => 'async',
@@ -65,6 +65,8 @@ module Punchblock
65
65
  its(:translator) { should be translator }
66
66
  its(:agi_env) { should be == agi_env }
67
67
 
68
+ before { translator.stub :handle_pb_event }
69
+
68
70
  describe '#shutdown' do
69
71
  it 'should terminate the actor' do
70
72
  subject.shutdown
@@ -170,7 +172,7 @@ module Punchblock
170
172
  :variable => "punchblock_call_id=#{subject.id}"
171
173
  }).tap { |a| a.request! }
172
174
 
173
- translator.should_receive(:execute_global_command!).once.with expected_action
175
+ translator.async.should_receive(:execute_global_command).once.with expected_action
174
176
  subject.dial dial_command
175
177
  end
176
178
 
@@ -188,7 +190,7 @@ module Punchblock
188
190
  :variable => "punchblock_call_id=#{subject.id}"
189
191
  }).tap { |a| a.request! }
190
192
 
191
- translator.should_receive(:execute_global_command!).once.with expected_action
193
+ translator.async.should_receive(:execute_global_command).once.with expected_action
192
194
  subject.dial dial_command
193
195
  end
194
196
  end
@@ -210,7 +212,7 @@ module Punchblock
210
212
  :timeout => 10000
211
213
  }).tap { |a| a.request! }
212
214
 
213
- translator.should_receive(:execute_global_command!).once.with expected_action
215
+ translator.async.should_receive(:execute_global_command).once.with expected_action
214
216
  subject.dial dial_command
215
217
  end
216
218
  end
@@ -231,7 +233,7 @@ module Punchblock
231
233
  :variable => "punchblock_call_id=#{subject.id},SIPADDHEADER51=\"X-foo: bar\",SIPADDHEADER52=\"X-doo: dah\""
232
234
  }).tap { |a| a.request! }
233
235
 
234
- translator.should_receive(:execute_global_command!).once.with expected_action
236
+ translator.async.should_receive(:execute_global_command).once.with expected_action
235
237
  subject.dial dial_command
236
238
  end
237
239
  end
@@ -302,6 +304,20 @@ module Punchblock
302
304
  subject.process_ami_event ami_event
303
305
  end
304
306
 
307
+ it "should not allow commands to be executed while components are shutting down" do
308
+ comp_command = Punchblock::Component::Input.new :grammar => {:value => '<grammar/>'}, :mode => :dtmf
309
+ comp_command.request!
310
+ component = subject.execute_command comp_command
311
+ comp_command.response(0.1).should be_a Ref
312
+
313
+ subject.async.process_ami_event ami_event
314
+
315
+ comp_command = Punchblock::Component::Input.new :grammar => {:value => '<grammar/>'}, :mode => :dtmf
316
+ comp_command.request!
317
+ subject.execute_command comp_command
318
+ comp_command.response(0.1).should == ProtocolError.new.setup(:item_not_found, "Could not find a call with ID #{subject.id}", subject.id)
319
+ end
320
+
305
321
  context "with an undefined cause" do
306
322
  let(:cause) { '0' }
307
323
  let(:cause_txt) { 'Undefined' }
@@ -429,7 +445,7 @@ module Punchblock
429
445
  context 'with an event for a known AGI command component' do
430
446
  let(:mock_component_node) { mock 'Punchblock::Component::Asterisk::AGI::Command', :name => 'EXEC ANSWER', :params_array => [] }
431
447
  let :component do
432
- Component::Asterisk::AGICommand.new mock_component_node, subject.translator
448
+ Component::Asterisk::AGICommand.new mock_component_node, subject
433
449
  end
434
450
 
435
451
  let(:ami_event) do
@@ -579,24 +595,54 @@ module Punchblock
579
595
  e['Privilege'] = "call,all"
580
596
  e['Response'] = "Success"
581
597
  e['Channel1'] = "SIP/foo"
582
- e['Channel2'] = "SIP/5678-00000000"
598
+ e['Channel2'] = other_channel
583
599
  end
584
600
  end
585
601
 
586
602
  let(:other_channel) { 'SIP/5678-00000000' }
587
- let(:other_call_id) { 'def567' }
588
- let :command do
589
- Punchblock::Command::Join.new :call_id => other_call_id
590
- end
591
603
 
592
- before do
593
- subject.pending_joins[other_channel] = command
594
- command.request!
604
+ context "when a join has been executed against another call" do
605
+ let :other_call do
606
+ Call.new other_channel, translator
607
+ end
608
+
609
+ let(:other_call_id) { other_call.id }
610
+ let :command do
611
+ Punchblock::Command::Join.new :call_id => other_call_id
612
+ end
613
+
614
+ before do
615
+ translator.register_call other_call
616
+ command.request!
617
+ subject.execute_command command
618
+ end
619
+
620
+ it 'retrieves and sets success on the correct Join' do
621
+ subject.process_ami_event ami_event
622
+ command.response(0.5).should be == true
623
+ end
624
+
625
+ context "with the channel names reversed" do
626
+ let :ami_event do
627
+ RubyAMI::Event.new('BridgeExec').tap do |e|
628
+ e['Privilege'] = "call,all"
629
+ e['Response'] = "Success"
630
+ e['Channel1'] = other_channel
631
+ e['Channel2'] = "SIP/foo"
632
+ end
633
+ end
634
+
635
+ it 'retrieves and sets success on the correct Join' do
636
+ subject.process_ami_event ami_event
637
+ command.response(0.5).should be == true
638
+ end
639
+ end
595
640
  end
596
641
 
597
- it 'retrieves and sets success on the correct Join' do
598
- subject.process_ami_event ami_event
599
- command.response(0.5).should be == true
642
+ context "with no matching join command" do
643
+ it "should do nothing" do
644
+ expect { subject.process_ami_event ami_event }.not_to raise_error
645
+ end
600
646
  end
601
647
  end
602
648
 
@@ -859,12 +905,12 @@ module Punchblock
859
905
  Punchblock::Component::Asterisk::AGI::Command.new :name => 'Answer'
860
906
  end
861
907
 
862
- let(:mock_action) { mock 'Component::Asterisk::AGI::Command', :id => 'foo' }
908
+ let(:mock_action) { Translator::Asterisk::Component::Asterisk::AGICommand.new(command, subject) }
863
909
 
864
910
  it 'should create an AGI command component actor and execute it asynchronously' do
865
911
  mock_action.should_receive(:internal=).never
866
912
  Component::Asterisk::AGICommand.should_receive(:new_link).once.with(command, subject).and_return mock_action
867
- mock_action.should_receive(:execute!).once
913
+ mock_action.async.should_receive(:execute).once
868
914
  subject.execute_command command
869
915
  end
870
916
  end
@@ -874,12 +920,12 @@ module Punchblock
874
920
  Punchblock::Component::Output.new
875
921
  end
876
922
 
877
- let(:mock_action) { mock 'Component::Asterisk::Output', :id => 'foo' }
923
+ let(:mock_action) { Translator::Asterisk::Component::Output.new(command, subject) }
878
924
 
879
925
  it 'should create an Output component and execute it asynchronously' do
880
926
  Component::Output.should_receive(:new_link).once.with(command, subject).and_return mock_action
881
927
  mock_action.should_receive(:internal=).never
882
- mock_action.should_receive(:execute!).once
928
+ mock_action.async.should_receive(:execute).once
883
929
  subject.execute_command command
884
930
  end
885
931
  end
@@ -889,12 +935,12 @@ module Punchblock
889
935
  Punchblock::Component::Input.new
890
936
  end
891
937
 
892
- let(:mock_action) { mock 'Component::Asterisk::Input', :id => 'foo' }
938
+ let(:mock_action) { Translator::Asterisk::Component::Input.new(command, subject) }
893
939
 
894
940
  it 'should create an Input component and execute it asynchronously' do
895
941
  Component::Input.should_receive(:new_link).once.with(command, subject).and_return mock_action
896
942
  mock_action.should_receive(:internal=).never
897
- mock_action.should_receive(:execute!).once
943
+ mock_action.async.should_receive(:execute).once
898
944
  subject.execute_command command
899
945
  end
900
946
  end
@@ -904,12 +950,12 @@ module Punchblock
904
950
  Punchblock::Component::Record.new
905
951
  end
906
952
 
907
- let(:mock_action) { mock 'Component::Asterisk::Record', :id => 'foo' }
953
+ let(:mock_action) { Translator::Asterisk::Component::Record.new(command, subject) }
908
954
 
909
955
  it 'should create a Record component and execute it asynchronously' do
910
956
  Component::Record.should_receive(:new_link).once.with(command, subject).and_return mock_action
911
957
  mock_action.should_receive(:internal=).never
912
- mock_action.should_receive(:execute!).once
958
+ mock_action.async.should_receive(:execute).once
913
959
  subject.execute_command command
914
960
  end
915
961
  end
@@ -974,6 +1020,18 @@ module Punchblock
974
1020
  subject.execute_command subsequent_command
975
1021
  subsequent_command.response.should be == ProtocolError.new.setup(:item_not_found, "Could not find a component with ID #{comp_id} for call #{subject.id}", subject.id, comp_id)
976
1022
  end
1023
+
1024
+ context "when we dispatch the command to it" do
1025
+ it 'sends an error in response to the command' do
1026
+ component = subject.component_with_id comp_id
1027
+
1028
+ component.should_receive(:execute_command).and_raise(Celluloid::DeadActorError)
1029
+
1030
+ subsequent_command.request!
1031
+ subject.execute_command subsequent_command
1032
+ subsequent_command.response.should be == ProtocolError.new.setup(:item_not_found, "Could not find a component with ID #{comp_id} for call #{subject.id}", subject.id, comp_id)
1033
+ end
1034
+ end
977
1035
  end
978
1036
 
979
1037
  context "for an unknown component ID" do
@@ -1015,12 +1073,6 @@ module Punchblock
1015
1073
  agi_command.name.should be == "EXEC Bridge"
1016
1074
  agi_command.params_array.should be == [other_channel]
1017
1075
  end
1018
-
1019
- it "adds the join to the @pending_joins hash" do
1020
- translator.should_receive(:call_with_id).with(other_call_id).and_return(other_call)
1021
- subject.execute_command command
1022
- subject.pending_joins[other_channel].should be command
1023
- end
1024
1076
  end
1025
1077
 
1026
1078
  context "with an unjoin command" do