adhearsion 2.0.0.alpha1 → 2.0.0.alpha2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. data/.gitignore +2 -0
  2. data/.travis.yml +12 -0
  3. data/CHANGELOG.md +17 -0
  4. data/adhearsion.gemspec +4 -3
  5. data/features/app_generator.feature +3 -1
  6. data/features/cli.feature +7 -7
  7. data/features/support/env.rb +46 -0
  8. data/lib/adhearsion.rb +1 -2
  9. data/lib/adhearsion/call.rb +59 -19
  10. data/lib/adhearsion/call_controller.rb +20 -24
  11. data/lib/adhearsion/call_controller/dial.rb +18 -18
  12. data/lib/adhearsion/cli_commands.rb +26 -9
  13. data/lib/adhearsion/configuration.rb +39 -10
  14. data/lib/adhearsion/console.rb +61 -42
  15. data/lib/adhearsion/foundation/libc.rb +13 -0
  16. data/lib/adhearsion/generators/app/app_generator.rb +4 -1
  17. data/lib/adhearsion/generators/app/templates/{Gemfile → Gemfile.erb} +1 -1
  18. data/lib/adhearsion/generators/app/templates/Rakefile +3 -22
  19. data/lib/adhearsion/generators/app/templates/gitignore +7 -0
  20. data/lib/adhearsion/generators/app/templates/lib/simon_game.rb +1 -0
  21. data/lib/adhearsion/generators/app/templates/script/ahn +1 -0
  22. data/lib/adhearsion/initializer.rb +24 -12
  23. data/lib/adhearsion/linux_proc_name.rb +41 -0
  24. data/lib/adhearsion/outbound_call.rb +10 -5
  25. data/lib/adhearsion/plugin.rb +29 -132
  26. data/lib/adhearsion/process.rb +4 -1
  27. data/lib/adhearsion/punchblock_plugin.rb +14 -5
  28. data/lib/adhearsion/punchblock_plugin/initializer.rb +8 -1
  29. data/lib/adhearsion/router/route.rb +1 -3
  30. data/lib/adhearsion/tasks.rb +6 -12
  31. data/lib/adhearsion/tasks/configuration.rb +7 -24
  32. data/lib/adhearsion/tasks/environment.rb +12 -0
  33. data/lib/adhearsion/tasks/plugins.rb +9 -14
  34. data/lib/adhearsion/version.rb +1 -1
  35. data/spec/adhearsion/call_controller/dial_spec.rb +46 -22
  36. data/spec/adhearsion/call_controller_spec.rb +48 -13
  37. data/spec/adhearsion/call_spec.rb +144 -23
  38. data/spec/adhearsion/calls_spec.rb +8 -4
  39. data/spec/adhearsion/console_spec.rb +24 -0
  40. data/spec/adhearsion/initializer/logging_spec.rb +0 -3
  41. data/spec/adhearsion/initializer_spec.rb +52 -37
  42. data/spec/adhearsion/logging_spec.rb +0 -3
  43. data/spec/adhearsion/outbound_call_spec.rb +12 -2
  44. data/spec/adhearsion/plugin_spec.rb +74 -184
  45. data/spec/adhearsion/process_spec.rb +59 -26
  46. data/spec/adhearsion/punchblock_plugin/initializer_spec.rb +3 -4
  47. data/spec/adhearsion/punchblock_plugin_spec.rb +11 -0
  48. data/spec/adhearsion/router/route_spec.rb +37 -6
  49. data/spec/adhearsion_spec.rb +31 -8
  50. data/spec/spec_helper.rb +14 -0
  51. data/spec/support/call_controller_test_helpers.rb +2 -2
  52. data/spec/support/logging_helpers.rb +2 -0
  53. metadata +85 -68
  54. data/lib/adhearsion/dialplan_controller.rb +0 -9
  55. data/lib/adhearsion/foundation/synchronized_hash.rb +0 -93
  56. data/lib/adhearsion/plugin/methods_container.rb +0 -6
  57. data/spec/adhearsion/dialplan_controller_spec.rb +0 -26
