<?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); } }