require 'spec_helper' module Punchblock module Translator class Asterisk describe Call do let(:channel) { 'SIP/foo' } let(:translator) { stub_everything 'Translator::Asterisk' } let(:env) { "agi_request%3A%20async%0Aagi_channel%3A%20SIP%2F1234-00000000%0Aagi_language%3A%20en%0Aagi_type%3A%20SIP%0Aagi_uniqueid%3A%201320835995.0%0Aagi_version%3A%201.8.4.1%0Aagi_callerid%3A%205678%0Aagi_calleridname%3A%20Jane%20Smith%0Aagi_callingpres%3A%200%0Aagi_callingani2%3A%200%0Aagi_callington%3A%200%0Aagi_callingtns%3A%200%0Aagi_dnid%3A%201000%0Aagi_rdnis%3A%20unknown%0Aagi_context%3A%20default%0Aagi_extension%3A%201000%0Aagi_priority%3A%201%0Aagi_enhanced%3A%200.0%0Aagi_accountcode%3A%20%0Aagi_threadid%3A%204366221312%0A%0A" } let(:agi_env) do { :agi_request => 'async', :agi_channel => 'SIP/1234-00000000', :agi_language => 'en', :agi_type => 'SIP', :agi_uniqueid => '1320835995.0', :agi_version => '1.8.4.1', :agi_callerid => '5678', :agi_calleridname => 'Jane Smith', :agi_callingpres => '0', :agi_callingani2 => '0', :agi_callington => '0', :agi_callingtns => '0', :agi_dnid => '1000', :agi_rdnis => 'unknown', :agi_context => 'default', :agi_extension => '1000', :agi_priority => '1', :agi_enhanced => '0.0', :agi_accountcode => '', :agi_threadid => '4366221312' } end let :sip_headers do { :x_agi_request => 'async', :x_agi_channel => 'SIP/1234-00000000', :x_agi_language => 'en', :x_agi_type => 'SIP', :x_agi_uniqueid => '1320835995.0', :x_agi_version => '1.8.4.1', :x_agi_callerid => '5678', :x_agi_calleridname => 'Jane Smith', :x_agi_callingpres => '0', :x_agi_callingani2 => '0', :x_agi_callington => '0', :x_agi_callingtns => '0', :x_agi_dnid => '1000', :x_agi_rdnis => 'unknown', :x_agi_context => 'default', :x_agi_extension => '1000', :x_agi_priority => '1', :x_agi_enhanced => '0.0', :x_agi_accountcode => '', :x_agi_threadid => '4366221312' } end subject { Call.new channel, translator, env } its(:id) { should be_a String } its(:channel) { should == channel } its(:translator) { should be translator } its(:agi_env) { should == agi_env } describe '#register_component' do it 'should make the component accessible by ID' do component_id = 'abc123' component = mock 'Translator::Asterisk::Component', :id => component_id subject.register_component component subject.component_with_id(component_id).should be component end end describe '#send_offer' do it 'sends an offer to the translator' do expected_offer = Punchblock::Event::Offer.new :call_id => subject.id, :to => '1000', :from => 'sip:5678', :headers => sip_headers translator.expects(:handle_pb_event!).with expected_offer subject.send_offer end it 'should make the call identify as inbound' do subject.send_offer subject.direction.should == :inbound subject.inbound?.should be true subject.outbound?.should be false end end describe '#dial' do let :dial_command do Punchblock::Command::Dial.new :to => 'SIP/1234', :from => 'sip:foo@bar.com' end before { dial_command.request! } it 'sends an Originate AMI action' do expected_action = Punchblock::Component::Asterisk::AMI::Action.new :name => 'Originate', :params => { :async => true, :application => 'AGI', :data => 'agi:async', :channel => 'SIP/1234', :callerid => 'sip:foo@bar.com', :variable => "punchblock_call_id=#{subject.id}" } translator.expects(:execute_global_command!).once.with expected_action subject.dial dial_command end it 'sends the call ID as a response to the Dial' do subject.dial dial_command dial_command.response dial_command.call_id.should == subject.id end it 'should make the call identify as outbound' do subject.dial dial_command subject.direction.should == :outbound subject.outbound?.should be true subject.inbound?.should be false end it 'causes accepting the call to be a null operation' do subject.dial dial_command accept_command = Command::Accept.new accept_command.request! subject.wrapped_object.expects(:send_agi_action).never subject.execute_command accept_command accept_command.response(0.5).should be true end end describe '#process_ami_event' do context 'with a Hangup event' do let :ami_event do RubyAMI::Event.new('Hangup').tap do |e| e['Uniqueid'] = "1320842458.8" e['Calleridnum'] = "5678" e['Calleridname'] = "Jane Smith" e['Cause'] = cause e['Cause-txt'] = cause_txt e['Channel'] = "SIP/1234-00000000" end end context "with a normal clearing cause" do let(:cause) { '16' } let(:cause_txt) { 'Normal Clearing' } it 'should send an end (hangup) event to the translator' do expected_end_event = Punchblock::Event::End.new :reason => :hangup, :call_id => subject.id translator.expects(:handle_pb_event!).with expected_end_event subject.process_ami_event ami_event end end context "with a user busy cause" do let(:cause) { '17' } let(:cause_txt) { 'User Busy' } it 'should send an end (busy) event to the translator' do expected_end_event = Punchblock::Event::End.new :reason => :busy, :call_id => subject.id translator.expects(:handle_pb_event!).with expected_end_event subject.process_ami_event ami_event end end { 18 => 'No user response', 102 => 'Recovery on timer expire' }.each_pair do |cause, cause_txt| context "with a #{cause_txt} cause" do let(:cause) { cause.to_s } let(:cause_txt) { cause_txt } it 'should send an end (timeout) event to the translator' do expected_end_event = Punchblock::Event::End.new :reason => :timeout, :call_id => subject.id translator.expects(:handle_pb_event!).with expected_end_event subject.process_ami_event ami_event end end end { 19 => 'No Answer', 21 => 'Call Rejected', 22 => 'Number Changed' }.each_pair do |cause, cause_txt| context "with a #{cause_txt} cause" do let(:cause) { cause.to_s } let(:cause_txt) { cause_txt } it 'should send an end (reject) event to the translator' do expected_end_event = Punchblock::Event::End.new :reason => :reject, :call_id => subject.id translator.expects(:handle_pb_event!).with expected_end_event subject.process_ami_event ami_event end end end { 1 => 'AST_CAUSE_UNALLOCATED', 2 => 'NO_ROUTE_TRANSIT_NET', 3 => 'NO_ROUTE_DESTINATION', 6 => 'CHANNEL_UNACCEPTABLE', 7 => 'CALL_AWARDED_DELIVERED', 27 => 'DESTINATION_OUT_OF_ORDER', 28 => 'INVALID_NUMBER_FORMAT', 29 => 'FACILITY_REJECTED', 30 => 'RESPONSE_TO_STATUS_ENQUIRY', 31 => 'NORMAL_UNSPECIFIED', 34 => 'NORMAL_CIRCUIT_CONGESTION', 38 => 'NETWORK_OUT_OF_ORDER', 41 => 'NORMAL_TEMPORARY_FAILURE', 42 => 'SWITCH_CONGESTION', 43 => 'ACCESS_INFO_DISCARDED', 44 => 'REQUESTED_CHAN_UNAVAIL', 45 => 'PRE_EMPTED', 50 => 'FACILITY_NOT_SUBSCRIBED', 52 => 'OUTGOING_CALL_BARRED', 54 => 'INCOMING_CALL_BARRED', 57 => 'BEARERCAPABILITY_NOTAUTH', 58 => 'BEARERCAPABILITY_NOTAVAIL', 65 => 'BEARERCAPABILITY_NOTIMPL', 66 => 'CHAN_NOT_IMPLEMENTED', 69 => 'FACILITY_NOT_IMPLEMENTED', 81 => 'INVALID_CALL_REFERENCE', 88 => 'INCOMPATIBLE_DESTINATION', 95 => 'INVALID_MSG_UNSPECIFIED', 96 => 'MANDATORY_IE_MISSING', 97 => 'MESSAGE_TYPE_NONEXIST', 98 => 'WRONG_MESSAGE', 99 => 'IE_NONEXIST', 100 => 'INVALID_IE_CONTENTS', 101 => 'WRONG_CALL_STATE', 103 => 'MANDATORY_IE_LENGTH_ERROR', 111 => 'PROTOCOL_ERROR', 127 => 'INTERWORKING' }.each_pair do |cause, cause_txt| context "with a #{cause_txt} cause" do let(:cause) { cause.to_s } let(:cause_txt) { cause_txt } it 'should send an end (error) event to the translator' do expected_end_event = Punchblock::Event::End.new :reason => :error, :call_id => subject.id translator.expects(:handle_pb_event!).with expected_end_event subject.process_ami_event ami_event end end end end context 'with an event for a known AGI command component' do let(:mock_component_node) { mock 'Punchblock::Component::Asterisk::AGI::Command', :name => 'EXEC ANSWER', :params_array => [] } let :component do Component::Asterisk::AGICommand.new mock_component_node, subject.translator end let(:ami_event) do RubyAMI::Event.new("AsyncAGI").tap do |e| e["SubEvent"] = "End" e["Channel"] = "SIP/1234-00000000" e["CommandID"] = component.id e["Command"] = "EXEC ANSWER" e["Result"] = "200%20result=123%20(timeout)%0A" end end before do subject.register_component component end it 'should send the event to the component' do component.expects(:handle_ami_event!).once.with ami_event subject.process_ami_event ami_event end end context 'with a Newstate event' do let :ami_event do RubyAMI::Event.new('Newstate').tap do |e| e['Privilege'] = 'call,all' e['Channel'] = 'SIP/1234-00000000' e['ChannelState'] = channel_state e['ChannelStateDesc'] = channel_state_desc e['CallerIDNum'] = '' e['CallerIDName'] = '' e['ConnectedLineNum'] = '' e['ConnectedLineName'] = '' e['Uniqueid'] = '1326194671.0' end end context 'ringing' do let(:channel_state) { '5' } let(:channel_state_desc) { 'Ringing' } it 'should send a ringing event' do expected_ringing = Punchblock::Event::Ringing.new expected_ringing.call_id = subject.id translator.expects(:handle_pb_event!).with expected_ringing subject.process_ami_event ami_event end end context 'up' do let(:channel_state) { '6' } let(:channel_state_desc) { 'Up' } it 'should send a ringing event' do expected_answered = Punchblock::Event::Answered.new expected_answered.call_id = subject.id translator.expects(:handle_pb_event!).with expected_answered subject.process_ami_event ami_event end end end context 'with a handler registered for a matching event' do let :ami_event do RubyAMI::Event.new('DTMF').tap do |e| e['Digit'] = '4' e['Start'] = 'Yes' e['End'] = 'No' e['Uniqueid'] = "1320842458.8" e['Channel'] = "SIP/1234-00000000" end end let(:response) { mock 'Response' } it 'should execute the handler' do response.expects(:call).once.with ami_event subject.register_handler :ami, :name => 'DTMF' do |event| response.call event end subject.process_ami_event ami_event end end end describe '#execute_command' do let :expected_agi_complete_event do Punchblock::Event::Complete.new.tap do |c| c.reason = Punchblock::Component::Asterisk::AGI::Command::Complete::Success.new :code => 200, :result => 'Success', :data => 'FOO' end end before do command.request! end context 'with an accept command' do let(:command) { Command::Accept.new } it "should send an EXEC RINGING AGI command and set the command's response" do subject.execute_command command agi_command = subject.wrapped_object.instance_variable_get(:'@current_agi_command') agi_command.name.should == "EXEC RINGING" agi_command.execute! agi_command.add_event expected_agi_complete_event command.response(0.5).should be true end end context 'with an answer command' do let(:command) { Command::Answer.new } it "should send an EXEC ANSWER AGI command and set the command's response" do subject.execute_command command agi_command = subject.wrapped_object.instance_variable_get(:'@current_agi_command') agi_command.name.should == "EXEC ANSWER" agi_command.execute! agi_command.add_event expected_agi_complete_event command.response(0.5).should be true end end context 'with a hangup command' do let(:command) { Command::Hangup.new } it "should send a Hangup AMI command and set the command's response" do subject.execute_command command ami_action = subject.wrapped_object.instance_variable_get(:'@current_ami_action') ami_action.name.should == "hangup" ami_action << RubyAMI::Response.new command.response(0.5).should be true end end context 'with an AGI command component' do let :command do Punchblock::Component::Asterisk::AGI::Command.new :name => 'Answer' end let(:mock_action) { mock 'Component::Asterisk::AGI::Command', :id => 'foo' } it 'should create an AGI command component actor and execute it asynchronously' do Component::Asterisk::AGICommand.expects(:new).once.with(command, subject).returns mock_action mock_action.expects(:execute!).once subject.execute_command command end end context 'with an Output component' do let :command do Punchblock::Component::Output.new end let(:mock_action) { mock 'Component::Asterisk::Output', :id => 'foo' } it 'should create an AGI command component actor and execute it asynchronously' do Component::Asterisk::Output.expects(:new).once.with(command, subject).returns mock_action mock_action.expects(:execute!).once subject.execute_command command end end context 'with an Input component' do let :command do Punchblock::Component::Input.new end let(:mock_action) { mock 'Component::Asterisk::Input', :id => 'foo' } it 'should create an AGI command component actor and execute it asynchronously' do Component::Asterisk::Input.expects(:new).once.with(command, subject).returns mock_action mock_action.expects(:execute!).once subject.execute_command command end end context 'with a component command' do let(:component_id) { 'foobar' } let :command do Punchblock::Component::Stop.new :component_id => component_id end let :mock_component do mock 'Component', :id => component_id end before { subject.register_component mock_component } it 'should send the command to the component for execution' do mock_component.expects(:execute_command!).once subject.execute_command command end end end describe '#send_agi_action' do it 'should send an appropriate AsyncAGI AMI action' do pending subject.wrapped_object.expects(:send_ami_action).once.with('AGI', 'Command' => 'FOO', 'Channel' => subject.channel) subject.send_agi_action 'FOO' end end describe '#send_ami_action' do let(:component_id) { UUIDTools::UUID.random_create } before { UUIDTools::UUID.stubs :random_create => component_id } it 'should send the action to the AMI client' do action = RubyAMI::Action.new 'foo', :foo => :bar translator.expects(:send_ami_action!).once.with action subject.send_ami_action 'foo', :foo => :bar end end end end end end