@@ -1,26 +1,9 @@
1
- require 'adhearsion/punchblock_plugin'
2
-
3
- begin
4
- Adhearsion.config # load default config vlaues
5
- require "#{Dir.pwd}/config/adhearsion.rb"
6
- rescue Exception => ex
7
- STDERR.puts "\nError while loading application configuration file: #{ex}"
8
- end
9
-
10
- namespace :adhearsion do
11
-
12
- namespace :config do
13
-
14
- desc "Show configuration values in STDOUT; it accepts a parameter: [nil|platform|<plugin-name>|all]"
15
- task :show, :name do |t, args|
16
- name = args.name.nil? ? :all : args.name.to_sym
17
- puts Adhearsion.config.description name, :show_values => true
18
- end
19
-
20
- desc "Show configuration description in STDOUT; it accepts a parameter: [nil|platform|<plugin-name>|all]"
21
- task :desc, :name do |t, args|
22
- name = args.name.nil? ? :all : args.name.to_sym
23
- puts Adhearsion.config.description name, :show_values => false
24
- end
1
+ namespace :config do
2
+ desc "Show configuration values; accepts a parameter: [nil|platform|<plugin-name>|all]"
3
+ task :show, :name, :needs => :environment do |t, args|
4
+ name = args.name.nil? ? :all : args.name.to_sym
5
+ puts "\nAdhearsion.config do |config|\n\n"
6
+ puts Adhearsion.config.description name, :show_values => true
7
+ puts "end"
25
8
  end
26
9
  end
@@ -0,0 +1,12 @@
1
+ task :environment do
2
+ require 'adhearsion/punchblock_plugin'
3
+
4
+ begin
5
+ Adhearsion.config # load default config vlaues
6
+ initializer = Adhearsion::Initializer.new
7
+ initializer.load_lib_folder
8
+ initializer.load_config
9
+ rescue Exception => ex
10
+ STDERR.puts "\nError while loading application configuration file: #{ex}"
11
+ end
12
+ end
@@ -1,17 +1,12 @@
1
- require 'adhearsion/punchblock_plugin'
2
-
3
- namespace :adhearsion do
4
-
5
- desc "List the configured plugins"
6
- task :plugins do |t,args|
7
- if Adhearsion::Plugin.subclasses.length > 0
8
- puts "You have #{Adhearsion::Plugin.subclasses.length} plugin(s) in your Adhearsion application:\n"
9
- Adhearsion::Plugin.subclasses.each do |plugin|
10
- puts "* #{plugin.plugin_name}: #{plugin.name}"
11
- end
12
- else
13
- puts "There is no Adhearsion plugin used in your application"
1
+ desc "List the configured plugins"
2
+ task :plugins => :environment do |t,args|
3
+ if Adhearsion::Plugin.subclasses.length > 0
4
+ puts "You have #{Adhearsion::Plugin.subclasses.length} plugin(s) in your Adhearsion application:\n"
5
+ Adhearsion::Plugin.subclasses.each do |plugin|
6
+ puts "* #{plugin.plugin_name}: #{plugin.name}"
14
7
  end
15
- puts "\n"
8
+ else
9
+ puts "There is no Adhearsion plugin used in your application"
16
10
  end
11
+ puts "\n"
17
12
  end
@@ -1,5 +1,5 @@
1
1
  module Adhearsion #:nodoc:
2
- VERSION = '2.0.0.alpha1'
2
+ VERSION = '2.0.0.alpha2'
3
3
 
4
4
  class PkgVersion
5
5
  include Comparable
@@ -6,17 +6,17 @@ module Adhearsion
6
6
  include CallControllerTestHelpers
7
7
 
8
8
  let(:to) { 'sip:foo@bar.com' }
9
-
10
- let(:other_call_id) { rand }
9
+ let(:other_call_id) { rand }
11
10
  let(:other_mock_call) { flexmock OutboundCall.new, :id => other_call_id }
12
11
 
13
- let(:mock_end) { flexmock Punchblock::Event::End.new, :reason => :hangup }
12
+ let(:second_to) { 'sip:baz@bar.com' }
13
+ let(:second_other_call_id) { rand }
14
+ let(:second_other_mock_call) { flexmock OutboundCall.new, :id => second_other_call_id }
15
+
16
+ let(:mock_end) { flexmock Punchblock::Event::End.new, :reason => :hangup }
14
17
  let(:mock_answered) { Punchblock::Event::Answered.new }
15
18
 
