<?php
require_once '../AmiProxy.php';
require_once 
'../ApSignalHandler.php';
require_once 
'../AmiConnector.php';
/**
 * this class should test that the AmiProxy accomplish with its responsabilities
 * AmiProxy Responsabilities:
 * - Connect to Asterisk Manager through the socket localhost:5038
 * - Wait for incoming connections of clients that wish to execute one of the
 *   following commands:
 *   ::SendAction:
 *               When a client issue this command, is requesting to send
 *               the specified action to Asterisk. The action can, or cannot
 *               have an ActionID associated to it.
 *   ::LaunchEvent:
 *               This command asks the AmiProxy to send the specified event
 *               to all the clients suscribed (see below) to the event. In
 *               this way, any client can make aware of any event to all the
 *               the clients that are interested in the event.
 *   ::SuscribeEvent:
 *               In this way, a client tells to AmiProxy that it wants to be
 *               aware of the specified event. So, when the event shows up,
 *               (either because of an Asterisk Event or other client Event)
 *               the event data will be sent to the client
 *   ::UnsuscribeEvent:
 *               When a client request UnsuscribeEvent, then it wont be aware
 *               anymore of the specified event. No more event data for that
 *               event will be sent to that client.
 *    *Note about Events:
 *               events are launched either by Asterisk or other clients. Until
 *               now, we have no mechanism to avoid conflicts/collisions between
 *               Asterisk events and clients events, so its up to the developer of
 *               the client dont use events that are already taken by Asterisk
 * - Send commands to Asterisk
 * - Receive commands responses from Asterisk
 * - Receive events from Asterisk
 * - Relay events to the clients
  */
