# encoding: utf-8 require 'spec_helper' class BrokenController < Adhearsion::CallController def run raise "Blat!" end end module Adhearsion describe Call do let(:mock_client) { double('Client').as_null_object } let(:call_id) { rand.to_s } let(:domain) { 'rayo.net' } let(:headers) { nil } let(:to) { 'sip:you@there.com' } let(:from) { 'sip:me@here.com' } let(:transport) { 'footransport' } let(:base_time) { Time.local(2008, 9, 1, 12, 0, 0) } let :offer do Punchblock::Event::Offer.new target_call_id: call_id, domain: domain, transport: transport, to: to, from: from, headers: headers, timestamp: base_time end subject { Adhearsion::Call.new offer } before do allow(offer).to receive(:client).and_return(mock_client) end after do Adhearsion.active_calls.clear end it "should do recursion detection on inspect" do subject[:foo] = subject Timeout.timeout(0.2) do expect(subject.inspect).to match('...') end end it "should allow timers to be registered from outside" do foo = :bar subject.after(1) { foo = :baz } sleep 1.1 expect(foo).to eq(:baz) end it { is_expected.to respond_to :<< } describe '#end_reason' do subject { super().end_reason } it { is_expected.to eq(nil) } end it { is_expected.to be_active } describe '#commands' do subject { super().commands } it { is_expected.to be_empty } end describe '#id' do subject { super().id } it { is_expected.to eq(call_id) } end describe '#domain' do subject { super().domain } it { is_expected.to eq(domain) } end describe '#uri' do subject { super().uri } it { is_expected.to eq("footransport:#{call_id}@#{domain}") } end describe '#to' do subject { super().to } it { is_expected.to eq(to) } end describe '#from' do subject { super().from } it { is_expected.to eq(from) } end describe '#auto_hangup' do subject { super().auto_hangup } it { is_expected.to be_truthy } end describe '#after_hangup_lifetime' do subject { super().after_hangup_lifetime } it { is_expected.to eq(nil) } end context "when the ID is nil" do let(:call_id) { nil } describe '#uri' do subject { super().uri } it { is_expected.to eq(nil) } end end context "when the domain is nil" do let(:domain) { nil } describe '#uri' do subject { super().uri } it { is_expected.to eq("footransport:#{call_id}") } end end context "when the transport is nil" do let(:transport) { nil } describe '#uri' do subject { super().uri } it { is_expected.to eq("#{call_id}@#{domain}") } end end it "should mark its start time" do expect(subject.start_time).to eq(base_time) end describe "#commands" do it "should use a duplicating accessor for the command registry" do expect(subject.commands).not_to be subject.commands end end describe "its variables" do context "with an offer" do context "with headers" do let(:headers) { {'X-foo' => 'bar'} } describe '#variables' do subject { super().variables } it { is_expected.to eq({'x_foo' => 'bar'}) } end it "should be made available via []" do expect(subject[:x_foo]).to eq('bar') end it "should be alterable using []=" do subject[:x_foo] = 'baz' expect(subject[:x_foo]).to eq('baz') end context "when receiving an event with headers" do let(:event) { Punchblock::Event::End.new :headers => {'X-bar' => 'foo'} } it "should merge later headers" do subject << event expect(subject.variables).to eq({'x_foo' => 'bar', 'x_bar' => 'foo'}) end context "with have symbol names" do let(:event) { Punchblock::Event::End.new :headers => {:x_bar => 'foo'} } it "should merge later headers" do subject << event expect(subject.variables).to eq({'x_foo' => 'bar', 'x_bar' => 'foo'}) end end end context "when sending a command with headers" do let(:command) { Punchblock::Command::Accept.new :headers => {'X-bar' => 'foo'} } it "should merge later headers" do subject.write_command command expect(subject.variables).to eq({'x_foo' => 'bar', 'x_bar' => 'foo'}) end end end context "without headers" do let(:headers) { nil } describe '#variables' do subject { super().variables } it { is_expected.to eq({}) } end end end context "without an offer" do let(:offer) { nil } describe '#variables' do subject { super().variables } it { is_expected.to eq({}) } end end end describe 'without an offer' do it 'should not raise an exception' do expect { Adhearsion::Call.new }.not_to raise_error end end context 'registered event handlers' do let(:event) { double 'Event' } let(:response) { double 'Response' } it 'are called when messages are delivered' do expect(event).to receive(:foo?).and_return true expect(response).to receive(:call).once subject.register_event_handler(:foo?) { response.call } subject << event end context 'when a handler raises' do it 'does not cause the call actor to crash' do subject.register_event_handler { raise 'Boom' } subject << event expect(subject).to be_alive end it "triggers an exception event" do e = StandardError.new('Boom') expect(Events).to receive(:trigger).once.with(:exception, [e, subject.logger]) subject.register_event_handler { raise e } subject << event end it 'executes all handlers for each event' do expect(response).to receive(:call).once subject.register_event_handler { raise 'Boom' } subject.register_event_handler { response.call } subject << event end end end describe "event handlers" do let(:response) { double 'Response' } describe "for joined events" do context "joined to another call" do let :event do Punchblock::Event::Joined.new call_uri: 'footransport:foobar@rayo.net' end it "should trigger any on_joined callbacks set for the matching call ID" do expect(response).to receive(:call).once.with(event) subject.on_joined(:call_uri => 'footransport:foobar@rayo.net') { |event| response.call event } subject << event end it "should trigger any on_joined callbacks set for the matching call ID as a string" do expect(response).to receive(:call).once.with(event) subject.on_joined('foobar') { |event| response.call event } subject << event end it "should trigger any on_joined callbacks set for the matching call" do expect(response).to receive(:call).once.with(event) call = Call.new allow(call.wrapped_object).to receive_messages id: 'foobar', domain: 'rayo.net', transport: 'footransport' subject.on_joined(call) { |event| response.call event } subject << event end it "should not trigger on_joined callbacks for other call IDs" do expect(response).to receive(:call).never subject.on_joined(:call_uri => 'barfoo') { |event| response.call event } subject << event end it "should not trigger on_joined callbacks for mixers" do expect(response).to receive(:call).never subject.on_joined(:mixer_name => 'foobar') { |event| response.call event } subject << event end end context "joined to a mixer" do let :event do Punchblock::Event::Joined.new :mixer_name => 'foobar' end it "should trigger on_joined callbacks for the matching mixer name" do expect(response).to receive(:call).once.with(event) subject.on_joined(:mixer_name => 'foobar') { |event| response.call event } subject << event end it "should not trigger on_joined callbacks for other mixer names" do expect(response).to receive(:call).never subject.on_joined(:mixer_name => 'barfoo') { |event| response.call event } subject << event end it "should not trigger any on_joined callbacks set for calls" do expect(response).to receive(:call).never subject.on_joined(:call_uri => 'foobar') { |event| response.call event } subject << event end it "should not trigger any on_joined callbacks set for the matching call ID as a string" do expect(response).to receive(:call).never subject.on_joined('foobar') { |event| response.call event } subject << event end it "should not trigger any on_joined callbacks set for the matching call" do expect(response).to receive(:call).never call = Call.new allow(call.wrapped_object).to receive_messages :id => 'foobar' subject.on_joined(call) { |event| response.call event } subject << event end end end describe "for unjoined events" do context "unjoined from another call" do let :event do Punchblock::Event::Unjoined.new call_uri: 'footransport:foobar@rayo.net' end it "should trigger any on_unjoined callbacks set for the matching call ID" do expect(response).to receive(:call).once.with(event) subject.on_unjoined(:call_uri => 'footransport:foobar@rayo.net') { |event| response.call event } subject << event end it "should trigger any on_unjoined callbacks set for the matching call ID as a string" do expect(response).to receive(:call).once.with(event) subject.on_unjoined('foobar') { |event| response.call event } subject << event end it "should trigger any on_unjoined callbacks set for the matching call" do expect(response).to receive(:call).once.with(event) call = Call.new allow(call.wrapped_object).to receive_messages id: 'foobar', domain: 'rayo.net', transport: 'footransport' subject.on_unjoined(call) { |event| response.call event } subject << event end it "should not trigger on_unjoined callbacks for other call IDs" do expect(response).to receive(:call).never subject.on_unjoined(:call_uri => 'barfoo') { |event| response.call event } subject << event end it "should not trigger on_unjoined callbacks for mixers" do expect(response).to receive(:call).never subject.on_joined(:mixer_name => 'foobar') { |event| response.call event } subject << event end end context "unjoined from a mixer" do let :event do Punchblock::Event::Unjoined.new :mixer_name => 'foobar' end it "should trigger on_unjoined callbacks for the matching mixer name" do expect(response).to receive(:call).once.with(event) subject.on_unjoined(:mixer_name => 'foobar') { |event| response.call event } subject << event end it "should not trigger on_unjoined callbacks for other mixer names" do expect(response).to receive(:call).never subject.on_unjoined(:mixer_name => 'barfoo') { |event| response.call event } subject << event end it "should not trigger any on_unjoined callbacks set for calls" do expect(response).to receive(:call).never subject.on_unjoined(:call_uri => 'foobar') { |event| response.call event } subject << event end it "should not trigger any on_unjoined callbacks set for the matching call ID as a string" do expect(response).to receive(:call).never subject.on_unjoined('foobar') { |event| response.call event } subject << event end it "should not trigger any on_unjoined callbacks set for the matching call" do expect(response).to receive(:call).never call = Call.new allow(call.wrapped_object).to receive_messages :id => 'foobar' subject.on_unjoined(call) { |event| response.call event } subject << event end end end describe "for end events" do let :event do Punchblock::Event::End.new :reason => :hangup end it "should trigger any on_end callbacks set" do expect(response).to receive(:call).once.with(event) subject.on_end { |event| response.call event } subject << event end end context "when raising an exception" do it "does not kill the call actor" do subject.register_event_handler { |e| raise 'foo' } expect { subject << :foo }.not_to raise_error sleep 1 expect(subject).to be_alive end it 'passes the exception through the Events system' do latch = CountDownLatch.new 1 Adhearsion::Events.exception do |e, l| expect(e).to be_a RuntimeError expect(l).to be subject.logger latch.countdown! end subject.register_event_handler { |e| raise 'foo' } expect { subject << :foo }.not_to raise_error expect(latch.wait(3)).to be true Adhearsion::Events.clear_handlers :exception end end end context "peer registry" do let(:other_call_uri) { 'xmpp:foobar@example.com' } let(:other_call) { Call.new } before { allow(other_call).to receive_messages uri: other_call_uri } let :joined_event do Punchblock::Event::Joined.new call_uri: other_call_uri end let :unjoined_event do Punchblock::Event::Unjoined.new call_uri: other_call_uri end context "when we know about the joined call" do before { Adhearsion.active_calls << other_call } after { Adhearsion.active_calls.remove_inactive_call other_call } it "should add the peer to its registry" do subject << joined_event expect(subject.peers).to eq({'xmpp:foobar@example.com' => other_call}) end context "in a handler for the joined event" do it "should have already populated the registry" do peer = nil subject.on_joined do |event| peer = subject.peers.keys.first end subject << joined_event expect(peer).to eq(other_call_uri) end end context "when being unjoined from a previously joined call" do before { subject << joined_event } it "should remove the peer from its registry" do expect(subject.peers).not_to eql({}) subject << unjoined_event expect(subject.peers).to eql({}) end context "in a handler for the unjoined event" do it "should have already been removed the registry" do peer_count = nil subject.on_unjoined do |event| peer_count = subject.peers.size end subject << unjoined_event expect(peer_count).to eq(0) end end end end context "when we don't know about the joined call" do it "should add a nil entry to its registry" do subject << joined_event expect(subject.peers).to eq({'xmpp:foobar@example.com' => nil}) end context "in a handler for the joined event" do it "should have already populated the registry" do peer = nil subject.on_joined do |event| peer = subject.peers.keys.first end subject << joined_event expect(peer).to eq(other_call_uri) end end context "when being unjoined from a previously joined call" do before { subject << joined_event } it "should remove the peer from its registry" do expect(subject.peers).not_to eql({}) subject << unjoined_event expect(subject.peers).to eql({}) end context "in a handler for the unjoined event" do it "should have already been removed the registry" do peer_count = nil subject.on_unjoined do |event| peer_count = subject.peers.size end subject << unjoined_event expect(peer_count).to eq(0) end end end end it "should not return the same registry every call" do expect(subject.peers).not_to be subject.peers end end describe "#<<" do describe "with a Punchblock End" do let :end_event do Punchblock::Event::End.new :reason => :hangup, :platform_code => 'arbitrary_code' end it "should mark the call as ended" do subject << end_event expect(subject).not_to be_active end it "should set the end reason" do subject << end_event expect(subject.end_reason).to eq(:hangup) end it "should set the end code" do subject << end_event expect(subject.end_code).to eq('arbitrary_code') end it "should set the end time" do finish_time = Time.local(2008, 9, 1, 12, 1, 3) Timecop.freeze finish_time expect(subject.end_time).to eq(nil) subject << end_event expect(subject.end_time).to eq(finish_time) end it "should set the call duration" do start_time = Time.local(2008, 9, 1, 12, 0, 0) Timecop.freeze start_time subject mid_point_time = Time.local(2008, 9, 1, 12, 0, 37) Timecop.freeze mid_point_time expect(subject.duration).to eq(37.0) finish_time = Time.local(2008, 9, 1, 12, 1, 3) Timecop.freeze finish_time subject << end_event future_time = Time.local(2008, 9, 1, 12, 2, 3) Timecop.freeze finish_time expect(subject.duration).to eq(63.0) end it "should instruct the command registry to terminate" do command = Punchblock::Command::Answer.new command.request! subject.future.write_and_await_response command subject << end_event expect(command.response(1)).to be_a Call::Hangup end it "removes itself from the active calls" do size_before = Adhearsion.active_calls.size Adhearsion.active_calls << subject expect(Adhearsion.active_calls.size).to be > size_before subject << end_event expect(Adhearsion.active_calls.size).to eq(size_before) end context "with no custom lifetime" do around do |example| old_val = Adhearsion.config.platform.after_hangup_lifetime begin example.run rescue Adhearsion.config.platform.after_hangup_lifetime = old_val end end it "shuts down the actor using platform config" do Adhearsion.config.platform.after_hangup_lifetime = 2 subject << end_event sleep 2.1 expect(subject.alive?).to be false expect { subject.id }.to raise_error Call::ExpiredError, /expired and is no longer accessible/ end end context "with a custom lifetime" do around do |example| old_val = Adhearsion.config.platform.after_hangup_lifetime begin example.run rescue Adhearsion.config.platform.after_hangup_lifetime = old_val end end it "shuts down the actor using the Call#after_hangup_lifetime" do Adhearsion.config.platform.after_hangup_lifetime = 1 subject.after_hangup_lifetime = 2 subject << end_event sleep 1.1 expect(subject.alive?).to be true sleep 1 expect(subject.alive?).to be false expect { subject.id }.to raise_error Call::ExpiredError, /expired and is no longer accessible/ end end end end describe "#wait_for_end" do let :end_event do Punchblock::Event::End.new reason: :hangup end context "when the call has already ended" do before { subject << end_event } it "should return the end reason" do expect(subject.wait_for_end).to eq(:hangup) end end context "when the call has not yet ended" do it "should block until the call ends and return the end reason" do fut = subject.future.wait_for_end sleep 0.5 expect(fut).not_to be_ready subject << end_event expect(fut.value).to eq(:hangup) end end end describe "tagging a call" do it 'with a single Symbol' do expect { subject.tag :moderator }.not_to raise_error end it 'with multiple Symbols' do expect { subject.tag :moderator subject.tag :female }.not_to raise_error end it 'with a non-Symbol, non-String object' do bad_objects = [123, Object.new, 888.88, nil, true, false, StringIO.new] bad_objects.each do |bad_object| expect { subject.tag bad_object }.to raise_error ArgumentError end end end it "#remove_tag" do subject.tag :moderator subject.tag :female subject.remove_tag :female subject.tag :male expect(subject.tags).to eq([:moderator, :male]) end describe "#tagged_with?" do it 'with one tag' do subject.tag :guest expect(subject.tagged_with?(:guest)).to be true expect(subject.tagged_with?(:authorized)).to be false end it 'with many tags' do subject.tag :customer subject.tag :authorized expect(subject.tagged_with?(:customer)).to be true expect(subject.tagged_with?(:authorized)).to be true end end describe "#write_command" do let(:command) { Punchblock::Command::Answer.new } it "should write the command to the Punchblock connection" do expect(subject.wrapped_object).to receive(:client).once.and_return mock_client expect(mock_client).to receive(:execute_command).once.with(Punchblock::Command::Answer.new(target_call_id: call_id, domain: domain)).and_return true subject.write_command command end describe "with a hungup call" do before do expect(subject.wrapped_object).to receive(:active?).and_return(false) end it "should raise a Hangup exception" do expect { subject.write_command command }.to raise_error(Call::Hangup) end describe "if the command is a Hangup" do let(:command) { Punchblock::Command::Hangup.new } it "should not raise a Hangup exception" do expect { subject.write_command command }.not_to raise_error end end end end describe '#write_and_await_response' do let(:message) { Punchblock::Command::Accept.new } let(:response) { :foo } before do expect(message).to receive(:execute!).and_return true message.response = response end it "writes a command to the call" do expect(subject.wrapped_object).to receive(:write_command).once.with(message) subject.write_and_await_response message end it "removes the command from the registry after execution" do subject.write_and_await_response message expect(subject.commands).to be_empty end it "blocks until a response is received" do slow_command = Punchblock::Command::Dial.new slow_command.request! Thread.new do sleep 0.5 slow_command.response = response end starting_time = Time.now subject.write_and_await_response slow_command expect(Time.now - starting_time).to be >= 0.5 end context "while waiting for a response" do let(:slow_command) { Punchblock::Command::Dial.new } before { slow_command.request! } it "does not block the whole actor while waiting for a response" do fut = subject.future.write_and_await_response slow_command expect(subject.id).to eq(call_id) slow_command.response = response fut.value end it "adds the command to the registry" do subject.future.write_and_await_response slow_command sleep 0.2 expect(subject.commands).not_to be_empty expect(subject.commands.first).to be slow_command end end describe "with a successful response" do it "returns the executed command" do expect(subject.write_and_await_response(message)).to be message end end describe "with an error response" do let(:new_exception) { Punchblock::ProtocolError } let(:response) { new_exception.new } it "raises the error" do expect(Events).to receive(:trigger).never expect { subject.write_and_await_response message }.to raise_error new_exception end context "where the name is :item_not_found" do let(:response) { new_exception.new.setup :item_not_found } it "should raise a Hangup exception" do expect(Events).to receive(:trigger).never expect { subject.write_and_await_response message }.to raise_error Call::Hangup end end end describe "when the response times out" do before do message.target_call_id = call_id message.domain = domain expect(message).to receive(:response).and_raise Timeout::Error end it "should raise the error in the caller but not crash the actor" do expect { subject.write_and_await_response message }.to raise_error Call::CommandTimeout, message.to_s sleep 0.5 expect(subject).to be_alive end end end describe "#send_message" do it "should send a message through the Punchblock connection using the call ID and domain" do expect(subject.wrapped_object).to receive(:client).once.and_return mock_client expect(mock_client).to receive(:send_message).once.with(subject.id, subject.domain, "Hello World!", {}) subject.send_message "Hello World!" end it "should send a message with the given subject" do expect(subject.wrapped_object).to receive(:client).once.and_return mock_client expect(mock_client).to receive(:send_message).once.with(subject.id, subject.domain, nil, :subject => "Important Message") subject.send_message nil, :subject => "Important Message" end end describe "basic control commands" do def expect_message_waiting_for_response(message = nil, fail = false, &block) expectation = expect(subject.wrapped_object).to receive(:write_and_await_response, &block).once expectation = expectation.with message if message if fail expectation.and_raise fail else expectation.and_return message end end describe '#accept' do describe "with no headers" do it 'should send an Accept message' do expect_message_waiting_for_response Punchblock::Command::Accept.new subject.accept end end describe "with headers set" do it 'should send an Accept message with the correct headers' do headers = {:foo => 'bar'} expect_message_waiting_for_response Punchblock::Command::Accept.new(:headers => headers) subject.accept headers end end describe "a second time" do it "should only send one Accept message" do expect_message_waiting_for_response Punchblock::Command::Accept.new subject.accept subject.accept end end context "with a failure response" do it 'should raise the error but not crash the actor' do error = Punchblock::ProtocolError.new.setup(:service_unavailable) expect_message_waiting_for_response Punchblock::Command::Accept.new, error expect { subject.accept }.to raise_error error sleep 0.2 expect(subject.alive?).to be true end end end describe '#answer' do describe "with no headers" do it 'should send an Answer message' do expect_message_waiting_for_response Punchblock::Command::Answer.new subject.answer end end describe "with headers set" do it 'should send an Answer message with the correct headers' do headers = {:foo => 'bar'} expect_message_waiting_for_response Punchblock::Command::Answer.new(:headers => headers) subject.answer headers end end context "with a failure response" do it 'should raise the error but not crash the actor' do error = Punchblock::ProtocolError.new.setup(:service_unavailable) expect_message_waiting_for_response Punchblock::Command::Answer.new, error expect { subject.answer }.to raise_error error sleep 0.2 expect(subject.alive?).to be true end end end describe '#reject' do describe "with a reason given" do it 'should send a Reject message with the correct reason' do expect_message_waiting_for_response Punchblock::Command::Reject.new(:reason => :decline) subject.reject :decline end end describe "with no reason given" do it 'should send a Reject message with the reason busy' do expect_message_waiting_for_response Punchblock::Command::Reject.new(:reason => :busy) subject.reject end end describe "with no headers" do it 'should send a Reject message' do expect_message_waiting_for_response do |c| c.is_a?(Punchblock::Command::Reject) && c.headers == {} end subject.reject end end describe "with headers set" do it 'should send a Hangup message with the correct headers' do headers = {:foo => 'bar'} expect_message_waiting_for_response do |c| c.is_a?(Punchblock::Command::Reject) && c.headers == headers end subject.reject nil, headers end end it "should immediately fire the :call_rejected event giving the call and the reason" do expect_message_waiting_for_response kind_of(Punchblock::Command::Reject) expect(Adhearsion::Events).to receive(:trigger_immediately).once.with(:call_rejected, :call => subject, :reason => :decline) subject.reject :decline end context "with a failure response" do it 'should raise the error but not crash the actor' do error = Punchblock::ProtocolError.new.setup(:service_unavailable) expect_message_waiting_for_response Punchblock::Command::Reject.new(reason: :busy), error expect { subject.reject }.to raise_error error sleep 0.2 expect(subject.alive?).to be true end end end describe '#redirect' do describe "with a target given" do it 'should send a Redirect message with the correct target' do expect_message_waiting_for_response Punchblock::Command::Redirect.new(to: 'sip:foo@bar.com') subject.redirect 'sip:foo@bar.com' end end describe "with no target given" do it 'should raise with ArgumentError' do expect { subject.redirect }.to raise_error(ArgumentError) end end describe "with no headers" do it 'should send a Redirect message' do expect_message_waiting_for_response do |c| c.is_a?(Punchblock::Command::Redirect) && c.headers == {} end subject.redirect 'sip:foo@bar.com' end end describe "with headers set" do it 'should send a Redirect message with the correct headers' do headers = {:foo => 'bar'} expect_message_waiting_for_response do |c| c.is_a?(Punchblock::Command::Redirect) && c.headers == headers end subject.redirect 'sip:foo@bar.com', headers end end context "with a failure response" do it 'should raise the error but not crash the actor' do error = Punchblock::ProtocolError.new.setup(:service_unavailable) expect_message_waiting_for_response Punchblock::Command::Redirect.new(to: 'sip:foo@bar.com'), error expect { subject.redirect 'sip:foo@bar.com' }.to raise_error error sleep 0.2 expect(subject.alive?).to be true end end end describe "#hangup" do describe "if the call is not active" do before do expect(subject.wrapped_object).to receive(:active?).and_return false end it "should do nothing and return false" do expect(subject).to receive(:write_and_await_response).never expect(subject.hangup).to be false end end describe "if the call is active" do it "should mark the call inactive" do expect_message_waiting_for_response Punchblock::Command::Hangup.new subject.hangup expect(subject).not_to be_active end describe "with no headers" do it 'should send a Hangup message' do expect_message_waiting_for_response Punchblock::Command::Hangup.new subject.hangup end end describe "with headers set" do it 'should send a Hangup message with the correct headers' do headers = {:foo => 'bar'} expect_message_waiting_for_response Punchblock::Command::Hangup.new(:headers => headers) subject.hangup headers end end end context "with a failure response" do it 'should raise the error but not crash the actor' do error = Punchblock::ProtocolError.new.setup(:service_unavailable) expect_message_waiting_for_response Punchblock::Command::Hangup.new, error expect { subject.hangup }.to raise_error error sleep 0.2 expect(subject.alive?).to be true end end end describe "#join" do def expect_join_with_options(options = {}) Punchblock::Command::Join.new(options).tap do |join| expect_message_waiting_for_response join end end context "with a call" do let(:call_id) { rand.to_s } let(:domain) { 'rayo.net' } let(:uri) { "footransport:#{call_id}@#{domain}" } let(:target) { described_class.new } before { allow(target.wrapped_object).to receive_messages uri: uri } it "should send a join command joining to the provided call ID" do expect_join_with_options call_uri: uri subject.join target end context "and direction/media options" do it "should send a join command with the correct options" do expect_join_with_options :call_id => uri, :media => :bridge, :direction => :recv subject.join target, :media => :bridge, :direction => :recv end end it "should return the command" do expect_join_with_options :call_id => uri, :media => :bridge, :direction => :recv result = subject.join target, :media => :bridge, :direction => :recv expect(result[:command]).to be_a Punchblock::Command::Join expect(result[:command].call_uri).to eql(uri) expect(result[:command].media).to eql(:bridge) expect(result[:command].direction).to eql(:recv) end it "should return something that can be blocked on until the join is complete" do expect_join_with_options :call_id => uri, :media => :bridge, :direction => :recv result = subject.join target, :media => :bridge, :direction => :recv expect(result[:joined_condition].wait(0.5)).to be_falsey subject << Punchblock::Event::Joined.new(call_uri: uri) expect(result[:joined_condition].wait(0.5)).to be_truthy end it "should return something that can be blocked on until the entities are unjoined" do expect_join_with_options :call_id => uri, :media => :bridge, :direction => :recv result = subject.join target, :media => :bridge, :direction => :recv expect(result[:unjoined_condition].wait(0.5)).to be_falsey subject << Punchblock::Event::Joined.new(call_uri: uri) expect(result[:unjoined_condition].wait(0.5)).to be_falsey subject << Punchblock::Event::Unjoined.new(call_uri: uri) expect(result[:unjoined_condition].wait(0.5)).to be_truthy end it "should unblock all conditions on call end if no joined/unjoined events are received" do expect_join_with_options :call_id => uri, :media => :bridge, :direction => :recv result = subject.join target, :media => :bridge, :direction => :recv expect(result[:joined_condition].wait(0.5)).to be_falsey expect(result[:unjoined_condition].wait(0.5)).to be_falsey subject << Punchblock::Event::End.new expect(result[:joined_condition].wait(0.5)).to be_truthy expect(result[:unjoined_condition].wait(0.5)).to be_truthy end it "should not error on call end when joined/unjoined events are received correctly" do expect_join_with_options :call_id => uri, :media => :bridge, :direction => :recv result = subject.join target, :media => :bridge, :direction => :recv subject << Punchblock::Event::Joined.new(call_uri: uri) subject << Punchblock::Event::Unjoined.new(call_uri: uri) subject << Punchblock::Event::End.new end it "should not error if multiple joined events are received for the same join" do expect_join_with_options :call_id => uri, :media => :bridge, :direction => :recv result = subject.join target, :media => :bridge, :direction => :recv subject << Punchblock::Event::Joined.new(call_uri: uri) subject << Punchblock::Event::Joined.new(call_uri: uri) expect(subject).to be_alive end end context "with a call ID" do let(:target) { rand.to_s } let(:uri) { "footransport:#{target}@#{subject.domain}" } it "should send a join command joining to the provided call ID" do expect_join_with_options call_uri: uri subject.join target end context "and direction/media options" do it "should send a join command with the correct options" do expect_join_with_options :call_uri => uri, :media => :bridge, :direction => :recv subject.join target, :media => :bridge, :direction => :recv end end it "should return the command" do expect_join_with_options :call_id => uri, :media => :bridge, :direction => :recv result = subject.join target, :media => :bridge, :direction => :recv expect(result[:command]).to be_a Punchblock::Command::Join expect(result[:command].call_uri).to eql(uri) expect(result[:command].media).to eql(:bridge) expect(result[:command].direction).to eql(:recv) end it "should return something that can be blocked on until the join is complete" do expect_join_with_options :call_id => uri, :media => :bridge, :direction => :recv result = subject.join target, :media => :bridge, :direction => :recv expect(result[:joined_condition].wait(0.5)).to be_falsey subject << Punchblock::Event::Joined.new(call_uri: uri) expect(result[:joined_condition].wait(0.5)).to be_truthy end it "should return something that can be blocked on until the entities are unjoined" do expect_join_with_options :call_id => uri, :media => :bridge, :direction => :recv result = subject.join target, :media => :bridge, :direction => :recv expect(result[:unjoined_condition].wait(0.5)).to be_falsey subject << Punchblock::Event::Joined.new(call_uri: uri) expect(result[:unjoined_condition].wait(0.5)).to be_falsey subject << Punchblock::Event::Unjoined.new(call_uri: uri) expect(result[:unjoined_condition].wait(0.5)).to be_truthy end it "should unblock all conditions on call end if no joined/unjoined events are received" do expect_join_with_options :call_id => uri, :media => :bridge, :direction => :recv result = subject.join target, :media => :bridge, :direction => :recv expect(result[:joined_condition].wait(0.5)).to be_falsey expect(result[:unjoined_condition].wait(0.5)).to be_falsey subject << Punchblock::Event::End.new expect(result[:joined_condition].wait(0.5)).to be_truthy expect(result[:unjoined_condition].wait(0.5)).to be_truthy end it "should not error on call end when joined/unjoined events are received correctly" do expect_join_with_options :call_id => uri, :media => :bridge, :direction => :recv result = subject.join target, :media => :bridge, :direction => :recv subject << Punchblock::Event::Joined.new(call_uri: uri) subject << Punchblock::Event::Unjoined.new(call_uri: uri) subject << Punchblock::Event::End.new end it "should not error if multiple joined events are received for the same join" do expect_join_with_options :call_id => uri, :media => :bridge, :direction => :recv result = subject.join target, :media => :bridge, :direction => :recv subject << Punchblock::Event::Joined.new(call_uri: uri) subject << Punchblock::Event::Joined.new(call_uri: uri) expect(subject).to be_alive end end context "with a call URI as a hash key" do let(:call_id) { rand.to_s } let(:uri) { call_id } let(:target) { { :call_uri => call_id } } it "should send a join command joining to the provided call ID" do expect_join_with_options :call_uri => call_id subject.join target end context "and direction/media options" do it "should send a join command with the correct options" do expect_join_with_options :call_uri => call_id, :media => :bridge, :direction => :recv subject.join target.merge({:media => :bridge, :direction => :recv}) end end it "should return the command" do expect_join_with_options :call_id => uri, :media => :bridge, :direction => :recv result = subject.join target.merge({:media => :bridge, :direction => :recv}) expect(result[:command]).to be_a Punchblock::Command::Join expect(result[:command].call_uri).to eql(uri) expect(result[:command].media).to eql(:bridge) expect(result[:command].direction).to eql(:recv) end it "should return something that can be blocked on until the join is complete" do expect_join_with_options :call_id => uri, :media => :bridge, :direction => :recv result = subject.join target.merge({:media => :bridge, :direction => :recv}) expect(result[:joined_condition].wait(0.5)).to be_falsey subject << Punchblock::Event::Joined.new(call_uri: uri) expect(result[:joined_condition].wait(0.5)).to be_truthy end it "should return something that can be blocked on until the entities are unjoined" do expect_join_with_options :call_id => uri, :media => :bridge, :direction => :recv result = subject.join target.merge({:media => :bridge, :direction => :recv}) expect(result[:unjoined_condition].wait(0.5)).to be_falsey subject << Punchblock::Event::Joined.new(call_uri: uri) expect(result[:unjoined_condition].wait(0.5)).to be_falsey subject << Punchblock::Event::Unjoined.new(call_uri: uri) expect(result[:unjoined_condition].wait(0.5)).to be_truthy end it "should unblock all conditions on call end if no joined/unjoined events are received" do expect_join_with_options :call_id => uri, :media => :bridge, :direction => :recv result = subject.join target.merge({:media => :bridge, :direction => :recv}) expect(result[:joined_condition].wait(0.5)).to be_falsey expect(result[:unjoined_condition].wait(0.5)).to be_falsey subject << Punchblock::Event::End.new expect(result[:joined_condition].wait(0.5)).to be_truthy expect(result[:unjoined_condition].wait(0.5)).to be_truthy end it "should not error on call end when joined/unjoined events are received correctly" do expect_join_with_options :call_id => uri, :media => :bridge, :direction => :recv result = subject.join target.merge({:media => :bridge, :direction => :recv}) subject << Punchblock::Event::Joined.new(call_uri: uri) subject << Punchblock::Event::Unjoined.new(call_uri: uri) subject << Punchblock::Event::End.new end it "should not error if multiple joined events are received for the same join" do expect_join_with_options :call_id => uri, :media => :bridge, :direction => :recv result = subject.join target.merge({:media => :bridge, :direction => :recv}) subject << Punchblock::Event::Joined.new(call_uri: uri) subject << Punchblock::Event::Joined.new(call_uri: uri) expect(subject).to be_alive end end context "with a mixer name as a hash key" do let(:mixer_name) { rand.to_s } let(:target) { { :mixer_name => mixer_name } } it "should send a join command joining to the provided call ID" do expect_join_with_options :mixer_name => mixer_name subject.join target end context "and direction/media options" do it "should send a join command with the correct options" do expect_join_with_options :mixer_name => mixer_name, :media => :bridge, :direction => :recv subject.join target.merge({:media => :bridge, :direction => :recv}) end end it "should return the command" do expect_join_with_options :mixer_name => mixer_name, :media => :bridge, :direction => :recv result = subject.join target.merge({:media => :bridge, :direction => :recv}) expect(result[:command]).to be_a Punchblock::Command::Join expect(result[:command].mixer_name).to eql(mixer_name) expect(result[:command].media).to eql(:bridge) expect(result[:command].direction).to eql(:recv) end it "should return something that can be blocked on until the join is complete" do expect_join_with_options :mixer_name => mixer_name, :media => :bridge, :direction => :recv result = subject.join target.merge({:media => :bridge, :direction => :recv}) expect(result[:joined_condition].wait(0.5)).to be_falsey subject << Punchblock::Event::Joined.new(mixer_name: mixer_name) expect(result[:joined_condition].wait(0.5)).to be_truthy end it "should return something that can be blocked on until the entities are unjoined" do expect_join_with_options :mixer_name => mixer_name, :media => :bridge, :direction => :recv result = subject.join target.merge({:media => :bridge, :direction => :recv}) expect(result[:unjoined_condition].wait(0.5)).to be_falsey subject << Punchblock::Event::Joined.new(mixer_name: mixer_name) expect(result[:unjoined_condition].wait(0.5)).to be_falsey subject << Punchblock::Event::Unjoined.new(mixer_name: mixer_name) expect(result[:unjoined_condition].wait(0.5)).to be_truthy end it "should unblock all conditions on call end if no joined/unjoined events are received" do expect_join_with_options :mixer_name => mixer_name, :media => :bridge, :direction => :recv result = subject.join target.merge({:media => :bridge, :direction => :recv}) expect(result[:joined_condition].wait(0.5)).to be_falsey expect(result[:unjoined_condition].wait(0.5)).to be_falsey subject << Punchblock::Event::End.new expect(result[:joined_condition].wait(0.5)).to be_truthy expect(result[:unjoined_condition].wait(0.5)).to be_truthy end it "should not error on call end when joined/unjoined events are received correctly" do expect_join_with_options :mixer_name => mixer_name, :media => :bridge, :direction => :recv result = subject.join target.merge({:media => :bridge, :direction => :recv}) subject << Punchblock::Event::Joined.new(mixer_name: mixer_name) subject << Punchblock::Event::Unjoined.new(mixer_name: mixer_name) subject << Punchblock::Event::End.new end it "should not error if multiple joined events are received for the same join" do expect_join_with_options :mixer_name => mixer_name, :media => :bridge, :direction => :recv result = subject.join target.merge({:media => :bridge, :direction => :recv}) subject << Punchblock::Event::Joined.new(mixer_name: mixer_name) subject << Punchblock::Event::Joined.new(mixer_name: mixer_name) expect(subject).to be_alive end end context "with a call ID and a mixer name as hash keys" do let(:call_id) { rand.to_s } let(:mixer_name) { rand.to_s } let(:target) { { :call_uri => call_id, :mixer_name => mixer_name } } it "should raise an ArgumentError" do expect { subject.join target }.to raise_error ArgumentError, /call URI and mixer name/ end end context "with a failure response" do it 'should raise the error but not crash the actor' do error = Punchblock::ProtocolError.new.setup(:service_unavailable) expect_message_waiting_for_response Punchblock::Command::Join.new(call_id: 'footransport:foo@rayo.net'), error expect { subject.join 'foo' }.to raise_error error sleep 0.2 expect(subject.alive?).to be true end end end describe "#unjoin" do def expect_unjoin_with_options(options = {}) Punchblock::Command::Unjoin.new(options).tap do |unjoin| expect_message_waiting_for_response unjoin end end context "without a target" do it "should send an unjoin command unjoining from every existing join" do expect_unjoin_with_options nil subject.unjoin end end context "with a call" do let(:call_id) { rand.to_s } let(:domain) { 'rayo.net' } let(:uri) { "footransport:#{call_id}@#{domain}" } let(:target) { described_class.new } before { allow(target.wrapped_object).to receive_messages uri: uri } it "should send an unjoin command unjoining from the provided call ID" do expect_unjoin_with_options call_uri: "footransport:#{call_id}@#{domain}" subject.unjoin target end end context "with a call ID" do let(:target) { rand.to_s } it "should send an unjoin command unjoining from the provided call ID" do expect_unjoin_with_options call_uri: "footransport:#{target}@#{subject.domain}" subject.unjoin target end end context "with a call URI as a hash key" do let(:call_id) { rand.to_s } let(:target) { { call_uri: call_id } } it "should send an unjoin command unjoining from the provided call ID" do expect_unjoin_with_options call_uri: call_id subject.unjoin target end end context "with a mixer name as a hash key" do let(:mixer_name) { rand.to_s } let(:target) { { :mixer_name => mixer_name } } it "should send an unjoin command unjoining from the provided call ID" do expect_unjoin_with_options :mixer_name => mixer_name subject.unjoin target end end context "with a call URI and a mixer name as hash keys" do let(:call_id) { rand.to_s } let(:mixer_name) { rand.to_s } let(:target) { { call_uri: call_id, mixer_name: mixer_name } } it "should raise an ArgumentError" do expect { subject.unjoin target }.to raise_error ArgumentError, /call URI and mixer name/ end end context "with a failure response" do it 'should raise the error but not crash the actor' do error = Punchblock::ProtocolError.new.setup(:service_unavailable) expect_message_waiting_for_response Punchblock::Command::Unjoin.new(call_id: 'footransport:foo@rayo.net'), error expect { subject.unjoin 'foo' }.to raise_error error sleep 0.2 expect(subject.alive?).to be true end end end describe "#mute" do it 'should send a Mute message' do expect_message_waiting_for_response Punchblock::Command::Mute.new subject.mute end context "with a failure response" do it 'should raise the error but not crash the actor' do error = Punchblock::ProtocolError.new.setup(:service_unavailable) expect_message_waiting_for_response Punchblock::Command::Mute.new, error expect { subject.mute }.to raise_error error sleep 0.2 expect(subject.alive?).to be true end end end describe "#unmute" do it 'should send a Mute message' do expect_message_waiting_for_response Punchblock::Command::Unmute.new subject.unmute end context "with a failure response" do it 'should raise the error but not crash the actor' do error = Punchblock::ProtocolError.new.setup(:service_unavailable) expect_message_waiting_for_response Punchblock::Command::Unmute.new, error expect { subject.unmute }.to raise_error error sleep 0.2 expect(subject.alive?).to be true end end end describe "#execute_controller" do let(:latch) { CountDownLatch.new 1 } let(:mock_controller) { CallController.new(subject) } before do allow(subject.wrapped_object).to receive_messages :write_and_await_response => true end it "should call #bg_exec on the controller instance" do expect(mock_controller).to receive(:exec).once subject.execute_controller mock_controller, lambda { |call| latch.countdown! } expect(latch.wait(3)).to be_truthy end it "should use the passed block as a controller if none is specified" do expect(mock_controller).to receive(:exec).once expect(CallController).to receive(:new).once.and_return mock_controller subject.execute_controller nil, lambda { |call| latch.countdown! } do foo end expect(latch.wait(3)).to be_truthy end it "should raise ArgumentError if both a controller and a block are passed" do expect { subject.execute_controller(mock_controller) { foo } }.to raise_error(ArgumentError) end it "should pass the exception to the events system" do latch = CountDownLatch.new 1 Adhearsion::Events.exception do |e, l| expect(e).to be_a RuntimeError expect(l).to be subject.logger latch.countdown! end subject.execute_controller BrokenController.new(subject), lambda { |call| latch.countdown! } expect(latch.wait(3)).to be true Adhearsion::Events.clear_handlers :exception end it "should execute a callback after the controller executes" do foo = nil subject.execute_controller mock_controller, lambda { |call| foo = call; latch.countdown! } expect(latch.wait(3)).to be_truthy expect(foo).to be subject end it "should prevent exceptions in controllers from being raised" do expect(mock_controller).to receive(:run).once.ordered.and_raise StandardError expect { subject.execute_controller mock_controller, lambda { |call| latch.countdown! } }.to_not raise_error expect(latch.wait(3)).to be_truthy expect(subject.alive?).to be true end end describe "#register_controller" do it "should add the controller to a list on the call" do subject.register_controller :foo expect(subject.controllers).to include :foo end end context "with two controllers registered" do let(:controller1) { double 'CallController1' } let(:controller2) { double 'CallController2' } before { subject.controllers << controller1 << controller2 } describe "#pause_controllers" do it "should pause each of the registered controllers" do expect(controller1).to receive(:pause!).once expect(controller2).to receive(:pause!).once subject.pause_controllers end end describe "#resume_controllers" do it "should resume each of the registered controllers" do expect(controller1).to receive(:resume!).once expect(controller2).to receive(:resume!).once subject.resume_controllers end end end describe "after termination" do it "should delete its logger" do logger = subject.logger subject.terminate expect(::Logging::Repository.instance[logger.name]).to be_nil end end end describe Call::CommandRegistry do subject { Call::CommandRegistry.new } it { is_expected.to be_empty } describe "#<<" do it "should add a command to the registry" do subject << :foo expect(subject).not_to be_empty end end describe "#delete" do it "should remove a command from the registry" do subject << :foo expect(subject).not_to be_empty subject.delete :foo expect(subject).to be_empty end end describe "#terminate" do let :commands do [ Punchblock::Command::Answer.new, Punchblock::Command::Answer.new ] end it "should set each command's response to an instance of Adhearsion::Hangup if it doesn't already have a response" do finished_command = Punchblock::Command::Answer.new finished_command.request! finished_command.response = :foo subject << finished_command commands.each do |command| command.request! subject << command end subject.terminate commands.each do |command| expect(command.response).to be_a Call::Hangup end expect(finished_command.response).to eq(:foo) end end end end end