punchblock 0.8.2 → 0.8.3

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # develop
2
2
 
3
+ # v0.8.3 - 2012-01-17
4
+ * Feature: Return an error when trying to execute a command for unknown calls/components or when not understood
5
+ * Feature: Log calls/translator shutting down
6
+ * Feature: Calls and components should log their IDs
7
+ * Feature: Components marked as internal should send events directly to the component node
8
+ * Bugfix: Fix Asterisk Call and Component logger IDs
9
+ * Bugfix: Fix a stupidly high log level
10
+ * Bugfix: AGI commands executed by a call/component that are a translation of a Rayo command should be marked internal
11
+ * Bugfix: Asterisk components should sent events via the connection
12
+ * Bugfix: Shutting down an asterisk connection should do a cascading shutdown of the translator and all of its calls
13
+ * Bugfix: Component actors should be terminated once they've sent a complete event
14
+ * Bugfix: Component events should be sent with the call ID
15
+ * Bugfix: AMIAction components do not have a call
16
+ * Bugfix: Add test coverage for comparison of complete events
17
+ * Bugfix: A call being hung up should terminate the call actor
18
+ * Bugfix: Fix a mock expectation error in a test
19
+
3
20
  # v0.8.2 - 2012-01-10
4
21
  * Feature: Support outbound dial on Asterisk
5
22
  * Bugfix: Asterisk hangup causes should map to correct Rayo End reason
@@ -24,6 +24,7 @@ module Punchblock
24
24
 
25
25
  def handle_event(event)
26
26
  event.client = self
27
+ pb_logger.debug "Handling event #{event} with source #{event.source}."
27
28
  if event.source
28
29
  event.source.add_event event
29
30
  else
@@ -18,6 +18,7 @@ module Punchblock
18
18
  end
19
19
 
20
20
  def stop
21
+ translator.shutdown!
21
22
  ami_client.stop
22
23
  end
23
24
 
@@ -43,6 +43,12 @@ module Punchblock
43
43
  @components[component_id]
44
44
  end
45
45
 
46
+ def shutdown
47
+ pb_logger.debug "Shutting down"
48
+ @calls.values.each &:shutdown!
49
+ current_actor.terminate!
50
+ end
51
+
46
52
  def handle_ami_event(event)
47
53
  return unless event.is_a? RubyAMI::Event
48
54
  pb_logger.trace "Handling AMI event #{event.inspect}"
@@ -95,11 +101,19 @@ module Punchblock
95
101
  end
96
102
 
97
103
  def execute_call_command(command)
98
- call_with_id(command.call_id).execute_command! command
104
+ if call = call_with_id(command.call_id)
105
+ call.execute_command! command
106
+ else
107
+ command.response = ProtocolError.new 'call-not-found', "Could not find a call with ID #{command.call_id}", command.call_id
108
+ end
99
109
  end
100
110
 
101
111
  def execute_component_command(command)
102
- component_with_id(command.component_id).execute_command! command
112
+ if (component = component_with_id(command.component_id))
113
+ component.execute_command! command
114
+ else
115
+ command.response = ProtocolError.new 'component-not-found', "Could not find a component with ID #{command.component_id}", command.call_id, command.component_id
116
+ end
103
117
  end
104
118
 
105
119
  def execute_global_command(command)
@@ -112,6 +126,8 @@ module Punchblock
112
126
  call = Call.new command.to, current_actor
113
127
  register_call call
114
128
  call.dial! command
129
+ else
130
+ command.response = ProtocolError.new 'command-not-acceptable', "Did not understand command"
115
131
  end
116
132
  end
117
133
 
@@ -38,6 +38,11 @@ module Punchblock
38
38
  send_pb_event offer_event
39
39
  end
40
40
 
41
+ def shutdown
42
+ pb_logger.debug "Shutting down"
43
+ current_actor.terminate!
44
+ end
45
+
41
46
  def to_s
42
47
  "#<#{self.class}:#{id} Channel: #{channel.inspect}>"
43
48
  end
@@ -76,7 +81,7 @@ module Punchblock
76
81
  case ami_event.name
77
82
  when 'Hangup'
78
83
  pb_logger.debug "Received a Hangup AMI event. Sending End event."
79
- send_pb_event Event::End.new(:reason => HANGUP_CAUSE_TO_END_REASON[ami_event['Cause'].to_i])
84
+ send_end_event HANGUP_CAUSE_TO_END_REASON[ami_event['Cause'].to_i]
80
85
  when 'AsyncAGI'
81
86
  pb_logger.debug "Received an AsyncAGI event. Looking for matching AGICommand component."
82
87
  if component = component_with_id(ami_event['CommandID'])
@@ -100,7 +105,11 @@ module Punchblock
100
105
  def execute_command(command)
101
106
  pb_logger.debug "Executing command: #{command.inspect}"
102
107
  if command.component_id
103
- component_with_id(command.component_id).execute_command! command
108
+ if component = component_with_id(command.component_id)
109
+ component.execute_command! command
110
+ else
111
+ command.response = ProtocolError.new 'component-not-found', "Could not find a component with ID #{command.component_id} for call #{id}", id, command.component_id
112
+ end
104
113
  end
105
114
  case command
106
115
  when Command::Accept
@@ -122,11 +131,13 @@ module Punchblock
122
131
  command.response = true
123
132
  end
124
133
  when Punchblock::Component::Asterisk::AGI::Command
125
- execute_agi_command command
134
+ execute_component Component::Asterisk::AGICommand, command
126
135
  when Punchblock::Component::Output
127
136
  execute_component Component::Asterisk::Output, command
128
137
  when Punchblock::Component::Input
129
138
  execute_component Component::Asterisk::Input, command
139
+ else
140
+ command.response = ProtocolError.new 'command-not-acceptable', "Did not understand command for call #{id}", id
130
141
  end
131
142
  end
132
143
 
