diff options
author | Arnold Daniels <arnold@jasny.net> | 2016-11-03 23:45:36 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-11-03 23:45:36 +0100 |
commit | f3d05dfca77843a869e938028824f19307780939 (patch) | |
tree | 846c5e559360e530d200d897f67b5af02f8795e5 | |
parent | aa31c40618b0cc7b43b7a1cb107e97b49e2c06f1 (diff) | |
parent | bd4cb77fbf04923fa31a08fdd1f33f2c0db87864 (diff) | |
download | router-1.0.0.zip router-1.0.0.tar.gz router-1.0.0.tar.bz2 |
Merge pull request #13 from jasny/fix-testsv1.0.0
Fix tests
30 files changed, 1597 insertions, 1429 deletions
diff --git a/composer.json b/composer.json index 147e6ea..ad3d7c8 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,8 @@ "require": { "php": ">=5.6.0", "jasny/php-functions": "^2.0", - "psr/http-message": "^1.0" + "psr/http-message": "^1.0", + "psr/log": "^1.0" }, "require-dev": { "jasny/php-code-quality": "^2.0" diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 1861d7c..e8a1ac3 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -2,7 +2,7 @@ <phpunit colors="true" - bootstrap="vendor/autoload.php" + bootstrap="tests/bootstrap.php" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" diff --git a/src/Router.php b/src/Router.php index 6d38678..5cc2108 100644 --- a/src/Router.php +++ b/src/Router.php @@ -2,8 +2,8 @@ namespace Jasny; -use Jasny\Router\Runner\RunnerFactory; -use Jasny\Router\Routes\Glob; +use Jasny\Router\Routes; +use Jasny\Router\RunnerFactory; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; @@ -14,9 +14,9 @@ class Router { /** * Specific routes - * @var array + * @var Routes */ - protected $routes = []; + protected $routes; /** * Middlewares actions @@ -28,22 +28,23 @@ class Router * Factory of Runner objects * @var RunnerFactory **/ - protected $factory = null; + protected $factory; + /** * Class constructor * - * @param array $routes + * @param Routes $routes */ - public function __construct(array $routes) + public function __construct(Routes $routes) { $this->routes = $routes; } /** - * Get a list of all routes + * Get a all routes * - * @return object + * @return Routes */ public function getRoutes() { @@ -51,7 +52,7 @@ class Router } /** - * Get middlewares + * Get all middlewares * * @return array */ @@ -60,6 +61,7 @@ class Router return $this->middlewares; } + /** * Get factory of Runner objects * @@ -100,7 +102,7 @@ class Router public function add($middleware) { if (!is_callable($middleware)) { - throw new \InvalidArgumentException("Middleware should be a callable"); + throw new \InvalidArgumentException("Middleware should be callable"); } $this->middlewares[] = $middleware; @@ -108,6 +110,7 @@ class Router return $this; } + /** * Run the action for the request * @@ -130,17 +133,16 @@ class Router */ public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next = null) { - $run = [$this, 'run']; - - #Call to $this->run will be executed last in the chain of middlewares - $next = function(ServerRequestInterface $request, ResponseInterface $response) use ($next, $run) { - return call_user_func($run, $request, $response, $next); - }; + if (empty($this->middlewares)) { + return $this->run($request, $response, $next); + } + + $stack = array_merge([[$this, 'run']], $this->middlewares); - #Build middlewares call chain, so that the last added was executed in first place - foreach ($this->middlewares as $middleware) { - $next = function(ServerRequestInterface $request, ResponseInterface $response) use ($next, $middleware) { - return $middleware($request, $response, $next); + // Turn the stack into a call chain + foreach ($stack as $handle) { + $next = function(ServerRequestInterface $request, ResponseInterface $response) use ($handle, $next) { + return $handle($request, $response, $next); }; } @@ -157,33 +159,32 @@ class Router */ public function run(ServerRequestInterface $request, ResponseInterface $response, $next = null) { - $glob = new Glob($this->routes); - $route = $glob->getRoute($request); + $route = $this->routes->getRoute($request); + + if (!$route) { + return $this->notFound($request, $response); + } + + $requestWithRoute = $request->withAttribute('route', $route); - if (!$route) return $this->notFound($response); - - $request->withAttribute('route', $route); $factory = $this->getFactory(); $runner = $factory($route); - return $runner($request, $response, $next); + return $runner($requestWithRoute, $response, $next); } /** * Return 'Not Found' response * + * @param ServerRequestInterface $request * @param ResponseInterface $response * @return ResponseInterface */ - protected function notFound(ResponseInterface $response) + protected function notFound(ServerRequestInterface $request, ResponseInterface $response) { - $message = 'Not Found'; - - $body = $response->getBody(); - $body->rewind(); - $body->write($message); + $notFound = $response->withStatus(404); + $notFound->getBody()->write('Not Found'); - return $response->withStatus(404, $message)->withBody($body); + return $notFound; } } - diff --git a/src/Router/Middleware/BasePath.php b/src/Router/Middleware/BasePath.php index a80d029..f356aed 100644 --- a/src/Router/Middleware/BasePath.php +++ b/src/Router/Middleware/BasePath.php @@ -57,13 +57,15 @@ class BasePath $uri = $request->getUri(); $path = $this->normalizePath($uri->getPath()); - if (!$this->hasBasePath($path)) return $this->setError($response); + if (!$this->hasBasePath($path)) { + return $this->notFound($request, $response); + } $noBase = $this->getBaselessPath($path); $noBaseUri = $uri->withPath($noBase); - $request = $request->withUri($noBaseUri)->withAttribute('original_uri', $uri); + $rewrittenRequest = $request->withUri($noBaseUri)->withAttribute('original_uri', $uri); - return call_user_func($next, $request, $response); + return $next($rewrittenRequest, $response); } /** @@ -100,19 +102,17 @@ class BasePath } /** - * Set error response + * Respond with 404 Not Found * - * @param ResponseInterface $response + * @param ServerRequestInterface $request + * @param ResponseInterface $response * @return ResponseInterface */ - protected function setError($response) + protected function notFound(ServerRequestInterface $request, ResponseInterface $response) { - $message = 'Not Found'; - - $body = $response->getBody(); - $body->rewind(); - $body->write($message); + $notFound = $response->withStatus(404); + $notFound->getBody()->write('Not Found'); - return $response->withStatus(404, $message)->withBody($body); + return $notFound; } } diff --git a/src/Router/Middleware/ErrorHandler.php b/src/Router/Middleware/ErrorHandler.php deleted file mode 100644 index 789c455..0000000 --- a/src/Router/Middleware/ErrorHandler.php +++ /dev/null @@ -1,54 +0,0 @@ -<?php - -namespace Jasny\Router\Middleware; - -use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Message\ResponseInterface; - -/** - * Handle error in following middlewares/app actions - */ -class ErrorHandler -{ - /** - * Run middleware action - * - * @param ServerRequestInterface $request - * @param ResponseInterface $response - * @param callback $next - * @return ResponseInterface - */ - public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next = null) - { - if ($next && !is_callable($next)) { - throw new \InvalidArgumentException("'next' should be a callback"); - } - - $error = false; - - try { - $response = $next ? call_user_func($next, $request, $response) : $response; - } catch(\Throwable $e) { - $error = true; - } catch(\Exception $e) { #This block can be removed when migrating to PHP7, because Throwable represents both Exception and Error - $error = true; - } - - return $error ? $this->handleError($response) : $response; - } - - /** - * Handle caught error - * - * @param ResponseInterface $response - * @return ResponseInterface - */ - protected function handleError($response) - { - $body = $response->getBody(); - $body->rewind(); - $body->write('Unexpected error'); - - return $response->withStatus(500, 'Internal Server Error')->withBody($body); - } -} diff --git a/src/Router/Middleware/NotFound.php b/src/Router/Middleware/NotFound.php index 97b4a51..2484a43 100644 --- a/src/Router/Middleware/NotFound.php +++ b/src/Router/Middleware/NotFound.php @@ -29,6 +29,7 @@ class NotFound **/ protected $methodNotAllowed = null; + /** * Class constructor * @@ -38,12 +39,22 @@ class NotFound */ public function __construct(Routes $routes, $notFound = 404, $methodNotAllowed = null) { - if (!(is_numeric($notFound) && $notFound >= 100 && $notFound <= 599) && !is_callable($notFound)) { - throw new \InvalidArgumentException("'Not found' parameter should be a code in range 100-599 or a callback"); + if (is_string($notFound) && ctype_digit($notFound)) { + $notFound = (int)$notFound; + } + if (!(is_int($notFound) && $notFound >= 100 && $notFound <= 999) && !is_callable($notFound)) { + throw new \InvalidArgumentException("'notFound' should be valid HTTP status code or a callback"); } - if ($methodNotAllowed && !(is_numeric($methodNotAllowed) && $methodNotAllowed >= 100 && $methodNotAllowed <= 599) && !is_callable($methodNotAllowed)) { - throw new \InvalidArgumentException("'Method not allowed' parameter should be a code in range 100-599 or a callback"); + if (is_string($methodNotAllowed) && ctype_digit($methodNotAllowed)) { + $methodNotAllowed = (int)$methodNotAllowed; + } + if ( + isset($methodNotAllowed) && + !(is_int($methodNotAllowed) && $methodNotAllowed >= 100 && $methodNotAllowed <= 999) && + !is_callable($methodNotAllowed) + ) { + throw new \InvalidArgumentException("'methodNotAllowed' should be valid HTTP status code or a callback"); } $this->routes = $routes; @@ -60,6 +71,7 @@ class NotFound { return $this->routes; } + /** * Run middleware action @@ -69,37 +81,34 @@ class NotFound * @param callback $next * @return ResponseInterface */ - public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next = null) + public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next) { - if ($next && !is_callable($next)) { + if (!is_callable($next)) { throw new \InvalidArgumentException("'next' should be a callback"); } - if ($this->getRoutes()->hasRoute($request)) { - return $next ? $next($request, $response) : $response; - } - - $status = $this->methodNotAllowed && $this->getRoutes()->hasRoute($request, false) ? - $this->methodNotAllowed : $this->notFound; + if (!$this->getRoutes()->hasRoute($request)) { + $status = $this->methodNotAllowed && $this->getRoutes()->hasRoute($request, false) ? + $this->methodNotAllowed : $this->notFound; - return is_numeric($status) ? $this->simpleResponse($response, $status) : call_user_func($status, $request, $response); + return is_numeric($status) ? $this->simpleResponse($response, $status) : $status($request, $response); + } + + return $next($request, $response); } /** * Simple response * * @param ResponseInterface $response - * @param int $code + * @param int $code * @return ResponseInterface */ protected function simpleResponse(ResponseInterface $response, $code) { - $message = 'Not Found'; - - $body = $response->getBody(); - $body->rewind(); - $body->write($message); + $notFound = $response->withStatus($code); + $notFound->getBody()->write('Not found'); - return $response->withStatus($code, $message)->withBody($body); + return $notFound; } } diff --git a/src/Router/Route.php b/src/Router/Route.php index be5a052..d2158ea 100644 --- a/src/Router/Route.php +++ b/src/Router/Route.php @@ -10,27 +10,21 @@ class Route extends \stdClass /** * Class constructor * - * @param array $values + * @param array|stdClass $values */ - public function __construct(array $values) - { - foreach ($values as $key => $value) { - $this->$key = $value; - } - } - - /** - * Factory method - * - * @param array|\stdClass $values - * @return Route - */ - public static function create($values) + public function __construct($values) { if ($values instanceof \stdClass) { $values = get_object_vars($values); } - return new static($values); + if (!is_array($values)) { + $type = (is_object($values) ? get_class($values) . ' ' : '') . gettype($values); + throw new \InvalidArgumentException("Route values should be an array, not a $type"); + } + + foreach ($values as $key => $value) { + $this->$key = $value; + } } } diff --git a/src/Router/Routes/Glob.php b/src/Router/Routes/Glob.php index fcece76..a4495ee 100644 --- a/src/Router/Routes/Glob.php +++ b/src/Router/Routes/Glob.php @@ -6,6 +6,7 @@ use ArrayObject; use Jasny\Router\UrlParsing; use Jasny\Router\Routes; use Jasny\Router\Route; +use Jasny\Router\Routes\RouteBinding; use Psr\Http\Message\ServerRequestInterface; /** @@ -14,6 +15,19 @@ use Psr\Http\Message\ServerRequestInterface; class Glob extends ArrayObject implements Routes { use UrlParsing; + use RouteBinding; + + /** + * Class constructor + * + * @param Routes[]|array|\Traversable $input + * @param int $flags + */ + public function __construct($input = [], $flags = 0) + { + $routes = $this->createRoutes($input); + parent::__construct($routes, $flags); + } /** * Create a route from an assisiative array or stdClass object @@ -35,14 +49,7 @@ class Glob extends ArrayObject implements Routes throw new \InvalidArgumentException("Unable to create a Route from value " . var_export($value, true)); } - $route = Route::create($value); - - if (!isset($route)) { - throw new \InvalidArgumentException("Unable to create a Route from " . var_export($value, true) . ": " - . "neither 'controller', 'fn' or 'file' key is defined"); - } - - return $route; + return new Route($value); } /** @@ -157,200 +164,6 @@ class Glob extends ArrayObject implements Routes /** - * Fill out the routes variables based on the url parts. - * - * @param array|\stdClass $vars Route variables - * @param ServerRequestInterface $request - * @param array $parts URL parts - * @return array - */ - protected function bind($vars, ServerRequestInterface $request, array $parts) - { - $values = []; - $type = is_array($vars) && array_keys($vars) === array_keys(array_keys($vars)) ? 'numeric' : 'assoc'; - - foreach ($vars as $key => $var) { - if (!isset($var)) continue; - - if (is_object($var) && !$var instanceof \stdClass) { - $part = array($var); - } elseif (!is_scalar($var)) { - $part = array($this->bind($var, $request, $parts)); - } elseif ($var[0] === '$') { - $options = array_map('trim', explode('|', $var)); - $part = $this->bindVar($type, $request, $parts, $options); - } elseif ($var[0] === '~' && substr($var, -1) === '~') { - $pieces = array_map('trim', explode('~', substr($var, 1, -1))); - $bound = array_filter($this->bind($pieces, $request, $parts)); - $part = array(join('', $bound)); - } else { - $part = array($var); - } - - if ($type === 'assoc') { - $values[$key] = $part[0]; - } else { - $values = array_merge($values, $part); - } - } - - if ($vars instanceof Route) { - $values = Route::create($values); - } elseif (is_object($vars) && $type === 'assoc') { - $values = (object)$values; - } - - return $values; - } - - /** - * Bind variable - * - * @param string $type 'assoc' or 'numeric' - * @param ServerRequestInterface $request - * @param array $parts - * @param array $options - * @return array - */ - protected function bindVar($type, ServerRequestInterface $request, array $parts, array $options) - { - foreach ($options as $option) { - $value = null; - - $bound = - $this->bindVarString($option, $value) || - $this->bindVarSuperGlobal($option, $request, $value) || - $this->bindVarRequestHeader($option, $request, $value) || - $this->bindVarMultipleUrlParts($option, $type, $parts, $value) || - $this->bindVarSingleUrlPart($option, $parts, $value); - - if ($bound && isset($value)) { - return $value; - } - } - - return [null]; - } - - /** - * Bind variable when option is a normal string - * - * @param string $option - * @param mixed $value OUTPUT - * @return boolean - */ - protected function bindVarString($option, &$value) - { - if ($option[0] !== '$') { - $value = [$option]; - return true; - } - - return false; - } - - /** - * Bind variable when option is a super global - * - * @param string $option - * @param mixed $value OUTPUT - * @return boolean - */ - protected function bindVarSuperGlobal($option, ServerRequestInterface $request, &$value) - { - if (preg_match('/^\$_(GET|POST|COOKIE)\[([^\[]*)\]$/i', $option, $matches)) { - list(, $var, $key) = $matches; - - $var = strtolower($var); - $data = null; - - if ($var === 'get') { - $data = $request->getQueryParams(); - } elseif ($var === 'post') { - $data = $request->getParsedBody(); - } elseif ($var === 'cookie') { - $data = $request->getCookieParams(); - } - - $value = isset($data[$key]) ? [$data[$key]] : null; - return true; - } - - return false; - } - - /** - * Bind variable when option is a request header - * - * @param string $option - * @param ServerRequestInterface $request - * @param mixed $value OUTPUT - * @return boolean - */ - protected function bindVarRequestHeader($option, ServerRequestInterface $request, &$value) - { - if (preg_match('/^\$(?:HTTP_)?([A-Z_]+)$/', $option, $matches)) { - $sentence = preg_replace('/[\W_]+/', ' ', $matches[1]); - $name = str_replace(' ', '-', ucwords($sentence)); - - $value = [$request->getHeaderLine($name)]; - return true; - } - - return false; - } - - /** - * Bind variable when option contains multiple URL parts - * - * @param string $option - * @param string $type 'assoc' or 'numeric' - * @param array $parts Url parts - * @param mixed $value OUTPUT - * @return boolean - */ - protected function bindVarMultipleUrlParts($option, $type, array $parts, &$value) - { - if (substr($option, -3) === '...' && ctype_digit(substr($option, 1, -3))) { - $i = (int)substr($option, 1, -3); - - if ($type === 'assoc') { - throw new \InvalidArgumentException("Binding multiple parts using '$option' is only allowed in numeric arrays"); - } else { - $value = array_slice($parts, $i - 1); - } - - return true; - } - - return false; - } - - /** - * Bind variable when option contains a single URL part - * - * @param string $option - * @param array $parts Url parts - * @param mixed $value OUTPUT - * @return boolean - */ - protected function bindVarSingleUrlPart($option, array $parts, &$value) - { - if (ctype_digit(substr($option, 1))) { - $i = (int)substr($option, 1); - $part = array_slice($parts, $i - 1, 1); - - if (!empty($part)) { - $value = $part; - return true; - } - } - - return false; - } - - - /** * Check if a route for the URL exists * * @param ServerRequestInterface $request diff --git a/src/Router/Routes/RouteBinding.php b/src/Router/Routes/RouteBinding.php new file mode 100644 index 0000000..91563c3 --- /dev/null +++ b/src/Router/Routes/RouteBinding.php @@ -0,0 +1,314 @@ +<?php + +namespace Jasny\Router\Routes; + +use Jasny\Router\Route; +use Psr\Http\Message\ServerRequestInterface; + +/** + * Functionality to set route properties based on url parameters + */ +trait RouteBinding +{ + /** + * Fill out the routes variables based on the url parts. + * + * @param array|\stdClass $vars Route variables + * @param ServerRequestInterface $request + * @param array $parts URL parts + * @return array + */ + protected function bind($vars, ServerRequestInterface $request, array $parts) + { + $type = is_array($vars) && array_keys($vars) === array_keys(array_keys($vars)) ? 'numeric' : 'assoc'; + + $values = $this->bindParts($vars, $type, $request, $parts); + + if ($vars instanceof Route) { + $class = get_class($vars); + $values = new $class($values); + } elseif (is_object($vars) && $type === 'assoc') { + $values = (object)$values; + } + + return $values; + } + + + /** + * Fill out the values based on the url parts. + * + * @param array|\stdClass $vars Route variables + * @param string $type + * @param ServerRequestInterface $request + * @param array $parts URL parts + * @return array + */ + protected function bindParts($vars, $type, ServerRequestInterface $request, array $parts) + { + $values = []; + + foreach ($vars as $key => $var) { + $part = null; + + $bound = + $this->bindPartObject($var, $part) || + $this->bindPartArray($var, $request, $parts, $part) || + $this->bindPartVar($var, $type, $request, $parts, $part) || + $this->bindPartConcat($var, $request, $parts, $part) || + $this->bindPartValue($var, $part); + + if (!$bound) continue; + + if ($type === 'assoc') { + $values[$key] = $part[0]; + } else { + $values = array_merge($values, $part); + } + } + + return $values; + } + + /** + * Bind part if it's an object + * + * @param mixed $var + * @param array $part OUTPUT + * @return boolean + */ + protected function bindPartObject($var, &$part) + { + if (!is_object($var) || $var instanceof \stdClass) { + return false; + } + + $part = [$var]; + return true; + } + + /** + * Bind part if it's an array + * + * @param mixed $var + * @param ServerRequestInterface $request + * @param array $parts + * @param array $part OUTPUT + * @return boolean + */ + protected function bindPartArray($var, ServerRequestInterface $request, array $parts, &$part) + { + if (!is_array($var) && !$var instanceof \stdClass) { + return false; + } + + $part = [$this->bind($var, $request, $parts)]; + return true; + } + + /** + * Bind part if it's an variable + * + * @param mixed $var + * @param string $type + * @param ServerRequestInterface $request + * @param array $parts + * @param array $part OUTPUT + * @return boolean + */ + protected function bindPartVar($var, $type, ServerRequestInterface $request, array $parts, &$part) + { + if (!is_string($var) || $var[0] !== '$') { + return false; + } + + $options = array_map('trim', explode('|', $var)); + $part = $this->bindVar($type, $request, $parts, $options); + return true; + } + + /** + * Bind part if it's an concatenation + * + * @param mixed $var + * @param ServerRequestInterface $request + * @param array $parts + * @param array $part OUTPUT + * @return boolean + */ + protected function bindPartConcat($var, ServerRequestInterface $request, array $parts, &$part) + { + if (!is_string($var) || $var[0] !== '~' || substr($var, -1) !== '~') { + return false; + } + + $pieces = array_map('trim', explode('~', substr($var, 1, -1))); + $bound = array_filter($this->bind($pieces, $request, $parts)); + $part = [join('', $bound)]; + + return true; + } + + /** + * Bind part if it's a normal value + * + * @param mixed $var + * @param array $part OUTPUT + * @return boolean + */ + protected function bindPartValue($var, &$part) + { + if (!isset($var)) { + return false; + } + + $part = [$var]; + return true; + } + + /** + * Bind variable + * + * @param string $type 'assoc' or 'numeric' + * @param ServerRequestInterface $request + * @param array $parts + * @param array $options + * @return array + */ + protected function bindVar($type, ServerRequestInterface $request, array $parts, array $options) + { + foreach ($options as $option) { + $value = null; + + $bound = + $this->bindVarString($option, $value) || + $this->bindVarSuperGlobal($option, $request, $value) || + $this->bindVarRequestHeader($option, $request, $value) || + $this->bindVarMultipleUrlParts($option, $type, $parts, $value) || + $this->bindVarSingleUrlPart($option, $parts, $value); + + if ($bound && isset($value)) { + return $value; + } + } + + return [null]; + } + + /** + * Bind variable when option is a normal string + * + * @param string $option + * @param mixed $value OUTPUT + * @return boolean + */ + protected function bindVarString($option, &$value) + { + if ($option[0] !== '$') { + $value = [$option]; + return true; + } + + return false; + } + + /** + * Bind variable when option is a super global + * + * @param string $option + * @param mixed $value OUTPUT + * @return boolean + */ + protected function bindVarSuperGlobal($option, ServerRequestInterface $request, &$value) + { + if (preg_match('/^\$_(GET|POST|COOKIE)\[([^\[]*)\]$/i', $option, $matches)) { + list(, $var, $key) = $matches; + + $var = strtolower($var); + $data = null; + + if ($var === 'get') { + $data = $request->getQueryParams(); + } elseif ($var === 'post') { + $data = $request->getParsedBody(); + } elseif ($var === 'cookie') { + $data = $request->getCookieParams(); + } + + $value = isset($data[$key]) ? [$data[$key]] : null; + return true; + } + + return false; + } + + /** + * Bind variable when option is a request header + * + * @param string $option + * @param ServerRequestInterface $request + * @param mixed $value OUTPUT + * @return boolean + */ + protected function bindVarRequestHeader($option, ServerRequestInterface $request, &$value) + { + if (preg_match('/^\$(?:HTTP_)?([A-Z_]+)$/', $option, $matches)) { + $sentence = preg_replace('/[\W_]+/', ' ', $matches[1]); + $name = str_replace(' ', '-', ucwords($sentence)); + + $value = [$request->getHeaderLine($name)]; + return true; + } + + return false; + } + + /** + * Bind variable when option contains multiple URL parts + * + * @param string $option + * @param string $type 'assoc' or 'numeric' + * @param array $parts Url parts + * @param mixed $value OUTPUT + * @return boolean + */ + protected function bindVarMultipleUrlParts($option, $type, array $parts, &$value) + { + if (substr($option, -3) === '...' && ctype_digit(substr($option, 1, -3))) { + $i = (int)substr($option, 1, -3); + + if ($type === 'assoc') { + throw new \InvalidArgumentException("Binding multiple parts using '$option' is only allowed in numeric arrays"); + } else { + $value = array_slice($parts, $i - 1); + } + + return true; + } + + return false; + } + + /** + * Bind variable when option contains a single URL part + * + * @param string $option + * @param array $parts Url parts + * @param mixed $value OUTPUT + * @return boolean + */ + protected function bindVarSingleUrlPart($option, array $parts, &$value) + { + if (ctype_digit(substr($option, 1))) { + $i = (int)substr($option, 1); + $part = array_slice($parts, $i - 1, 1); + + if (!empty($part)) { + $value = $part; + return true; + } + } + + return false; + } +} diff --git a/src/Router/Runner.php b/src/Router/Runner.php index 87f082e..c671fa9 100644 --- a/src/Router/Runner.php +++ b/src/Router/Runner.php @@ -4,7 +4,6 @@ namespace Jasny\Router; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; -use Jasny\Router\Route; /** * A runner can be invoked in order to run the action specified in a route @@ -23,20 +22,19 @@ abstract class Runner /** * Invoke the action specified in the route and call the next method * - * @param ServerRequestInterface $request - * @param ResponseInterface $response - * @param callback $next Callback for if runner is used as middleware + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * @param callback $next Callback for if runner is used as middleware * @return ResponseInterface */ public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next = null) { - $response = $this->run($request, $response); + $newResponse = $this->run($request, $response); if (isset($next)) { - $response = call_user_func($next, $request, $response); + $newResponse = call_user_func($next, $request, $newResponse); } - return $response; + return $newResponse; } } - diff --git a/src/Router/Runner/Callback.php b/src/Router/Runner/Callback.php index 7f4c457..8fa8c0e 100644 --- a/src/Router/Runner/Callback.php +++ b/src/Router/Runner/Callback.php @@ -29,6 +29,6 @@ class Callback extends Runner throw new \RuntimeException("'fn' property of route shoud be a callable"); } - return call_user_func($callback, $request, $response); + return $callback($request, $response); } } diff --git a/src/Router/Runner/Controller.php b/src/Router/Runner/Controller.php index 56cf2b5..caca48f 100644 --- a/src/Router/Runner/Controller.php +++ b/src/Router/Runner/Controller.php @@ -7,13 +7,34 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; /** - * Description of Controller - * - * @author arnold + * Run a route using a controller */ class Controller extends Runner { /** + * Get class name from controller name + * + * @param string $name + * @return string + */ + protected function getClass($name) + { + return strstr($name, '-') ? \Jasny\studlycase($name) : $name; + } + + /** + * Instantiate a controller object + * @codeCoverageIgnore + * + * @param string $class + * @return callable|object + */ + protected function instantiate($class) + { + return new $class(); + } + + /** * Route to a controller * * @param ServerRequestInterface $request @@ -23,18 +44,20 @@ class Controller extends Runner public function run(ServerRequestInterface $request, ResponseInterface $response) { $route = $request->getAttribute('route'); - $class = !empty($route->controller) ? $route->controller : null; + $name = !empty($route->controller) ? $route->controller : null; + $class = $this->getClass($name); + if (!class_exists($class)) { throw new \RuntimeException("Can not route to controller '$class': class not exists"); } - + if (!method_exists($class, '__invoke')) { throw new \RuntimeException("Can not route to controller '$class': class does not have '__invoke' method"); } - - $controller = new $class($route); - + + $controller = $this->instantiate($class); + return $controller($request, $response); } } diff --git a/src/Router/Runner/PhpScript.php b/src/Router/Runner/PhpScript.php index d223bd7..51480c5 100644 --- a/src/Router/Runner/PhpScript.php +++ b/src/Router/Runner/PhpScript.php @@ -10,7 +10,18 @@ use Psr\Http\Message\ResponseInterface; * Route to a PHP script */ class PhpScript extends Runner -{ +{ + /** + * Include a file + * @param type $file + * @param ServerRequestInterface $request + * @param ResponseInterface $response + */ + protected function includeScript($file, ServerRequestInterface $request, ResponseInterface $response) + { + return include $file; + } + /** * Route to a file * @@ -23,16 +34,16 @@ class PhpScript extends Runner $route = $request->getAttribute('route'); $file = !empty($route->file) ? ltrim($route->file, '/') : ''; - if (!file_exists($file)) { - throw new \RuntimeException("Failed to route using '$file': File '$file' doesn't exist."); - } - if ($file[0] === '~' || strpos($file, '..') !== false) { - throw new \RuntimeException("Won't route using '$file': '~', '..' are not allowed in filename."); + throw new \RuntimeException("Won't route to '$file': '~', '..' are not allowed in filename"); } - $result = include $file; + if (!file_exists($file)) { + throw new \RuntimeException("Failed to route using '$file': File doesn't exist"); + } + + $result = $this->includeScript($file, $request, $response); - return $result === true ? $response : $result; + return $result === true || $result === 1 ? $response : $result; } } diff --git a/src/Router/Runner/RunnerFactory.php b/src/Router/RunnerFactory.php index e32d2cc..08ed7b0 100644 --- a/src/Router/Runner/RunnerFactory.php +++ b/src/Router/RunnerFactory.php @@ -1,8 +1,9 @@ <?php -namespace Jasny\Router\Runner; +namespace Jasny\Router; use Jasny\Router\Route; +use Jasny\Router\Runner; /** * Factory of Runner instances @@ -18,11 +19,11 @@ class RunnerFactory public function __invoke(Route $route) { if (isset($route->controller)) { - $class = Controller::class; + $class = Runner\Controller::class; } elseif (isset($route->fn)) { - $class = Callback::class; + $class = Runner\Callback::class; } elseif (isset($route->file)) { - $class = PhpScript::class; + $class = Runner\PhpScript::class; } else { throw new \InvalidArgumentException("Route has neither 'controller', 'fn' or 'file' defined"); } @@ -30,4 +31,3 @@ class RunnerFactory return new $class(); } } - diff --git a/src/Router/UrlParsing.php b/src/Router/UrlParsing.php index 5727085..ee89260 100644 --- a/src/Router/UrlParsing.php +++ b/src/Router/UrlParsing.php @@ -31,10 +31,6 @@ trait UrlParsing $url = rtrim($url, '/'); } - if (substr($url, 0, 2) == '/:') { - $url = substr($url, 2); - } - return $url; } } diff --git a/tests/Router/Middleware/BasePathTest.php b/tests/Router/Middleware/BasePathTest.php index eb9a2e8..d615d70 100644 --- a/tests/Router/Middleware/BasePathTest.php +++ b/tests/Router/Middleware/BasePathTest.php @@ -1,25 +1,22 @@ <?php +namespace Jasny\Router\Middleware; + use Jasny\Router\Middleware\BasePath; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\StreamInterface; use Psr\Http\Message\UriInterface; -class BasePathTest extends PHPUnit_Framework_TestCase -{ - /** - * Test creating middleware with invalid parameter - * - * @dataProvider invalidConstructProvider - */ - public function testInvalidConstruct($basePath) - { - $this->expectException(\InvalidArgumentException::class); - - $pathHandler = new BasePath($basePath); - } +use Jasny\Router\TestHelpers; +/** + * @covers Jasny\Router\Middleware\BasePath + */ +class BasePathTest extends \PHPUnit_Framework_TestCase +{ + use TestHelpers; + /** * Provide data for testing invalid BasePath creation * @@ -39,17 +36,14 @@ class BasePathTest extends PHPUnit_Framework_TestCase } /** - * Test creating BasePath instance + * Test creating middleware with invalid parameter * - * @dataProvider validConstructProvider - * @param string $basePath + * @dataProvider invalidConstructProvider + * @expectedException InvalidArgumentException */ - public function testValidConstruct($basePath, $validBasePath) + public function testInvalidConstruct($basePath) { - $pathHandler = new BasePath($basePath); - - $this->assertNotEmpty($pathHandler->getBasePath(), "Empty base path"); - $this->assertEquals($validBasePath, $pathHandler->getBasePath(), "Base path was not set correctly"); + new BasePath($basePath); } /** @@ -71,41 +65,32 @@ class BasePathTest extends PHPUnit_Framework_TestCase } /** - * Test invoke with invalid 'next' param + * Test creating BasePath instance + * + * @dataProvider validConstructProvider + * @param string $basePath */ - public function testInvokeInvalidNext() + public function testValidConstruct($basePath, $validBasePath) { - $middleware = new BasePath('/foo'); - list($request, $response) = $this->getRequests(); - - $this->expectException(\InvalidArgumentException::class); + $pathHandler = new BasePath($basePath); - $result = $middleware($request, $response, 'not_callable'); + $this->assertNotEmpty($pathHandler->getBasePath(), "Empty base path"); + $this->assertEquals($validBasePath, $pathHandler->getBasePath(), "Base path was not set correctly"); } /** - * Test case when given request path does not starts with given base path - * - * @dataProvider notFoundProvider - * @param string $basePath - * @param string $path + * Test invoke with invalid 'next' param + * + * @expectedException InvalidArgumentException */ - public function testNotFound($basePath, $path) + public function testInvokeInvalidNext() { - $middleware = new BasePath($basePath); - list($request, $response) = $this->getRequests(); - - $this->expectRequestGetPath($request, $path); - $this->expectNotFound($response); - - $result = $middleware($request, $response, function($response, $request) { - $response->nextCalled = true; + $request = $this->createMock(ServerRequestInterface::class); + $response = $this->createMock(ResponseInterface::class); - return $response; - }); + $middleware = new BasePath('/foo'); - $this->assertEquals(get_class($response), get_class($result), "Middleware should return response object"); - $this->assertFalse(isset($response->nextCalled), "'next' was called"); + $middleware($request, $response, 'not_callable'); } /** @@ -128,28 +113,27 @@ class BasePathTest extends PHPUnit_Framework_TestCase } /** - * Test correct case, when path contains base path - * - * @dataProvider foundProvider + * Test case when given request path does not starts with given base path + * @dataProvider notFoundProvider + * * @param string $basePath * @param string $path - * @param string $noBasePath */ - public function testFound($basePath, $path, $noBasePath) + public function testNotFound($basePath, $path) { - $middleware = new BasePath($basePath); - list($request, $response) = $this->getRequests(); - - $this->expectRequestSetBasePath($request, $basePath, $path, $noBasePath); + $request = $this->createMock(ServerRequestInterface::class); + $response = $this->createMock(ResponseInterface::class); - $result = $middleware($request, $response, function($request, $response) { - $response->nextCalled = true; + $middleware = new BasePath($basePath); + + $this->expectRequestGetPath($request, $path); + $finalResponse = $this->expectNotFound($response); - return $response; - }); + $next = $this->createCallbackMock($this->never()); + + $result = $middleware($request, $response, $next); - $this->assertEquals(get_class($response), get_class($result), "Middleware should return response object"); - $this->assertTrue($response->nextCalled, "'next' was not called"); + $this->assertSame($finalResponse, $result); } /** @@ -172,19 +156,31 @@ class BasePathTest extends PHPUnit_Framework_TestCase } /** - * Get requests for testing - * - * @param string $path - * @return array + * Test correct case, when path contains base path + * @dataProvider foundProvider + * + * @param string $basePath + * @param string $path + * @param string $noBasePath */ - public function getRequests($path = null) + public function testFound($basePath, $path, $noBasePath) { $request = $this->createMock(ServerRequestInterface::class); $response = $this->createMock(ResponseInterface::class); + $finalRespose = $this->createMock(ResponseInterface::class); + + $middleware = new BasePath($basePath); + + $this->expectRequestSetBasePath($request, $basePath, $path, $noBasePath); - return [$request, $response]; + $next = $this->createCallbackMock($this->once(), [$request, $response], $finalRespose); + + $result = $middleware($request, $response, $next); + + $this->assertSame($finalRespose, $result); } + /** * Expect that request will return a path * @@ -221,15 +217,19 @@ class BasePathTest extends PHPUnit_Framework_TestCase * Expect for not found error * * @param ResponseInterface $response + * @return ResponseInterface */ public function expectNotFound(ResponseInterface $response) { + $finalResponse = $this->createMock(ResponseInterface::class); + + $response->expects($this->once())->method('withStatus')->with(404)->willReturn($finalResponse); + $stream = $this->createMock(StreamInterface::class); - $stream->expects($this->once())->method('rewind'); $stream->expects($this->once())->method('write')->with($this->equalTo('Not Found')); - $response->method('getBody')->will($this->returnValue($stream)); - $response->expects($this->once())->method('withBody')->with($this->equalTo($stream))->will($this->returnSelf()); - $response->expects($this->once())->method('withStatus')->with($this->equalTo(404), $this->equalTo('Not Found'))->will($this->returnSelf()); + $finalResponse->expects($this->once())->method('getBody')->willReturn($stream); + + return $finalResponse; } } diff --git a/tests/Router/Middleware/ErrorHandlerTest.php b/tests/Router/Middleware/ErrorHandlerTest.php deleted file mode 100644 index c4b6313..0000000 --- a/tests/Router/Middleware/ErrorHandlerTest.php +++ /dev/null @@ -1,86 +0,0 @@ -<?php - -use Jasny\Router\Middleware\ErrorHandler; -use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\StreamInterface; - -class ErrorHandlerTest extends PHPUnit_Framework_TestCase -{ - /** - * Test invoke with invalid 'next' param - */ - public function testInvokeInvalidNext() - { - $middleware = new ErrorHandler(); - list($request, $response) = $this->getRequests(); - - $this->expectException(\InvalidArgumentException::class); - - $result = $middleware($request, $response, 'not_callable'); - } - - /** - * Test that exception in 'next' callback is caught - */ - public function testInvokeCatchError() - { - $middleware = new ErrorHandler(); - list($request, $response) = $this->getRequests(); - - $this->expectCatchError($response); - - $result = $middleware($request, $response, function($request, $response) { - throw new Exception('Test exception'); - }); - - $this->assertEquals(get_class($response), get_class($result), "Middleware should return response object"); - } - - /** - * Test case when there is no error - */ - public function testInvokeNoError() - { - $middleware = new ErrorHandler(); - list($request, $response) = $this->getRequests(); - - $result = $middleware($request, $response, function($request, $response) { - $response->nextCalled = true; - - return $response; - }); - - $this->assertEquals(get_class($response), get_class($result), "Middleware should return response object"); - $this->assertTrue($result->nextCalled, "'next' was not called"); - } - - /** - * Get requests for testing - * - * @return array - */ - public function getRequests() - { - $request = $this->createMock(ServerRequestInterface::class); - $response = $this->createMock(ResponseInterface::class); - - return [$request, $response]; - } - - /** - * Expect for error - * - * @param ResponseInterface $response - */ - public function expectCatchError($response) - { - $stream = $this->createMock(StreamInterface::class); - $stream->expects($this->once())->method('rewind'); - $stream->expects($this->once())->method('write')->with($this->equalTo('Unexpected error')); - - $response->method('getBody')->will($this->returnValue($stream)); - $response->expects($this->once())->method('withBody')->with($this->equalTo($stream))->will($this->returnSelf()); - $response->expects($this->once())->method('withStatus')->with($this->equalTo(500), $this->equalTo('Internal Server Error'))->will($this->returnSelf()); - } -} diff --git a/tests/Router/Middleware/NotFoundTest.php b/tests/Router/Middleware/NotFoundTest.php index 0c40a68..b8cd657 100644 --- a/tests/Router/Middleware/NotFoundTest.php +++ b/tests/Router/Middleware/NotFoundTest.php @@ -5,261 +5,222 @@ use Jasny\Router\Middleware\NotFound; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\StreamInterface; +use PHPUnit_Framework_MockObject_MockObject as MockObject; +/** + * @covers Jasny\Router\Middleware\NotFound + */ class NotFoundTest extends PHPUnit_Framework_TestCase { - /** - * Test creating object with false parameters - * - * @dataProvider constructProvider - * @param string $notFound - * @param string $notAllowed - * @param boolean $positive - */ - public function testConstruct($notFound, $notAllowed, $positive) - { - if (!$positive) $this->expectException(\InvalidArgumentException::class); - - $middleware = new NotFound($this->getRoutes(), $notFound, $notAllowed); - - if ($positive) $this->skipTest(); - } - - /** - * Provide data for testing '__contruct' - */ - public function constructProvider() + public function invalidStatusProvider() { return [ - [null, 405, false], - [true, true, false], - [99, null, false], - [600, null, false], - [404, 99, false], - [404, 600, false], - [200, 405, true], - [404, 200, true] + [0], + [true], + ['foo bar zoo'], + [1000], + [['abc']] ]; } /** - * Test invoke with invalid 'next' param + * @dataProvider invalidStatusProvider + * @expectedException InvalidArgumentException + * + * @param string $status */ - public function testInvokeInvalidNext() - { - $middleware = new NotFound($this->getRoutes(), 404, 405); - list($request, $response) = $this->getRequests(); - - $this->expectException(\InvalidArgumentException::class); - - $result = $middleware($request, $response, 'not_callable'); - } - - /** - * Test that 'next' callback is invoked when route is found - * - * @dataProvider invokeProvider - * @param callback|int $notFound - * @param callback|int $notAllowed - * @param callback $next - */ - public function testInvokeFound($notFound, $notAllowed, $next) + public function testConstructInvalidNotFound($status) { - if (!$next) return $this->skipTest(); - - list($request, $response) = $this->getRequests(); - $routes = $this->getRoutes(); - $middleware = new NotFound($routes, $notFound, $notAllowed); - - $this->expectRoute($routes, $request, 'found'); - $this->notExpectSimpleError($response); - - $result = $middleware($request, $response, $next); - - $this->assertEquals(get_class($response), get_class($result), "Result must be an instance of 'ResponseInterface'"); - $this->assertTrue($result->nextCalled, "'next' was not called"); - $this->assertFalse(isset($result->notAllowedCalled), "'Not allowed' callback was called"); - $this->assertFalse(isset($result->notFoundCalled), "'Not found' callback was called"); + new NotFound($this->createMock(Routes::class), $status); } /** - * Test __invoke method in case of route is found with another method - * - * @dataProvider invokeProvider - * @param callback|int $notFound - * @param callback|int $notAllowed - * @param callback $next + * @expectedException InvalidArgumentException */ - public function testInvokeNotAllowed($notFound, $notAllowed, $next) + public function testConstructNotFoundNotNull() { - if (!$notAllowed) return $this->skipTest(); - - list($request, $response) = $this->getRequests(); - $routes = $this->getRoutes(); - $middleware = new NotFound($routes, $notFound, $notAllowed); - - $this->expectRoute($routes, $request, 'notAllowed'); - if (is_numeric($notAllowed)) { - $this->expectSimpleError($response, $notAllowed); - } - - $result = $middleware($request, $response, $next); - - $this->assertEquals(get_class($response), get_class($result), "Result must be an instance of 'ResponseInterface'"); - $this->assertFalse(isset($result->nextCalled), "'next' was called"); - - if (is_callable($notAllowed)) { - $this->assertTrue($result->notAllowedCalled, "'Not allowed' callback was not called"); - } + new NotFound($this->createMock(Routes::class), null); } /** - * Test __invoke method in case of route not found at all - * - * @dataProvider invokeProvider - * @param callback|int $notFound - * @param callback|int $notAllowed - * @param callback $next + * @dataProvider invalidStatusProvider + * @expectedException InvalidArgumentException + * + * @param string $status */ - public function testInvokeNotFound($notFound, $notAllowed, $next) + public function testConstructInvalidMethodNotAllowed($status) { - list($request, $response) = $this->getRequests(); - $routes = $this->getRoutes(); - $middleware = new NotFound($routes, $notFound, $notAllowed); - - $case = $notAllowed ? 'notFoundTwice' : 'notFoundOnce'; - $this->expectRoute($routes, $request, $case); - - if (is_numeric($notFound)) { - $this->expectSimpleError($response, $notFound); - } - - $result = $middleware($request, $response, $next); - - $this->assertEquals(get_class($response), get_class($result), "Result must be an instance of 'ResponseInterface'"); - $this->assertFalse(isset($result->nextCalled), "'next' was called"); - - if (is_callable($notAllowed)) { - $this->assertFalse(isset($result->notAllowedCalled), "'Not allowed' callback was called"); - } - if (is_callable($notFound)) { - $this->assertTrue($result->notFoundCalled, "'Not found' callback was not called"); - } + new NotFound($this->createMock(Routes::class), 404, $status); } /** - * Set expectations on finding route - * - * @param Routes $routes - * @param ServerRequestInterface $request - * @param string $case + * @expectedException InvalidArgumentException */ - public function expectRoute($routes, $request, $case) - { - if ($case === 'found' || $case === 'notFoundOnce') { - $found = $case === 'found'; + public function testInvokeInvalidNext() + { + $request = $this->createMock(ServerRequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + + $middleware = new NotFound($this->createMock(Routes::class)); - $routes->expects($this->once())->method('hasRoute') - ->with($this->equalTo($request))->will($this->returnValue($found)); - } elseif ($case === 'notAllowed' || $case === 'notFoundTwice') { - $routes->expects($this->exactly(2))->method('hasRoute') - ->withConsecutive( - [$this->equalTo($request)], - [$this->equalTo($request), $this->equalTo(false)] - )->will($this->returnCallback(function($request, $searchMethod = true) use ($case) { - return $case === 'notFoundTwice' ? false : !$searchMethod; - })); - } + $middleware($request, $response, 'foo bar zoo'); } + /** * Provide data for testing invoke method */ public function invokeProvider() { - $callbacks = []; - foreach (['notFound', 'notAllowed', 'next'] as $type) { - $var = $type . 'Called'; - - $callbacks[$type] = function($request, $response) use ($var) { - $response->$var = true; - return $response; - }; - } - + $request = $this->createMock(ServerRequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + + $mockCallback = function() { + return $this->getMockBuilder(\stdClass::class)->setMethods(['__invoke'])->getMock(); + }; + return [ - [404, 405, $callbacks['next']], - [404, 405, null], - [404, null, $callbacks['next']], - [404, null, null], - [$callbacks['notFound'], $callbacks['notAllowed'], $callbacks['next']], - [$callbacks['notFound'], $callbacks['notAllowed'], null], - [$callbacks['notFound'], null, $callbacks['next']], - [$callbacks['notFound'], null, null] + [$request, $response, 404, 405, $mockCallback()], + [$request, $response, 404, null, $mockCallback()], + [$request, $response, '200', '402', $mockCallback()], + [$request, $response, $mockCallback(), $mockCallback(), $mockCallback()], + [$request, $response, $mockCallback(), null, $mockCallback()] ]; } /** - * Expect that response is set to simple deny answer - * - * @param ResponseInterface $response - * @param int $statusCode + * Test that 'next' callback is invoked when route is found + * @dataProvider invokeProvider + * + * @param ServerRequestInterface|MockObject $request + * @param ResponseInterface|MockObject $response + * @param callback|MockObject|int $notFound + * @param callback|MockObject|int $methodNotAllowed + * @param callback|MockObject $next */ - public function expectSimpleError(ResponseInterface $response, $statusCode) + public function testInvokeFound($request, $response, $notFound, $methodNotAllowed, $next) { - $stream = $this->createMock(StreamInterface::class); - $stream->expects($this->once())->method('rewind'); - $stream->expects($this->once())->method('write')->with($this->equalTo('Not Found')); + $finalResponse = $this->createMock(ResponseInterface::class); + + if ($notFound instanceof MockObject) { + $notFound->expects($this->never())->method('__invoke'); + } + + if ($methodNotAllowed instanceof MockObject) { + $methodNotAllowed->expects($this->never())->method('__invoke'); + } + + $next->expects($this->once())->method('__invoke')->with($request, $response)->willReturn($finalResponse); + + $response->expects($this->never())->method('withStatus'); + + $routes = $this->createMock(Routes::class); + $routes->expects($this->once())->method('hasRoute')->with($request)->willReturn(true); + + $middleware = new NotFound($routes, $notFound, $methodNotAllowed); + + $result = $middleware($request, $response, $next); - $response->method('getBody')->will($this->returnValue($stream)); - $response->expects($this->once())->method('withBody')->with($this->equalTo($stream))->will($this->returnSelf()); - $response->expects($this->once())->method('withStatus')->with($this->equalTo($statusCode), $this->equalTo('Not Found'))->will($this->returnSelf()); + $this->assertSame($finalResponse, $result); } /** - * Expect that there would be no simple error response - * - * @param ResponseInterface $response + * Test __invoke method in case of route is found with another method + * @dataProvider invokeProvider + * + * @param ServerRequestInterface|MockObject $request + * @param ResponseInterface|MockObject $response + * @param callback|MockObject|int $notFound + * @param callback|MockObject|int $methodNotAllowed + * @param callback|MockObject $next */ - public function notExpectSimpleError(ResponseInterface $response) + public function testInvokeNotFound($request, $response, $notFound, $methodNotAllowed, $next) { + $finalResponse = $this->createMock(ResponseInterface::class); $stream = $this->createMock(StreamInterface::class); - $stream->expects($this->never())->method('rewind'); - $stream->expects($this->never())->method('write'); - - $response->expects($this->never())->method('getBody'); - $response->expects($this->never())->method('withBody'); - $response->expects($this->never())->method('withStatus'); - } + + if ($notFound instanceof MockObject) { + $notFound->expects($this->once())->method('__invoke') + ->with($request, $response) + ->willReturn($finalResponse); + + $response->expects($this->never())->method('withStatus'); + } else { + $response->expects($this->once())->method('withStatus') + ->with($notFound) + ->willReturn($finalResponse); + + $finalResponse->expects($this->once())->method('getBody')->willReturn($stream); + $stream->expects($this->once())->method('write')->with('Not found'); + } + + if ($methodNotAllowed instanceof MockObject) { + $methodNotAllowed->expects($this->never())->method('__invoke'); + } + + $next->expects($this->never())->method('__invoke'); + + $routes = $this->createMock(Routes::class); + + $routes->expects($this->exactly(isset($methodNotAllowed) ? 2 : 1))->method('hasRoute') + ->withConsecutive([$request], [$request, false]) + ->willReturn(false); + + $middleware = new NotFound($routes, $notFound, $methodNotAllowed); - /** - * Get requests for testing - * - * @return array - */ - public function getRequests() - { - $request = $this->createMock(ServerRequestInterface::class); - $response = $this->createMock(ResponseInterface::class); + $result = $middleware($request, $response, $next); - return [$request, $response]; + $this->assertSame($finalResponse, $result); } /** - * Get routes array - * - * @return Routes + * Test __invoke method in case of route is found with another method + * @dataProvider invokeProvider + * + * @param ServerRequestInterface|MockObject $request + * @param ResponseInterface|MockObject $response + * @param callback|MockObject|int $notFound + * @param callback|MockObject|int $methodNotAllowed + * @param callback|MockObject $next */ - public function getRoutes() + public function testInvokeMethodNotAllowed($request, $response, $notFound, $methodNotAllowed, $next) { - return $this->getMockBuilder(Routes::class)->disableOriginalConstructor()->getMock(); - } + $finalResponse = $this->createMock(ResponseInterface::class); + $stream = $this->createMock(StreamInterface::class); + + $expect = $methodNotAllowed ?: $notFound; + + if ($expect !== $notFound && $notFound instanceof MockObject) { + $notFound->expects($this->never())->method('__invoke'); + } + + if ($expect instanceof MockObject) { + $expect->expects($this->once())->method('__invoke') + ->with($request, $response) + ->willReturn($finalResponse); + + $response->expects($this->never())->method('withStatus'); + } else { + $response->expects($this->once())->method('withStatus') + ->with($expect) + ->willReturn($finalResponse); + + $finalResponse->expects($this->once())->method('getBody')->willReturn($stream); + $stream->expects($this->once())->method('write')->with('Not found'); + } + + $next->expects($this->never())->method('__invoke'); + + $routes = $this->createMock(Routes::class); + + $routes->expects($this->exactly(isset($methodNotAllowed) ? 2 : 1))->method('hasRoute') + ->withConsecutive([$request], [$request, false]) + ->will($this->onConsecutiveCalls(false, true)); + + $middleware = new NotFound($routes, $notFound, $methodNotAllowed); - /** - * Skip the test pass - */ - public function skipTest() - { - return $this->assertTrue(true); + $result = $middleware($request, $response, $next); + + $this->assertSame($finalResponse, $result); } } diff --git a/tests/Router/RouteTest.php b/tests/Router/RouteTest.php new file mode 100644 index 0000000..4a72d27 --- /dev/null +++ b/tests/Router/RouteTest.php @@ -0,0 +1,42 @@ +<?php + +namespace Jasny\Router; + +use Jasny\Router\Route; + +/** + * @covers Jasny\Router\Route + */ +class RouteTest extends \PHPUnit_Framework_TestCase +{ + public function provider() + { + return [ + [['foo' => '$1', 'color' => 'red', 'number' => 42]], + [(object)['foo' => '$1', 'color' => 'red', 'number' => 42]] + ]; + } + + /** + * @dataProvider provider + * + * @param array|stdClass $values + */ + public function testConstructionWithObject($values) + { + $route = new Route($values); + + $this->assertAttributeSame('$1', 'foo', $route); + $this->assertAttributeSame('red', 'color', $route); + $this->assertAttributeSame(42, 'number', $route); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Route values should be an array, not a string + */ + public function testConstructionInvalidArgument() + { + new Route('foo'); + } +} diff --git a/tests/Router/Routes/GlobTest.php b/tests/Router/Routes/GlobTest.php index 3ed4d21..5efa77f 100644 --- a/tests/Router/Routes/GlobTest.php +++ b/tests/Router/Routes/GlobTest.php @@ -3,13 +3,17 @@ namespace Jasny\Router\Routes; use Jasny\Router\Routes\Glob; +use Jasny\Router\Route; use Psr\Http\Message\ServerRequestInterface; use ArrayObject; use BadMethodCallException; use InvalidArgumentException; -use AppendIterator; +/** + * @covers Jasny\Router\Routes\Glob + * @covers Jasny\Router\UrlParsing + */ class GlobTest extends \PHPUnit_Framework_TestCase { /** @@ -17,63 +21,55 @@ class GlobTest extends \PHPUnit_Framework_TestCase */ public function testConstructor() { - #Test empty constructor $glob = new Glob(); $this->assertInstanceOf('ArrayObject', $glob, "Should be an instance of 'ArrayObject'"); $this->assertEquals(0, $glob->count(), "Default count is not empty"); $this->assertEquals(0, $glob->getFlags(), "Default flags are not empty"); $this->assertEquals('ArrayIterator', $glob->getIteratorClass(), "Default iterator class is not correct"); - #Actual check for public values + // Actual check for available routes $count = 0; foreach ($glob as $value) { $count++; break; } - $this->assertEquals(0, $count); + $this->assertEquals(0, $count); } public function testConstructorWithArguments() { - #Test with params $values = [ '/foo/bar' => ['controller' => 'value1'], '/foo/*' => ['fn' => 'value3'], '/foo/*/bar' => ['file' => 'value5'], ]; - $glob = new Glob($values, ArrayObject::ARRAY_AS_PROPS, AppendIterator::class); + $glob = new Glob($values, ArrayObject::ARRAY_AS_PROPS); - $this->assertEquals(count($values), $glob->count(), "Routes count is not match"); + $this->assertCount(3, $glob, "Routes count do not match"); $this->assertEquals(ArrayObject::ARRAY_AS_PROPS, $glob->getFlags(), "Flags are not correct"); - $this->assertEquals(AppendIterator::class, $glob->getIteratorClass(), "Iterator class is not correct"); - foreach ($values as $pattern => $options) { - $this->assertTrue($glob->offsetExists($pattern), "Key '$pattern' is missing"); + foreach ($glob as $pattern => $route) { + $this->assertInstanceof(Route::class, $route); + $this->assertArrayHasKey($pattern, $values); + $this->assertArraysEqual($values[$pattern], (array)$route); } + + return $glob; } - + /** - * Test ArrayObject::exchangeArray method + * @depends testConstructorWithArguments * - * @dataProvider exchangeArrayProvider + * @param Glob $original */ - public function testExchangeArray($set, $reset) + public function testConstructorTraversable(Glob $original) { - $glob = new Glob($set); - $old = $glob->exchangeArray($reset); - - $this->assertEquals(count($set), count($old), "Old routes count is not match"); - $this->assertEquals(count($reset), $glob->count(), "Routes count is not match"); - - foreach ($reset as $pattern => $options) { - $this->assertTrue($glob->offsetExists($pattern), "Key is missing"); - } - foreach ($set as $pattern => $options) { - $this->assertTrue(!empty($old[$pattern]), "Old key is missing"); - $this->assertFalse($glob->offsetExists($pattern), "Key exists, but should not"); - } + $glob = new Glob($original); + + $this->assertCount(3, $glob, "Routes count do not match"); + $this->assertEquals($original->getArrayCopy(), $glob->getArrayCopy()); } /** @@ -98,70 +94,93 @@ class GlobTest extends \PHPUnit_Framework_TestCase } /** - * Test ArrayObject::offsetSet method + * Test ArrayObject::exchangeArray method * - * @dataProvider offsetSetProvider - * @param string $pattern - * @param array $options - * @param string $exception + * @dataProvider exchangeArrayProvider */ - public function testOffsetSet($pattern, $options, $exception) + public function testExchangeArray($set, $reset) { - if ($exception) $this->expectException($exception); - - $glob = new Glob(); - $glob->offsetSet($pattern, $options); + $glob = new Glob($set); + $old = $glob->exchangeArray($reset); - if ($exception) return; + $this->assertEquals(count($set), count($old), "Old routes count do not match"); + $this->assertEquals(count($reset), $glob->count(), "Routes count do not match"); - $this->assertEquals(1, $glob->count(), "Routes count is not match"); - $this->assertTrue($glob->offsetExists($pattern), "Key is missing"); - - #Verify that value was set correctly - $value = (array)$glob->offsetGet($pattern); - $this->assertEmpty(array_diff($options, $value), "Route was not set correct"); + foreach ($reset as $pattern => $options) { + $this->assertTrue($glob->offsetExists($pattern), "Key is missing"); + } + foreach ($set as $pattern => $options) { + $this->assertTrue(!empty($old[$pattern]), "Old key is missing"); + $this->assertFalse($glob->offsetExists($pattern), "Key exists, but should not"); + } } + /** * Provide data for testOffsetSet() */ public function offsetSetProvider() { return [ - ['/foo/*', ['controller' => 'bar'], ''], - ['/foo/*', ['fn' => 'bar'], ''], - ['/foo/*', ['file' => 'bar'], ''], - ['', ['controller' => 'bar'], BadMethodCallException::class], - ['foo', 'bar', InvalidArgumentException::class], - ['', '', BadMethodCallException::class] + ['/foo/*', ['controller' => 'bar']], + ['/foo/*', ['fn' => 'bar']], + ['/foo/*', ['file' => 'bar']], + ['/foo/*', $this->getMockBuilder(Route::class)->setConstructorArgs([['controller' => 'bar']])->getMock()] ]; } + + /** + * Test ArrayObject::offsetSet method + * @dataProvider offsetSetProvider + * + * @param string $pattern + * @param array $options + */ + public function testOffsetSet($pattern, $options) + { + $glob = new Glob(); + $glob[$pattern] = $options; + $this->assertCount(1, $glob); + $this->assertTrue(isset($glob[$pattern])); + + $route = $glob[$pattern]; + $this->assertInstanceOf(Route::class, $route); + + if ($options instanceof Route) { + $this->assertSame($options, $route); + } else { + $this->assertEquals([], array_diff($options, (array)$route)); + } + } + /** - * Test ArrayObject::append method - * * @expectedException BadMethodCallException */ - public function testAppend() + public function testOffsetSetInvalidPattern() { $glob = new Glob(); - $glob->append(['controller' => 'bar']); + $glob[''] = ['controller' => 'bar']; + } + + /** + * @expectedException InvalidArgumentException + */ + public function testOffsetSetInvalidRoute() + { + $glob = new Glob(); + $glob['/foo'] = 'bar'; } /** - * Test matching of url pattern to given uri + * Test ArrayObject::append method * - * @dataProvider fnMatchProvider - * @param string $pattern - * @param string $uri - * @param boolean $positive + * @expectedException BadMethodCallException */ - public function testFnMatch($pattern, $uri, $positive) - { + public function testAppend() + { $glob = new Glob(); - - $this->assertEquals($positive, $glob->fnmatch($pattern, $uri), - "Pattern and uri should " . ($positive ? "" : "not") . " match"); + $glob->append(['controller' => 'bar']); } /** @@ -205,12 +224,28 @@ class GlobTest extends \PHPUnit_Framework_TestCase } /** - * Testing getting route and it's existense + * Test matching of url pattern to given uri + * @dataProvider fnMatchProvider * + * @param string $pattern + * @param string $uri + * @param boolean $positive + */ + public function testFnMatch($pattern, $uri, $positive) + { + $glob = new Glob(); + + $this->assertEquals($positive, $glob->fnmatch($pattern, $uri), + "Pattern and uri should " . ($positive ? "" : "not") . " match"); + } + + /** + * Testing getting route and it's existense * @dataProvider getHasRouteProvider - * @param string $uri Uri of ServerRequest - * @param string $method Query method name - * @param boolean $positive If the test should be positive or negative + * + * @param string $uri Uri of ServerRequest + * @param string $method Query method name + * @param boolean $positive If the test should be positive or negative */ public function testGetHasRoute($uri, $method, $positive) { @@ -241,7 +276,7 @@ class GlobTest extends \PHPUnit_Framework_TestCase $this->assertTrue($exist, "Route not exists"); $this->assertTrue((bool)$match, "Found no match of uri with patterns"); - $this->assertEquals($values[$match]['controller'], $route['controller'], "False route obtained"); + $this->assertEquals($values[$match]['controller'], $route->controller, "False route obtained"); } /** @@ -295,152 +330,6 @@ class GlobTest extends \PHPUnit_Framework_TestCase } /** - * Test binding simple string when getting route - */ - public function testBindVarString() - { - $uri = '/foo/bar'; - $values = [$uri => ['controller' => 'value1', 'check' => 'value1']]; - - $glob = new Glob($values); - $request = $this->getServerRequest($uri); - $route = $glob->getRoute($request); - - $this->assertEquals($route['check'], $values[$uri]['check'], "Option value is not correct"); - } - - /** - * Test binding single url part to route option - * @dataProvider bindVarSingleUrlPartProvider - * @param string $patter - * @param string $uri - * @param array $options Route options - */ - public function testBindVarSingleUrlPart($pattern, $uri, $options) - { - $values = [$pattern => $options]; - - $glob = new Glob($values); - $request = $this->getServerRequest($uri); - $route = $glob->getRoute($request); - - $this->assertTrue((bool)$route, "Route not found"); - - if (!empty($options['check'])) { - $this->assertEquals('test', $route['check'], "Single pocket did not match"); - } elseif(empty($options['check2'])) { - $this->assertEquals('test1/test2', $route['check1'], "Single compound pocket did not match"); - } else { - $this->assertEquals('test1', $route['check1'], "First of two pockets did not match"); - $this->assertEquals('test2', $route['check2'], "Second of two pockets did not match"); - } - } - - /** - * Provide uri's and corresponding patterns for testBindVarSingleUrlPart() - */ - public function bindVarSingleUrlPartProvider() - { - return [ - ['/*', '/test', ['controller' => 'value', 'check' => '$1']], - ['/foo/*/bar', '/foo/test/bar', ['controller' => 'value', 'check' => '$2']], - ['/foo/bar/*', '/foo/bar/test', ['controller' => 'value', 'check' => '$3']], - ['/foo/bar/*/zet/*', '/foo/bar/test1/zet/test2', ['controller' => 'value', 'check1' => '$3', 'check2' => '$5']], - ['/foo/bar/*/zet/*', '/foo/bar/test1/zet/test2', ['controller' => 'value', 'check1' => '~$3~/~$5~']] - ]; - } - - /** - * Test binding multyple url parts to route option - * - * @dataProvider bindVarMultipleUrlPartsProvider - * @param string $uri - * @param array $options Route options - * @param boolean $positive - * @param string $exception - */ - public function testBindVarMultipleUrlParts($uri, $options, $positive, $exception) - { - if ($exception) $this->expectException(InvalidArgumentException::class); - - $values = [$uri => $options]; - $glob = new Glob($values); - $request = $this->getServerRequest($uri); - $route = $glob->getRoute($request); - - if ($exception) return; - - $values = explode('/', trim($uri, '/')); - $this->assertTrue((bool)$route, "Route not found"); - - $positive ? - $this->assertArraysEqual($values, $route['check'], "Multyple url parts are not picked correctly") : - $this->assertEmpty($route['check'], "Multyple parts element should be empty"); - - if (!empty($options['check2'])) { - array_shift($values); - $this->assertArraysEqual($values, $route['check2'], "Secondary multyple url parts are not picked correctly"); - } - } - - /** - * Provide uri's and corresponding patterns for testBindVarMultipleUrlParts() - */ - public function bindVarMultipleUrlPartsProvider() - { - return [ - ['/foo', ['controller' => 'value', 'check' => '$1...'], false, InvalidArgumentException::class], - ['/', ['controller' => 'value', 'check' => ['$1...']], false, ''], - ['/foo', ['controller' => 'value', 'check' => ['$1...']], true, ''], - ['/foo/bar', ['controller' => 'value', 'check' => ['$1...'], 'check2' => ['$2...']], true, ''] - ]; - } - - /** - * Test binding element of superglobal array to route option - * - * @dataProvider bindVarSuperGlobalProvider - * @param string $uri - * @param array $options - * @param string $type ('get', 'post', 'cookie') - */ - public function testBindVarSuperGlobal($uri, $options, $type) - { - $test = ['check' => 'test']; - $glob = new Glob([$uri => $options]); - $request = $this->getServerRequest($uri, 'GET', [$type => $test]); - $route = $glob->getRoute($request); - - $this->assertEquals($test['check'], $route['check'], "Did not obtaine value for superglobal '$type'"); - } - - /** - * Provide uri's and corresponding patterns for testBindVarMultipleUrlParts() - */ - public function bindVarSuperGlobalProvider() - { - return [ - ['/foo', ['controller' => 'value', 'check' => '$_GET[check]'], 'get'], - ['/foo', ['controller' => 'value', 'check' => '$_POST[check]'], 'post'], - ['/foo', ['controller' => 'value', 'check' => '$_COOKIE[check]'], 'cookie'] - ]; - } - - /** - * Test binding element of superglobal array to route option - */ - public function testBindVarRequestHeader() - { - $uri = '/foo/bar'; - $test = 'test_header_value'; - $glob = new Glob([$uri => ['controller' => 'value', 'check' => '$HTTP_REFERER']]); - $request = $this->getServerRequest($uri, 'GET', [], $test); - $route = $glob->getRoute($request); - - $this->assertEquals($test, $route['check'], "Did not obtaine value for header"); - } - - /** * Get ServerRequestInterface object * * @param string $uri @@ -469,7 +358,7 @@ class GlobTest extends \PHPUnit_Framework_TestCase */ public function assertArraysEqual(array $array1, array $array2) { - $this->assertEquals(count($array1), count($array2)); - $this->assertEmpty(array_diff($array1, $array2)); + $this->assertEmpty(array_diff($array2, $array1), 'Missing items'); + $this->assertEmpty(array_diff($array1, $array2), 'Additional items'); } } diff --git a/tests/Router/Routes/RouteBindingTest.php b/tests/Router/Routes/RouteBindingTest.php new file mode 100644 index 0000000..96536ac --- /dev/null +++ b/tests/Router/Routes/RouteBindingTest.php @@ -0,0 +1,263 @@ +<?php + +namespace Jasny\Router\Routes; + +use Jasny\Router\Routes\Glob; +use Jasny\Router\Route; +use Psr\Http\Message\ServerRequestInterface; + +use InvalidArgumentException; + +/** + * @covers Jasny\Router\Routes\RouteBinding + */ +class RouteBindingTest extends \PHPUnit_Framework_TestCase +{ + /** + * Test binding simple string when getting route + */ + public function testBindVarString() + { + $uri = '/foo/bar'; + $values = [$uri => ['controller' => 'value1', 'check' => 'value1']]; + + $glob = new Glob($values); + $request = $this->getServerRequest($uri); + $route = $glob->getRoute($request); + + $this->assertEquals($values[$uri]['check'], $route->check); + } + + /** + * Provide uri's and corresponding patterns for testBindVarSingleUrlPart() + */ + public function bindVarSingleUrlPartProvider() + { + return [ + ['/*', '/test', ['check' => '$1'], 'test'], + ['/', '/', ['check' => '$1|test'], 'test'], + ['/foo/*/bar', '/foo/test/bar', ['check' => '$2'], 'test'], + ['/foo/bar/*', '/foo/bar/test', ['check' => '$3'], 'test'], + ['/foo/bar/*/zet/*', '/foo/bar/test1/zet/test2', ['check' => '$3', 'checkB' => '$5'], 'test1', 'test2'], + ['/foo/bar/*/zet/*', '/foo/bar/test1/zet/test2', ['check' => '~$3~/~$5~'], 'test1/test2'], + ['/', '/', ['check' => '$foo'], null], + ['/', '/', ['check' => 'test', 'checkB' => null], 'test', null] + ]; + } + + /** + * Test binding single url part to route option + * @dataProvider bindVarSingleUrlPartProvider + * + * @param string $pattern + * @param string $uri + * @param array $options Route options + * @param string $check Expected value for `check` + * @param string $checkB Expected value for `checkB` + */ + public function testBindVarSingleUrlPart($pattern, $uri, $options, $check, $checkB = null) + { + $values = [$pattern => $options]; + + $glob = new Glob($values); + $request = $this->getServerRequest($uri); + $route = $glob->getRoute($request); + + $this->assertNotNull($route, "Route not found"); + $this->assertInstanceOf(Route::class, $route); + + $this->assertEquals($check, $route->check); + + if (isset($checkB)) { + $this->assertEquals($checkB, $route->checkB); + } else { + $this->assertObjectNotHasAttribute('checkB', $route); + } + } + + public function testBindVarWithObject() + { + $object = new \Exception(); // Could be anything, just not stdClass + $glob = new Glob(['/' => ['object' => $object]]); + + $request = $this->getServerRequest('/'); + $route = $glob->getRoute($request); + + $this->assertNotNull($route, "Route not found"); + $this->assertInstanceOf(Route::class, $route); + + $this->assertSame($object, $route->object); + } + + public function bindVarWithSubProvider() + { + return [ + [['group' => ['check' => '$1']], 'array'], + [['group' => (object)['check' => '$1']], 'object'], + [['group' => ['sub' => (object)['check' => '$1']]], 'array', 'object'], + [['group' => (object)['sub' => ['check' => '$1']]], 'object', 'array'] + ]; + } + + /** + * @dataProvider bindVarWithSubProvider + * + * @param array $options + * @param string $type + * @param string $subtype + */ + public function testBindVarWithSub(array $options, $type, $subtype = null) + { + $glob = new Glob(['/*' => $options]); + + $request = $this->getServerRequest('/test'); + $route = $glob->getRoute($request); + + $this->assertNotNull($route, "Route not found"); + $this->assertInstanceOf(Route::class, $route); + + $this->assertInternalType($type, $route->group); + + $group = (array)$route->group; + + if (isset($subtype)) { + $this->assertArrayHasKey('sub', $group); + $this->assertInternalType($subtype, $group['sub']); + + $group = (array)$group['sub']; + } + + $this->assertEquals($group, ['check' => 'test']); + } + + + /** + * Provide uri's and corresponding patterns for testBindVarMultipleUrlParts() + */ + public function bindVarMultipleUrlPartsProvider() + { + return [ + ['/foo', ['check' => '$1...'], false, InvalidArgumentException::class], + ['/', ['check' => ['$1...']], false], + ['/foo', ['check' => ['$1...']], true], + ['/foo/bar', ['check' => ['$1...'], 'checkB' => ['$2...']], + InvalidArgumentException::class] + ]; + } + + /** + * Test binding multyple url parts to route option + * @dataProvider bindVarMultipleUrlPartsProvider + * + * @param string $uri + * @param array $options Route options + * @param boolean $positive + * @param string $exception + */ + public function testBindVarMultipleUrlParts($uri, $options, $positive, $exception = false) + { + if ($exception) { + $this->expectException($exception); + } + + $glob = new Glob([$uri => $options]); + $request = $this->getServerRequest($uri); + $route = $glob->getRoute($request); + + if ($exception) return; + + $this->assertNotNull($route, "Route not found"); + $this->assertInstanceOf(Route::class, $route); + + $values = explode('/', trim($uri, '/')); + + $positive ? + $this->assertArraysEqual($values, $route->check, "Multyple url parts are not picked correctly") : + $this->assertEmpty($route->check, "Multyple parts element should be empty"); + + if (!empty($options->checkB)) { + array_shift($values); + $this->assertArraysEqual($values, $route->checkB, "Secondary multyple url parts are not picked correctly"); + } + } + + /** + * Provide uri's and corresponding patterns for testBindVarMultipleUrlParts() + */ + public function bindVarSuperGlobalProvider() + { + return [ + ['/foo', ['check' => '$_GET[check]'], 'get'], + ['/foo', ['check' => '$_POST[check]'], 'post'], + ['/foo', ['check' => '$_COOKIE[check]'], 'cookie'] + ]; + } + + /** + * Test binding element of superglobal array to route option + * @dataProvider bindVarSuperGlobalProvider + * + * @param string $uri + * @param array $options + * @param string $type ('get', 'post', 'cookie') + */ + public function testBindVarSuperGlobal($uri, $options, $type) + { + $test = ['check' => 'test']; + $glob = new Glob([$uri => $options]); + $request = $this->getServerRequest($uri, 'GET', [$type => $test]); + $route = $glob->getRoute($request); + + $this->assertEquals($test['check'], $route->check, "Did not obtaine value for superglobal '$type'"); + } + + /** + * Test binding element of superglobal array to route option + */ + public function testBindVarRequestHeader() + { + $uri = '/foo/bar'; + $test = 'test_header_value'; + + $glob = new Glob([$uri => ['check' => '$HTTP_REFERER']]); + $request = $this->getServerRequest($uri, 'GET', [], $test); + + $route = $glob->getRoute($request); + $this->assertNotNull($route, "Route not found"); + + $this->assertEquals($test, $route->check); + } + + /** + * Get ServerRequestInterface object + * + * @param string $uri + * @param string $method Http query method + * @return ServerRequestInterface + */ + public function getServerRequest($uri, $method = 'GET', $globals = [], $header = '') + { + $request = $this->createMock(ServerRequestInterface::class); + $request->method('getUri')->willReturn($uri); + $request->method('getMethod')->willReturn($method); + $request->method('getQueryParams')->willReturn(isset($globals['get']) ? $globals['get'] : []); + $request->method('getParsedBody')->willReturn(isset($globals['post']) ? $globals['post'] : []); + $request->method('getCookieParams')->willReturn(isset($globals['cookie']) ? $globals['cookie'] : []); + $request->method('getHeaderLine')->willReturn($header); + + return $request; + } + + /** + * Assert that two 1-dimensional arrays are equal. + * Use if array elements are scalar values, or objects with __toString() method + * + * @param array $array1 + * @param array $array2 + */ + public function assertArraysEqual(array $array1, array $array2) + { + $this->assertEmpty(array_diff($array2, $array1), 'Missing items'); + $this->assertEmpty(array_diff($array1, $array2), 'Additional items'); + } +} diff --git a/tests/Router/Runner/CallbackTest.php b/tests/Router/Runner/CallbackTest.php index 8a31794..39c2798 100644 --- a/tests/Router/Runner/CallbackTest.php +++ b/tests/Router/Runner/CallbackTest.php @@ -1,64 +1,73 @@ <?php +namespace Jasny\Router; + use Jasny\Router\Route; use Jasny\Router\Runner\Callback; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; -class CallbackTest extends PHPUnit_Framework_TestCase +use Jasny\Router\TestHelpers; + +/** + * @covers Jasny\Router\Runner\Callback + */ +class CallbackTest extends \PHPUnit_Framework_TestCase { + use TestHelpers; + /** * Test creating Callback runner - * - * @dataProvider callbackProvider - * @param Route $route - * @param boolean $positive */ - public function testCallback($route, $positive) + public function testCallback() { - $runner = new Callback($route); - $request = $this->createMock(ServerRequestInterface::class); $response = $this->createMock(ResponseInterface::class); - $request->expects($this->once())->method('getAttribute')->with($this->equalTo('route'))->will($this->returnValue($route)); - - if (!$positive) $this->expectException(\RuntimeException::class); - $result = $runner->run($request, $response); - - if (!$positive) return; - - $this->assertEquals($request, $result['request'], "Request object was not passed correctly to result"); - $this->assertEquals($response, $result['response'], "Response object was not passed correctly to result"); + $finalResponse = $this->createMock(ResponseInterface::class); + + $route = $this->createMock(Route::class); + $route->fn = $this->createCallbackMock($this->once(), [$request, $response], $finalResponse); + + $request->expects($this->once())->method('getAttribute')->with('route')->willReturn($route); + + $runner = new Callback($route); + $result = $runner($request, $response); + + $this->assertSame($finalResponse, $result); } - - + /** - * Provide data fpr testing 'create' method + * @expectedException RuntimeException + * @expectedExceptionMessage 'fn' property of route shoud be a callable */ - public function callbackProvider() + public function testNoCallback() { - $callback = function($request, $response) { - return ['request' => $request, 'response' => $response]; - }; - - return [ - [Route::create(['fn' => $callback, 'value' => 'test']), true], - [Route::create(['fn' => [$this, 'getCallback'], 'value' => 'test']), true], - [Route::create(['controller' => 'TestController', 'value' => 'test']), false], - [Route::create(['file' => 'some_file.php', 'value' => 'test']), false], - [Route::create(['test' => 'test']), false], - ]; + $request = $this->createMock(ServerRequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + + $route = $this->createMock(Route::class); + + $request->expects($this->once())->method('getAttribute')->with('route')->willReturn($route); + + $runner = new Callback($route); + $runner($request, $response); } - + /** - * Testable callback for creating Route - * - * @param ServerRequestInterface $request - * @param ResponseInterface $response - * @return array + * @expectedException RuntimeException + * @expectedExceptionMessage 'fn' property of route shoud be a callable */ - public function getCallback($request, $response) + public function testInvalidCallback() { - return ['request' => $request, 'response' => $response]; + $request = $this->createMock(ServerRequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + + $route = $this->createMock(Route::class); + $route->fn = 'foo bar zoo'; + + $request->expects($this->once())->method('getAttribute')->with('route')->willReturn($route); + + $runner = new Callback($route); + $runner($request, $response); } } diff --git a/tests/Router/Runner/ControllerTest.php b/tests/Router/Runner/ControllerTest.php index 433d36d..694fe5b 100644 --- a/tests/Router/Runner/ControllerTest.php +++ b/tests/Router/Runner/ControllerTest.php @@ -1,140 +1,80 @@ <?php +namespace Jasny\Router; + use Jasny\Router\Route; -use Jasny\Router\Runner\Controller; +use Jasny\Router\Runner; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; -class ControllerTest extends PHPUnit_Framework_TestCase -{ - /** - * Tmp scripts - * @var array - **/ - public static $files = []; - - /** - * Test creating Controller runner - * - * @dataProvider phpScriptProvider - * @param Route $route - * @param boolean $positive - */ - public function testPhpScript($route, $positive) - { - $runner = new Controller($route); +use Jasny\Router\TestHelpers; +/** + * @covers Jasny\Router\Runner\Controller; + */ +class ControllerTest extends \PHPUnit_Framework_TestCase +{ + use TestHelpers; + + public function testInvoke() + { $request = $this->createMock(ServerRequestInterface::class); $response = $this->createMock(ResponseInterface::class); - $request->expects($this->once())->method('getAttribute')->with($this->equalTo('route'))->will($this->returnValue($route)); - - if (!$positive) $this->expectException(\RuntimeException::class); - $result = $runner->run($request, $response); - - $this->assertEquals($request, $result['request'], "Request object was not passed correctly to result"); - $this->assertEquals($response, $result['response'], "Response object was not passed correctly to result"); + $finalResponse = $this->createMock(ResponseInterface::class); + + $controller = $this->createCallbackMock($this->once(), [$request, $response], $finalResponse); + $class = get_class($controller); + + $route = $this->createMock(Route::class); + $route->controller = $class; + + $request->expects($this->once())->method('getAttribute')->with('route')->willReturn($route); + + $runner = $this->getMockBuilder(Runner\Controller::class)->setMethods(['instantiate'])->getMock(); + $runner->expects($this->once())->method('instantiate')->with($class)->willReturn($controller); + + $result = $runner($request, $response); + + $this->assertSame($finalResponse, $result); } - - /** - * Provide data for testing 'create' method - */ - public function phpScriptProvider() - { - foreach (['noInvoke', 'withInvoke'] as $type) { - list($class, $path) = static::createTmpScript($type); - static::$files[$type] = compact('class', 'path'); - } - - return [ - [Route::create(['test' => 'test']), false], - [Route::create(['fn' => 'testFunction', 'value' => 'test']), false], - [Route::create(['controller' => 'TestController', 'value' => 'test']), false], - [Route::create(['controller' => '', 'value' => 'test']), false], - [Route::create(['controller' => static::$files['noInvoke']['class'], 'path' => static::$files['noInvoke']['path']]), false], - [Route::create(['controller' => static::$files['withInvoke']['class'], 'path' => static::$files['withInvoke']['path']]), true], - ]; - } - - /** - * Delete tmp test scripts - */ - public static function tearDownAfterClass() - { - foreach (static::$files as $path) { - unlink($path['path']); - } - } - + /** - * Create single tmp script file for testing - * - * @param string $type ('returnTrue', 'returnNotTrue') - * @return string $path + * @expectedException RuntimeException + * @expectedExceptionMessage Can not route to controller 'FooBarZoo': class not exists */ - public static function createTmpScript($type) - { - $dir = rtrim(sys_get_temp_dir(), '/'); - - do { - $name = static::getRandomString() . '-test-script.php'; - $path = $dir . '/' . $name; - - if (!file_exists($path)) break; - } while (true); - - if ($type === 'noInvoke') { - $class = 'RunnerTestConrtollerInvalid'; - $content = -<<<CONTENT -<?php - -class $class { - public \$route = null; - - public function __construct(\$route) + public function testInvalidClass() { - \$this->route = \$route; - } -} -CONTENT; - } else { - $class = 'RunnerTestConrtoller'; - $content = -<<<CONTENT -<?php - -class $class { - public \$route = null; - - public function __construct(\$route) - { - \$this->route = \$route; + $request = $this->createMock(ServerRequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + + $route = $this->createMock(Route::class); + $route->controller = 'foo-bar-zoo'; + + $request->expects($this->once())->method('getAttribute')->with('route')->willReturn($route); + + $runner = $this->getMockBuilder(Runner\Controller::class)->setMethods(['instantiate'])->getMock(); + $runner->expects($this->never())->method('instantiate'); + + $runner($request, $response); } - public function __invoke(Psr\Http\Message\ServerRequestInterface \$request, Psr\Http\Message\ResponseInterface \$response) - { - return ['request' => \$request, 'response' => \$response]; - } -} -CONTENT; - } - - $bytes = file_put_contents($path, $content); - static::assertTrue((int)$bytes > 0); - - require_once $path; - - return [$class, $path]; - } - /** - * Get random string of given length (no more then length of md5 hash) - * - * @param int $length - * @return string + * @expectedException RuntimeException + * @expectedExceptionMessage Can not route to controller 'StdClass': class does not have '__invoke' method */ - public static function getRandomString($length = 10) - { - return substr(md5(microtime(true) * mt_rand()), 0, $length); + public function testInvokeNotCallable() + { + $request = $this->createMock(ServerRequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + + $route = $this->createMock(Route::class); + $route->controller = 'std-class'; + + $request->expects($this->once())->method('getAttribute')->with('route')->willReturn($route); + + $runner = $this->getMockBuilder(Runner\Controller::class)->setMethods(['instantiate'])->getMock(); + $runner->expects($this->never())->method('instantiate'); + + $runner($request, $response); } } diff --git a/tests/Router/Runner/PhpScriptTest.php b/tests/Router/Runner/PhpScriptTest.php index 2f31f73..658998b 100644 --- a/tests/Router/Runner/PhpScriptTest.php +++ b/tests/Router/Runner/PhpScriptTest.php @@ -1,90 +1,128 @@ <?php +namespace Jasny\Router; + use Jasny\Router\Route; -use Jasny\Router\Runner\PhpScript; +use Jasny\Router\Runner; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; -class PhpScriptTest extends PHPUnit_Framework_TestCase +use org\bovigo\vfs\vfsStream; +use org\bovigo\vfs\vfsStreamDirectory; + +/** + * @covers Jasny\Router\Runner\PhpScript + */ +class PhpScriptTest extends \PHPUnit_Framework_TestCase { /** - * Test creating PhpScript runner - * - * @dataProvider phpScriptProvider - * @param Route $route - * @param boolean $positive + * @var vfsStreamDirectory */ - public function testPhpScript($route, $positive) - { - $runner = new PhpScript($route); + protected $root; + + public function setUp() + { + $this->root = vfsStream::setup('root'); + $this->root->addChild(vfsStream::newFile('true.php')->setContent('<?php ?>')); + $this->root->addChild(vfsStream::newFile('foo.php')->setContent('<?php return "foo"; ?>')); + } + public function testInvoke() + { $request = $this->createMock(ServerRequestInterface::class); $response = $this->createMock(ResponseInterface::class); - $request->expects($this->once())->method('getAttribute')->with($this->equalTo('route'))->will($this->returnValue($route)); - - if (!$positive) $this->expectException(\RuntimeException::class); - $result = $runner->run($request, $response); + $finalResponse = $this->createMock(ResponseInterface::class); - if ($route->type === 'returnTrue') { - $this->assertEquals($response, $result, "Request object was not returned as result"); - } else { - $this->assertEquals($request, $result['request'], "Request object was not passed correctly to result"); - $this->assertEquals($response, $result['response'], "Response object was not passed correctly to result"); - } + $route = $this->createMock(Route::class); + $route->file = vfsStream::url('root/foo.php'); + + $request->expects($this->once())->method('getAttribute')->with('route')->willReturn($route); + + $runner = $this->getMockBuilder(Runner\PhpScript::class)->setMethods(['includeScript'])->getMock(); + $runner->expects($this->once())->method('includeScript') + ->with(vfsStream::url('root/foo.php'), $request, $response) + ->willReturn($finalResponse); - unlink($route->file); + $result = $runner($request, $response); + + $this->assertSame($finalResponse, $result); } - - /** - * Provide data fpr testing 'create' method - */ + public function phpScriptProvider() { + $routeTrue = $this->createMock(Route::class); + $routeTrue->file = vfsStream::url('root/true.php'); + + $routeFoo = $this->createMock(Route::class); + $routeFoo->file = vfsStream::url('root/foo.php'); + return [ - [Route::create(['test' => 'test']), false], - [Route::create(['fn' => 'testFunction', 'value' => 'test']), false], - [Route::create(['controller' => 'TestController', 'value' => 'test']), false], - [Route::create(['file' => '', 'value' => 'test']), false], - [Route::create(['file' => 'some_file.php', 'value' => 'test']), false], - [Route::create(['file' => '../' . basename(getcwd()), 'value' => 'test']), false], - [Route::create(['file' => $this->createTmpScript('returnTrue'), 'type' => 'returnTrue']), true], - [Route::create(['file' => $this->createTmpScript('returnNotTrue'), 'type' => 'returnNotTrue']), true] + [$routeTrue, 1], + [$routeFoo, 'foo'] ]; } - + /** - * Create single tmp script file for testing - * - * @param string $type ('returnTrue', 'returnNotTrue') - * @return string $path + * @dataProvider phpScriptProvider + * + * @param Route $route + * @param mixed $expected */ - public function createTmpScript($type) + public function testInvokeIncludeScript($route, $expected) { - $dir = rtrim(sys_get_temp_dir(), '/'); - - do { - $name = $this->getRandomString() . '-test-script.php'; - $path = $dir . '/' . $name; + $request = $this->createMock(ServerRequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + $request->expects($this->once())->method('getAttribute')->with($this->equalTo('route'))->will($this->returnValue($route)); - if (!file_exists($path)) break; - } while (true); + $runner = new Runner\PhpScript($route); + + if ($expected === 1) { + $expected = $response; + } + + $result = $runner->run($request, $response); + + $this->assertSame($expected, $result); + } - $content = $type === 'returnTrue' ? "<?php\n return true;" : "<?php\n return ['request' => \$request, 'response' => \$response];"; - $bytes = file_put_contents($path, $content); + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage Failed to route using 'vfs://root/bar.php': File doesn't exist + */ + public function testInvokeWithNonExistingFile() + { + $request = $this->createMock(ServerRequestInterface::class); + $response = $this->createMock(ResponseInterface::class); - $this->assertTrue((int)$bytes > 0); + $route = $this->createMock(Route::class); + $route->file = vfsStream::url('root/bar.php'); + + $request->expects($this->once())->method('getAttribute')->with('route')->willReturn($route); + + $runner = $this->getMockBuilder(Runner\PhpScript::class)->setMethods(['includeScript'])->getMock(); + $runner->expects($this->never())->method('includeScript'); - return $path; + $runner($request, $response); } /** - * Get random string of given length (no more then length of md5 hash) - * - * @param int $length - * @return string + * @expectedException \RuntimeException + * @expectedExceptionMessage Won't route to 'vfs://root/../bar.php': '~', '..' are not allowed in filename */ - public function getRandomString($length = 10) - { - return substr(md5(microtime(true) * mt_rand()), 0, $length); + public function testInvokeWithIlligalFilename() + { + $request = $this->createMock(ServerRequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + + $route = $this->createMock(Route::class); + $route->file = vfsStream::url('root/../bar.php'); + + $request->expects($this->once())->method('getAttribute')->with('route')->willReturn($route); + + $runner = $this->getMockBuilder(Runner\PhpScript::class)->setMethods(['includeScript'])->getMock(); + $runner->expects($this->never())->method('includeScript'); + + $runner($request, $response); } + } diff --git a/tests/Router/Runner/RunnerFactoryTest.php b/tests/Router/Runner/RunnerFactoryTest.php deleted file mode 100644 index c18656d..0000000 --- a/tests/Router/Runner/RunnerFactoryTest.php +++ /dev/null @@ -1,41 +0,0 @@ -<?php - -use Jasny\Router\Route; -use Jasny\Router\Runner\RunnerFactory; -use Jasny\Router\Runner\Controller; -use Jasny\Router\Runner\Callback; -use Jasny\Router\Runner\PhpScript; - -class RunnerFactoryTest extends PHPUnit_Framework_TestCase -{ - /** - * Test creating Runner object using factory - * - * @dataProvider createProvider - * @param Route $route - * @param string $class Runner class to use - * @param boolean $positive - */ - public function testCreate($route, $class, $positive) - { - if (!$positive) $this->expectException(\InvalidArgumentException::class); - - $factory = new RunnerFactory(); - $runner = $factory($route); - - $this->assertInstanceOf($class, $runner, "Runner object has invalid class"); - } - - /** - * Provide data fpr testing 'create' method - */ - public function createProvider() - { - return [ - [Route::create(['controller' => 'TestController', 'value' => 'test']), Controller::class, true], - [Route::create(['fn' => 'testFunction', 'value' => 'test']), Callback::class, true], - [Route::create(['file' => 'some_file.php', 'value' => 'test']), PhpScript::class, true], - [Route::create(['test' => 'test']), '', false], - ]; - } -} diff --git a/tests/Router/RunnerFactoryTest.php b/tests/Router/RunnerFactoryTest.php new file mode 100644 index 0000000..a9fbe92 --- /dev/null +++ b/tests/Router/RunnerFactoryTest.php @@ -0,0 +1,58 @@ +<?php + +namespace Jasny\Router; + +use Jasny\Router\RunnerFactory; +use Jasny\Router\Route; +use Jasny\Router\Runner; + +class RunnerFactoryTest extends \PHPUnit_Framework_TestCase +{ + /** + * Provide data fpr testing 'create' method + */ + public function createProvider() + { + $routeController = $this->createMock(Route::class); + $routeController->controller = 'foo-bar'; + + $routeCallback = $this->createMock(Route::class); + $routeCallback->fn = function() {}; + + $routePhpScript = $this->createMock(Route::class); + $routePhpScript->file = 'some_file.php'; + + return [ + [$routeController, Runner\Controller::class], + [$routeCallback, Runner\Callback::class], + [$routePhpScript, Runner\PhpScript::class], + ]; + } + + /** + * Test creating Runner object using factory + * @dataProvider createProvider + * + * @param Route $route + * @param string $class Runner class to use + */ + public function testCreate($route, $class) + { + $factory = new RunnerFactory(); + $runner = $factory($route); + + $this->assertInstanceOf($class, $runner, "Runner object has invalid class"); + } + + /** + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Route has neither 'controller', 'fn' or 'file' defined + */ + public function testCreatWithInvalideRoute() + { + $route = $this->createMock(Route::class); + + $factory = new RunnerFactory(); + $factory($route); + } +} diff --git a/tests/Router/RunnerTest.php b/tests/Router/RunnerTest.php index 7db4c02..3e558b4 100644 --- a/tests/Router/RunnerTest.php +++ b/tests/Router/RunnerTest.php @@ -1,46 +1,39 @@ <?php -use Jasny\Router\Route; use Jasny\Router\Runner; -use Jasny\Router\Runner\Controller; -use Jasny\Router\Runner\Callback; -use Jasny\Router\Runner\PhpScript; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; +use Jasny\Router\TestHelpers; + +/** + * @covers Jasny\Router\Runner + */ class RunnerTest extends PHPUnit_Framework_TestCase { + use TestHelpers; + /** * Test runner __invoke method */ public function testInvoke() { - $runner = $this->getMockBuilder(Runner::class)->disableOriginalConstructor()->getMockForAbstractClass(); - $queries = [ - 'request' => $this->createMock(ServerRequestInterface::class), - 'response' => $this->createMock(ResponseInterface::class) - ]; - - #Test that 'run' receives correct arguments inside '__invoke' - $runner->method('run')->will($this->returnCallback(function($arg1, $arg2) { - return ['request' => $arg1, 'response' => $arg2]; - })); - - $result = $runner($queries['request'], $queries['response']); - $this->assertEquals($result['request'], $queries['request'], "Request was not returned correctly from 'run'"); - $this->assertEquals($result['response'], $queries['response'], "Response was not returned correctly from 'run'"); - - #The same test with calling 'next' callback - $result = $runner($queries['request'], $queries['response'], function($request, $prevResponse) use ($queries) { - $this->assertEquals($request, $queries['request'], "Request is not correct in 'next'"); - $this->assertEquals($prevResponse['request'], $queries['request'], "Prev response was not passed correctly to 'next'"); - $this->assertEquals($prevResponse['response'], $queries['response'], "Prev response was not passed correctly to 'next'"); - - return $queries + ['next_called' => true]; - }); + $runner = $this->getMockBuilder(Runner::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $request = $this->createMock(ServerRequestInterface::class); + + $response = $this->createMock(ResponseInterface::class); + $runResponse = $this->createMock(ResponseInterface::class); + $finalResponse = $this->createMock(ResponseInterface::class); - $this->assertTrue($result['next_called'], "'Next' callback was not called"); - $this->assertEquals($result['request'], $queries['request'], "Request was not returned correctly from 'run' with 'next'"); - $this->assertEquals($result['response'], $queries['response'], "Request was not returned correctly from 'run' with 'next'"); + $runner->expects($this->once())->method('run') + ->with($request, $response) + ->willReturn($runResponse); + + $next = $this->createCallbackMock($this->once(), [$request, $runResponse], $finalResponse); + + $runner($request, $response, $next); } } diff --git a/tests/RouterTest.php b/tests/RouterTest.php index 46574c4..c036c71 100644 --- a/tests/RouterTest.php +++ b/tests/RouterTest.php @@ -1,267 +1,229 @@ <?php +namespace Jasny\Router; + use Jasny\Router; use Jasny\Router\Route; -use Jasny\Router\Runner\RunnerFactory; +use Jasny\Router\Routes; +use Jasny\Router\RunnerFactory; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\StreamInterface; -class RouterTest extends PHPUnit_Framework_TestCase +use Jasny\Router\TestHelpers; + +/** + * @covers Jasny\Router + */ +class RouterTest extends \PHPUnit_Framework_TestCase { + use TestHelpers; + /** * Test creating Router */ - public function testConstruct() + public function testGetRoutes() { - $routes = [ - '/foo' => ['fn' => 'test_function'], - '/foo/bar' => ['controller' => 'TestController'] - ]; + $routes = $this->createMock(Routes::class); $router = new Router($routes); - $this->assertEquals($routes, $router->getRoutes(), "Routes were not set correctly"); - } - - /** - * Test that on router 'handle', method '__invoke' is called - */ - public function testHandle() - { - $router = $this->createMock(Router::class, ['__invoke']); - list($request, $response) = $this->getRequests(); - - $router->method('__invoke')->will($this->returnCallback(function($arg1, $arg2) { - return ['request' => $arg1, 'response' => $arg2]; - })); - - $result = $router->handle($request, $response); - - $this->assertEquals($request, $result['request'], "Request was not processed correctly"); - $this->assertEquals($response, $result['response'], "Response was not processed correctly"); + $this->assertSame($routes, $router->getRoutes(), "Routes were not set correctly"); } + /** - * Test '__invoke' method + * Test getting runner factory */ - public function testInvoke() + public function testGetFactory() { - $routes = [ - '/foo/bar' => Route::create(['controller' => 'TestController']), - '/foo' => Route::create(['fn' => function($arg1, $arg2) { - return ['request' => $arg1, 'response' => $arg2]; - }]) - ]; - - list($request, $response) = $this->getRequests(); - $this->expectRequestRoute($request, $routes['/foo']); - - $router = new Router($routes); - $result = $router($request, $response); + $router = new Router($this->createMock(Routes::class)); + $factory = $router->getFactory(); - $this->assertEquals($request, $result['request'], "Request was not processed correctly"); - $this->assertEquals($response, $result['response'], "Response was not processed correctly"); + $this->assertInstanceOf(RunnerFactory::class, $factory); } - + /** - * Test '__invoke' method with 'next' callback + * Test setting runner factory */ - public function testInvokeNext() + public function testSetFactory() { - $routes = [ - '/foo/bar' => Route::create(['controller' => 'TestController']), - '/foo' => Route::create(['fn' => function($request, $response) { - return $response; - }]) - ]; - - list($request, $response) = $this->getRequests(); - $this->expectRequestRoute($request, $routes['/foo']); - - $router = new Router($routes); - $result = $router($request, $response, function($arg1, $arg2) { - return ['request' => $arg1, 'response' => $arg2]; - }); - - $this->assertEquals($request, $result['request'], "Request was not processed correctly"); - $this->assertEquals($response, $result['response'], "Response was not processed correctly"); + $factoryMock = $this->createCallbackMock($this->never()); + + $router = new Router($this->createMock(Routes::class)); + + $ret = $router->setFactory($factoryMock); + $this->assertSame($router, $ret); + + $this->assertSame($factoryMock, $router->getFactory()); } /** - * Test case when route is not found + * @expectedException \InvalidArgumentException */ - public function testNotFound() + public function testSetInvalidFactory() { - $routes = [ - '/foo/bar' => Route::create(['controller' => 'TestController']) - ]; - - list($request, $response) = $this->getRequests(); - $this->expectNotFound($response); - - $router = new Router($routes); - $result = $router($request, $response); - - $this->assertEquals(get_class($response), get_class($result), "Returned result is not an instance of 'ServerRequestInterface'"); + $router = new Router($this->createMock(Routes::class)); + $router->setFactory('foo bar zoo'); } + /** - * Test adding middleware action - * - * @dataProvider addProvider - * @param mixed $middleware1 - * @param callable $middleware2 - * @param boolean $positive + * Test that on router 'handle', method '__invoke' is called */ - public function testAdd($middleware1, $middleware2, $positive) + public function testHandle() { - $router = new Router([]); - $this->assertEquals(0, count($router->getMiddlewares()), "Middlewares array should be empty"); - - if (!$positive) $this->expectException(\InvalidArgumentException::class); - - $result = $router->add($middleware1); - $this->assertEquals(1, count($router->getMiddlewares()), "There should be only one item in middlewares array"); - $this->assertEquals($middleware1, reset($router->getMiddlewares()), "Wrong item in middlewares array"); - $this->assertEquals($router, $result, "'Add' should return '\$this'"); + $request = $this->createMock(ServerRequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + $finalResponse = $this->createMock(ResponseInterface::class); + + $router = $this->getMockBuilder(Router::class)->disableOriginalConstructor() + ->setMethods(['__invoke'])->getMock(); + $router->expects($this->once())->method('__invoke')->with($request, $response)->willReturn($finalResponse); - if (!$middleware2) return; + $result = $router->handle($request, $response); - $router->add($middleware2); - $this->assertEquals(2, count($router->getMiddlewares()), "There should be two items in middlewares array"); - foreach ($router->getMiddlewares() as $action) { - $this->assertTrue($action == $middleware1 || $action == $middleware2, "Wrong item in middlewares array"); - } + $this->assertSame($finalResponse, $result); } - /** - * Provide data for testing 'add' method - */ - public function addProvider() + public function nextProvider() { return [ - ['wrong_callback', null, false], - [[$this, 'getMiddlewareCalledFirst'], null, true], - [[$this, 'getMiddlewareCalledFirst'], [$this, 'getMiddlewareCalledLast'], true] - ]; - } - - /** - * Test executing router with middlewares chain (test only execution order) - */ - public function testRunMiddlewares() - { - $routes = [ - '/foo' => Route::create(['fn' => function($request, $response) { - $response->testMiddlewareCalls[] = 'run'; - return $response; - }]) + [null], + [$this->createCallbackMock($this->any())] ]; - - list($request, $response) = $this->getRequests(); - $this->expectRequestRoute($request, $routes['/foo']); - - $router = new Router($routes); - $router->add([$this, 'getMiddlewareCalledLast'])->add([$this, 'getMiddlewareCalledFirst']); - - $result = $router($request, $response, function($request, $response) { - $response->testMiddlewareCalls[] = 'outer'; - return $response; - }); - - $this->assertEquals(['first','last','run','outer'], $response->testMiddlewareCalls, "Actions were executed in wrong order"); } /** - * Test getting and setting runner factory + * Test '__invoke' method + * + * @dataProvider nextProvider */ - public function testRunnerFactory() + public function testInvoke($next) { - $router = new Router([]); - $factory = $router->getFactory(); - - $this->assertEquals(RunnerFactory::class, get_class($factory), "By default 'getFactory' should return 'RunnerFactory' instance, not " . get_class($factory)); - - $self = $router->setFactory(function() { - return 'test'; - }); - $factory = $router->getFactory(); + $route = $this->createMock(Route::class); + + $request = $this->createMock(ServerRequestInterface::class); + $requestWithRoute = $this->createMock(ServerRequestInterface::class); + $request->expects($this->once())->method('withAttribute')->with('route')->willReturn($requestWithRoute); + + $response = $this->createMock(ResponseInterface::class); + $finalResponse = $this->createMock(ResponseInterface::class); + + $runner = $this->createCallbackMock($this->once(), [$requestWithRoute, $response, $next], $finalResponse); + $factory = $this->createCallbackMock($this->once(), [$route], $runner); - $this->assertEquals($router, $self, "'setFactory' must return an instance of router"); - $this->assertEquals('test', $factory(), "Factory was not set or got correctly"); + $routes = $this->createMock(Routes::class); + $routes->expects($this->once())->method('getRoute')->with($request)->willReturn($route); - $this->expectException(\InvalidArgumentException::class); - $router->setFactory('test'); + $router = new Router($routes); + $router->setFactory($factory); + + $result = $router($request, $response, $next); + + $this->assertSame($finalResponse, $result); } /** - * Get requests for testing - * - * @return array + * Test case when route is not found */ - public function getRequests() + public function testNotFound() { $request = $this->createMock(ServerRequestInterface::class); $response = $this->createMock(ResponseInterface::class); + $finalResponse = $this->createMock(ResponseInterface::class); + $body = $this->createMock(StreamInterface::class); - $request->method('getUri')->will($this->returnValue('/foo')); - $request->method('getMethod')->will($this->returnValue('GET')); + $response->expects($this->once())->method('withStatus')->with(404)->willReturn($finalResponse); + $finalResponse->expects($this->once())->method('getBody')->willReturn($body); + $body->expects($this->once())->method('write')->with('Not Found'); + + $factory = $this->createCallbackMock($this->never()); - return [$request, $response]; + $routes = $this->createMock(Routes::class); + $routes->expects($this->once())->method('getRoute')->with($request)->willReturn(null); + + $router = new Router($routes); + $router->setFactory($factory); + + $result = $router($request, $response); + + $this->assertSame($finalResponse, $result); } /** - * Get middleware action, that should ba called first in middleware chain - * - * @param ServerRequestInterface $request - * @param ResponseInterface $response - * @param callback $next - * @return ResponseInterface + * Test adding middleware action */ - public function getMiddlewareCalledFirst(ServerRequestInterface $request, ResponseInterface $response, $next) + public function testAdd() { - $response->testMiddlewareCalls[] = 'first'; - return $next($request, $response); + $middlewareOne = $this->createCallbackMock($this->never()); + $middlewareTwo = $this->createCallbackMock($this->never()); + + $router = new Router($this->createMock(Routes::class)); + + $this->assertEquals([], $router->getMiddlewares(), "Middlewares array should be empty"); + + $ret = $router->add($middlewareOne); + $this->assertSame($router, $ret); + + $router->add($middlewareTwo); + + $this->assertSame([$middlewareOne, $middlewareTwo], $router->getMiddlewares()); } /** - * Get middleware action, that should be called last in middleware chain - * - * @param ServerRequestInterface $request - * @param ResponseInterface $response - * @param callback $next - * @return ResponseInterface + * @expectedException \InvalidArgumentException */ - public function getMiddlewareCalledLast(ServerRequestInterface $request, ResponseInterface $response, $next) + public function testAddInvalidMiddleware() { - $response->testMiddlewareCalls[] = 'last'; - return $next($request, $response); + $router = new Router($this->createMock(Routes::class)); + $router->add('foo bar zoo'); } /** - * Expect 'not found' response - * - * @param ResponseInterface + * Test executing router with middlewares chain (test only execution order) */ - public function expectNotFound(ResponseInterface $response) + public function testRunMiddlewares() { - $stream = $this->createMock(StreamInterface::class); - $stream->expects($this->once())->method('rewind'); - $stream->expects($this->once())->method('write')->with($this->equalTo('Not Found')); + $route = $this->createMock(Route::class); - $response->method('getBody')->will($this->returnValue($stream)); - $response->expects($this->once())->method('withBody')->with($this->equalTo($stream))->will($this->returnSelf()); - $response->expects($this->once())->method('withStatus')->with($this->equalTo(404), $this->equalTo('Not Found'))->will($this->returnSelf()); - } + $request = $this->createMock(ServerRequestInterface::class); + $request->expects($this->once())->method('withAttribute')->with('route')->willReturn($request); + $requestOne = $this->createMock(ServerRequestInterface::class); + $requestTwo = $this->createMock(ServerRequestInterface::class); + + $response = $this->createMock(ResponseInterface::class); + $responseOne = $this->createMock(ResponseInterface::class); + $responseTwo = $this->createMock(ResponseInterface::class); + $finalResponse = $this->createMock(ResponseInterface::class); + + $middlewareOne = $this->getMockBuilder(\stdClass::class)->setMethods(['__invoke'])->getMock(); + $middlewareOne->expects($this->once())->method('__invoke')->id('one') + ->with($request, $response, $this->isInstanceOf(Closure::class)) + ->will($this->returnCallback(function($a, $b, $next) use ($requestOne, $responseOne) { + return $next($requestOne, $responseOne); + })); + + $middlewareTwo = $this->getMockBuilder(\stdClass::class)->setMethods(['__invoke'])->getMock(); + $middlewareTwo->expects($this->once())->method('__invoke')->id('two')->after('one') + ->with($request, $response, $this->isInstanceOf(Closure::class)) + ->will($this->returnCallback(function($a, $b, $next) use ($requestTwo, $responseTwo) { + return $next($requestTwo, $responseTwo); + })); + + $runner = $this->createCallbackMock($this->once(), [$requestTwo, $responseTwo], $finalResponse); + $factory = $this->createCallbackMock($this->once(), [$route], $runner); + + $routes = $this->createMock(Routes::class); + $routes->expects($this->once())->method('getRoute')->with($request)->willReturn($route); - /** - * Expect that request will return given route - * - * @param ServerRequestInterface $request - * @param Route $route - */ - public function expectRequestRoute(ServerRequestInterface $request, $route) - { - $request->expects($this->once())->method('getAttribute')->with($this->equalTo('route'))->will($this->returnValue($route)); + $router = new Router($routes); + $router->setFactory($factory); + + $router->add($middlewareOne); + $router->add($middlewareTwo); + + $result = $router($request, $response); + + $this->assertSame($finalResponse, $result); } } diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..af5e7b3 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,4 @@ +<?php + +require_once('vendor/autoload.php'); +require_once('support/TestHelpers.php'); diff --git a/tests/support/TestHelpers.php b/tests/support/TestHelpers.php new file mode 100644 index 0000000..e4fdff7 --- /dev/null +++ b/tests/support/TestHelpers.php @@ -0,0 +1,30 @@ +<?php + +namespace Jasny\Router; + +use PHPUnit_Framework_MockObject_Matcher_Invocation as Invocation; +use PHPUnit_Framework_MockObject_MockObject as MockObject; + +/** + * Helper methods for PHPUnit tests + */ +trait TestHelpers +{ + /** + * Create mock for next callback + * + * @param Invocation $matcher + * @param array $with With arguments + * @param mixed $return + * @return MockObject + */ + protected function createCallbackMock(Invocation $matcher, $with = [], $return = null) + { + $callback = $this->getMockBuilder(\stdClass::class)->setMethods(['__invoke'])->getMock(); + $callback->expects($matcher)->method('__invoke') + ->with(...$with) + ->willReturn($return); + + return $callback; + } +} |