├── bin ├── composer-nette └── composer-nette.php ├── src ├── DbConnectionMock.php ├── DoctrineDbTestCase.php ├── IntegrationTestCase.php ├── Api │ ├── ApiaryLogger.php │ ├── HttpRequestMock.php │ ├── DocumentationGenerator.php │ ├── HttpResponseMock.php │ ├── HttpLogger.php │ └── RequestProcessor.php ├── ApiTestCase.php ├── Bootstrap.php ├── CompiledContainer.php ├── PresenterRunner.php ├── DoctrineDbSetup.php └── HttpServer.php ├── composer.json ├── README.md └── license.md /bin/composer-nette: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | onConnect($this); 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/DoctrineDbTestCase.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | abstract class DoctrineDbTestCase extends TestCase 24 | { 25 | 26 | use DoctrineDbSetup; 27 | 28 | 29 | 30 | protected function tearDown() 31 | { 32 | $this->tearDownContainer(); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/IntegrationTestCase.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | abstract class IntegrationTestCase extends TestCase 25 | { 26 | 27 | use CompiledContainer; 28 | 29 | 30 | 31 | protected function tearDown() 32 | { 33 | $this->tearDownContainer(); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kdyby/tester-extras", 3 | "type": "library", 4 | "description": "Useful classes for testing Nette applications and libraries with Nette Tester", 5 | "keywords": ["nette", "tester"], 6 | "homepage": "http://kdyby.org", 7 | "license": ["BSD-3-Clause", "GPL-2.0", "GPL-3.0"], 8 | "authors": [ 9 | { 10 | "name": "Filip Procházka", 11 | "homepage": "http://filip-prochazka.com", 12 | "email": "filip@prochazka.su" 13 | } 14 | ], 15 | "support": { 16 | "email": "filip@prochazka.su", 17 | "issues": "https://github.com/Kdyby/TesterExtras/issues" 18 | }, 19 | "require": { 20 | "ext-curl": "*", 21 | "nette/tester": "~1.3@dev", 22 | "symfony/process": "~2.6" 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "Kdyby\\TesterExtras\\": "src/" 27 | } 28 | }, 29 | "bin": [ 30 | "bin/composer-nette" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Kdyby/TesterExtras 2 | ====== 3 | 4 | [![Build Status](https://travis-ci.org/Kdyby/TesterExtras.svg?branch=master)](https://travis-ci.org/Kdyby/TesterExtras) 5 | [![Downloads this Month](https://img.shields.io/packagist/dm/Kdyby/TesterExtras.svg)](https://packagist.org/packages/Kdyby/TesterExtras) 6 | [![Latest stable](https://img.shields.io/packagist/v/Kdyby/TesterExtras.svg)](https://packagist.org/packages/Kdyby/TesterExtras) 7 | [![Join the chat at https://gitter.im/Kdyby/Help](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Kdyby/Help) 8 | 9 | 10 | Requirements 11 | ------------ 12 | 13 | Kdyby/TesterExtras requires PHP 5.3.2 or higher. 14 | 15 | - [Nette Tester](https://github.com/nette/tester) 16 | 17 | 18 | 19 | ----- 20 | 21 | Homepage [http://www.kdyby.org](http://www.kdyby.org) and repository [http://github.com/Kdyby/TesterExtras](http://github.com/Kdyby/TesterExtras). 22 | -------------------------------------------------------------------------------- /src/Api/ApiaryLogger.php: -------------------------------------------------------------------------------- 1 | 22 | * @author Matěj Koubík 23 | */ 24 | class ApiaryLogger extends HttpLogger 25 | { 26 | 27 | /** 28 | * @var string 29 | */ 30 | public $baseUri; 31 | 32 | 33 | 34 | protected function formatRequestHead(HttpRequestMock $request, $path) 35 | { 36 | $contentType = $request->headers['content-type']; 37 | return '+ Request' . ($contentType ? " ($contentType)" : '') . PHP_EOL; 38 | } 39 | 40 | 41 | 42 | protected function formatResponseHead(HttpResponseMock $response) 43 | { 44 | $contentType = $response->headers['Content-Type']; 45 | return sprintf('+ Response %s', $response->getCode()) . ($contentType ? " ($contentType)" : '') . PHP_EOL; 46 | } 47 | 48 | 49 | 50 | public function formatHeaders(array $headers) 51 | { 52 | unset($headers['Content-Type'], $headers['Vary'], $headers['Cache-Control'], $headers['Expires']); 53 | return parent::formatHeaders($headers); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/Api/HttpRequestMock.php: -------------------------------------------------------------------------------- 1 | 21 | * @author Matěj Koubík 22 | * 23 | * @property array $headers 24 | * @property-read array $headers 25 | */ 26 | class HttpRequestMock extends Nette\Http\Request 27 | { 28 | 29 | /** 30 | * @var string 31 | */ 32 | private $rawBody = NULL; 33 | 34 | /** 35 | * @var array 36 | */ 37 | private $headers; 38 | 39 | 40 | 41 | public function __construct( 42 | UrlScript $url, $query = NULL, $post = NULL, $files = NULL, $cookies = NULL, 43 | $headers = NULL, $method = NULL, $remoteAddress = NULL, $remoteHost = NULL, $rawBodyCallback = NULL 44 | ) { 45 | parent::__construct($url, $query, $post, $files, $cookies, $headers, $method, $remoteAddress, $remoteHost, $rawBodyCallback); 46 | $this->headers = (array) $headers; 47 | } 48 | 49 | 50 | 51 | /** 52 | * @return array 53 | */ 54 | public function getOriginalHeaders() 55 | { 56 | return $this->headers; 57 | } 58 | 59 | 60 | 61 | public function setRawBody($body) 62 | { 63 | $this->rawBody = $body; 64 | return $this; 65 | } 66 | 67 | 68 | 69 | public function getRawBody() 70 | { 71 | return $this->rawBody; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/ApiTestCase.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | abstract class ApiTestCase extends TestCase 25 | { 26 | 27 | use DoctrineDbSetup; 28 | 29 | /** 30 | * @var Nette\Application\IRouter 31 | */ 32 | protected $router; 33 | 34 | /** 35 | * @var string 36 | */ 37 | protected $baseUrl = 'Https://www.kdyby.org/api/v1'; 38 | 39 | 40 | 41 | protected function setUp() 42 | { 43 | parent::setUp(); 44 | 45 | $sl = $this->getContainer(); 46 | $services = $sl->findByType('Nette\Application\IRouter'); 47 | $this->router = $sl->createService($services[0]); 48 | } 49 | 50 | 51 | 52 | /** 53 | * @param string $method 54 | * @param string $path 55 | * @param array $headers 56 | * @param string|null $body 57 | * @return Http\Response 58 | */ 59 | protected function getHttpResponse($method, $path, array $headers = [], $body = NULL) 60 | { 61 | $sl = $this->getContainer(); 62 | 63 | $processor = new Api\RequestProcessor($sl, $logger = new Api\HttpLogger(), $this->baseUrl); 64 | $processor->setRouter($this->router); 65 | 66 | try { 67 | $processor->request($method, $path, $headers, $body); 68 | 69 | } finally { 70 | $this->appResponse = $processor->getAppResponse(); 71 | 72 | $classRefl = new \ReflectionClass($this); 73 | $logger->write(dirname($classRefl->getFileName()) . '/requests.log'); 74 | } 75 | 76 | return $processor->getHttpResponse(); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/Api/DocumentationGenerator.php: -------------------------------------------------------------------------------- 1 | 23 | * @author Matěj Koubík 24 | */ 25 | class DocumentationGenerator extends Nette\Object 26 | { 27 | 28 | use Kdyby\TesterExtras\DoctrineDbSetup; 29 | 30 | /** 31 | * @var array 32 | */ 33 | public $defaultHeaders = []; 34 | 35 | /** 36 | * @var Container 37 | */ 38 | protected $sl; 39 | 40 | /** 41 | * @var ApiaryLogger 42 | */ 43 | protected $logger; 44 | 45 | /** 46 | * @var array 47 | */ 48 | protected $configs; 49 | 50 | /** 51 | * @var string 52 | */ 53 | protected $basePath; 54 | 55 | 56 | 57 | public function __construct($basePath = 'https://www.kdyby.org/api/v1', array $configs = []) 58 | { 59 | $this->logger = new ApiaryLogger(); 60 | $this->configs = $configs; 61 | $this->basePath = $basePath; 62 | } 63 | 64 | 65 | 66 | public function execute(\Closure $callback) 67 | { 68 | $this->sl = $this->createContainer($this->configs); 69 | 70 | try { 71 | call_user_func($callback, $this->sl, $this->logger); 72 | 73 | } finally { 74 | /** @var Nette\Http\Session $session */ 75 | $session = $this->sl->getByType('Nette\Http\Session'); 76 | if ($session->isStarted()) { 77 | $session->destroy(); 78 | } 79 | 80 | $this->logger->outputFilter = NULL; 81 | } 82 | } 83 | 84 | 85 | 86 | /** 87 | * @param string $method 88 | * @param string $path 89 | * @param array $headers 90 | * @param string|null $body 91 | * @return RequestProcessor 92 | */ 93 | public function request($method, $path, array $headers = [], $body = NULL) 94 | { 95 | $processor = new RequestProcessor($this->sl, $this->logger, $this->basePath); 96 | 97 | try { 98 | $processor->request($method, $path, $headers + $this->defaultHeaders, $body); 99 | 100 | } finally { 101 | $this->logger->output(); 102 | } 103 | 104 | return $processor; 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | Licenses 2 | ======== 3 | 4 | Good news! You may use Kdyby Framework under the terms of either 5 | the New BSD License or the GNU General Public License (GPL) version 2 or 3. 6 | 7 | The BSD License is recommended for most projects. It is easy to understand and it 8 | places almost no restrictions on what you can do with the framework. If the GPL 9 | fits better to your project, you can use the framework under this license. 10 | 11 | You don't have to notify anyone which license you are using. You can freely 12 | use Kdyby Framework in commercial projects as long as the copyright header 13 | remains intact. 14 | 15 | 16 | 17 | New BSD License 18 | --------------- 19 | 20 | Copyright (c) 2008 Filip Procházka (http://filip-prochazka.com) 21 | All rights reserved. 22 | 23 | Redistribution and use in source and binary forms, with or without modification, 24 | are permitted provided that the following conditions are met: 25 | 26 | * Redistributions of source code must retain the above copyright notice, 27 | this list of conditions and the following disclaimer. 28 | 29 | * Redistributions in binary form must reproduce the above copyright notice, 30 | this list of conditions and the following disclaimer in the documentation 31 | and/or other materials provided with the distribution. 32 | 33 | * Neither the name of "Kdyby Framework" nor the names of its contributors 34 | may be used to endorse or promote products derived from this software 35 | without specific prior written permission. 36 | 37 | This software is provided by the copyright holders and contributors "as is" and 38 | any express or implied warranties, including, but not limited to, the implied 39 | warranties of merchantability and fitness for a particular purpose are 40 | disclaimed. In no event shall the copyright owner or contributors be liable for 41 | any direct, indirect, incidental, special, exemplary, or consequential damages 42 | (including, but not limited to, procurement of substitute goods or services; 43 | loss of use, data, or profits; or business interruption) however caused and on 44 | any theory of liability, whether in contract, strict liability, or tort 45 | (including negligence or otherwise) arising in any way out of the use of this 46 | software, even if advised of the possibility of such damage. 47 | 48 | 49 | 50 | GNU General Public License 51 | -------------------------- 52 | 53 | GPL licenses are very very long, so instead of including them here we offer 54 | you URLs with full text: 55 | 56 | - [GPL version 2](http://www.gnu.org/licenses/gpl-2.0.html) 57 | - [GPL version 3](http://www.gnu.org/licenses/gpl-3.0.html) 58 | -------------------------------------------------------------------------------- /src/Bootstrap.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class Bootstrap 24 | { 25 | 26 | public static function setup($rootDir) 27 | { 28 | // configure environment 29 | umask(0); 30 | Tester\Environment::setup(); 31 | class_alias('Tester\Assert', 'Assert'); 32 | date_default_timezone_set('Europe/Prague'); 33 | 34 | // create temporary directory 35 | define('TEMP_DIR', $rootDir . '/tmp/' . (isset($_SERVER['argv']) ? md5(serialize($_SERVER['argv'])) : getmypid())); 36 | Tester\Helpers::purge(TEMP_DIR); 37 | @chmod(TEMP_DIR, 0777); 38 | 39 | // Tracy may not be defined 40 | if(class_exists('Tracy\Debugger')) { 41 | Tracy\Debugger::$logDirectory = TEMP_DIR; 42 | } 43 | 44 | // $_SERVER = array_intersect_key($_SERVER, array_flip(array( 45 | // 'PHP_SELF', 'SCRIPT_NAME', 'SERVER_ADDR', 'SERVER_SOFTWARE', 'HTTP_HOST', 'DOCUMENT_ROOT', 'OS', 'argc', 'argv' 46 | // ))); 47 | $_SERVER['REQUEST_TIME'] = 1234567890; 48 | $_ENV = $_GET = $_POST = $_FILES = array(); 49 | } 50 | 51 | 52 | 53 | /** 54 | * @param array $dirs 55 | * @return Nette\Loaders\RobotLoader 56 | */ 57 | public static function createRobotLoader($dirs = array()) 58 | { 59 | if (!is_array($dirs)) { 60 | $dirs = func_get_args(); 61 | } 62 | 63 | $loader = new Nette\Loaders\RobotLoader; 64 | $loader->setCacheStorage(new Nette\Caching\Storages\FileStorage(TEMP_DIR)); 65 | $loader->autoRebuild = TRUE; 66 | 67 | foreach ($dirs as $dir) { 68 | $loader->addDirectory($dir); 69 | } 70 | 71 | return $loader->register(); 72 | } 73 | 74 | 75 | 76 | public static function setupDoctrineDatabase(Nette\DI\Container $sl, $sqls = array(), $prefix = 'kdyby', &$id = NULL) 77 | { 78 | $db = $sl->getByType('Kdyby\Doctrine\Connection'); // default connection 79 | /** @var \Kdyby\Doctrine\Connection $db */ 80 | 81 | $testDbName = $prefix . '_test_' . ($id = ($id ?: getmypid())); 82 | $db->exec("DROP DATABASE IF EXISTS `$testDbName`"); 83 | $db->exec("CREATE DATABASE `$testDbName`"); 84 | $db->exec("USE `$testDbName`"); 85 | 86 | foreach ($sqls as $file) { 87 | Kdyby\Doctrine\Helpers::loadFromFile($db, $file); 88 | } 89 | 90 | // drop on shutdown 91 | register_shutdown_function(function () use ($db, $testDbName) { 92 | $db->exec("DROP DATABASE IF EXISTS `$testDbName`"); 93 | }); 94 | 95 | return $testDbName; 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /src/CompiledContainer.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | trait CompiledContainer 24 | { 25 | 26 | /** 27 | * @var Container|\SystemContainer 28 | */ 29 | private $container; 30 | 31 | 32 | 33 | /** 34 | * @return Container 35 | */ 36 | protected function getContainer() 37 | { 38 | if ($this->container === NULL) { 39 | $this->container = $this->createContainer(); 40 | } 41 | 42 | return $this->container; 43 | } 44 | 45 | 46 | 47 | /** 48 | * @return bool 49 | */ 50 | protected function isContainerCreated() 51 | { 52 | return $this->container !== NULL; 53 | } 54 | 55 | 56 | 57 | protected function refreshContainer() 58 | { 59 | $container = $this->getContainer(); 60 | 61 | /** @var Session $session */ 62 | if (($session = $container->getByType('Nette\Http\Session')) && $session->isStarted()) { 63 | $session->close(); 64 | } 65 | 66 | $this->container = new $container(); 67 | $this->container->initialize(); 68 | } 69 | 70 | 71 | 72 | /** 73 | * @return bool 74 | */ 75 | protected function tearDownContainer() 76 | { 77 | if ($this->container) { 78 | /** @var Session $session */ 79 | $session = $this->getContainer()->getByType('Nette\Http\Session'); 80 | if ($session->isStarted()) { 81 | $session->destroy(); 82 | } 83 | 84 | $this->container = NULL; 85 | 86 | return TRUE; 87 | } 88 | 89 | return FALSE; 90 | } 91 | 92 | 93 | 94 | /** 95 | * @return Nette\Configurator 96 | */ 97 | protected function doCreateConfiguration() 98 | { 99 | $config = new Nette\Configurator(); 100 | $config->addParameters([ 101 | // vendor/kdyby/tester-extras/src 102 | 'rootDir' => $rootDir = dirname(dirname(dirname(dirname(__DIR__)))), 103 | 'appDir' => $rootDir . '/app', 104 | 'wwwDir' => $rootDir . '/www', 105 | ]); 106 | 107 | // shared compiled container for faster tests 108 | $config->setTempDirectory(dirname(TEMP_DIR)); 109 | 110 | return $config; 111 | } 112 | 113 | 114 | 115 | /** 116 | * @param array $configs 117 | * @return Container 118 | */ 119 | protected function createContainer(array $configs = []) 120 | { 121 | $config = $this->doCreateConfiguration(); 122 | 123 | foreach ($configs as $file) { 124 | $config->addConfig($file); 125 | } 126 | 127 | /** @var Container $container */ 128 | $container = $config->createContainer(); 129 | 130 | return $container; 131 | } 132 | 133 | 134 | 135 | /** 136 | * @param string $type 137 | * @return object 138 | */ 139 | public function getService($type) 140 | { 141 | $container = $this->getContainer(); 142 | if ($object = $container->getByType($type, FALSE)) { 143 | return $object; 144 | } 145 | 146 | return $container->createInstance($type); 147 | } 148 | 149 | } 150 | -------------------------------------------------------------------------------- /src/Api/HttpResponseMock.php: -------------------------------------------------------------------------------- 1 | 23 | * @author Matěj Koubík 24 | * 25 | * @property array $headers 26 | */ 27 | class HttpResponseMock extends Http\Response implements Http\IResponse 28 | { 29 | 30 | /** 31 | * @var int 32 | */ 33 | private $code = self::S200_OK; 34 | 35 | /** 36 | * @var array 37 | */ 38 | private $headers = []; 39 | 40 | /** 41 | * @var string 42 | */ 43 | private $content; 44 | 45 | /** 46 | * @var Nette\Application\IResponse|JsonResponse 47 | */ 48 | private $appResponse; 49 | 50 | 51 | 52 | public function setCode($code) 53 | { 54 | $this->code = $code; 55 | } 56 | 57 | 58 | 59 | public function getCode() 60 | { 61 | return $this->code; 62 | } 63 | 64 | 65 | 66 | /** 67 | * @return Nette\Application\IResponse|JsonResponse 68 | */ 69 | public function getAppResponse() 70 | { 71 | return $this->appResponse; 72 | } 73 | 74 | 75 | 76 | /** 77 | * @param Nette\Application\IResponse|JsonResponse $response 78 | */ 79 | public function setAppResponse(Nette\Application\IResponse $response) 80 | { 81 | $this->appResponse = $response; 82 | } 83 | 84 | 85 | 86 | /** 87 | * @param string $name 88 | * @param string $value 89 | * @param string $time 90 | * @param string $path 91 | * @param string $domain 92 | * @param bool $secure 93 | * @param bool $httpOnly 94 | * @return Http\Response 95 | */ 96 | public function setCookie($name, $value, $time, $path = NULL, $domain = NULL, $secure = NULL, $httpOnly = NULL) 97 | { 98 | return $this->setHeader('Set-Cookie', sprintf( 99 | '%s; Expires=%s', 100 | http_build_query([$name => $value]), 101 | Nette\Utils\DateTime::from($time)->format('U')) 102 | ); 103 | } 104 | 105 | 106 | 107 | /** 108 | * @param string $name 109 | * @param string $value 110 | */ 111 | public function setHeader($name, $value) 112 | { 113 | $this->headers[$name] = $value; 114 | 115 | return $this; 116 | } 117 | 118 | 119 | 120 | /** 121 | * @param string $name 122 | * @param string $value 123 | */ 124 | public function addHeader($name, $value) 125 | { 126 | $this->headers[$name] = $value; 127 | 128 | return $this; 129 | } 130 | 131 | 132 | 133 | /** 134 | * @param string $header 135 | * @param string $default 136 | * @return string|NULL 137 | */ 138 | public function getHeader($header, $default = NULL) 139 | { 140 | return isset($this->headers[$header]) ? $this->headers[$header] : $default; 141 | } 142 | 143 | 144 | 145 | public function getHeaders() 146 | { 147 | return $this->headers; 148 | } 149 | 150 | 151 | 152 | public function getContent() 153 | { 154 | return $this->content; 155 | } 156 | 157 | 158 | 159 | public function setContent($content) 160 | { 161 | $this->content = $content; 162 | } 163 | 164 | 165 | 166 | /** 167 | * @return array|\stdClass 168 | */ 169 | public function getPayload() 170 | { 171 | if (!$this->appResponse instanceof JsonResponse) { 172 | throw new \RuntimeException("Unexpected response"); 173 | } 174 | 175 | return $this->appResponse->getPayload(); 176 | } 177 | 178 | } 179 | -------------------------------------------------------------------------------- /src/PresenterRunner.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | trait PresenterRunner 23 | { 24 | 25 | /** 26 | * @var Nette\Application\UI\Presenter 27 | */ 28 | protected $presenter; 29 | 30 | /** 31 | * @var Nette\Http\UrlScript 32 | */ 33 | private $fakeUrl; 34 | 35 | 36 | 37 | protected function openPresenter($fqa) 38 | { 39 | /** @var IntegrationTestCase|PresenterRunner $this */ 40 | 41 | $sl = $this->getContainer(); 42 | 43 | //insert fake HTTP Request for Presenter - for presenter->link() etc. 44 | $params = $sl->getParameters(); 45 | $this->fakeUrl = new Nette\Http\UrlScript(isset($params['console']['url']) ? $params['console']['url'] : 'localhost'); 46 | 47 | $sl->removeService('httpRequest'); 48 | $sl->addService('httpRequest', new HttpRequest($this->fakeUrl, NULL, [], [], [], [], PHP_SAPI, '127.0.0.1', '127.0.0.1')); 49 | 50 | /** @var Nette\Application\IPresenterFactory $presenterFactory */ 51 | $presenterFactory = $sl->getByType('Nette\Application\IPresenterFactory'); 52 | 53 | $name = substr($fqa, 0, $namePos = strrpos($fqa, ':')); 54 | $class = $presenterFactory->getPresenterClass($name); 55 | 56 | if (!class_exists($overriddenPresenter = 'DamejidloTests\\' . $class)) { 57 | $classPos = strrpos($class, '\\'); 58 | eval('namespace DamejidloTests\\' . substr($class, 0, $classPos) . '; class ' . substr($class, $classPos + 1) . ' extends \\' . $class . ' { ' 59 | . 'protected function startup() { if ($this->getParameter("__terminate") == TRUE) { $this->terminate(); } parent::startup(); } ' 60 | . 'public static function getReflection() { return parent::getReflection()->getParentClass(); } ' 61 | . '}'); 62 | } 63 | 64 | $this->presenter = $sl->createInstance($overriddenPresenter); 65 | $sl->callInjects($this->presenter); 66 | 67 | $app = $this->getService('Nette\Application\Application'); 68 | $appRefl = new \ReflectionProperty($app, 'presenter'); 69 | $appRefl->setAccessible(TRUE); 70 | $appRefl->setValue($app, $this->presenter); 71 | 72 | $this->presenter->autoCanonicalize = FALSE; 73 | $this->presenter->run(new Nette\Application\Request($name, 'GET', ['action' => substr($fqa, $namePos + 1) ?: 'default', '__terminate' => TRUE])); 74 | } 75 | 76 | 77 | 78 | /** 79 | * @param $action 80 | * @param string $method 81 | * @param array $params 82 | * @param array $post 83 | * @return Nette\Application\IResponse 84 | */ 85 | protected function runPresenterAction($action, $method = 'GET', $params = [], $post = []) 86 | { 87 | /** @var IntegrationTestCase|PresenterRunner $this */ 88 | 89 | if (!$this->presenter) { 90 | throw new \LogicException("You have to open the presenter using \$this->openPresenter(\$name); before calling actions"); 91 | } 92 | 93 | $request = new Nette\Application\Request($this->presenter->getName(), $method, ['action' => $action] + $params, $post); 94 | 95 | return $this->presenter->run($request); 96 | } 97 | 98 | 99 | 100 | /** 101 | * @return Nette\Application\IResponse 102 | */ 103 | protected function runPresenterSignal($action, $signal, $params = [], $post = []) 104 | { 105 | /** @var IntegrationTestCase|PresenterRunner $this */ 106 | 107 | return $this->runPresenterAction($action, $post ? 'POST' : 'GET', ['do' => $signal] + $params, $post); 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /src/Api/HttpLogger.php: -------------------------------------------------------------------------------- 1 | 22 | * @author Matěj Koubík 23 | */ 24 | class HttpLogger extends Nette\Object 25 | { 26 | 27 | /** 28 | * @var callable|NULL 29 | */ 30 | public $outputFilter; 31 | 32 | /** 33 | * @var string 34 | */ 35 | public $baseUri; 36 | 37 | /** 38 | * @var string 39 | */ 40 | protected $buffer = ''; 41 | 42 | 43 | 44 | public function log(HttpRequestMock $request, HttpResponseMock $response) 45 | { 46 | $buffer = $this->formatRequest($request); 47 | $buffer .= $this->formatResponse($response); 48 | 49 | $this->buffer .= $buffer . PHP_EOL . PHP_EOL; 50 | } 51 | 52 | 53 | 54 | public function write($file) 55 | { 56 | @file_put_contents($file, $this->buffer, FILE_APPEND | LOCK_EX); 57 | $this->buffer = ''; 58 | } 59 | 60 | 61 | 62 | public function output() 63 | { 64 | echo $this->buffer; 65 | $this->buffer = ''; 66 | } 67 | 68 | 69 | 70 | public function formatRequest(HttpRequestMock $request) 71 | { 72 | $path = str_replace($this->baseUri, '', $request->getUrl()->getHostUrl() . $request->getUrl()->getPath()); 73 | 74 | $return = $this->formatRequestHead($request, $path); 75 | $return .= $this->formatHeaders($request->getOriginalHeaders()); 76 | $return .= $this->formatBody($request->getRawBody(), TRUE); 77 | 78 | return $return . PHP_EOL; 79 | } 80 | 81 | 82 | 83 | protected function formatRequestHead(HttpRequestMock $request, $path) 84 | { 85 | return (new \DateTime())->format('[Y-m-d H:i:s]') . PHP_EOL 86 | . sprintf('> %s %s', $request->getMethod(), $path) . PHP_EOL; 87 | } 88 | 89 | 90 | 91 | public function formatResponse(HttpResponseMock $response) 92 | { 93 | $return = $this->formatResponseHead($response); 94 | $return .= $this->formatHeaders($response->getHeaders()); 95 | $return .= $this->formatBody($response->getContent(), FALSE); 96 | 97 | return $return . PHP_EOL; 98 | } 99 | 100 | 101 | 102 | protected function formatResponseHead(HttpResponseMock $response) 103 | { 104 | return sprintf('< %s', $response->getCode()) . PHP_EOL; 105 | } 106 | 107 | 108 | 109 | public function formatHeaders(array $headers) 110 | { 111 | if (empty($headers)) { 112 | return ''; 113 | } 114 | 115 | $result = ''; 116 | foreach ($headers as $name => $value) { 117 | $result .= "$name: $value" . PHP_EOL; 118 | } 119 | 120 | $result = Strings::indent($result, 8, ' '); 121 | $result = Strings::indent("+ Headers" . PHP_EOL . PHP_EOL . $result, 4, ' '); 122 | 123 | return PHP_EOL . $result; 124 | } 125 | 126 | 127 | 128 | public function formatBody($body, $request = TRUE) 129 | { 130 | if (empty($body)) { 131 | return ''; 132 | } 133 | 134 | $body = $this->tryFormatJson($body, $request); 135 | 136 | $result = Strings::indent($body, 8, ' '); 137 | $result = Strings::indent("+ Body" . PHP_EOL . PHP_EOL . $result, 4, ' '); 138 | 139 | return PHP_EOL . $result; 140 | } 141 | 142 | 143 | 144 | protected function tryFormatJson($string, $request) 145 | { 146 | try { 147 | $data = Json::decode($string); 148 | if ($data === NULL) { 149 | return ''; 150 | } 151 | 152 | if ($this->outputFilter) { 153 | $data = call_user_func($this->outputFilter, $data, $request); 154 | } 155 | 156 | return Json::encode($data, Json::PRETTY); 157 | 158 | } catch (Nette\Utils\JsonException $e) { 159 | return $string; 160 | } 161 | } 162 | 163 | } 164 | -------------------------------------------------------------------------------- /src/DoctrineDbSetup.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | trait DoctrineDbSetup 25 | { 26 | 27 | use CompiledContainer { 28 | CompiledContainer::createContainer as parentCreateContainer; 29 | CompiledContainer::refreshContainer as parentRefreshContainer; 30 | } 31 | 32 | 33 | /** 34 | * @var string|NULL 35 | */ 36 | protected $databaseName; 37 | 38 | 39 | 40 | protected function refreshContainer() 41 | { 42 | /** @var Connection $oldDb */ 43 | $oldDb = $this->getContainer()->getByType('Doctrine\DBAL\Connection'); 44 | 45 | $this->parentRefreshContainer(); 46 | 47 | /** @var Connection $newDb */ 48 | $newDb = $this->getContainer()->getByType('Doctrine\DBAL\Connection'); 49 | $newDb->__construct( 50 | ['pdo' => $oldDb->getWrappedConnection()] + $oldDb->getParams(), 51 | $newDb->getDriver(), 52 | $newDb->getConfiguration(), 53 | $newDb->getEventManager() 54 | ); 55 | } 56 | 57 | 58 | 59 | /** 60 | * @param array $configs 61 | * @return Nette\DI\Container 62 | */ 63 | protected function createContainer(array $configs = []) 64 | { 65 | $sl = $this->parentCreateContainer($configs); 66 | 67 | /** @var DbConnectionMock $db */ 68 | $db = $sl->getByType('Doctrine\DBAL\Connection'); 69 | if (!$db instanceof DbConnectionMock) { 70 | $serviceNames = $sl->findByType('Doctrine\DBAL\Connection'); 71 | throw new \LogicException(sprintf( 72 | 'The service %s should be instance of Kdyby\TesterExtras\DbConnectionMock, to allow lazy schema initialization', 73 | reset($serviceNames) 74 | )); 75 | } 76 | 77 | $db->onConnect[] = function (Connection $db) use ($sl) { 78 | if ($this->databaseName !== NULL) { 79 | return; 80 | } 81 | 82 | try { 83 | if (!method_exists($this, 'doSetupDatabase')) { 84 | throw new \LogicException(sprintf("Method %s:%s is not implemented", get_class($this), __FUNCTION__)); 85 | } 86 | 87 | $this->doSetupDatabase($db); 88 | 89 | } catch (\Exception $e) { 90 | Tracy\Debugger::log($e, Tracy\Debugger::ERROR); 91 | Assert::fail($e->getMessage()); 92 | } 93 | }; 94 | 95 | return $sl; 96 | } 97 | 98 | 99 | 100 | /** 101 | * THIS IS AN EXAMPLE IMPLEMENTATION FOR YOUR "BaseTestCase" 102 | * 103 | protected function doSetupDatabase(Connection $db) 104 | { 105 | $this->databaseName = 'kdyby_tests_' . getmypid(); 106 | 107 | $db->exec("DROP DATABASE IF EXISTS `{$this->databaseName}`"); 108 | $db->exec("CREATE DATABASE `{$this->databaseName}` COLLATE 'utf8_general_ci'"); 109 | 110 | $sqls = array( 111 | __DIR__ . '/../../app/sql/schema.sql', 112 | __DIR__ . '/../sql/fixtures.sql', 113 | ); 114 | 115 | $db->exec("USE `{$this->databaseName}`"); 116 | $db->transactional(function (Connection $db) use ($sqls) { 117 | $db->exec("SET foreign_key_checks = 0;"); 118 | $db->exec("SET @disable_triggers = 1;"); 119 | 120 | foreach ($sqls as $file) { 121 | Kdyby\Doctrine\Helpers::loadFromFile($db, $file); 122 | } 123 | 124 | // move autoincrement 125 | $db->insert('orders', array( 126 | 'id' => (string) (1200000000 + (int) ((time() - 1381357699) . substr(getmypid(), -1))), 127 | 'id_restaurant' => 1 128 | )); 129 | }); 130 | 131 | $db->exec("SET foreign_key_checks = 1;"); 132 | $db->exec("SET @disable_triggers = NULL;"); 133 | 134 | $databaseName = $this->databaseName; 135 | register_shutdown_function(function () use ($db, $databaseName) { 136 | $db->exec("DROP DATABASE IF EXISTS `{$databaseName}`"); 137 | }); 138 | 139 | } 140 | 141 | */ 142 | 143 | } 144 | -------------------------------------------------------------------------------- /src/HttpServer.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class HttpServer 24 | { 25 | 26 | /** 27 | * @var array 28 | */ 29 | private $pipes = array(); 30 | 31 | /** 32 | * @var resource 33 | */ 34 | private $process; 35 | 36 | /** 37 | * @var UrlScript 38 | */ 39 | private $url; 40 | 41 | /** 42 | * @var array 43 | */ 44 | private static $spec = array( 45 | 0 => array("pipe", "r"), // stdin is a pipe that the child will read from 46 | 1 => array("pipe", "w"), // stdout is a pipe that the child will write to 47 | 2 => array("pipe", "w"), // errors 48 | ); 49 | 50 | 51 | 52 | public function __construct() 53 | { 54 | register_shutdown_function(function () { 55 | $this->slaughter(); 56 | }); 57 | } 58 | 59 | 60 | 61 | public function __destruct() 62 | { 63 | $this->slaughter(); 64 | } 65 | 66 | 67 | 68 | /** 69 | * @return UrlScript 70 | */ 71 | public function getUrl() 72 | { 73 | return clone $this->url; 74 | } 75 | 76 | 77 | 78 | /** 79 | * @param string $router 80 | * @param array $env 81 | * @return UrlScript 82 | * @throws \RuntimeException 83 | */ 84 | public function start($router, $env = array()) 85 | { 86 | $this->slaughter(); 87 | 88 | static $port; 89 | 90 | if ($port === NULL) { 91 | do { 92 | $port = rand(8000, 10000); 93 | if (isset($lock)) 94 | @fclose($lock); 95 | $lock = fopen(dirname(TEMP_DIR) . '/http-server-' . $port . '.lock', 'w'); 96 | } while (!flock($lock, LOCK_EX | LOCK_NB, $wouldBlock) || $wouldBlock); 97 | } 98 | 99 | $ini = NULL; 100 | if (($pid = getmypid()) && ($myprocess = `ps -ww -fp $pid`)) { 101 | $fullArgs = preg_split('~[ \\t]+~', explode("\n", $myprocess)[1], 8)[7]; 102 | if (preg_match('~\\s\\-c\\s(?P[^ \\t]+)\\s~i', $fullArgs, $m)) { 103 | $ini = '-c ' . $m['ini'] . ' -n'; 104 | } 105 | } 106 | 107 | $executable = new PhpExecutableFinder(); 108 | $cmd = sprintf('%s %s -d register_argc_argv=on -t %s -S %s:%d %s', escapeshellcmd($executable->find()), $ini, escapeshellarg(dirname($router)), $ip = '127.0.0.1', $port, escapeshellarg($router)); 109 | if (!is_resource($this->process = proc_open($cmd, self::$spec, $this->pipes, dirname($router), $env))) { 110 | throw new HttpServerException("Could not execute: `$cmd`"); 111 | } 112 | 113 | sleep(1); // give him some time to boot up 114 | 115 | $status = proc_get_status($this->process); 116 | if (!$status['running']) { 117 | throw new HttpServerException("Failed to start php server: " . stream_get_contents($this->pipes[2])); 118 | } 119 | 120 | $this->url = new UrlScript('http://' . $ip . ':' . $port); 121 | 122 | return $this->getUrl(); 123 | } 124 | 125 | 126 | 127 | public function slaughter() 128 | { 129 | if (!is_resource($this->process)) { 130 | return; 131 | } 132 | 133 | $status = proc_get_status($this->process); 134 | if ($status['running'] == TRUE) { 135 | fclose($this->pipes[1]); //stdout 136 | fclose($this->pipes[2]); //stderr 137 | 138 | //get the parent pid of the process we want to kill 139 | $pPid = $status['pid']; 140 | 141 | //use ps to get all the children of this process, and kill them 142 | $cmd = PHP_OS === 'Darwin' ? "ps -o pid,ppid | grep -e ' $pPid$'" : "ps -o pid --no-heading --ppid $pPid"; 143 | foreach (array_filter(preg_split('/\s+/', `$cmd`)) as $pid) { 144 | if (is_numeric($pid)) { 145 | posix_kill($pid, 9); // SIGKILL signal 146 | } 147 | } 148 | } 149 | 150 | fclose($this->pipes[0]); 151 | if ($status['running']) proc_terminate($this->process, 9); // SIGKILL 152 | proc_close($this->process); 153 | 154 | $this->process = NULL; 155 | } 156 | 157 | } 158 | 159 | 160 | 161 | /** 162 | * @author Filip Procházka 163 | */ 164 | class HttpServerException extends \RuntimeException 165 | { 166 | 167 | } 168 | -------------------------------------------------------------------------------- /src/Api/RequestProcessor.php: -------------------------------------------------------------------------------- 1 | 24 | * @author Matěj Koubík 25 | * 26 | * @method onBeforeRequest(Nette\Application\Request $appRequest, Nette\DI\Container $sl) 27 | */ 28 | class RequestProcessor extends Nette\Object 29 | { 30 | 31 | /** 32 | * @var array 33 | */ 34 | public $onBeforeRequest = []; 35 | 36 | /** 37 | * @var Nette\DI\Container 38 | */ 39 | private $sl; 40 | 41 | /** 42 | * @var Nette\Application\IRouter 43 | */ 44 | private $router; 45 | 46 | /** 47 | * @var Nette\Application\IResponse 48 | */ 49 | private $appResponse; 50 | 51 | /** 52 | * @var HttpRequestMock 53 | */ 54 | private $httpRequest; 55 | 56 | /** 57 | * @var HttpResponseMock 58 | */ 59 | private $httpResponse; 60 | 61 | /** 62 | * @var string 63 | */ 64 | private $baseUri; 65 | 66 | /** 67 | * @var HttpLogger 68 | */ 69 | private $logger; 70 | 71 | 72 | 73 | public function __construct(Nette\DI\Container $container, HttpLogger $logger, $baseUri) 74 | { 75 | $this->sl = $container; 76 | $this->baseUri = $baseUri; 77 | 78 | $this->logger = $logger; 79 | $this->logger->baseUri = $baseUri; 80 | } 81 | 82 | 83 | 84 | public function setRouter(Nette\Application\IRouter $router) 85 | { 86 | $this->router = $router; 87 | } 88 | 89 | 90 | 91 | protected function getRouter() 92 | { 93 | if ($this->router === NULL) { 94 | $this->router = $this->sl->getByType(Nette\Application\IRouter::class); 95 | } 96 | 97 | return $this->router; 98 | } 99 | 100 | 101 | 102 | /** 103 | * @param string $method 104 | * @param string $path 105 | * @param array $headers 106 | * @param string|null $body 107 | * @return Http\Response 108 | */ 109 | public function request($method, $path, array $headers = [], $body = NULL) 110 | { 111 | $this->appResponse = NULL; 112 | 113 | $url = new Http\UrlScript($this->baseUri . $path); 114 | $this->httpRequest = (new HttpRequestMock($url, NULL, [], [], [], $headers, $method, '127.0.0.1', '127.0.0.1'))->setRawBody($body); 115 | $this->httpResponse = new HttpResponseMock(); 116 | 117 | // mock request & response 118 | $this->sl->removeService('httpRequest'); 119 | $this->sl->addService('httpRequest', $this->httpRequest); 120 | 121 | $this->sl->removeService('httpResponse'); 122 | $this->sl->addService('httpResponse', $this->httpResponse); 123 | 124 | /** @var Kdyby\FakeSession\Session $session */ 125 | $session = $this->sl->getService('session'); 126 | $session->__construct(new Http\Session($this->httpRequest, $this->httpResponse)); 127 | 128 | /** @var Nette\Application\IPresenterFactory $presenterFactory */ 129 | $presenterFactory = $this->sl->getByType('Nette\Application\IPresenterFactory'); 130 | 131 | /** @var Application $application */ 132 | $application = $this->sl->getByType('Nette\Application\Application'); 133 | $application->__construct($presenterFactory, $this->getRouter(), $this->httpRequest, $this->httpResponse); 134 | 135 | $application->onResponse[] = function (Application $application, Nette\Application\IResponse $response) { 136 | $this->appResponse = $response; 137 | $this->httpResponse->setAppResponse($response); 138 | }; 139 | 140 | $appRequest = $this->getRouter()->match($this->httpRequest); 141 | 142 | $this->onBeforeRequest($appRequest, $this->sl); 143 | 144 | try { 145 | ob_start(); 146 | 147 | try { 148 | $this->appResponse = NULL; 149 | $application->processRequest($appRequest); 150 | 151 | } catch (\Exception $e) { 152 | $application->processException($e); 153 | } 154 | 155 | $this->httpResponse->setContent(ob_get_clean()); 156 | 157 | } finally { 158 | $this->logger->log($this->httpRequest, $this->httpResponse); 159 | } 160 | 161 | return $this->httpResponse; 162 | } 163 | 164 | 165 | 166 | /** 167 | * @return Nette\Application\IResponse 168 | */ 169 | public function getAppResponse() 170 | { 171 | return $this->appResponse; 172 | } 173 | 174 | 175 | 176 | /** 177 | * @return HttpResponseMock 178 | */ 179 | public function getHttpResponse() 180 | { 181 | return $this->httpResponse; 182 | } 183 | 184 | 185 | 186 | /** 187 | * @return HttpRequestMock 188 | */ 189 | public function getHttpRequest() 190 | { 191 | return $this->httpRequest; 192 | } 193 | 194 | } 195 | -------------------------------------------------------------------------------- /bin/composer-nette.php: -------------------------------------------------------------------------------- 1 | $version) { 72 | if (!in_array(strtolower($dep), $nettePackages, TRUE)) { 73 | continue; 74 | } 75 | 76 | $composer[$req][$dep] = $callback($dep, $version); 77 | } 78 | } 79 | 80 | return $composer; 81 | }; 82 | 83 | $parseVersion = function ($version) { 84 | preg_match('~^(?P\\~|\\^|(\\>|\\<)?=)?(?P[^\\@]+)?(?:\\@(?P[^\\|]+))?(?P.*)\\z~', $version, $v); 85 | return ((array) $v) + array('modifier' => '', 'number' => '0', 'stability' => 'stable'); 86 | }; 87 | 88 | $isNewerVersion = function ($what, $against) use ($parseVersion) { 89 | $w = $parseVersion($what); 90 | $a = $parseVersion($against); 91 | return version_compare($w['number'], $a['number'], '>'); 92 | }; 93 | 94 | $asStable = function ($version) use ($parseVersion) { 95 | $v = $parseVersion($version); 96 | return $v['modifier'] . $v['number']; // remove stability modifier 97 | }; 98 | 99 | switch ($version) { 100 | case 'nette-2.4': 101 | $composer = $modifyRequirement(function ($dep, $version) use ($isNewerVersion, $asStable) { 102 | if ($isNewerVersion($version, '~2.4')) { 103 | return $asStable($version); 104 | } 105 | 106 | if (in_array($dep, array('nette/component-model', 'nette/deprecated', 'nette/safe-stream'), TRUE)) { 107 | return '2.3.*|2.4.*'; 108 | } 109 | 110 | if (in_array($dep, array('nette/tokenizer'), TRUE)) { 111 | return '2.2.*|2.3.*|2.4.*'; 112 | } 113 | 114 | return '~2.4'; 115 | }); 116 | 117 | unset($composer['require']['nette/nette'], $composer['require-dev']['nette/nette']); 118 | 119 | break; 120 | 121 | case 'nette-2.4-dev': 122 | $allPackages = $composer['require'] + $composer['require-dev']; 123 | if ($diff = array_diff_key(array_fill_keys($nettePackages, '~2.4@dev'), $allPackages)) { 124 | 125 | $formatted = ''; 126 | foreach ($diff as $dep => $version) { 127 | $formatted .= sprintf("\t\"%s\": \"%s\",\n", $dep, $version); 128 | } 129 | 130 | out(5, "There are missing packages in the require-dev section of composer.json:\n\n" . $formatted); 131 | } 132 | 133 | out(0, 'Nothing to change'); 134 | break; 135 | 136 | case 'nette-2.3': 137 | $composer = $modifyRequirement(function ($dep, $version) use ($isNewerVersion, $asStable) { 138 | if ($isNewerVersion($version, '~2.3')) { 139 | return $asStable($version); 140 | } 141 | 142 | if (in_array($dep, array('nette/component-model', 'nette/tokenizer'), TRUE)) { 143 | return '2.2.*|2.3.*'; 144 | } 145 | 146 | return '2.3.*'; 147 | }); 148 | 149 | $composer['require-dev'] = array('nette/nette' => '2.3.*') + $composer['require-dev']; 150 | 151 | break; 152 | 153 | case 'nette-2.3-dev': 154 | out(0, 'Nothing to change'); 155 | break; 156 | 157 | case 'nette-2.2': 158 | $composer = $modifyRequirement(function ($dep, $version) { 159 | return '2.2.*'; 160 | }); 161 | 162 | $composer['require-dev'] = array('nette/nette' => '2.2.*') + $composer['require-dev']; 163 | 164 | break; 165 | 166 | case 'nette-2.2-dev': 167 | out(0, 'Nothing to change'); 168 | break; 169 | 170 | case 'nette-2.1': 171 | $composer = $modifyRequirement(function ($dep, $version) { 172 | return '2.1.*'; 173 | }); 174 | break; 175 | 176 | case 'nette-2.0': 177 | $composer = $modifyRequirement(function ($dep, $version) { 178 | return '2.0.*'; 179 | }); 180 | break; 181 | 182 | case 'default': 183 | out(0, 'Nothing to change'); 184 | break; 185 | 186 | default: 187 | out(4, 'Unsupported requirement: ' . $version); 188 | } 189 | 190 | if (!empty($composer['require']['nette/deprecated'])) { 191 | $composer['require']['nette/deprecated'] = '@dev'; 192 | } 193 | 194 | if (!empty($composer['require-dev']['nette/deprecated'])) { 195 | $composer['require-dev']['nette/deprecated'] = '@dev'; 196 | } 197 | 198 | $content = defined('JSON_PRETTY_PRINT') ? json_encode($composer, JSON_PRETTY_PRINT) : json_encode($composer); 199 | $content = preg_replace_callback('~^( )+~m', function (array $m) { 200 | return str_replace(' ', "\t", $m[0]); 201 | }, $content); 202 | file_put_contents($composerJsonFile, $content . "\n"); 203 | 204 | echo "\n", print_r(array( 205 | 'require' => $composer['require'], 206 | 'require-dev' => !empty($composer['require-dev']) ? $composer['require-dev'] : array(), 207 | ), TRUE); 208 | 209 | out(0); 210 | }); 211 | --------------------------------------------------------------------------------