@@ -138,7 +149,7 @@ module Punchblock
138
149
  pb_logger.debug "AGI action received complete event #{e.inspect}"
139
150
  block.call e
140
151
  end
141
- execute_agi_command @current_agi_command
152
+ execute_component Component::Asterisk::AGICommand, @current_agi_command, :internal => true
142
153
  end
143
154
 
144
155
  def send_ami_action(name, headers = {}, &block)
@@ -149,15 +160,21 @@ module Punchblock
149
160
  end
150
161
  end
151
162
 
163
+ def logger_id
164
+ "#{self.class}: #{id}"
165
+ end
166
+
152
167
  private
153
168
 
154
- def execute_agi_command(command)
155
- execute_component Component::Asterisk::AGICommand, command
169
+ def send_end_event(reason)
170
+ send_pb_event Event::End.new(:reason => reason)
171
+ current_actor.terminate!
156
172
  end
157
173
 
158
- def execute_component(type, command)
174
+ def execute_component(type, command, options = {})
159
175
  type.new(command, current_actor).tap do |component|
160
176
  register_component component
177
+ component.internal = true if options[:internal]
161
178
  component.execute!
162
179
  end
163
180
  end
@@ -10,6 +10,7 @@ module Punchblock
10
10
  include Celluloid
11
11
 
12
12
  attr_reader :id, :call
13
+ attr_accessor :internal
13
14
 
14
15
  def initialize(component_node, call = nil)
15
16
  @component_node, @call = component_node, call
@@ -21,6 +22,43 @@ module Punchblock
21
22
  def setup
22
23
  end
23
24
 
25
+ def execute_command(command)
26
+ command.response = ProtocolError.new 'command-not-acceptable', "Did not understand command for component #{id}", call_id, id
27
+ end
28
+
29
+ def send_complete_event(reason)
30
+ event = Punchblock::Event::Complete.new.tap do |c|
31
+ c.reason = reason
32
+ end
33
+ send_event event
34
+ current_actor.terminate!
35
+ end
36
+
37
+ def send_event(event)
38
+ event.component_id = id
39
+ event.call_id = call_id
40
+ pb_logger.debug "Sending event #{event}"
41
+ if internal
42
+ @component_node.add_event event
43
+ else
44
+ translator.connection.handle_event event
45
+ end
46
+ end
47
+
48
+ def logger_id
49
+ "#{self.class}: #{call ? "Call ID: #{call.id}, Component ID: #{id}" : id}"
50
+ end
51
+
52
+ def call_id
53
+ call.id if call
54
+ end
55
+
56
+ private
57
+
58
+ def translator
59
+ call.translator
60
+ end
61
+
24
62
  def set_node_response(value)
25
63
  pb_logger.debug "Setting response on component node to #{value}"
26
64
  @component_node.response = value
@@ -33,18 +71,6 @@ module Punchblock
33
71
  def with_error(name, text)
34
72
  set_node_response ProtocolError.new(name, text)
35
73
  end
36
-
37
- def complete_event(reason)
38
- Punchblock::Event::Complete.new.tap do |c|
39
- c.reason = reason
40
- end
41
- end
42
-
43
- def send_event(event)
44
- event.component_id = id
45
- pb_logger.debug "Sending event #{event}"
46
- @component_node.add_event event
47
- end
48
74
  end
49
75
  end
50
76
  end
@@ -22,7 +22,7 @@ module Punchblock
22
22
  if event.name == 'AsyncAGI'
23
23
  if event['SubEvent'] == 'Exec'
24
24
  pb_logger.debug "Received AsyncAGI:Exec event, sending complete event."
25
- send_event complete_event(success_reason(event))
25
+ send_complete_event success_reason(event)
26
26
  end
27
27
  end
28
28
  end
@@ -4,10 +4,10 @@ module Punchblock
4
4
  module Component
5
5
  module Asterisk
6
6
  class AMIAction < Component
7
- attr_reader :action
7
+ attr_reader :action, :translator
8
8
 
9
9
  def initialize(component_node, translator)
10
- super
10
+ super component_node, nil
11
11
  @translator = translator
12
12
  end
13
13
 
@@ -21,6 +21,17 @@ module Punchblock
21
21
  send_ref
22
22
  end
23
23
 
24
+ def handle_response(response)
25
+ pb_logger.debug "Handling response #{response.inspect}"
26
+ case response
27
+ when RubyAMI::Error
28
+ send_complete_event error_reason(response)
29
+ when RubyAMI::Response
30
+ send_events
31
+ send_complete_event success_reason(response)
32
+ end
33
+ end
34
+
24
35
  private
25
36
 
26
37
  def create_action
@@ -28,8 +39,9 @@ module Punchblock
28
39
  @component_node.params_hash.each_pair do |key, value|
29
40
  headers[key.to_s.capitalize] = value
30
41
  end
42
+ component = current_actor
31
43
  RubyAMI::Action.new @component_node.name, headers do |response|
32
- handle_response response
44
+ component.handle_response! response
33
45
  end
34
46
  end
35
47
 
@@ -37,17 +49,6 @@ module Punchblock
37
49
  @translator.send_ami_action! @action
38
50
  end
39
51
 
40
- def handle_response(response)
41
- pb_logger.debug "Handling response #{response.inspect}"
42
- case response
43
- when RubyAMI::Error
44
- send_event complete_event(error_reason(response))
45
- when RubyAMI::Response
46
- send_events
47
- send_event complete_event(success_reason(response))
48
- end
49
- end
50
-
51
52
  def error_reason(response)
52
53
  Punchblock::Event::Complete::Error.new :details => response.message
53
54
  end
@@ -106,7 +106,7 @@ module Punchblock
106
106
  @active = false
107
107
  cancel_initial_timer
108
108
  cancel_inter_digit_timer
109
- send_event complete_event(reason)
109
+ send_complete_event reason
110
110
  end
111
111
  end
112
112
  end
