punchblock 0.8.2 → 0.8.3

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