16
- #added for multiple dial testing
17
- let(:second_to) { 'sip:baz@bar.com' }
18
- let(:second_other_call_id) { rand }
19
- let(:second_other_mock_call) { flexmock OutboundCall.new, :id => second_other_call_id }
19
+ let(:latch) { CountDownLatch.new 1 }
20
20
 
21
21
  def mock_dial
22
22
  flexmock(OutboundCall).new_instances.should_receive(:dial).and_return true
@@ -25,10 +25,11 @@ module Adhearsion
25
25
  describe "#dial" do
26
26
  it "should create a new call and return it" do
27
27
  mock_dial
28
- Thread.new do
29
- subject.dial(to).should be_a OutboundCall
28
+ t = Thread.new do
29
+ subject.dial(to, {}, latch).should be_a OutboundCall
30
30
  end
31
- other_mock_call << mock_end
31
+ latch.countdown!
32
+ t.join
32
33
  end
33
34
 
34
35
  it "should dial the call to the correct endpoint" do
@@ -44,7 +45,7 @@ module Adhearsion
44
45
  end
45
46
 
46
47
  describe "without a block" do
47
- it "blocks the original dialplan until the new call ends" do
48
+ it "blocks the original controller until the new call ends" do
48
49
  other_mock_call
49
50
 
50
51
  flexmock(other_mock_call).should_receive(:dial).once
@@ -68,7 +69,7 @@ module Adhearsion
68
69
  other_mock_call
69
70
 
70
71
  flexmock(other_mock_call).should_receive(:dial).once
71
- flexmock(other_mock_call).should_receive(:join).once.with(call_id)
72
+ flexmock(other_mock_call).should_receive(:join).once.with(call)
72
73
  flexmock(OutboundCall).should_receive(:new).and_return other_mock_call
73
74
 
74
75
  latch = CountDownLatch.new 1
@@ -93,19 +94,19 @@ module Adhearsion
93
94
  second_other_mock_call
94
95
 
95
96
  flexmock(other_mock_call).should_receive(:dial).once
96
- flexmock(other_mock_call).should_receive(:join).once.with(call_id)
97
+ flexmock(other_mock_call).should_receive(:join).once.with(call)
97
98
  flexmock(other_mock_call).should_receive(:hangup!).never
98
99
 
99
100
  flexmock(second_other_mock_call).should_receive(:dial).once
100
101
  flexmock(second_other_mock_call).should_receive(:hangup!).once
101
102
 
102
-
103
103
  flexmock(OutboundCall).should_receive(:new).and_return other_mock_call, second_other_mock_call
104
104
  latch = CountDownLatch.new 1
105
105
 
106
- Thread.new do
107
- subject.dial [to, second_to]
106
+ t = Thread.new do
107
+ calls = subject.dial [to, second_to]
108
108
  latch.countdown!
109
+ calls
109
110
  end
110
111
 
111
112
  latch.wait(1).should be_false
@@ -113,20 +114,43 @@ module Adhearsion
113
114
  other_mock_call << mock_answered
114
115
  other_mock_call << mock_end
115
116
 
116
- latch.wait(1).should be_true
117
+ latch.wait(2).should be_true
118
+
119
+ t.join
120
+ calls = t.value
121
+ calls.should have(2).calls
122
+ calls.each { |c| c.should be_a OutboundCall }
117
123
  end
118
124
  end
119
125
 
120
126
  describe "with a timeout specified" do
121
- it "should abort the dial after the specified timeout"
122
- end
127
+ let(:timeout) { 3 }
123
128
 
124
- describe "with a from specified" do
125
- it "originates the call from the specified caller ID"
129
+ it "should abort the dial after the specified timeout" do
130
+ other_mock_call
131
+
132
+ flexmock(other_mock_call).should_receive(:dial).once
133
+ flexmock(OutboundCall).should_receive(:new).and_return other_mock_call
134
+
135
+ latch = CountDownLatch.new 1
136
+
137
+ value = nil
138
+ time = Time.now
139
+
140
+ Thread.new do
141
+ value = subject.dial to, :timeout => timeout
142
+ latch.countdown!
143
+ end
144
+
145
+ latch.wait
146
+ time = Time.now - time
147
+ time.to_i.should == timeout
148
+ value.should == false
149
+ end
126
150
  end
127
151
 
128
152
  describe "with a block" do
129
- it "uses the block as the dialplan for the new call"
153
+ it "uses the block as the controller for the new call"
130
154
 