@@ -47,7 +47,7 @@ module Punchblock
47
47
  send_ref
48
48
  @call.send_agi_action! 'EXEC MRCPSynth', doc, mrcpsynth_options do |complete_event|
49
49
  pb_logger.debug "MRCPSynth completed with #{complete_event}."
50
- send_event complete_event(success_reason)
50
+ send_complete_event success_reason
51
51
  end
52
52
  end
53
53
  end
@@ -57,7 +57,7 @@ module Punchblock
57
57
  pb_logger.debug "Received action completion. Now waiting on #{@pending_actions} actions."
58
58
  if @pending_actions < 1
59
59
  pb_logger.debug "Sending complete event"
60
- send_event complete_event(success_reason)
60
+ send_complete_event success_reason
61
61
  end
62
62
  end
63
63
 
@@ -1,3 +1,3 @@
1
1
  module Punchblock
2
- VERSION = "0.8.2"
2
+ VERSION = "0.8.3"
3
3
  end
@@ -42,6 +42,11 @@ module Punchblock
42
42
  subject.ami_client.expects(:stop).once
43
43
  subject.stop
44
44
  end
45
+
46
+ it 'shuts down the translator' do
47
+ subject.translator.expects(:shutdown!).once
48
+ subject.stop
49
+ end
45
50
  end
46
51
 
47
52
  it 'sends events from RubyAMI to the translator' do
@@ -7,6 +7,64 @@ module Punchblock
7
7
  RayoNode.class_from_registration(:complete, 'urn:xmpp:rayo:ext:1').should == Complete
8
8
  end
9
9
 
10
+ describe "comparing for equality" do
11
+ subject do
12
+ Complete.new.tap do |c|
13
+ c.reason = Complete::Stop.new
14
+ c.call_id = '1234'
15
+ c.component_id = 'abcd'
16
+ end
17
+ end
18
+
19
+ let :other_complete do
20
+ Complete.new.tap do |c|
21
+ c.reason = reason
22
+ c.call_id = call_id
23
+ c.component_id = component_id
24
+ end
25
+ end
26
+
27
+ context 'with reason, call id and component id the same' do
28
+ let(:reason) { Complete::Stop.new }
29
+ let(:call_id) { '1234' }
30
+ let(:component_id) { 'abcd' }
31
+
32
+ it "should be equal" do
33
+ subject.should == other_complete
34
+ end
35
+ end
36
+
37
+ context 'with a different reason' do
38
+ let(:reason) { Complete::Hangup.new }
39
+ let(:call_id) { '1234' }
40
+ let(:component_id) { 'abcd' }
41
+
42
+ it "should not be equal" do
43
+ subject.should_not == other_complete
44
+ end
45
+ end
46
+
47
+ context 'with a different call id' do
48
+ let(:reason) { Complete::Stop.new }
49
+ let(:call_id) { '5678' }
50
+ let(:component_id) { 'abcd' }
51
+
52
+ it "should not be equal" do
53
+ subject.should_not == other_complete
54
+ end
55
+ end
56
+
57
+ context 'with a different component id' do
58
+ let(:reason) { Complete::Stop.new }
59
+ let(:call_id) { '1234' }
60
+ let(:component_id) { 'efgh' }
61
+
62
+ it "should not be equal" do
63
+ subject.should_not == other_complete
64
+ end
65
+ end
66
+ end
67
+
10
68
  describe "from a stanza" do
11
69
  let :stanza do
12
70
  <<-MESSAGE
@@ -64,6 +64,13 @@ module Punchblock
64
64
  its(:translator) { should be translator }
65
65
  its(:agi_env) { should == agi_env }
66
66
 
67
+ describe '#shutdown' do
68
+ it 'should terminate the actor' do
69
+ subject.shutdown
70
+ subject.should_not be_alive
71
+ end
72
+ end
73
+
67
74
  describe '#register_component' do
68
75
  it 'should make the component accessible by ID' do
69
76
  component_id = 'abc123'
@@ -149,6 +156,15 @@ module Punchblock
149
156
  end
150
157
  end
151
158
 
159
+ let(:cause) { '16' }
160
+ let(:cause_txt) { 'Normal Clearing' }
161
+
162
+ it "should cause the actor to be terminated" do
163
+ translator.expects(:handle_pb_event!).once
164
+ subject.process_ami_event ami_event
165
+ subject.should_not be_alive
166
+ end
167
+
152
168
  context "with a normal clearing cause" do
153
169
  let(:cause) { '16' }
154
170
  let(:cause_txt) { 'Normal Clearing' }
@@ -367,7 +383,8 @@ module Punchblock
367
383
  let(:command) { Command::Accept.new }
368
384
 
369
385
  it "should send an EXEC RINGING AGI command and set the command's response" do
370
- subject.execute_command command
386
+ component = subject.execute_command command
387
+ component.internal.should be_true
371
388
  agi_command = subject.wrapped_object.instance_variable_get(:'@current_agi_command')
372
389
  agi_command.name.should == "EXEC RINGING"
373
390
  agi_command.execute!
@@ -380,7 +397,8 @@ module Punchblock
380
397
  let(:command) { Command::Answer.new }
381
398
 
382
399
  it "should send an EXEC ANSWER AGI command and set the command's response" do
383
- subject.execute_command command
400
+ component = subject.execute_command command
401
+ component.internal.should be_true
384
402
  agi_command = subject.wrapped_object.instance_variable_get(:'@current_agi_command')
385
403
  agi_command.name.should == "EXEC ANSWER"
386
404
  agi_command.execute!
@@ -409,6 +427,7 @@ module Punchblock
409
427
  let(:mock_action) { mock 'Component::Asterisk::AGI::Command', :id => 'foo' }
410
428
 
411
429
  it 'should create an AGI command component actor and execute it asynchronously' do
430
+ mock_action.expects(:internal=).never
412
431
  Component::Asterisk::AGICommand.expects(:new).once.with(command, subject).returns mock_action
