<?php
/**
* Base class for all controller test cases.
*
* @copyright 2012 Perforce Software. All rights reserved.
* @license Please see LICENSE.txt in top-level folder of this distribution.
* @version <release>/<patch>
*/
namespace ModuleTest;
use P4\Connection\ConnectionInterface;
use P4\Spec\Protections;
use P4Test\TestCase;
use Zend\Dom\Query as DomQuery;
use Zend\Mvc\ResponseSender\SendResponseEvent;
use Zend\Mvc\Service\ServiceManagerConfig;
use Zend\ServiceManager\ServiceManager;
use Zend\Stdlib\Parameters;
class TestControllerCase extends TestCase
{
protected $application;
protected $configuration;
protected $superP4;
protected $userP4;
/**
* Extends parent by setting up the application and initializing the site.
*/
public function setUp()
{
// run parent to set up perforce connection and prepare directories
parent::setUp();
// disable console to behave like we communicate over http
\Zend\Console\Console::overrideIsConsole(false);
// initialize the application
$this->initApplication();
}
/**
* Set application config file. Useful for testing of this class.
*
* @param string $configuration path to the application config file
*/
public function setConfiguration($configuration)
{
$this->configuration = (string) $configuration;
}
/**
* Return application configuration.
*
* @return array application configuration
*/
public function getConfiguration()
{
$config = $this->configuration ?: array(
'modules' => array_map(
'basename',
array_map('dirname', glob(BASE_PATH . '/module/*/Module.php'))
),
'module_listener_options' => array(
'module_paths' => array(BASE_PATH . '/module')
),
);
return is_string($config) ? include $config : $config;
}
/**
* Return the mvc application instance. It will also initialize
* the application if it was not done before.
*
* @return \Zend\Mvc\Application application instance
*/
public function getApplication()
{
if (!$this->application) {
$this->initApplication();
}
return $this->application;
}
/**
* Reset the application.
*/
public function resetApplication()
{
$this->application = null;
}
/**
* Dispatch to a given url.
*
* @param string $url url to dispatch to
* @param bool $autoCsrf optional - by default auto includes CSRF token as needed
* @return string raw output likely the same as getResponse()->getContent()
*/
public function dispatch($url, $autoCsrf = true)
{
// set url on the request and run the application
$request = $this->getRequest();
$request->setUri($url);
// handle query parameters (if any)
$uriQuery = $request->getUri()->getQueryAsArray();
if ($request->getQuery()->count() == 0 && $uriQuery) {
$request->setQuery(new Parameters($uriQuery));
}
// if autoCsrf is enabled and this isn't a get; include the correct token
if ($autoCsrf && !$request->isGet()) {
$request->getPost()->set('_csrf', $this->application->getServiceManager()->get('csrf')->getToken());
}
// run the application and capture the response in the output buffer
ob_start();
$this->getApplication()->run();
return ob_get_clean();
}
/**
* Get the request instance.
*
* @return \Zend\Stdlib\RequestInterface request instance
*/
public function getRequest()
{
return $this->getApplication()->getRequest();
}
/**
* Get the application response.
*
* @return \Zend\Stdlib\ResponseInterface mvc-event response
*/
public function getResponse()
{
return $this->getApplication()->getResponse();
}
/**
* Get the application result (usualy what is returned by the controller).
*
* @return mixed mvc-event result
*/
public function getResult()
{
$event = $this->getApplication()->getMvcEvent();
return $event->getResult();
}
/**
* Evaluate if the given module was dispatched.
*
* @param string $moduleName name of the module to check
* @param string $message optional message
* @throws \PHPUnit_Framework_ExpectationFailedException
*/
public function assertModule($moduleName, $message = '')
{
$this->addToAssertionCount(1);
$controllerClass = $this->getMatchedControllerClass();
$matchedModule = current(explode('\\', $controllerClass));
$moduleName = strtolower($moduleName);
$matchedModule = strtolower($matchedModule);
if ($moduleName !== $matchedModule) {
$this->fail(
sprintf(
"Failed asserting module name was '%s', actual module is '%s'\n%s",
$moduleName,
$matchedModule,
$message
)
);
}
}
/**
* Evaluate if the given controller was dispatched.
*
* @param string $controllerClass name of the controller class to check
* @param string $message optional message
* @throws \PHPUnit_Framework_ExpectationFailedException
*/
public function assertController($controllerClass, $message = '')
{
$this->addToAssertionCount(1);
$controllerClass = strtolower($controllerClass);
$matchedController = strtolower($this->getMatchedControllerClass());
if ($controllerClass !== $matchedController) {
$this->fail(
sprintf(
"Failed asserting controller class was '%s', actual controller is '%s'\n%s",
$controllerClass,
$matchedController,
$message
)
);
}
}
/**
* Evaluate if the given action was dispatched.
*
* @param string $actionName name of the action to check
* @param string $message optional message
* @throws \PHPUnit_Framework_ExpectationFailedException
*/
public function assertAction($actionName, $message = '')
{
$this->addToAssertionCount(1);
$routeMatch = $this->getApplication()->getMvcEvent()->getRouteMatch();
$actionName = strtolower($actionName);
$matchedAction = strtolower($routeMatch->getParam('action'));
if ($actionName !== $matchedAction) {
$this->fail(
sprintf(
"Failed asserting action was '%s', actual action is '%s'\n%s",
$actionName,
$matchedAction,
$message
)
);
}
}
/**
* Convenient function for evaluating what module & controller & action were
* matched by the router in one method call.
*
* @param string $moduleName name of the module to check
* @param string $controllerClass name of the controller class to check
* @param string $actionName name of the action to check
* @param string $message optional message
*/
public function assertRouteMatch($moduleName, $controllerClass, $actionName, $message = '')
{
$this->assertModule($moduleName, $message);
$this->assertController($controllerClass, $message);
$this->assertAction($actionName, $message);
}
/**
* Evaluate if the given route was used.
*
* @param string $routeName route name to check for
* @param string $message optional message
*/
public function assertRoute($routeName, $message = '')
{
$this->addToAssertionCount(1);
$routeMatch = $this->getApplication()->getMvcEvent()->getRouteMatch();
$routeName = strtolower($routeName);
$matchedRouteName = strtolower($routeMatch->getMatchedRouteName());
if ($routeName !== $matchedRouteName) {
$this->fail(
sprintf(
"Failed asserting matched route was '%s', actual route is '%s'\n%s",
$routeName,
$matchedRouteName,
$message
)
);
}
}
/**
* Evaluate if the status code from the response matches the given value.
*
* @param int $statusCode status code to check
* @param string $message optional message
* @throws \PHPUnit_Framework_ExpectationFailedException
*/
public function assertResponseStatusCode($statusCode, $message = '')
{
$this->addToAssertionCount(1);
$matchedCode = $this->getResponse()->getStatusCode();
if ($statusCode !== $matchedCode) {
$this->fail(
sprintf(
"Failed asserting response code is %d, actual value is %d\n%s",
$statusCode,
$matchedCode,
$message
)
);
}
}
/**
* Evaluate the path to verify that it exists in the response body.
*
* @param string $path path to check
* @throws \PHPUnit_Framework_ExpectationFailedException
*/
public function assertQuery($path, $message = '')
{
$this->addToAssertionCount(1);
$match = $this->query($path);
if (count($match) <= 0) {
$this->fail(
sprintf(
"Failed asserting node denoted by %s exists\n%s",
$path,
$message
)
);
}
}
/**
* Evaluate the path to verify that it doesn't exist in the response body.
*
* @param string $path path to check
* @throws \PHPUnit_Framework_ExpectationFailedException
*/
public function assertNotQuery($path, $message = '')
{
$this->addToAssertionCount(1);
$match = $this->query($path);
if (count($match) > 0) {
$this->fail(
sprintf(
"Failed asserting node denoted by %s does not exist\n%s",
$path,
$message
)
);
}
}
/**
* Evaluate the path to verify that it occurs in the response body exactly
* the given numer of times.
*
* @param string $path path to check
* @throws \PHPUnit_Framework_ExpectationFailedException
*/
public function assertQueryCount($path, $count, $message = '')
{
$this->addToAssertionCount(1);
$match = $this->query($path);
if (count($match) !== $count) {
$this->fail(
sprintf(
"Failed asserting node denoted by %s occurs exactly %d times\n%s",
$path,
$count,
$message
)
);
}
}
/**
* Evaluate the dom node specified by the path to verify that it contains the given content.
* @param string $path dom path to check
* @param string $content content to check for dom node value
* @param string $message optional message
*/
public function assertQueryContentContains($path, $content, $message = '')
{
$this->addToAssertionCount(1);
$nodeList = $this->query($path);
$found = false;
$nodeList->rewind();
while (!$found && $nodeList->valid()) {
$found = strpos($nodeList->current()->nodeValue, $content) !== false;
$nodeList->next();
}
if (!$found) {
$this->fail(
sprintf(
"Failed asserting node denoted by %s contains %s\n%s",
$path,
$content,
$message
)
);
}
}
/**
* Perform a CSS selector query. Return number of occurences the path
* exists in the response body.
*
* @param string $path path to check for
* @return \Zend\Dom\NodeList list of dom nodes with the given path
*/
protected function query($path)
{
$dom = new DomQuery($this->getResponse()->getBody());
return $dom->execute($path);
}
/**
* Return class name of the controller matched during the route event.
*
* @return string matched controller class name
*/
protected function getMatchedControllerClass()
{
$application = $this->getApplication();
$routeMatch = $application->getMvcEvent()->getRouteMatch();
$controller = $routeMatch->getParam('controller');
$controllerManager = $application->getServiceManager()->get('ControllerLoader');
return get_class($controllerManager->get($controller));
}
/**
* Initialize the application - set the ServiceManager, load modules and
* bootstrap it. It then takes the aggregated application/modules config,
* substitutes values for testing and sets it back to the ServiceManager.
* Also marks the request object to denote the testing environment.
*/
protected function initApplication()
{
// load modules with default application configuration
$configuration = $this->getConfiguration();
$serviceManager = new ServiceManager(new ServiceManagerConfig());
$serviceManager->setService('ApplicationConfig', $configuration);
$serviceManager->get('ModuleManager')->loadModules();
// configure service manager for testing
$this->configureServiceManager($serviceManager);
// mark request to denote we are in testing environment
$application = $serviceManager->get('Application');
$request = $application->getRequest();
$request->isTest = true;
// mark response event to denote we are testing
$responseListener = $serviceManager->get('SendResponseListener')->getEventManager();
$responseListener->attach(
SendResponseEvent::EVENT_SEND_RESPONSE,
function ($event) {
$event->setParam('isTest', true);
}
);
// bootstrap application
$application = $serviceManager->get('Application');
$application->bootstrap();
$this->application = $application;
}
/**
* Create a Perforce connection for testing. The perforce connection will
* connect using a p4d started with the -i (run for inetd) flag.
*
* Extends parent to ensure we create a super, admin and plain user connection
* (parent only makes super).
*
* Also, sets the admin connection as the default connection and $this->p4
* ensuring the bulk of our work is done with those permissions (to more
* accurately mirror our suggested deployment configuration).
*
* @param string|null $type allow caller to force the API
* implementation.
* @return P4\Connection\ConnectionInterface a Perforce API implementation
*/
public function createP4Connection($type = null)
{
// let parent take care of the super user connection creation super
// user will have the id defined by p4Params, tester by default
parent::createP4Connection($type);
// pull out the parent created super connection for later use
$superP4 = $this->superP4 = $this->p4;
// create the admin user and store its connection
$adminP4 = $this->p4 = \P4\Connection\Connection::factory(
$this->getP4Params('port'),
'admin',
null,
''
);
$userForm = array(
'User' => 'admin',
'Email' => 'admin@testhost',
'FullName' => 'Admin User',
'Password' => ''
);
$adminP4->run('user', '-i', $userForm);
// add 'admin' protections for our new admin user
$protections = Protections::fetch($this->superP4);
$protections->addProtection('admin', 'user', 'admin', '*', '//...');
$protections->save();
// clear the client from the super p4 and set a client on the new admin $this->p4
$superP4->setClient(null);
$clientForm = array(
'Client' => $this->p4Params['client'],
'Owner' => 'admin',
'Root' => $this->p4Params['clientRoot'] . '/adminuser',
'View' => array('//depot/... //' . $this->p4Params['client'] . '/...')
);
$superP4->run('client', array('-d', $this->p4Params['client']));
$adminP4->run('client', '-i', $clientForm);
$adminP4->setClient($this->p4Params['client']);
\P4\Connection\Connection::setDefaultConnection($adminP4);
// lastly create the nonadmin standard user account
$userP4 = \P4\Connection\Connection::factory(
$this->getP4Params('port'),
'nonadmin',
null,
''
);
// actually create the user
$userForm = array(
'User' => 'nonadmin',
'Email' => 'nonadmin@testhost',
'FullName' => 'Test User',
'Password' => ''
);
$userP4->run('user', '-i', $userForm);
$this->userP4 = $userP4;
}
/**
* Extend parrent to pass the super user connection as $this->p4 now refers to admin connection.
*
* @param string $user user to create
* @param array $paths list of paths to grant user access to each path can be specified as:
* ['path' => 'permission'] or ['path']
* @param ConnectionInterafce $p4Super optional - super user connection needed
* to modify protections table
* @return Connection connection for the new user
*/
public function connectWithAccess($user, array $paths, ConnectionInterface $p4Super = null)
{
return parent::connectWithAccess($user, $paths, $p4Super ?: $this->superP4);
}
/**
* Configure service manager for testing environment.
*
* @param ServiceManager $serviceManager service manager instance
*/
protected function configureServiceManager(ServiceManager $serviceManager)
{
// allow overriding
$allowOverride = $serviceManager->getAllowOverride();
$serviceManager->setAllowOverride(true);
// set p4 factory to return p4 connection for testing
if (!$this->p4 instanceof \P4\Connection\ConnectionInterface
|| !$this->userP4 instanceof \P4\Connection\ConnectionInterface
|| !$this->superP4 instanceof \P4\Connection\ConnectionInterface
) {
$this->createP4Connection();
// now that we have a non-admin user, ensure only admins can
// access keys to allow us to actually exercise things.
// this is only supported on 13.1+ servers so we do it selectively.
if ($this->superP4->isServerMinVersion('2013.1')) {
$this->superP4->run('configure', array('set', 'dm.keys.hide=1'));
}
}
// (re)create the cache, client pool, and translator services for the admin account
$adminP4 = $this->p4;
$adminP4->setService(
'cache',
function ($p4) {
$cache = new \Record\Cache\Cache($p4);
$cache->setCacheDir(DATA_PATH . '/cache');
return $cache;
}
);
$adminP4->setService(
'clients',
function ($p4) {
$clients = new \P4\ClientPool\ClientPool($p4);
$clients->setMax(10)->setRoot(DATA_PATH . '/clients')->setPrefix('test-');
return $clients;
}
);
$adminP4->setService(
'translator',
function ($p4) use ($serviceManager) {
return $serviceManager->get('translator');
}
);
// setup the super connection to use the same cache and translator and have a client pool
$superP4 = $this->superP4;
$superP4->setService(
'cache',
function () use ($adminP4) {
return $adminP4->getService('cache');
}
);
$superP4->setService(
'clients',
function ($p4) {
$clients = new \P4\ClientPool\ClientPool($p4);
$clients->setMax(10)->setRoot(DATA_PATH . '/clients')->setPrefix('test-');
return $clients;
}
);
$superP4->setService(
'translator',
function () use ($adminP4) {
return $adminP4->getService('translator');
}
);
// (re)create the client pool and borrow the admin account's cache and translator services for the user account
$userP4 = $this->userP4;
$userP4->setService(
'clients',
function ($p4) {
$clients = new \P4\ClientPool\ClientPool($p4);
$clients->setMax(10)->setRoot(DATA_PATH . '/clients')->setPrefix('test-');
return $clients;
}
);
$userP4->setService(
'cache',
function () use ($adminP4) {
return $adminP4->getService('cache');
}
);
$userP4->setService(
'translator',
function () use ($adminP4) {
return $adminP4->getService('translator');
}
);
// configure the application service manager to use our test connections
$serviceManager->setFactory(
'p4',
function () use ($userP4) {
return $userP4;
}
);
$serviceManager->setFactory(
'p4_admin',
function () use ($adminP4) {
return $adminP4;
}
);
$serviceManager->setFactory(
'p4_super',
function () use ($superP4) {
return $superP4;
}
);
$serviceManager->setFactory(
'p4_user',
function () use ($userP4) {
return $userP4;
}
);
// pretend the non-admin user is logged in
$serviceManager->setFactory(
'auth',
function () {
$storage = new \Zend\Authentication\Storage\NonPersistent;
$storage->write(array('id' => 'nonadmin'));
return new \Zend\Authentication\AuthenticationService($storage);
}
);
// configure mail transport to write messages to disk
$path = DATA_PATH . '/mail';
$config = $serviceManager->get('config');
$config['mail']['transport'] = array('path' => $path);
$serviceManager->setService('config', $config);
@mkdir($path);
}
}