131
155
  it "joins the new call to the existing call once the block returns"
132
156
 
@@ -1,5 +1,8 @@
1
1
  require 'spec_helper'
2
2
 
3
+ class FinancialWizard < Adhearsion::CallController
4
+ end
5
+
3
6
  module Adhearsion
4
7
  describe CallController do
5
8
  include CallControllerTestHelpers
@@ -20,10 +23,6 @@ module Adhearsion
20
23
  end
21
24
  end
22
25
 
23
- it "should add plugin dialplan methods" do
24
- subject.should respond_to :foo
25
- end
26
-
27
26
  its(:logger) { should be call.logger }
28
27
  its(:variables) { should be call.variables }
29
28
 
@@ -78,6 +77,20 @@ module Adhearsion
78
77
  flexmock(Events).should_receive(:trigger).once.with(:exception, StandardError).ordered
79
78
  subject.execute!
80
79
  end
80
+
81
+ context "when a block is specified" do
82
+ let :block do
83
+ Proc.new { foo value }
84
+ end
85
+
86
+ its(:block) { should be block }
87
+
88
+ it "should execute the block in the context of the controller" do
89
+ flexmock subject, :value => :bar
90
+ subject.should_receive(:foo).once.with(:bar)
91
+ subject.run
92
+ end
93
+ end
81
94
  end
82
95
 
83
96
  class SecondController < CallController
@@ -285,45 +298,52 @@ module Adhearsion
285
298
 
286
299
  describe '#accept' do
287
300
  it "should delegate to the call" do
288
- flexmock(subject.call).should_receive(:accept).once.with(:foo)
301
+ flexmock(call).should_receive(:accept).once.with(:foo)
289
302
  subject.accept :foo
290
303
  end
291
304
  end
292
305
 
293
306
  describe '#answer' do
294
307
  it "should delegate to the call" do
295
- flexmock(subject.call).should_receive(:answer).once.with(:foo)
308
+ flexmock(call).should_receive(:answer).once.with(:foo)
296
309
  subject.answer :foo
297
310
  end
298
311
  end
299
312
 
300
313
  describe '#reject' do
301
314
  it "should delegate to the call" do
302
- flexmock(subject.call).should_receive(:reject).once.with(:foo, :bar)
315
+ flexmock(call).should_receive(:reject).once.with(:foo, :bar)
303
316
  subject.reject :foo, :bar
304
317
  end
305
318
  end
306
319
 
307
320
  describe '#hangup' do
308
321
  it "should delegate to the call" do
309
- flexmock(subject.call).should_receive(:hangup!).once.with(:foo)
322
+ flexmock(call).should_receive(:hangup!).once.with(:foo)
310
323
  subject.hangup :foo
311
324
  end
312
325
  end
313
326
 
314
327
  describe '#mute' do
315
- it 'should send a Mute message' do
316
- expect_message_waiting_for_response Punchblock::Command::Mute.new
328
+ it 'should delegate to the call' do
329
+ flexmock(call).should_receive(:mute).once
317
330
  subject.mute
318
331
  end
319
332
  end
320
333
 
321
334
  describe '#unmute' do
322
- it 'should send an Unmute message' do
323
- expect_message_waiting_for_response Punchblock::Command::Unmute.new
335
+ it 'should delegate to the call' do
336
+ flexmock(call).should_receive(:unmute).once
324
337
  subject.unmute
325
338
  end
326
339
  end
340
+
341
+ describe '#join' do
342
+ it 'should delegate to the call' do
343
+ flexmock(call).should_receive(:join).once.with(:foo)
344
+ subject.join :foo
345
+ end
346
+ end
327
347
  end
328
348
  end
329
349
 
@@ -383,7 +403,7 @@ describe ExampleCallController do
383
403
  subject.execute!
384
404
  end
385
405
 
386
- context "when the controller finishes without a hangup" do
406
+ describe "when the controller finishes without a hangup" do
387
407
  it "should execute the after_call callbacks" do
388
408
  subject[:skip_hangup] = true
389
409
  subject.should_receive(:join_to_conference).once.ordered
@@ -392,4 +412,19 @@ describe ExampleCallController do
392
412
  subject.execute!
393
413
  end
394
414
  end