413
432
  mock_action.expects(:execute!).once
414
433
  subject.execute_command command
@@ -424,6 +443,7 @@ module Punchblock
424
443
 
425
444
  it 'should create an AGI command component actor and execute it asynchronously' do
426
445
  Component::Asterisk::Output.expects(:new).once.with(command, subject).returns mock_action
446
+ mock_action.expects(:internal=).never
427
447
  mock_action.expects(:execute!).once
428
448
  subject.execute_command command
429
449
  end
@@ -438,6 +458,7 @@ module Punchblock
438
458
 
439
459
  it 'should create an AGI command component actor and execute it asynchronously' do
440
460
  Component::Asterisk::Input.expects(:new).once.with(command, subject).returns mock_action
461
+ mock_action.expects(:internal=).never
441
462
  mock_action.expects(:execute!).once
442
463
  subject.execute_command command
443
464
  end
@@ -454,11 +475,31 @@ module Punchblock
454
475
  mock 'Component', :id => component_id
455
476
  end
456
477
 
457
- before { subject.register_component mock_component }
478
+ context "for a known component ID" do
479
+ before { subject.register_component mock_component }
480
+
481
+ it 'should send the command to the component for execution' do
482
+ mock_component.expects(:execute_command!).once
483
+ subject.execute_command command
484
+ end
485
+ end
486
+
487
+ context "for an unknown component ID" do
488
+ it 'sends an error in response to the command' do
489
+ subject.execute_command command
490
+ command.response.should == ProtocolError.new('component-not-found', "Could not find a component with ID #{component_id} for call #{subject.id}", subject.id, component_id)
491
+ end
492
+ end
493
+ end
494
+
495
+ context 'with a command we do not understand' do
496
+ let :command do
497
+ Punchblock::Component::Record.new
498
+ end
458
499
 
459
- it 'should send the command to the component for execution' do
460
- mock_component.expects(:execute_command!).once
500
+ it 'sends an error in response to the command' do
461
501
  subject.execute_command command
502
+ command.response.should == ProtocolError.new('command-not-acceptable', "Did not understand command for call #{subject.id}", subject.id)
462
503
  end
463
504
  end
464
505
  end
@@ -7,7 +7,13 @@ module Punchblock
7
7
  module Asterisk
8
8
  describe AGICommand do
9
9
  let(:channel) { 'SIP/foo' }
10
- let(:mock_call) { mock 'Call', :channel => channel }
10
+ let(:connection) do
11
+ mock_connection_with_event_handler do |event|
12
+ command.add_event event
13
+ end
14
+ end
15
+ let(:translator) { Punchblock::Translator::Asterisk.new mock('AMI'), connection }
16
+ let(:mock_call) { Punchblock::Translator::Asterisk::Call.new channel, translator }
11
17
  let(:component_id) { UUIDTools::UUID.random_create }
12
18
 
13
19
  before { UUIDTools::UUID.stubs :random_create => component_id }
@@ -112,7 +118,7 @@ module Punchblock
112
118
 
113
119
  complete_event = command.complete_event 0.5
114
120
 
115
- complete_event.component_id.should == subject.id
121
+ complete_event.component_id.should == component_id.to_s
116
122
  complete_event.reason.should == expected_complete_reason
117
123
  end
118
124
  end
@@ -6,7 +6,12 @@ module Punchblock
6
6
  module Component
7
7
  module Asterisk
8
8
  describe AMIAction do
9
- let(:mock_translator) { mock 'Translator::Asterisk' }
9
+ let(:connection) do
10
+ mock_connection_with_event_handler do |event|
11
+ command.add_event event
12
+ end
13
+ end
14
+ let(:mock_translator) { Punchblock::Translator::Asterisk.new mock('AMI'), connection }
10
15
 
11
16
  let :command do
12
17
  Punchblock::Component::Asterisk::AMI::Action.new :name => 'ExtensionStatus', :params => { :context => 'default', :exten => 'idonno' }
@@ -57,14 +62,8 @@ module Punchblock
57
62
 
58
63
  context 'for a non-causal action' do
59
64
  it 'should send a complete event to the component node' do
60
- subject.action.response = response
61
-
62
- command.should be_complete
63
-
64
- complete_event = command.complete_event 0.5
65
-
66
- complete_event.component_id.should == subject.id
67
- complete_event.reason.should == expected_complete_reason
65
+ subject.wrapped_object.expects(:send_complete_event).once.with expected_complete_reason
66
+ subject.handle_response response
68
67
  end
69
68
  end
70
69
 
@@ -115,12 +114,14 @@ module Punchblock
115
114
  end
116
115
 
117
116
  it 'should send events to the component node' do
117
+ event_node
118
118
  command.register_handler :internal, Punchblock::Event::Asterisk::AMI::Event do |event|
119
119
  @event = event
120
120
  end
121
- subject.action << event
122
- subject.action << response
123
- subject.action << terminating_event
121
+ action = subject.action
122
+ action << event
123
+ subject.handle_response response
124
+ action << terminating_event
124
125
  @event.should == event_node
125
126
  end
126
127
 
@@ -138,16 +139,12 @@ module Punchblock
138
139
  end
139
140
 
140
141
  let :expected_complete_reason do
141
- Punchblock::Event::Complete::Error.new :component_id => subject.id, :details => 'Action failed'
142
+ Punchblock::Event::Complete::Error.new :details => 'Action failed'
142
143
  end
143
144
 
144
145
  it 'should send a complete event to the component node' do
145
- subject.action << error
146
-
147
- complete_event = command.complete_event 0.5
148
-
149
- complete_event.component_id.should == subject.id
150
- complete_event.reason.should == expected_complete_reason
146
+ subject.wrapped_object.expects(:send_complete_event).once.with expected_complete_reason
147
+ subject.handle_response error
151
148
  end
152
149
  end
153
150
  end
