# encoding: utf-8 require 'spec_helper' class FinancialWizard < Adhearsion::CallController end module Adhearsion describe CallController do include CallControllerTestHelpers let(:call) { Adhearsion::Call.new mock_offer(nil, :x_foo => 'bar') } its(:call) { should be call } its(:metadata) { should be == {:doo => :dah} } describe "setting meta-data" do it "should preserve data correctly" do subject[:foo].should be nil subject[:foo] = 7 subject[:foo].should be == 7 subject[:bar] = 10 subject[:bar].should be == 10 subject[:foo].should be == 7 end end its(:logger) { should be call.logger } its(:variables) { should be call.variables } describe "execution on a call" do before do flexmock subject, :execute_component_and_await_completion => nil flexmock call.wrapped_object, :write_and_await_response => nil end it "catches Hangup exceptions and logs the hangup" do subject.should_receive(:run).once.and_raise(Call::Hangup).ordered flexmock(subject.logger).should_receive(:info).once.with(/Call was hung up/).ordered subject.execute! end it "catches standard errors, triggering an exception event" do subject.should_receive(:run).once.and_raise(StandardError).ordered latch = CountDownLatch.new 1 ex = lo = nil Events.exception do |e, l| ex, lo = e, l latch.countdown! end subject.execute! latch.wait(1).should be true ex.should be_a StandardError lo.should be subject.logger end context "when a block is specified" do let :block do Proc.new { foo value } end its(:block) { should be block } it "should execute the block in the context of the controller" do flexmock subject, :value => :bar subject.should_receive(:foo).once.with(:bar) subject.run end end end class SecondController < CallController def run md_check metadata answer end def md_check(md) end end class SecondControllerWithRemoteHangup < SecondController def run super simulate_remote_hangup end def simulate_remote_hangup raise Call::Hangup end end describe "#invoke" do class InvokeController < CallController def run before invoke second_controller, :foo => 'bar' after end def before end def after end def second_controller metadata[:second_controller] || SecondController end end subject { InvokeController.new call } before do flexmock subject, :execute_component_and_await_completion => nil flexmock call.wrapped_object, :write_and_await_response => nil flexmock call, :register_controller! => nil flexmock(Events).should_receive(:trigger).with(:exception, Exception).never end it "should invoke another controller before returning to the current controller" do subject.should_receive(:before).once.ordered call.should_receive(:answer).once.ordered subject.should_receive(:after).once.ordered subject.execute! end it "should invoke the new controller with metadata" do flexmock(SecondController).new_instances.should_receive(:md_check).once.with :foo => 'bar' subject.execute! end it "should allow the outer controller to cease execution and handle remote hangups" do subject[:second_controller] = SecondControllerWithRemoteHangup subject.should_receive(:before).once.ordered call.should_receive(:answer).once.ordered subject.should_receive(:after).never.ordered subject.execute! end end describe "#pass" do class PassController < CallController after_call :foobar def run before pass SecondController, :foo => 'bar' after end def before end def after end def foobar end end subject { PassController.new call } before do flexmock call.wrapped_object, :write_and_await_response => nil flexmock call, :register_controller! => nil flexmock subject, :execute_component_and_await_completion => nil flexmock(SecondController).new_instances.should_receive(:md_check).once.with :foo => 'bar' flexmock(Events).should_receive(:trigger).with(:exception, Exception).never end it "should cease execution of the current controller, and instruct the call to execute another" do subject.should_receive(:before).once.ordered call.should_receive(:answer).once.ordered subject.should_receive(:after).never.ordered CallController.exec subject end it "should execute after_call callbacks before passing control" do subject.should_receive(:before).once.ordered subject.should_receive(:foobar).once.ordered call.should_receive(:answer).once.ordered CallController.exec subject end end describe "#write_and_await_response" do let(:message) { Punchblock::Command::Accept.new } it "delegates to the call, blocking first until it is allowed to execute" do flexmock(subject).should_receive(:block_until_resumed).once.ordered flexmock(subject.call).should_receive(:write_and_await_response).once.ordered.with(message) subject.write_and_await_response message end end [ :answer, :reject, :hangup, :mute, :unmute].each do |method_name| describe "##{method_name}" do it "delegates to the call, blocking first until it is allowed to execute" do flexmock(subject).should_receive(:block_until_resumed).once.ordered flexmock(subject.call).should_receive(method_name).once.ordered subject.send method_name end end end describe "#join" do it "delegates to the call, blocking first until it is allowed to execute, and unblocking when an unjoined event is received" do flexmock(subject).should_receive(:block_until_resumed).once.ordered flexmock(subject.call).should_receive(:join).once.with('call1', :foo => :bar).ordered.and_return Punchblock::Command::Join.new(:other_call_id => 'call1') latch = CountDownLatch.new 1 Thread.new do subject.join 'call1', :foo => :bar latch.countdown! end latch.wait(1).should be false subject.call << Punchblock::Event::Joined.new(:other_call_id => 'call1') latch.wait(1).should be false subject.call << Punchblock::Event::Unjoined.new(:other_call_id => 'call1') latch.wait(1).should be true end context "with a mixer" do it "delegates to the call, blocking first until it is allowed to execute, and unblocking when an unjoined event is received" do flexmock(subject).should_receive(:block_until_resumed).once.ordered flexmock(subject.call).should_receive(:join).once.with({:mixer_name => 'foobar', :foo => :bar}, {}).ordered.and_return Punchblock::Command::Join.new(:mixer_name => 'foobar') latch = CountDownLatch.new 1 Thread.new do subject.join :mixer_name => 'foobar', :foo => :bar latch.countdown! end latch.wait(1).should be false subject.call << Punchblock::Event::Joined.new(:mixer_name => 'foobar') latch.wait(1).should be false subject.call << Punchblock::Event::Unjoined.new(:mixer_name => 'foobar') latch.wait(1).should be true end end context "with :async => true" do it "delegates to the call, blocking first until it is allowed to execute, and unblocking when the joined event is received" do flexmock(subject).should_receive(:block_until_resumed).once.ordered flexmock(subject.call).should_receive(:join).once.with('call1', :foo => :bar).ordered.and_return Punchblock::Command::Join.new(:other_call_id => 'call1') latch = CountDownLatch.new 1 Thread.new do subject.join 'call1', :foo => :bar, :async => true latch.countdown! end latch.wait(1).should be false subject.call << Punchblock::Event::Joined.new(:other_call_id => 'call1') latch.wait(1).should be true end context "with a mixer" do it "delegates to the call, blocking first until it is allowed to execute, and unblocking when the joined event is received" do flexmock(subject).should_receive(:block_until_resumed).once.ordered flexmock(subject.call).should_receive(:join).once.with({:mixer_name => 'foobar', :foo => :bar}, {}).ordered.and_return Punchblock::Command::Join.new(:mixer_name => 'foobar') latch = CountDownLatch.new 1 Thread.new do subject.join :mixer_name => 'foobar', :foo => :bar, :async => true latch.countdown! end latch.wait(1).should be false subject.call << Punchblock::Event::Joined.new(:mixer_name => 'foobar') latch.wait(1).should be true end end end end describe "#block_until_resumed" do context "when the controller has not been paused" do it "should not block" do t1 = Time.now subject.block_until_resumed t2 = Time.now (t2 - t1).should < 0.2 end end context "when the controller is paused" do before { subject.pause! } it "should unblock when the controller is unpaused" do t1 = t2 = nil latch = CountDownLatch.new 1 Thread.new do t1 = Time.now subject.block_until_resumed t2 = Time.now latch.countdown! end sleep 0.5 subject.resume! latch.wait(1).should be_true (t2 - t1).should >= 0.5 end end end describe "#execute_component_and_await_completion" do let(:component) { Punchblock::Component::Output.new } let(:response) { Punchblock::Event::Complete.new } before do expect_message_waiting_for_response component component.execute! component.complete_event = response end it "writes component to the server and waits on response" do subject.execute_component_and_await_completion component end it "takes a block which is executed after acknowledgement but before waiting on completion" do @comp = nil subject.execute_component_and_await_completion(component) { |comp| @comp = comp }.should be == component @comp.should be == component end describe "with a successful completion" do it "returns the executed component" do subject.execute_component_and_await_completion(component).should be component end end describe "with an error response" do let(:response) do Punchblock::Event::Complete.new.tap do |complete| complete << error end end let(:error) do |error| Punchblock::Event::Complete::Error.new.tap do |e| e << details end end let(:details) { "Oh noes, it's all borked" } it "raises the error" do lambda { subject.execute_component_and_await_completion component }.should raise_error(Adhearsion::Error, "#{details}: #{component}") end end it "blocks until the component receives a complete event" do slow_component = Punchblock::Component::Output.new slow_component.request! slow_component.execute! Thread.new do sleep 0.5 slow_component.complete_event = response end starting_time = Time.now subject.execute_component_and_await_completion slow_component (Time.now - starting_time).should > 0.5 end end end end class ExampleCallController < Adhearsion::CallController before_call { setup_models } before_call :setup_models after_call { clean_up_models } after_call :clean_up_models def setup_models end def clean_up_models end def run join_to_conference hangup unless metadata[:skip_hangup] foobar end def join_to_conference end def foobar end end describe ExampleCallController do include CallControllerTestHelpers before do flexmock subject, :execute_component_and_await_completion => nil flexmock call.wrapped_object, :write_and_await_response => nil end it "should execute the before_call callbacks before processing the call" do subject.should_receive(:setup_models).twice.ordered subject.should_receive(:join_to_conference).once.ordered subject.execute! end it "should execute the after_call callbacks after the call is hung up" do subject.should_receive(:join_to_conference).once.ordered subject.should_receive(:clean_up_models).twice.ordered subject.should_receive(:foobar).once.ordered subject.execute! end it "should capture errors in callbacks" do subject.should_receive(:setup_models).and_raise StandardError subject.should_receive(:clean_up_models).and_raise StandardError latch = CountDownLatch.new 4 Adhearsion::Events.exception do |e, l| e.should be_a StandardError l.should be subject.logger latch.countdown! end subject.execute! latch.wait(1).should be true Adhearsion::Events.clear_handlers :exception end describe "when the controller finishes without a hangup" do it "should execute the after_call callbacks" do subject[:skip_hangup] = true subject.should_receive(:join_to_conference).once.ordered subject.should_receive(:foobar).once.ordered subject.should_receive(:clean_up_models).twice.ordered subject.execute! end end describe "providing hooks to include call functionality" do let(:call) { Adhearsion::Call.new mock_offer(nil, :x_foo => 'bar') } it "should allow mixing in a module globally on all CallController classes" do Adhearsion::CallController.mixin TestBiscuit Adhearsion::CallController.new(call).should respond_to :throwadogabone end it "should allow mixing in a module on a single CallController class" do FinancialWizard.mixin MarmaladeIsBetterThanJam FinancialWizard.new(call).should respond_to :sobittersweet Adhearsion::CallController.new(call).should_not respond_to :sobittersweet end end end