415
+
416
+ describe "providing hooks to include call functionality" do
417
+ let(:call) { Adhearsion::Call.new mock_offer(nil, :x_foo => 'bar') }
418
+
419
+ it "should allow mixing in a module globally on all CallController classes" do
420
+ Adhearsion::CallController.mixin TestBiscuit
421
+ Adhearsion::CallController.new(call).should respond_to :throwadogabone
422
+ end
423
+
424
+ it "should allow mixing in a module on a single CallController class" do
425
+ FinancialWizard.mixin MarmaladeIsBetterThanJam
426
+ FinancialWizard.new(call).should respond_to :sobittersweet
427
+ Adhearsion::CallController.new(call).should_not respond_to :sobittersweet
428
+ end
429
+ end
395
430
  end
@@ -2,10 +2,25 @@ require 'spec_helper'
2
2
 
3
3
  module Adhearsion
4
4
  describe Call do
5
+ let(:mock_client) { flexmock('Client').tap &:should_ignore_missing }
6
+
7
+ let(:call_id) { rand }
5
8
  let(:headers) { nil }
6
- let(:offer) { mock_offer nil, headers }
9
+ let(:to) { 'sip:you@there.com' }
10
+ let(:from) { 'sip:me@here.com' }
11
+ let :offer do
12
+ Punchblock::Event::Offer.new :call_id => call_id,
13
+ :to => to,
14
+ :from => from,
15
+ :headers => headers
16
+ end
17
+
7
18
  subject { Adhearsion::Call.new offer }
8
19
 
20
+ before do
21
+ flexmock(offer).should_receive(:client).and_return(mock_client)
22
+ end
23
+
9
24
  after do
10
25
  Adhearsion.active_calls.clear!
11
26
  end
@@ -18,10 +33,43 @@ module Adhearsion
18
33
  its(:commands) { should be_a Call::CommandRegistry }
19
34
  its(:commands) { should be_empty }
20
35
 
36
+ its(:id) { should == call_id }
37
+ its(:to) { should == to }
38
+ its(:from) { should == from }
39
+ its(:offer) { should be offer }
40
+ its(:client) { should be mock_client }
41
+
21
42
  describe "its variables" do
22
43
  context "with an offer with headers" do
23
44
  let(:headers) { {:x_foo => 'bar'} }
24
45
  its(:variables) { should == headers }
46
+
47
+ it "should be made available via []" do
48
+ subject[:x_foo].should == 'bar'
49
+ end
50
+
51
+ it "should be alterable using []=" do
52
+ subject[:x_foo] = 'baz'
53
+ subject[:x_foo].should == 'baz'
54
+ end
55
+
56
+ context "when receiving an event with headers" do
57
+ let(:event) { Punchblock::Event::End.new :headers => {:x_bar => 'foo'} }
58
+
59
+ it "should merge later headers" do
60
+ subject << event
61
+ subject.variables.should == {:x_foo => 'bar', :x_bar => 'foo'}
62
+ end
63
+ end
64
+
65
+ context "when sending a command with headers" do
66
+ let(:command) { Punchblock::Command::Accept.new :headers => {:x_bar => 'foo'} }
67
+
68
+ it "should merge later headers" do
69
+ subject.write_command command
70
+ subject.variables.should == {:x_foo => 'bar', :x_bar => 'foo'}
71
+ end
72
+ end
25
73
  end
26
74
 
27
75
  context "with an offer without headers" do
@@ -35,33 +83,16 @@ module Adhearsion
35
83
  end
36
84
  end
37
85
 
38
- it '#id should return the ID from the Offer' do
39
- offer = mock_offer
40
- Adhearsion::Call.new(offer).id.should == offer.call_id
41
- end
42
-
43
- it 'should store the original offer' do
44
- offer = mock_offer
45
- Adhearsion::Call.new(offer).offer.should == offer
46
- end
47
-
48
86
  describe 'without an offer' do
49
87
  it 'should not raise an exception' do
50
88
  lambda { Adhearsion::Call.new }.should_not raise_error
51
89
  end
52
90
  end
53
91
 
54
- it "should store the Punchblock client from the Offer" do
55
- offer = mock_offer
56
- client = flexmock('Client')
57
- offer.should_receive(:client).once.and_return(client)
58
- Adhearsion::Call.new(offer).client.should == client
59
- end
60
-
61
92
  it 'a hungup call removes itself from the active calls' do