@@ -6,10 +6,15 @@ module Punchblock
6
6
  module Component
7
7
  module Asterisk
8
8
  describe Input do
9
+ let(:connection) do
10
+ mock_connection_with_event_handler do |event|
11
+ command.add_event event
12
+ end
13
+ end
9
14
  let(:media_engine) { nil }
10
- let(:translator) { Punchblock::Translator::Asterisk.new mock('AMI'), mock('Client'), media_engine }
15
+ let(:translator) { Punchblock::Translator::Asterisk.new mock('AMI'), connection, media_engine }
11
16
  let(:call) { Punchblock::Translator::Asterisk::Call.new 'foo', translator }
12
- let(:command_options) { nil }
17
+ let(:command_options) { {} }
13
18
 
14
19
  let :command do
15
20
  Punchblock::Component::Input.new command_options
@@ -31,12 +36,6 @@ module Punchblock
31
36
  end
32
37
  end
33
38
 
34
- let :command_options do
35
- {
36
-
37
- }
38
- end
39
-
40
39
  subject { Input.new command, call }
41
40
 
42
41
  describe '#execute' do
@@ -72,7 +71,10 @@ module Punchblock
72
71
  let(:reason) { command.complete_event(5).reason }
73
72
 
74
73
  describe "receiving DTMF events" do
75
- before { subject.execute }
74
+ before do
75
+ subject.execute
76
+ expected_event
77
+ end
76
78
 
77
79
  context "when a match is found" do
78
80
  before do
@@ -80,8 +82,17 @@ module Punchblock
80
82
  send_ami_events_for_dtmf 2
81
83
  end
82
84
 
85
+ let :expected_event do
86
+ Punchblock::Component::Input::Complete::Success.new :mode => :dtmf,
87
+ :confidence => 1,
88
+ :utterance => '12',
89
+ :interpretation => 'dtmf-1 dtmf-2',
90
+ :component_id => subject.id,
91
+ :call_id => call.id
92
+ end
93
+
83
94
  it "should send a success complete event with the relevant data" do
84
- reason.should == Punchblock::Component::Input::Complete::Success.new(:mode => :dtmf, :confidence => 1, :utterance => '12', :interpretation => 'dtmf-1 dtmf-2', :component_id => subject.id)
95
+ reason.should == expected_event
85
96
  end
86
97
  end
87
98
 
@@ -91,8 +102,13 @@ module Punchblock
91
102
  send_ami_events_for_dtmf '#'
92
103
  end
93
104
 
105
+ let :expected_event do
106
+ Punchblock::Component::Input::Complete::NoMatch.new :component_id => subject.id,
107
+ :call_id => call.id
108
+ end
109
+
94
110
  it "should send a nomatch complete event" do
95
- reason.should == Punchblock::Component::Input::Complete::NoMatch.new(:component_id => subject.id)
111
+ reason.should == expected_event
96
112
  end
97
113
  end
98
114
  end
@@ -6,10 +6,14 @@ module Punchblock
6
6
  module Component
7
7
  module Asterisk
8
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 }
9
+ let(:connection) do
10
+ mock_connection_with_event_handler do |event|
11
+ command.add_event event
12
+ end
13
+ end
14
+ let(:media_engine) { nil }
15
+ let(:translator) { Punchblock::Translator::Asterisk.new mock('AMI'), connection, media_engine }
16
+ let(:mock_call) { Punchblock::Translator::Asterisk::Call.new 'foo', translator }
13
17
 
14
18
  let :command do
15
19
  Punchblock::Component::Output.new command_options
@@ -316,7 +320,7 @@ module Punchblock
316
320
  end
317
321
  subject.execute
318
322
  latch.wait 2
319
- sleep 0.1
323
+ sleep 2
320
324
  end
321
325
 
322
326
  it 'should send a complete event after the final file has finished playback' do
@@ -6,6 +6,87 @@ module Punchblock
6
6
  describe Component do
7
7
 
8
8
  end
9
+
10
+ module Component
11
+ describe Component do
12
+ let(:connection) { Punchblock::Connection::Asterisk.new }
13
+ let(:translator) { connection.translator }
14
+ let(:call) { Punchblock::Translator::Asterisk::Call.new 'foo', translator }
15
+ let(:command) { Punchblock::Component::Input.new }
16
+
17
+ subject { Component.new command, call }
18
+
19
+ before { command.request! }
20
+
21
+ describe "#send_event" do
22
+ before { command.execute! }
23
+
24
+ let :event do
25
+ Punchblock::Event::Complete.new
26
+ end
27
+
28
+ let :expected_event do
29
+ Punchblock::Event::Complete.new.tap do |e|
30
+ e.call_id = call.id
31
+ e.component_id = subject.id
32
+ end
33
+ end
34
+
35
+ it "should send the event to the connection" do
36
+ connection.expects(:handle_event).once.with expected_event
37
+ subject.send_event event
38
+ end
39
+
40
+ context "when marked internal" do
41
+ before { subject.internal = true }
42
+
43
+ it "should add the event to the command" do
44
+ command.expects(:add_event).once.with expected_event
45
+ subject.send_event event
46
+ end
47
+ end
48
+ end
49
+
50
+ describe "#send_complete_event" do
51
+ before { command.execute! }
52
+
53
+ let(:reason) { Punchblock::Event::Complete::Stop.new }
54
+ let :expected_event do
55
+ Punchblock::Event::Complete.new.tap do |c|
56
+ c.reason = Punchblock::Event::Complete::Stop.new
57
+ end
58
+ end
59
+
60
+ it "should send a complete event with the specified reason" do
61
+ subject.wrapped_object.expects(:send_event).once.with expected_event
62
+ subject.send_complete_event reason
63
+ end
64
+
65
+ it "should cause the actor to be shut down" do
66
+ subject.wrapped_object.stubs(:send_event).returns true
67
+ subject.send_complete_event reason
68
+ subject.should_not be_alive
69
+ end
70
+ end
71
+
72
+ describe '#execute_command' do
73
+ before do
74
+ component_command.request!
75
+ end
76
+
77
+ context 'with a command we do not understand' do
78
+ let :component_command do
79
+ Punchblock::Component::Stop.new :component_id => subject.id
80
+ end
81
+
82
+ it 'sends an error in response to the command' do
83
+ subject.execute_command component_command
84
+ component_command.response.should == ProtocolError.new('command-not-acceptable', "Did not understand command for component #{subject.id}", call.id, subject.id)
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
9
90
  end