final class AmiProxyTest extends UnitTestCase
{
    
/**
     * callback method that will receive the commands responses
     * @const
     */
    
const DEFAULT_COMMANDS_CALLBACK 'ReceiveCommandResponse';
    
    
/**
     * callback method that will receive the events notifications
     * @const
     */
    
const DEFAULT_EVENTS_CALLBACK   'ReceiveEventNotification';
    
    
/**
     * reference to the ApSignalHandler object
     * @var object ApSignalHandler
     * @access private
     */
    
private $client_reference        NULL;
    
    
/**
     * reference to the object that makes the connection with Asterisk
     * @var object AmiConnector
     * @access private
     */
    
private $connector_reference     NULL;
    
    
/**
     * reference to the AmiProxy object
     * @var object AmiProxy
     * @access private
     */
    
private $server_reference        NULL;
    
    
/**
     * socket uri to be used for the AmiProxy server and the clients
     * @var string
     * @access private
     */
    
private $ami_socket_uri        '';
    
    
/**
     * username to connect to Asterisk Manager
     * @var string
     * @access private
     */
    
private $ami_username          '';
    
    
/**
     * password to connect to Asterisk Manager
     * @var string
     * @access private
     */
    
private $ami_password          '';
    
    
/**
     * flag file to send simple messages between processes (yeah, simple, not need unix sockets)
     * @var string
     * @access private
     */
    
private $assert_file           '/tmp/assert_AmiProxyTest';
    
    
/**
     * this message is used to test connectivity with asterisk
     * @var string
     * @access private
     */
    
private $pong_received_message 'pong received!';
    
    
/**
     * ApSignalHandler (client) process ID
     * @var int
     * @access private
     */
    
private $client_pid            = -1;
    
    
/**
     * AmiProxy (server) process ID
     * @var int
     * @access private
     */
    
private $server_pid            = -1;
    
    
/**
     * name of the last attached event
     * @var string
     * @access private
    */
    
private $last_attached_event   '';
    
    
/**
     * this var will help to test that the commands are being sent and responses are being relayed
     * this commands were selected because give a constant one line only output.
     * so commands like 'sip show peers' may make the test fail if are not implemented correctly
     * in general is a good idea not touch this, unless you really know what are you doing of course :)
     * @var array
     * @access private
     */
    
private $test_commands         = array
    (
        
'iax2 no debug' => 'IAX2 Debugging Disabled',
        
'sip no debug'  => 'SIP Debugging Disabled',
        
'agi no debug'  => 'AGI Debugging Disabled'
    
);
    
    
/**
     * this var will help to test that the events are being received and relayed
     * @var array
     * @access private
     */
    
private $test_events           = array
    (
        
'Link'          => array
        (
            
'Event'     => 'Link',
            
'Channel1'  => 'SIP/101-3f3f',
            
'Channel2'  => 'Zap/2-1',
            
'Uniqueid1' => '1094154427.10',
            
'Uniqueid2' => '1094154427.11'
        
),
        
'Newexten'        => array
        (
            
'Event'       => 'Newexten',
            
'Channel'     => 'SIP/101-3f3f',
            
'Context'     => 'localplans.development_1',
            
'Extension'   => '917070',
            
'Priority'    => '1',
            
'Application' => 'MAGI',
            
'AppData'     => '/etc/asterisk/agi/ks_doorman_pickup.py|channel_up',
            
'Uniqueid'    => '1094154427.10'
        
),
        
'Newchannel' => array
        (
            
'Event'       => 'Newchannel',
            
'Channel'     => 'Zap/2-1',
            
'State'       => 'Rsrvd',
            
'Callerid'    => '<unknown>',
            
'Uniqueid'    => '1094154427.11'
        
),
        
'Alarm'      => array
        (
            
'Event'   => 'Alarm',
            
'Alarm'   => 'Red',
            
'Channel' => '3'
        
)
    );
    
    
/**
     * queue of events to be attached
     * @var array
     * @access private
     */
    
private $events_queue        = array();
    
    
/**
     * set the default data
     * @return void
     * @param void
     * @access public
     */
    
public function __construct()
    {
        
$this->SetAmiProxyData('vaio''managerpass''unix:///var/run/issmanagerproxy.sock');
    }
    
    
/**
     * set the AmiProxy server info, user and passwd to connect with Asterisk and
     * the socket URI where will be listening for incoming client connections
     * @return void
     * @param string $UserName
     * @param string $Password
     * @param string $SocketUri
     * @access private
     */
    
private function SetAmiProxyData($UserName$Password$SocketUri)
    {
        
$this->ami_username   $UserName;
        
$this->ami_password   $Password;
        
$this->ami_socket_uri $SocketUri;
    }
    
    
/**
     * launch the AmiProxy and ApSignalHandler applications in different process.
     * so, at the end of this method 3 process exists:
     * - Test Process ( where all the asserts are done, and the only one will survive at the end )
     * - Server Process ( where AmiProxy is running and waiting for clients )
     * - Client Process ( where ApSignalHandler is running and attempting to connecto to AmiProxy )
     * by using pcntl_fork() call, but can be changed to launch the processes with bash commands
     * the parameter $ClientCallback it must be a method to execute before the client starts
     * the second argument is optional, its the connection to Asterisk (usually AmiConnector)
     * but it could be substituted by other class ( like AmiConnectorMock )
     * @return void
     * @param string $ClientCallback
     * @param string $AmiConnectorClass
     * @access private
     */
    
private function LaunchParallelProcesses($ClientCallback$ServerCallback NULL$AmiConnectorClass 'AmiConnector')
    {
        
$this->server_pid          pcntl_fork(); /* make a fork for the server application */
        
if ( $this->server_pid )
        {
            
$this->client_pid pcntl_fork(); /* make a fork for the client application */
            
if ( $this->client_pid )
            {
                return;
            }
            else
            {
                
ob_start();
                
$this->client_reference = new ApSignalHandler($this->ami_socket_uri);
                
$this->client_reference->SetDefaultCallbackObject($this);
                
$this->$ClientCallback();
                
$this->client_reference->HandleProxyData();
            }
        }
        else
        {
            
ob_start();
            
sleep(1);
            
$this->server_reference    = new AmiProxy($this->ami_socket_uri);
            
$this->connector_reference = new $AmiConnectorClass();
            
$this->connector_reference->SetAuthenticationInfo($this->ami_username$this->ami_password);
            if ( 
$ServerCallback !== NULL )
            {
                
$this->$ServerCallback();
            }
            
$this->connector_reference->ConnectToManager();
            
$this->server_reference->StartProxy($this->connector_reference);
        }
    }
    
    
/**
     * dummy wrapper to wait until the server and client processes do their thing
     * by default 5 seconds seems to be enough
     * @return void
     * @param int $WaitSeconds
     * @access private
     */
    
private function WaitForResults($WaitSeconds 5)
    {
        
sleep($WaitSeconds);
    }
    
    
/**
     * helper to test connectivity, this method is a callback for RegisterAction('Ping')
     * @return void
     * @param int $ActionId
     * @param array $ResponseData
     * @access public
     */
    
public function SentTestActionResponsePong($ActionId$ResponseData)
    {
        
file_put_contents($this->assert_file$this->pong_received_message);
    }
    
    
/**
     * helper to test send commands and responses, this method is a callback for RegisterCommand
     * @param int $ActionId
     * @param array $ResponseData
     * @access public
     */
    
public function ReceiveCommandResponse($ActionId$ResponseData)
    {
        
$response        str_replace("\n--END COMMAND--"''$ResponseData['ExtraData']);
        
$response_string $response[0] . "\n";
        
$file_descriptor fopen($this->assert_file'a');
        
fwrite($file_descriptor$response_string);
        
fclose($file_descriptor);
    }
    
    
/**
     * helper to test the event data received and the Detach() method
     * @param array $EventData
     * @access public
     */
    
public function ReceiveEventNotification($EventData)
    {
        
$event_name  = isset($EventData['Event']) ? $EventData['Event'] : NULL;
        if ( 
$event_name == $this->last_attached_event )
        {
            
$difference  array_diff_assoc($this->test_events[$event_name], $EventData);
            
$difference2 array_diff_assoc($EventData$this->test_events[$event_name]);
            if ( empty(
$difference) && empty($difference2) )
            {
                
$file_descriptor fopen($this->assert_file'a');
                
fwrite($file_descriptor"{$EventData['Event']}\n");
                
fclose($file_descriptor);
                
$this->client_reference->DetachEvent($EventData['Event']);
                
$this->AttachTestEvent();
                return;
            }
            exit;
        }
        exit;
    }
    
    
/**
     * belongs to simpletest API. used to remove the assert_file before each test
     * @return void
     * @param void
     * @access public
     */
    
public function setUp()
    {
        @
unlink($this->assert_file);
    }
    
    
/**
     * belongs to simpletest API. used to kill the server and client processes after each test
     * @return void
     * @param void
     * @access public
     */
    
public function tearDown()
    {
        
posix_kill($this->server_pid9);
        
posix_kill($this->client_pid9);
    }
    
    
/**
     * helper to test connectivity with asterisk, this method is a callback
     * for the LaunchParallelProcesses method
     * @return void
     * @param void
     * @access private
     */
    
private function RegisterTestAction()
    {
        
$this->client_reference->RegisterAction('Ping');
        
$this->client_reference->OnActionResponse('SentTestActionResponsePong');
    }
    
    
/**
     * helper to test sending and relaying commands, this method is a callback
     * for the LaunchParallelProcesses method
     * @return void
     * @param void
     * @access private
     */
    
private function RegisterTestCommands()
    {
        foreach ( 
$this->test_commands as $command_name => $command_expected_response )
        {
            
$this->client_reference->RegisterCommand($command_name);
            
$this->client_reference->OnActionResponse(AmiProxyTest::DEFAULT_COMMANDS_CALLBACK);
        }
    }
    
    
/**
     * helper to test sending and relaying commands, this method is a callback
     * for the LaunchParallelProcesses method
     * @return void
     * @param void
     * @access private
     */
    
private function AttachTestEvent()
    {
        
$next_event array_shift($this->events_queue);
        if ( 
$next_event !== NULL )
        {
            
$this->client_reference->AttachEvent($next_eventAmiProxyTest::DEFAULT_EVENTS_CALLBACK);
            
$this->last_attached_event $next_event;
            return;
        }
        exit;
    }
    
    
/**
     * helper to test reading and relaying events, this method is a callback
     * for the LaunchParallelProcesses method
     * @return void
     * @param void
     * @access private
     */
    
private function SetAmiConnectorExpectations()
    {
        
$counter       0;
        
$login_success = array('Response' => 'Success''Message' => 'Authentication accepted');
        
$this->connector_reference->setReturnValueAt($counter'ReceiveManagerPackets', array($login_success));
        
$counter++;
        for ( 
$i 0$i 100$i++ )
        {
            foreach ( 
$this->test_events as $event_name => $expected_response )
            {
                
$this->connector_reference->setReturnValueAt($counter'ReceiveManagerPackets', array($expected_response));
                
$counter++;
            }
        }
        
$this->connector_reference->setReturnValue('ReadManagerData'NULL);
        
$this->connector_reference->tally();
    }
    
    
/**
     * TEST: this tests connectivity with Asterisk sending a Ping action
     * @return void
     * @param void
     * @access public
     */
    
public function testConnectingToManager()
    {
        
$this->LaunchParallelProcesses('RegisterTestAction');
        
$this->WaitForResults();
        if ( 
file_exists($this->assert_file) )
        {
            
$client_message file_get_contents($this->assert_file);
            
$this->assertTrue($client_message  == $this->pong_received_message);
            return;
        }
        
$this->assertTrue(FALSE);
    }
    
    
/**
     * TEST: this tests sending and relaying asterisk commands
     * @return void
     * @param void
     * @access public
     */
    
public function testSendingCommands()
    {
        
$this->LaunchParallelProcesses('RegisterTestCommands');
        
$this->WaitForResults();
        if ( 
file_exists($this->assert_file) )
        {
            
$response_lines str_replace("\n"""file($this->assert_file));
            foreach ( 
$this->test_commands as $command_name => $expected_response )
            {
                
$this->assertEqual(array_shift($response_lines), $expected_response);
            }
            return;
        }
        
$this->assertTrue(FALSE);
    }
    
    
/**
     * TEST: this tests receiving and relaying asterisk Events
     * @return void
     * @param void
     * @access public
     */
    
public function testReceiveAndRelayEvents()
    {
        
$this->events_queue array_keys($this->test_events);
        
$override_methods   = array
        (
             
'ReceiveManagerPackets'
        
);
        
$mocked_class     'AmiConnectorMock';
        
Mock::generatePartial('AmiConnector'$mocked_class$override_methods);
        
$this->LaunchParallelProcesses('AttachTestEvent''SetAmiConnectorExpectations'$mocked_class);
        
$this->WaitForResults(10);
        
$file_exists file_exists($this->assert_file);
        
/* testing that at least some data has been received */
        
$this->assertTrue($file_exists);
        
/* testing that all the events has been received with the correct data
           actually the data is checked in the event callback, so here we only have
           to check that the event is in the file
        */
        
if ( $file_exists )
        {
            
$events_received str_replace("\n"""file($this->assert_file));
            foreach ( 
$this->test_events as $event_name => $expected_response )
            {
                
$this->assertEqual(array_shift($events_received), $event_name);
            }
        }
    }
}
?>