62
93
  size_before = Adhearsion.active_calls.size
63
94
 
64
- call = Adhearsion.active_calls.from_offer mock_offer
95
+ call = Adhearsion.active_calls.from_offer offer
65
96
  Adhearsion.active_calls.size.should > size_before
66
97
  call.hangup
67
98
  Adhearsion.active_calls.size.should == size_before
@@ -350,11 +381,101 @@ module Adhearsion
350
381
  end
351
382
 
352
383
  describe "#join" do
353
- let(:other_call_id) { rand }
384
+ def expect_join_with_options(options = {})
385
+ Punchblock::Command::Join.new(options).tap do |join|
386
+ expect_message_waiting_for_response join
387
+ end
388
+ end
389
+
390
+ context "with a call" do
391
+ let(:call_id) { rand.to_s }
392
+ let(:target) { flexmock Call.new, :id => call_id }
393
+
394
+ it "should send a join command joining to the provided call ID" do
395
+ expect_join_with_options :other_call_id => call_id
396
+ subject.join target
397
+ end
398
+
399
+ context "and direction/media options" do
400
+ it "should send a join command with the correct options" do
401
+ expect_join_with_options :other_call_id => call_id, :media => :bridge, :direction => :recv
402
+ subject.join target, :media => :bridge, :direction => :recv
403
+ end
404
+ end
405
+ end
406
+
407
+ context "with a call ID" do
408
+ let(:target) { rand.to_s }
409
+
410
+ it "should send a join command joining to the provided call ID" do
411
+ expect_join_with_options :other_call_id => target
412
+ subject.join target
413
+ end
414
+
415
+ context "and direction/media options" do
416
+ it "should send a join command with the correct options" do
417
+ expect_join_with_options :other_call_id => target, :media => :bridge, :direction => :recv
418
+ subject.join target, :media => :bridge, :direction => :recv
419
+ end
420
+ end
421
+ end
422
+
423
+ context "with a call ID as a hash key" do
424
+ let(:call_id) { rand.to_s }
425
+ let(:target) { { :call_id => call_id } }
426
+
427
+ it "should send a join command joining to the provided call ID" do
428
+ expect_join_with_options :other_call_id => call_id
429
+ subject.join target
430
+ end
431
+
432
+ context "and direction/media options" do
433
+ it "should send a join command with the correct options" do
434
+ expect_join_with_options :other_call_id => call_id, :media => :bridge, :direction => :recv
435
+ subject.join target.merge({:media => :bridge, :direction => :recv})
436
+ end
437
+ end
438
+ end
439
+
440
+ context "with a mixer name as a hash key" do
441
+ let(:mixer_name) { rand.to_s }
442
+ let(:target) { { :mixer_name => mixer_name } }
443
+
444
+ it "should send a join command joining to the provided call ID" do
445
+ expect_join_with_options :mixer_name => mixer_name
446
+ subject.join target
447
+ end
448
+
449
+ context "and direction/media options" do
450
+ it "should send a join command with the correct options" do
451
+ expect_join_with_options :mixer_name => mixer_name, :media => :bridge, :direction => :recv
452
+ subject.join target.merge({:media => :bridge, :direction => :recv})
453
+ end
454
+ end
455
+ end
456
+
457
+ context "with a call ID and a mixer name as hash keys" do
458
+ let(:call_id) { rand.to_s }
459
+ let(:mixer_name) { rand.to_s }
460
+ let(:target) { { :call_id => call_id, :mixer_name => mixer_name } }
461
+
462
+ it "should raise an ArgumentError" do
463
+ lambda { subject.join target }.should raise_error ArgumentError, /call ID and mixer name/
464
+ end
465
+ end
466
+ end
467
+
468
+ describe "#mute" do
469
+ it 'should send a Mute message' do
470
+ expect_message_waiting_for_response Punchblock::Command::Mute.new
471
+ subject.mute
472
+ end
473
+ end
354
474
 
355
- it "should mark the call inactive" do
356
- expect_message_waiting_for_response Punchblock::Command::Join.new :other_call_id => other_call_id
357
- subject.join other_call_id
475
+ describe "#unmute" do
476
+ it 'should send a Mute message' do
477
+ expect_message_waiting_for_response Punchblock::Command::Unmute.new
478
+ subject.unmute
358
479
  end
359
480
  end
360
481