10
91
  end
11
92
  end
@@ -14,7 +14,7 @@ module Punchblock
14
14
  its(:ami_client) { should be ami_client }
15
15
  its(:connection) { should be connection }
16
16
 
17
- after { translator.terminate }
17
+ after { translator.terminate if translator.alive? }
18
18
 
19
19
  context 'with a configured media engine of :asterisk' do
20
20
  let(:media_engine) { :asterisk }
@@ -26,6 +26,20 @@ module Punchblock
26
26
  its(:media_engine) { should == :unimrcp }
27
27
  end
28
28
 
29
+ describe '#shutdown' do
30
+ it "instructs all calls to shutdown" do
31
+ call = Asterisk::Call.new 'foo', subject
32
+ call.expects(:shutdown!).once
33
+ subject.register_call call
34
+ subject.shutdown
35
+ end
36
+
37
+ it "terminates the actor" do
38
+ subject.shutdown
39
+ subject.should_not be_alive
40
+ end
41
+ end
42
+
29
43
  describe '#execute_command' do
30
44
  describe 'with a call command' do
31
45
  let(:command) { Command::Answer.new }
@@ -95,16 +109,29 @@ module Punchblock
95
109
  describe '#execute_call_command' do
96
110
  let(:call_id) { 'abc123' }
97
111
  let(:call) { Translator::Asterisk::Call.new 'SIP/foo', subject }
98
- let(:command) { mock 'Command::Answer', :call_id => call_id }
112
+ let(:command) { Command::Answer.new.tap { |c| c.call_id = call_id } }
99
113
 
100
114
  before do
115
+ command.request!
101
116
  call.stubs(:id).returns call_id
102
- subject.register_call call
103
117
  end
104
118
 
105
- it 'sends the command to the call for execution' do
106
- call.expects(:execute_command!).once.with command
107
- subject.execute_call_command command
119
+ context "with a known call ID" do
120
+ before do
121
+ subject.register_call call
122
+ end
123
+
124
+ it 'sends the command to the call for execution' do
125
+ call.expects(:execute_command!).once.with command
126
+ subject.execute_call_command command
127
+ end
128
+ end
129
+
130
+ context "with an unknown call ID" do
131
+ it 'sends an error in response to the command' do
132
+ subject.execute_call_command command
133
+ command.response.should == ProtocolError.new('call-not-found', "Could not find a call with ID #{call_id}", call_id, nil)
134
+ end
108
135
  end
109
136
  end
110
137
 
@@ -112,15 +139,28 @@ module Punchblock
112
139
  let(:component_id) { '123abc' }
113
140
  let(:component) { mock 'Translator::Asterisk::Component', :id => component_id }
114
141
 
115
- let(:command) { mock 'Component::Stop', :component_id => component_id }
142
+ let(:command) { Component::Stop.new.tap { |c| c.component_id = component_id } }
116
143
 
117
144
  before do
118
- subject.register_component component
145
+ command.request!
119
146
  end
120
147
 
121
- it 'sends the command to the component for execution' do
122
- component.expects(:execute_command!).once.with command
123
- subject.execute_component_command command
148
+ context 'with a known component ID' do
149
+ before do
150
+ subject.register_component component
151
+ end
152
+
153
+ it 'sends the command to the component for execution' do
154
+ component.expects(:execute_command!).once.with command
155
+ subject.execute_component_command command
156
+ end
157
+ end
158
+
159
+ context "with an unknown component ID" do
160
+ it 'sends an error in response to the command' do
161
+ subject.execute_component_command command
162
+ command.response.should == ProtocolError.new('component-not-found', "Could not find a component with ID #{component_id}", nil, component_id)
163
+ end
124
164
  end
125
165
  end
126
166
 
@@ -130,9 +170,10 @@ module Punchblock
130
170
  Command::Dial.new :to => 'SIP/1234', :from => 'abc123'
131
171
  end
132
172
 
133
- before { command.request! }
134
-
135
- let(:mock_action) { stub_everything 'Asterisk::Component::Asterisk::AMIAction' }
173
+ before do
174
+ command.request!
175
+ ami_client.stub_everything
176
+ end
136
177
 
137
178
  it 'should be able to look up the call by channel ID' do
138
179
  subject.execute_global_command command
@@ -167,6 +208,17 @@ module Punchblock
167
208
  subject.execute_global_command command
168
209
  end
169
210
  end
211
+
212
+ context "with a command we don't understand" do
213
+ let :command do
214
+ Command::Answer.new
215
+ end
216
+
217
+ it 'sends an error in response to the command' do
218
+ subject.execute_command command
219
+ command.response.should == ProtocolError.new('command-not-acceptable', "Did not understand command")
220
+ end
221
+ end
170
222
  end
171
223
 
172
224
  describe '#handle_pb_event' do
@@ -0,0 +1,22 @@
1
+ # This is a nasty hack due to the fact that Mocha does not support expectations returning a value calculated by executing a block with the parameters passed.
2
+ # If it did, we could do this in our component tests:
3
+ # mc = mock 'Connection'
4
+ # mc.stubs(:handle_event).returns { |event| command.add_event event }
5
+ #
6
+ # Mocha does not support this feature because really, it's a smell in the tests
7
+ # We shouldn't really be mocking out behaviour like this if we actually need it to have side effects
8
+ # We only do this because of the difficulties in synchronising the tests with an asynchronous target in another thread.
9
+ #
10
+ def mock_connection_with_event_handler(&block)
11
+ mock('Connection').tap do |mc|
12
+ class << mc
13
+ attr_accessor :target
14
+ end
15
+
16
+ mc.target = block
17
+
18
+ def mc.handle_event(event)
19
+ target.call event
20
+ end
21
+ end
22
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: punchblock
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.2
4
+ version: 0.8.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -11,11 +11,11 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2012-01-10 00:00:00.000000000 Z
14
+ date: 2012-01-17 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: niceogiri
18
- requirement: &2168470140 !ruby/object:Gem::Requirement
18
+ requirement: &2155905140 !ruby/object:Gem::Requirement
19
19
  none: false
20
20
  requirements:
21
21
  - - ! '>='
@@ -23,10 +23,10 @@ dependencies:
23
23
  version: 0.0.4
24
24
  type: :runtime
25
25
  prerelease: false
26
- version_requirements: *2168470140
26
+ version_requirements: *2155905140
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: blather
29
- requirement: &2168468720 !ruby/object:Gem::Requirement
29
+ requirement: &2155903300 !ruby/object:Gem::Requirement
30
30
  none: false
31
31
  requirements:
32
32
  - - ! '>='
@@ -34,10 +34,10 @@ dependencies:
34
34
  version: 0.5.12
35
35
  type: :runtime
36
36
  prerelease: false
37
- version_requirements: *2168468720
37
+ version_requirements: *2155903300
38
38
  - !ruby/object:Gem::Dependency
39
39
  name: activesupport
40
- requirement: &2168467360 !ruby/object:Gem::Requirement
40
+ requirement: &2155901420 !ruby/object:Gem::Requirement
41
41
  none: false
42
42
  requirements:
43
43
  - - ! '>='
@@ -45,10 +45,10 @@ dependencies:
45
45
  version: 2.1.0
46
46
  type: :runtime
47
47
  prerelease: false
48
- version_requirements: *2168467360
48
+ version_requirements: *2155901420
49
49
  - !ruby/object:Gem::Dependency
50
50
  name: state_machine
51
- requirement: &2168466140 !ruby/object:Gem::Requirement
51
+ requirement: &2168536880 !ruby/object:Gem::Requirement
52
52
  none: false
53
53
  requirements:
54
54
  - - ! '>='
@@ -56,10 +56,10 @@ dependencies:
56
56
  version: 1.0.1
57
57
  type: :runtime
58
58
  prerelease: false
59
- version_requirements: *2168466140
59
+ version_requirements: *2168536880
60
60
  - !ruby/object:Gem::Dependency
61
61
  name: future-resource
62
- requirement: &2168464520 !ruby/object:Gem::Requirement
62
+ requirement: &2168535180 !ruby/object:Gem::Requirement
63
63
  none: false
64
64
  requirements:
65
65
  - - ! '>='
@@ -67,10 +67,10 @@ dependencies:
67
67
  version: 0.0.2
68
68
  type: :runtime
69
69
  prerelease: false
70
- version_requirements: *2168464520
70
+ version_requirements: *2168535180
71
71
  - !ruby/object:Gem::Dependency
72
72
  name: has-guarded-handlers
73
- requirement: &2168508400 !ruby/object:Gem::Requirement
73
+ requirement: &2168531860 !ruby/object:Gem::Requirement
74
74
  none: false
75
75
  requirements:
76
76
  - - ! '>='
@@ -78,10 +78,10 @@ dependencies:
78
78
  version: 0.1.0
79
79
  type: :runtime
80
80
  prerelease: false
81
- version_requirements: *2168508400
81
+ version_requirements: *2168531860
82
82
  - !ruby/object:Gem::Dependency
83
83
  name: celluloid
84
- requirement: &2168506560 !ruby/object:Gem::Requirement
84
+ requirement: &2168527860 !ruby/object:Gem::Requirement
85
85
  none: false
86
86
  requirements:
87
87
  - - ! '>='
@@ -89,10 +89,10 @@ dependencies:
89
89
  version: 0.6.0
90
90
  type: :runtime
91
91
  prerelease: false
92
- version_requirements: *2168506560
92
+ version_requirements: *2168527860
93
93
  - !ruby/object:Gem::Dependency
94
94
  name: ruby_ami
95
- requirement: &2168504680 !ruby/object:Gem::Requirement
95
+ requirement: &2168526320 !ruby/object:Gem::Requirement
96
96
  none: false
97
97
  requirements:
98
98
  - - ! '>='
@@ -100,10 +100,10 @@ dependencies:
100
100
  version: 0.1.3
101
101
  type: :runtime
102
102
  prerelease: false
103
- version_requirements: *2168504680
103
+ version_requirements: *2168526320
104
104
  - !ruby/object:Gem::Dependency
105
105
  name: ruby_speech
106
- requirement: &2168503400 !ruby/object:Gem::Requirement
106
+ requirement: &2168524860 !ruby/object:Gem::Requirement
107
107
  none: false
108
108
  requirements:
109
109
  - - ! '>='
@@ -111,10 +111,10 @@ dependencies:
111
111
  version: 0.5.1
112
112
  type: :runtime
113
113
  prerelease: false
114
- version_requirements: *2168503400
114
+ version_requirements: *2168524860
115
115
  - !ruby/object:Gem::Dependency
116
116
  name: bundler
117
- requirement: &2168500560 !ruby/object:Gem::Requirement
117
+ requirement: &2168523980 !ruby/object:Gem::Requirement
118
118
  none: false
119
119
  requirements:
120
120
  - - ~>
@@ -122,10 +122,10 @@ dependencies:
122
122
  version: 1.0.0
123
123
  type: :development
124
124
  prerelease: false
125
- version_requirements: *2168500560
125
+ version_requirements: *2168523980
126
126
  - !ruby/object:Gem::Dependency
127
127
  name: rspec
128
- requirement: &2168497720 !ruby/object:Gem::Requirement
128
+ requirement: &2168522200 !ruby/object:Gem::Requirement
129
129
  none: false
130
130
  requirements:
131
131
  - - ~>
@@ -133,10 +133,10 @@ dependencies:
133
133
  version: 2.7.0
134
134
  type: :development
135
135
  prerelease: false
136
- version_requirements: *2168497720
136
+ version_requirements: *2168522200
137
137
  - !ruby/object:Gem::Dependency
138
138
  name: ci_reporter
139
- requirement: &2152104200 !ruby/object:Gem::Requirement
139
+ requirement: &2168516560 !ruby/object:Gem::Requirement
140
140
  none: false
141
141
  requirements:
142
142
  - - ! '>='
@@ -144,10 +144,10 @@ dependencies:
144
144
  version: 1.6.3
145
145
  type: :development
146
146
  prerelease: false
147
- version_requirements: *2152104200
147
+ version_requirements: *2168516560
148
148
  - !ruby/object:Gem::Dependency
149
149
  name: yard
150
- requirement: &2152102500 !ruby/object:Gem::Requirement
150
+ requirement: &2168515600 !ruby/object:Gem::Requirement
151
151
  none: false
152
152
  requirements:
153
153
  - - ~>
@@ -155,10 +155,10 @@ dependencies:
155
155
  version: 0.6.0
156
156
  type: :development
157
157
  prerelease: false
158
- version_requirements: *2152102500
158
+ version_requirements: *2168515600
159
159
  - !ruby/object:Gem::Dependency
160
160
  name: rcov
161
- requirement: &2152101300 !ruby/object:Gem::Requirement
161
+ requirement: &2168475680 !ruby/object:Gem::Requirement
162
162
  none: false
163
163
  requirements:
164
164
  - - ! '>='
@@ -166,10 +166,10 @@ dependencies:
166
166
  version: '0'
167
167
  type: :development
168
168
  prerelease: false
169
- version_requirements: *2152101300
169
+ version_requirements: *2168475680
170
170
  - !ruby/object:Gem::Dependency
171
171
  name: rake
172
- requirement: &2152100160 !ruby/object:Gem::Requirement
172
+ requirement: &2168496000 !ruby/object:Gem::Requirement
173
173
  none: false
174
174
  requirements:
175
175
  - - ! '>='
@@ -177,10 +177,10 @@ dependencies:
177
177
  version: '0'
178
178
  type: :development
179
179
  prerelease: false
180
- version_requirements: *2152100160
180
+ version_requirements: *2168496000
181
181
  - !ruby/object:Gem::Dependency
182
182
  name: mocha
183
- requirement: &2152097280 !ruby/object:Gem::Requirement
183
+ requirement: &2152431080 !ruby/object:Gem::Requirement
184
184
  none: false
185
185
  requirements:
186
186
  - - ! '>='
@@ -188,10 +188,10 @@ dependencies:
188
188
  version: '0'
189
189
  type: :development
190
190
  prerelease: false
191
- version_requirements: *2152097280
191
+ version_requirements: *2152431080
192
192
  - !ruby/object:Gem::Dependency
193
193
  name: i18n
194
- requirement: &2152084420 !ruby/object:Gem::Requirement
194
+ requirement: &2152427440 !ruby/object:Gem::Requirement
195
195
  none: false
196
196
  requirements:
197
197
  - - ! '>='
@@ -199,10 +199,10 @@ dependencies:
199
199
  version: '0'
200
200
  type: :development
201
201
  prerelease: false
202
- version_requirements: *2152084420
202
+ version_requirements: *2152427440
203
203
  - !ruby/object:Gem::Dependency
204
204
  name: countdownlatch
205
- requirement: &2152079640 !ruby/object:Gem::Requirement
205
+ requirement: &2152370940 !ruby/object:Gem::Requirement
206
206
  none: false
207
207
  requirements:
208
208
  - - ! '>='
@@ -210,10 +210,10 @@ dependencies:
210
210
  version: '0'
211
211
  type: :development
212
212
  prerelease: false
213
- version_requirements: *2152079640
213
+ version_requirements: *2152370940
214
214
  - !ruby/object:Gem::Dependency
215
215
  name: guard-rspec
216
- requirement: &2152075260 !ruby/object:Gem::Requirement
216
+ requirement: &2152361220 !ruby/object:Gem::Requirement
217
217
  none: false
218
218
  requirements:
219
219
  - - ! '>='
@@ -221,7 +221,7 @@ dependencies:
221
221
  version: '0'
222
222
  type: :development
223
223
  prerelease: false
224
- version_requirements: *2152075260
224
+ version_requirements: *2152361220
225
225
  description: Like Rack is to Rails and Sinatra, Punchblock provides a consistent API
226
226
  on top of several underlying third-party call control protocols.
227
227
  email: punchblock@adhearsion.com
@@ -348,6 +348,7 @@ files:
348
348
  - spec/punchblock/translator/asterisk/component_spec.rb
349
349
  - spec/punchblock/translator/asterisk_spec.rb
350
350
  - spec/spec_helper.rb
351
+ - spec/support/mock_connection_with_event_handler.rb
351
352
  homepage: http://github.com/adhearsion/punchblock
352
353
  licenses:
353
354
  - MIT
@@ -363,7 +364,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
363
364
  version: '0'
364
365
  segments:
365
366
  - 0
366
- hash: -2044158183945900193
367
+ hash: 243839831400829248
367
368
  required_rubygems_version: !ruby/object:Gem::Requirement
368
369
  none: false
369
370
  requirements:
@@ -419,3 +420,4 @@ test_files:
419
420
  - spec/punchblock/translator/asterisk/component_spec.rb
420
421
  - spec/punchblock/translator/asterisk_spec.rb
421
422
  - spec/spec_helper.rb
423
+ - spec/support/mock_connection_with_event_handler.rb