Browse files

Merge branch 'feature/error-handling'

New error handling proposal for Expressive.
  • Loading branch information...
2 parents 197709d + 84b507f commit 03461a4b45f6ff4936fa5f2a07e83ea4dd8b341e @weierophinney committed Aug 24, 2016
View
2 composer.json
@@ -55,6 +55,6 @@
"scripts": {
"cs-check": "phpcs",
"cs-fix": "phpcbf",
- "test": "phpunit"
+ "test": "phpunit --colors=always"
}
}
View
15 config/autoload/dependencies.global.php
@@ -4,21 +4,20 @@
* @copyright Copyright (c) Matthew Weier O'Phinney
*/
-use Mwop\Auth;
-use Mwop\Blog;
-use Mwop\Console;
-use Mwop\Factory;
-use Mwop\Feed;
-use Mwop\Github;
+namespace Mwop;
+
use Zend\Expressive\Application;
use Zend\Expressive\Container\ApplicationFactory;
+use Zend\Expressive\FinalHandler;
use Zend\Expressive\Helper;
use Zend\Feed\Reader\Http\ClientInterface as FeedReaderHttpClientInterface;
+use Zend\ServiceManager\Factory\InvokableFactory;
return ['dependencies' => [
'factories' => [
'mail.transport' => Factory\MailTransport::class,
'session' => Factory\Session::class,
+ Application::class => ApplicationFactory::class,
Auth\AuthCallback::class => Auth\AuthCallbackFactory::class,
Auth\Auth::class => Auth\AuthFactory::class,
Auth\Logout::class => Auth\LogoutFactory::class,
@@ -30,10 +29,10 @@
Console\FeedAggregator::class => Console\FeedAggregatorFactory::class,
Console\PrepOfflinePages::class => Factory\PrepOfflinePagesFactory::class,
FeedReaderHttpClientInterface::class => Feed\HttpPlugClientFactory::class,
+ FinalHandler::class => Factory\FinalHandlerFactory::class,
Github\AtomReader::class => Github\AtomReaderFactory::class,
Github\Console\Fetch::class => Github\Console\FetchFactory::class,
Helper\UrlHelper::class => Helper\UrlHelperFactory::class,
- Application::class => ApplicationFactory::class,
- 'Zend\Expressive\FinalHandler' => Factory\ErrorHandlerFactory::class,
+ UnauthorizedResponseFactory::class => UnauthorizedResponseFactoryFactory::class,
],
]];
View
30 config/autoload/local.php.dist
@@ -0,0 +1,30 @@
+<?php
+/**
+ * @license http://opensource.org/licenses/BSD-2-Clause BSD-2-Clause
+ * @copyright Copyright (c) Matthew Weier O'Phinney
+ */
+
+namespace Mwop;
+
+use Zend\Expressive\Container;
+use Zend\Expressive\Whoops;
+use Zend\Expressive\WhoopsPageHandler;
+
+/**
+ * This serves as an example of how to enable the Whoops formatter
+ * for use with the ErrorHandler middleware.
+ */
+return [
+ 'debug' => true,
+ 'dependencies' => [
+ 'delegators' => [
+ ErrorHandler::class => [
+ ErrorHandler\WhoopsFormatterFactory::class,
+ ],
+ ],
+ 'factories' => [
+ Whoops::class => Container\WhoopsFactory::class,
+ WhoopsPageHandler::class => Container\WhoopsPageHandlerFactory::class,
+ ],
+ ],
+];
View
23 config/autoload/middleware-pipeline.global.php
@@ -4,27 +4,20 @@
* @copyright Copyright (c) Matthew Weier O'Phinney
*/
-use Mwop\Auth\Middleware as AuthMiddleware;
-use Mwop\Auth\MiddlewareFactory as AuthMiddlewareFactory;
-use Mwop\ErrorHandler;
-use Mwop\Factory\Unauthorized as UnauthorizedFactory;
-use Mwop\NotFound;
-use Mwop\Redirects;
-use Mwop\Unauthorized;
-use Mwop\XClacksOverhead;
-use Zend\Expressive\Container\ApplicationFactory;
+namespace Mwop;
+
use Zend\Expressive\Helper;
+use Zend\ServiceManager\Factory\InvokableFactory;
return [
'dependencies' => [
- 'invokables' => [
- Redirects::class => Redirects::class,
- XClacksOverhead::class => XClacksOverhead::class,
- ],
'factories' => [
- AuthMiddleware::class => AuthMiddlewareFactory::class,
+ Auth\Middleware::class => Auth\MiddlewareFactory::class,
+ ErrorHandler::class => Factory\ErrorHandlerFactory::class,
Helper\UrlHelperMiddleware::class => Helper\UrlHelperMiddlewareFactory::class,
- Unauthorized::class => UnauthorizedFactory::class,
+ Redirects::class => InvokableFactory::class,
+ NotFound::class => Factory\NotFoundFactory::class,
+ XClacksOverhead::class => InvokableFactory::class,
],
],
];
View
1 phpcs.xml
@@ -27,6 +27,7 @@
<!-- Paths to check -->
<file>bin</file>
<file>config</file>
+ <file>config/autoload/local.php.dist</file>
<file>public/index.php</file>
<file>src</file>
<file>test</file>
View
3 public/index.php
@@ -24,13 +24,14 @@
/* Piped middleware */
+$app->pipe(ErrorHandler::class);
$app->pipe(XClacksOverhead::class);
$app->pipe(Redirects::class);
$app->pipe('/auth', Auth\Middleware::class);
$app->pipeRoutingMiddleware();
$app->pipe(Helper\UrlHelperMiddleware::class);
$app->pipeDispatchMiddleware();
-$app->pipeErrorHandler(Unauthorized::class);
+$app->pipe(NotFound::class);
/* Routed middleware */
View
23 src/ComicsPage.php
@@ -8,15 +8,32 @@
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
+use Zend\Diactoros\Response\HtmlResponse;
+use Zend\Expressive\Template\TemplateRendererInterface;
-class ComicsPage extends Page
+class ComicsPage
{
+ const TEMPLATE = 'mwop::comics.page';
+
+ private $renderer;
+
+ private $unauthorizedResponseFactory;
+
+ public function __construct(TemplateRendererInterface $renderer, callable $unauthorizedResponseFactory)
+ {
+ $this->renderer = $renderer;
+ $this->unauthorizedResponseFactory = $unauthorizedResponseFactory;
+ }
+
public function __invoke(Request $request, Response $response, callable $next) : Response
{
if (! $request->getAttribute('user', false)) {
- return $next($request, $response->withStatus(401), 401);
+ $factory = $this->unauthorizedResponseFactory;
+ return $factory($request);
}
- return parent::__invoke($request, $response, $next);
+ return new HtmlResponse(
+ $this->renderer->render(self::TEMPLATE)
+ );
}
}
View
139 src/ErrorHandler.php
@@ -6,137 +6,88 @@
namespace Mwop;
+use DomainException;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Throwable;
use Zend\Diactoros\Response\HtmlResponse;
use Zend\Expressive\Template\TemplateRendererInterface;
-use Zend\Expressive\TemplatedErrorHandler;
-use Zend\Stratigility\Http\Response as StratigilityResponse;
class ErrorHandler
{
+ const TEMPLATE_ERROR = 'error::500';
+
private $displayErrors;
- private $originalResponse;
+
+ private $errorFormatter;
+
private $renderer;
- private $template404;
- private $templateError;
public function __construct(
TemplateRendererInterface $renderer,
- bool $displayErrors = false,
- string $template404 = 'error::404',
- string $templateError = 'error::500',
- Response $originalResponse = null
+ bool $displayErrors = false
) {
$this->renderer = $renderer;
$this->displayErrors = $displayErrors;
- $this->template404 = $template404;
- $this->templateError = $templateError;
- if ($originalResponse) {
- $this->setOriginalResponse($originalResponse);
- }
}
- public function setOriginalResponse(Response $response)
+ public function setErrorFormatter(callable $formatter)
{
- $this->originalResponse = $response;
+ $this->errorFormatter = $formatter;
}
- public function __invoke(Request $request, Response $response, $err = null) : Response
+ public function getErrorFormatter() : callable
{
- if (! $err) {
- return $this->marshalNonErrorResponse($request, $response);
- }
-
- return $this->handleErrorResponse($err, $request, $response);
- }
-
- private function marshalNonErrorResponse(Request $request, Response $response) : Response
- {
- if (! $this->originalResponse) {
- return $this->marshalReceivedResponse($request, $response);
- }
-
- $originalResponse = $this->originalResponse;
- $decoratedResponse = $response instanceof StratigilityResponse
- ? $response->getOriginalResponse()
- : $response;
-
- if ($originalResponse !== $response
- && $originalResponse !== $decoratedResponse
- ) {
- // Response does not match either the original response or the
- // decorated response; return it verbatim.
- return $response;
+ if (! $this->errorFormatter) {
+ return $this->getDefaultErrorFormatter();
}
- if (($originalResponse === $response || $decoratedResponse === $response)
- && $this->bodySize !== $response->getBody()->getSize()
- ) {
- // Response matches either the original response or the
- // decorated response; but the body size has changed; return it
- // verbatim.
- return $response;
- }
-
- return $this->create404($request, $response);
+ return $this->errorFormatter;
}
- private function marshalReceivedResponse(Request $request, Response $response) : Response
+ public function __invoke(Request $request, Response $response, callable $next) : Response
{
- if ($response->getStatusCode() === 200
- && $response->getBody()->getSize() === 0
- ) {
- return $this->create404($request, $response);
+ try {
+ $result = $next($request, $response);
+ if ($result instanceof Response) {
+ return $result;
+ }
+
+ throw new DomainException('No middleware returned a response.');
+ } catch (Throwable $e) {
+ return $this->createErrorResponse($e, $request);
}
-
- return $response;
- }
-
- private function create404(Request $request, Response $response) : Response
- {
- return new HtmlResponse(
- $this->renderer->render($this->template404, []),
- 404
- );
}
- private function handleErrorResponse($error, Request $request, Response $response) : Response
+ public function createErrorResponse(Throwable $e, Request $request) : Response
{
$error = $this->displayErrors
- ? $this->prepareError($error)
- : [];
- return new HtmlResponse(
- $this->renderer->render($this->templateError, ['error' => $error]),
- 500
- );
+ ? $this->prepareError($e, $request)
+ : $this->renderer->render(self::TEMPLATE_ERROR);
+
+ return new HtmlResponse($error, 500);
}
- private function prepareError($error) : string
+ private function getDefaultErrorFormatter() : callable
{
- if (is_scalar($error)) {
- return (string) $error;
- }
-
- if ($error instanceof Throwable) {
- return $this->prepareException($error);
- }
-
- return json_encode($error, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
+ return function (Throwable $e) : string {
+ $message = '';
+ do {
+ $message .= sprintf(
+ "Exception: %s (%d)\nTrace:\n%s\n",
+ $e->getMessage(),
+ $e->getCode(),
+ $e->getTraceAsString()
+ );
+ } while ($e = $e->getPrevious());
+
+ return $this->renderer->render(self::TEMPLATE_ERROR, ['error' => $message]);
+ };
}
- private function prepareException(Throwable $e) : string
+ private function prepareError(Throwable $error, Request $request) : string
{
- $message = '';
- do {
- $message .= sprintf(
- "Exception: %s (%d)\nTrace:\n%s\n",
- $e->getMessage(),
- $e->getCode(),
- $e->getTraceAsString()
- );
- } while ($e = $e->getPrevious());
- return $message;
+ $formatter = $this->getErrorFormatter();
+ return $formatter($error, $request);
}
}
View
56 src/ErrorHandler/WhoopsFormatter.php
@@ -0,0 +1,56 @@
+<?php
+/**
+ * @license http://opensource.org/licenses/BSD-2-Clause BSD-2-Clause
+ * @copyright Copyright (c) Matthew Weier O'Phinney
+ */
+
+namespace Mwop\ErrorHandler;
+
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Throwable;
+use Whoops\Handler\PrettyPageHandler;
+use Whoops\Run as Whoops;
+use Zend\Stratigility\Http\Request as StratigilityRequest;
+
+class WhoopsFormatter
+{
+ private $whoops;
+ private $whoopsHandler;
+
+ public function __construct(Whoops $whoops, PrettyPageHandler $whoopsHandler)
+ {
+ $this->whoops = $whoops;
+ $this->whoopsHandler = $whoopsHandler;
+ }
+
+ public function __invoke(Throwable $e, Request $request)
+ {
+ $this->prepareWhoopsHandler($request);
+ $this->whoops->pushHandler($this->whoopsHandler);
+ return $this->whoops->handleException($e);
+ }
+
+ /**
+ * Prepare the Whoops page handler with a table displaying request information.
+ *
+ * @return void
+ */
+ private function prepareWhoopsHandler(Request $request)
+ {
+ if ($request instanceof StratigilityRequest) {
+ $request = $request->getOriginalRequest();
+ }
+
+ $uri = $request->getUri();
+ $this->whoopsHandler->addDataTable('Expressive Application Request', [
+ 'HTTP Method' => $request->getMethod(),
+ 'URI' => (string) $uri,
+ 'Script' => $request->getServerParams()['SCRIPT_NAME'],
+ 'Headers' => $request->getHeaders(),
+ 'Cookies' => $request->getCookieParams(),
+ 'Attributes' => $request->getAttributes(),
+ 'Query String Arguments' => $request->getQueryParams(),
+ 'Body Params' => $request->getParsedBody(),
+ ]);
+ }
+}
View
34 src/ErrorHandler/WhoopsFormatterFactory.php
@@ -0,0 +1,34 @@
+<?php
+/**
+ * @license http://opensource.org/licenses/BSD-2-Clause BSD-2-Clause
+ * @copyright Copyright (c) Matthew Weier O'Phinney
+ */
+
+namespace Mwop\ErrorHandler;
+
+use Interop\Container\ContainerInterface;
+use Mwop\ErrorHandler;
+use Zend\Expressive\Whoops;
+use Zend\Expressive\WhoopsPageHandler;
+use Zend\ServiceManager\Factory\DelegatorFactoryInterface;
+
+class WhoopsFormatterFactory implements DelegatorFactoryInterface
+{
+ public function __invoke(
+ ContainerInterface $container,
+ $requestedName,
+ callable $callback,
+ array $options = null
+ ) : ErrorHandler {
+ $errorHandler = $callback();
+
+ $errorHandler->setErrorFormatter(
+ new WhoopsFormatter(
+ $container->get(Whoops::class),
+ $container->get(WhoopsPageHandler::class)
+ )
+ );
+
+ return $errorHandler;
+ }
+}
View
5 src/Factory/ComicsPage.php
@@ -8,6 +8,7 @@
use Interop\Container\ContainerInterface;
use Mwop\ComicsPage as Page;
+use Mwop\UnauthorizedResponseFactory;
use Zend\Stratigility\MiddlewarePipe;
use Zend\Expressive\Template\TemplateRendererInterface;
@@ -19,8 +20,8 @@ public function __invoke(ContainerInterface $container) : callable
$pipeline->pipe($container->get('Mwop\Auth\UserSession'));
$pipeline->pipe(new Page(
- 'mwop::comics.page',
- $container->get(TemplateRendererInterface::class)
+ $container->get(TemplateRendererInterface::class),
+ $container->get(UnauthorizedResponseFactory::class)
));
return $pipeline;
View
7 src/Factory/ErrorHandlerFactory.php
@@ -14,13 +14,10 @@ class ErrorHandlerFactory
{
public function __invoke(ContainerInterface $container) : ErrorHandler
{
- $config = $container->get('config');
- $displayErrors = array_key_exists('debug', $config)
- ? (bool) $config['debug']
- : false;
+ $config = $container->has('config') ? $container->get('config') : [];
return new ErrorHandler(
$container->get(TemplateRendererInterface::class),
- $displayErrors
+ $config['debug'] ?? false
);
}
}
View
32 src/Factory/FinalHandlerFactory.php
@@ -0,0 +1,32 @@
+<?php
+/**
+ * @license http://opensource.org/licenses/BSD-2-Clause BSD-2-Clause
+ * @copyright Copyright (c) Matthew Weier O'Phinney
+ */
+
+namespace Mwop\Factory;
+
+use Interop\Container\ContainerInterface;
+use Mwop\ErrorHandler;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Throwable;
+
+class FinalHandlerFactory
+{
+ public function __invoke(ContainerInterface $container) : callable
+ {
+ return function (Request $request, Response $response, $err = null) use ($container) : Response {
+ if ($err instanceof Throwable) {
+ $errorHandler = $container->get(ErrorHandler::class);
+ return $errorHandler->createErrorResponse($err, $request);
+ }
+
+ if ($err) {
+ error_log(sprintf("FinalHandler received a non-throwable error: %s", var_export($err, true)));
+ }
+
+ return $response;
+ };
+ }
+}
View
9 src/Factory/Unauthorized.php → src/Factory/NotFoundFactory.php
@@ -7,14 +7,15 @@
namespace Mwop\Factory;
use Interop\Container\ContainerInterface;
-use Mwop\Unauthorized as Middleware;
+use Mwop\NotFound;
use Zend\Expressive\Template\TemplateRendererInterface;
-class Unauthorized
+class NotFoundFactory
{
- public function __invoke(ContainerInterface $container) : Middleware
+ public function __invoke(ContainerInterface $container) : NotFound
{
- return new Middleware(
+ return new NotFound(
+ $container->get('config')['debug'] ?? false,
$container->get(TemplateRendererInterface::class)
);
}
View
47 src/NotFound.php
@@ -0,0 +1,47 @@
+<?php
+/**
+ * @license http://opensource.org/licenses/BSD-2-Clause BSD-2-Clause
+ * @copyright Copyright (c) Matthew Weier O'Phinney
+ */
+
+namespace Mwop;
+
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Zend\Diactoros\Response\HtmlResponse;
+use Zend\Expressive\Router\RouteResult;
+use Zend\Expressive\Template\TemplateRendererInterface;
+
+class NotFound
+{
+ const TEMPLATE_NOTFOUND = 'error::404';
+ const TEMPLATE_ERROR = 'error::500';
+
+ private $debug;
+
+ private $renderer;
+
+ public function __construct(
+ bool $debug,
+ TemplateRendererInterface $renderer
+ ) {
+ $this->debug = $debug;
+ $this->renderer = $renderer;
+ }
+
+ public function __invoke(Request $request, Response $response, callable $next) : Response
+ {
+ if (false !== $request->getAttribute(RouteResult::class, false)) {
+ $view = $this->debug ? ['error' => 'An inner middleware did not return a response'] : [];
+ return new HtmlResponse(
+ $this->renderer->render(self::TEMPLATE_ERROR, $view),
+ 500
+ );
+ }
+
+ return new HtmlResponse(
+ $this->renderer->render(self::TEMPLATE_NOTFOUND),
+ 404
+ );
+ }
+}
View
44 src/Unauthorized.php
@@ -1,44 +0,0 @@
-<?php
-/**
- * @license http://opensource.org/licenses/BSD-2-Clause BSD-2-Clause
- * @copyright Copyright (c) Matthew Weier O'Phinney
- */
-
-namespace Mwop;
-
-use Psr\Http\Message\ResponseInterface as Response;
-use Psr\Http\Message\ServerRequestInterface as Request;
-use Zend\Diactoros\Response\HtmlResponse;
-use Zend\Expressive\Template\TemplateRendererInterface;
-
-class Unauthorized
-{
- private $renderer;
- private $template;
-
- public function __construct(
- TemplateRendererInterface $renderer,
- string $template = 'error::401'
- ) {
- $this->renderer = $renderer;
- $this->template = $template;
- }
-
- public function __invoke($err, Request $req, Response $res, callable $next) : Response
- {
- if ($res->getStatusCode() !== 401) {
- return $next($req, $res, $err);
- }
-
- $new = $req->getUri()->withPath('/auth');
- $view = [
- 'auth_path' => (string) $new,
- 'redirect' => (string) $req->getOriginalRequest()->getUri(),
- ];
-
- return new HtmlResponse(
- $this->renderer->render($this->template, $view),
- 401
- );
- }
-}
View
37 src/UnauthorizedResponseFactoryFactory.php
@@ -0,0 +1,37 @@
+<?php
+/**
+ * @license http://opensource.org/licenses/BSD-2-Clause BSD-2-Clause
+ * @copyright Copyright (c) Matthew Weier O'Phinney
+ */
+
+namespace Mwop;
+
+use Interop\Container\ContainerInterface;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Zend\Diactoros\Response\HtmlResponse;
+use Zend\Expressive\Template\TemplateRendererInterface;
+
+class UnauthorizedResponseFactoryFactory
+{
+ const TEMPLATE = 'error::401';
+
+ public function __invoke(ContainerInterface $container) : callable
+ {
+ return function (Request $request) use ($container) {
+ $originalRequest = method_exists($request, 'getOriginalRequest')
+ ? $request->getOriginalRequest()
+ : $request;
+
+ $view = [
+ 'auth_path' => (string) $request->getUri()->withPath('/auth'),
+ 'redirect' => (string) $originalRequest->getUri(),
+ ];
+
+ $renderer = $container->get(TemplateRendererInterface::class);
+ return new HtmlResponse(
+ $renderer->render(self::TEMPLATE, $view),
+ 401
+ );
+ };
+ }
+}
View
35 test/ComicsPageTest.php
@@ -9,6 +9,7 @@
use Mwop\ComicsPage;
use PHPUnit_Framework_TestCase as TestCase;
use Prophecy\Argument;
+use Psr\Http\Message\ServerRequestInterface as Request;
use Zend\Diactoros\Response\HtmlResponse;
use Zend\Expressive\Template\TemplateRendererInterface;
@@ -19,39 +20,45 @@ class ComicsPageTest extends TestCase
public function setUp()
{
$this->renderer = $this->prophesize(TemplateRendererInterface::class);
- $this->middleware = new ComicsPage('comics', $this->renderer->reveal());
}
- public function testMiddlewareInvokesNextErrorMiddlewareWith401StatusIfUserAttributeIsMissing()
+ public function testMiddlewareReturnsInvokes401HandlerIfUserAttributeIsMissing()
{
- $middleware = $this->middleware;
+ $expectedResponse = $this->createResponseMock()->reveal();
+ $unauthFactory = function (Request $request) use ($expectedResponse) {
+ return $expectedResponse;
+ };
+
+ $middleware = new ComicsPage($this->renderer->reveal(), $unauthFactory);
+
$request = $this->createRequestMock();
- $response = $this->createResponseMock();
- $finalResponse = $this->createResponseMock()->reveal();
+ $response = $this->createResponseMock()->reveal();
$request->getAttribute('user', false)->willReturn(false);
- $response->withStatus(401)->willReturn($finalResponse);
$next = $this->nextShouldExpectAndReturn(
- $finalResponse,
+ $expectedResponse,
$request->reveal(),
- $finalResponse,
- 401
+ $response
);
- $this->renderer->render(Argument::any(), Argument::any())->shouldNotBeCalled();
+ $this->renderer->render(Argument::any())->shouldNotBeCalled();
$this->assertSame(
- $finalResponse,
- $middleware($request->reveal(), $response->reveal(), $next)
+ $expectedResponse,
+ $middleware($request->reveal(), $response, $next)
);
}
public function testMiddlewareReturnsHtmlResponseInjectedWithResultsOfRendereringPage()
{
- $middleware = $this->middleware;
+ $unauthFactory = function (Request $request) {
+ $this->fail('Factory for generating unauthorized response was invoked when it should not be');
+ };
+
+ $middleware = new ComicsPage($this->renderer->reveal(), $unauthFactory);
$request = $this->createRequestMock();
$request->getAttribute('user', false)->willReturn('mwop');
- $this->renderer->render('comics', [])->willReturn('content')->shouldBeCalled();
+ $this->renderer->render('mwop::comics.page')->willReturn('content')->shouldBeCalled();
$response = $middleware(
$request->reveal(),
$this->createResponseMock()->reveal(),
View
164 test/ErrorHandlerTest.php
@@ -0,0 +1,164 @@
+<?php
+/**
+ * @license http://opensource.org/licenses/BSD-2-Clause BSD-2-Clause
+ * @copyright Copyright (c) Matthew Weier O'Phinney
+ */
+
+namespace MwopTest;
+
+use Exception;
+use Mwop\ErrorHandler;
+use PHPUnit_Framework_TestCase as TestCase;
+use Prophecy\Argument;
+use TypeError;
+use Zend\Diactoros\Response\HtmlResponse;
+use Zend\Expressive\Router\RouteResult;
+use Zend\Expressive\Template\TemplateRendererInterface;
+
+class ErrorHandlerTest extends TestCase
+{
+ use HttpMessagesTrait;
+
+ public function setUp()
+ {
+ $this->renderer = $this->prophesize(TemplateRendererInterface::class);
+ }
+
+ public function debugValues()
+ {
+ return [
+ 'true' => [true],
+ 'false' => [false],
+ ];
+ }
+
+ public function testGetErrorFormatterReturnsCallable()
+ {
+ $handler = new ErrorHandler($this->renderer->reveal(), true);
+ $formatter = $handler->getErrorFormatter();
+ $this->assertInternalType('callable', $formatter);
+ return [
+ 'handler' => $handler,
+ 'formatter' => $formatter,
+ ];
+ }
+
+ /**
+ * @depends testGetErrorFormatterReturnsCallable
+ */
+ public function testErrorFormatterIsMutable(array $originals)
+ {
+ $handler = $originals['handler'];
+ $new = clone $originals['formatter'];
+ $handler->setErrorFormatter($new);
+ $this->assertSame($new, $handler->getErrorFormatter());
+ }
+
+ public function testReturnsOriginalResponseIfNextReturnsResponse()
+ {
+ $request = $this->createRequestMock()->reveal();
+ $response = $this->createResponseMock()->reveal();
+ $next = $this->nextShouldExpectAndReturn(
+ $response,
+ $request,
+ $response
+ );
+
+ $handler = new ErrorHandler($this->renderer->reveal(), true);
+ $result = $handler($request, $response, $next);
+ $this->assertSame($response, $result);
+ }
+
+ public function throwables()
+ {
+ return [
+ 'exception' => [new Exception('exception message', 500)],
+ 'throwable' => [new TypeError('throwable message', 501)],
+ ];
+ }
+
+ /**
+ * @dataProvider throwables
+ */
+ public function testRendersErrorTemplateWithoutErrorWhenDebugIsDisabledAndThrowableCaught($throwable)
+ {
+ $next = function () use ($throwable) {
+ throw $throwable;
+ };
+
+ $this->renderer->render(ErrorHandler::TEMPLATE_ERROR)->willReturn('error content');
+
+ $handler = new ErrorHandler($this->renderer->reveal(), false);
+ $result = $handler(
+ $this->createRequestMock()->reveal(),
+ $this->createResponseMock()->reveal(),
+ $next
+ );
+
+ $this->assertInstanceOf(HtmlResponse::class, $result);
+ $this->assertEquals(500, $result->getStatusCode());
+ $this->assertEquals('error content', (string) $result->getBody());
+ }
+
+ /**
+ * @dataProvider throwables
+ */
+ public function testInvokesDefaultErrorFormatterWhenDebugIsEnabledAndThrowableCaught($throwable)
+ {
+ $next = function () use ($throwable) {
+ throw $throwable;
+ };
+
+ $this->renderer->render(ErrorHandler::TEMPLATE_ERROR, Argument::that(function ($argument) use ($throwable) {
+ TestCase::assertInternalType('array', $argument);
+ TestCase::assertArrayHasKey('error', $argument);
+ $error = $argument['error'];
+ TestCase::assertContains($throwable->getMessage(), $error);
+ TestCase::assertContains((string) $throwable->getCode(), $error);
+ TestCase::assertContains($throwable->getTraceAsString(), $error);
+ return true;
+ }))->willReturn('error content');
+
+ $handler = new ErrorHandler($this->renderer->reveal(), true);
+ $result = $handler(
+ $this->createRequestMock()->reveal(),
+ $this->createResponseMock()->reveal(),
+ $next
+ );
+
+ $this->assertInstanceOf(HtmlResponse::class, $result);
+ $this->assertEquals(500, $result->getStatusCode());
+ $this->assertEquals('error content', (string) $result->getBody());
+ }
+
+ /**
+ * @dataProvider throwables
+ */
+ public function testInvokesInjectedErrorFormatterWhenDebugIsEnabledAndThrowableCaught($throwable)
+ {
+ $next = function () use ($throwable) {
+ throw $throwable;
+ };
+ $request = $this->createRequestMock()->reveal();
+
+ $formatter = function ($error, $req) use ($throwable, $request) {
+ $this->assertSame($throwable, $error);
+ $this->assertSame($request, $req);
+ return 'error content';
+ };
+
+ $this->renderer->render(Argument::any(), Argument::any())->shouldNotBeCalled();
+
+ $handler = new ErrorHandler($this->renderer->reveal(), true);
+ $handler->setErrorFormatter($formatter);
+ $result = $handler(
+ $request,
+ $this->createResponseMock()->reveal(),
+ $next
+ );
+
+ $this->assertInstanceOf(HtmlResponse::class, $result);
+ $this->assertEquals(500, $result->getStatusCode());
+ $this->assertEquals('error content', (string) $result->getBody());
+ }
+}
View
61 test/FinalHandlerTest.php
@@ -0,0 +1,61 @@
+<?php
+/**
+ * @license http://opensource.org/licenses/BSD-2-Clause BSD-2-Clause
+ * @copyright Copyright (c) Matthew Weier O'Phinney
+ */
+
+namespace MwopTest;
+
+use Exception;
+use Interop\Container\ContainerInterface;
+use Mwop\ErrorHandler;
+use Mwop\Factory\FinalHandlerFactory;
+use PHPUnit_Framework_TestCase as TestCase;
+use TypeError;
+
+class FinalHandlerTest extends TestCase
+{
+ use HttpMessagesTrait;
+
+ public function setUp()
+ {
+ $this->errorHandler = $this->prophesize(ErrorHandler::class);
+ $this->container = $this->prophesize(ContainerInterface::class);
+ $this->container->get(ErrorHandler::class)->will([$this->errorHandler, 'reveal']);
+
+ $factory = new FinalHandlerFactory();
+ $this->handler = $factory($this->container->reveal());
+ }
+
+ public function testReturnsOriginalResponseWhenNoErrorPresent()
+ {
+ $handler = $this->handler;
+ $response = $this->createResponseMock()->reveal();
+
+ $result = $handler($this->createRequestMock()->reveal(), $response);
+ $this->assertSame($response, $result);
+ }
+
+ public function throwables()
+ {
+ return [
+ 'exception' => [new Exception()],
+ 'throwable' => [new TypeError()],
+ ];
+ }
+
+ /**
+ * @dataProvider throwables
+ */
+ public function testInvokesErrorHandlerWhenThrowableErrorPresent($throwable)
+ {
+ $handler = $this->handler;
+ $request = $this->createRequestMock()->reveal();
+ $response = $this->createResponseMock()->reveal();
+ $generatedResponse = $this->createResponseMock()->reveal();
+ $this->errorHandler->createErrorResponse($throwable, $request)->willReturn($generatedResponse);
+
+ $result = $handler($request, $response, $throwable);
+ $this->assertSame($generatedResponse, $result);
+ }
+}
View
79 test/NotFoundTest.php
@@ -0,0 +1,79 @@
+<?php
+/**
+ * @license http://opensource.org/licenses/BSD-2-Clause BSD-2-Clause
+ * @copyright Copyright (c) Matthew Weier O'Phinney
+ */
+
+namespace MwopTest;
+
+use Mwop\NotFound;
+use PHPUnit_Framework_TestCase as TestCase;
+use Zend\Diactoros\Response\HtmlResponse;
+use Zend\Expressive\Router\RouteResult;
+use Zend\Expressive\Template\TemplateRendererInterface;
+
+class NotFoundTest extends TestCase
+{
+ use HttpMessagesTrait;
+
+ public function setUp()
+ {
+ $this->renderer = $this->prophesize(TemplateRendererInterface::class);
+ }
+
+ public function debugValues()
+ {
+ return [
+ 'true' => [true],
+ 'false' => [false],
+ ];
+ }
+
+ /**
+ * @dataProvider debugValues
+ */
+ public function testReturnsHtmlResponseWithRenderedNotFoundTemplateWhenRouteResultNotPresent($debug)
+ {
+ $this->renderer->render(NotFound::TEMPLATE_NOTFOUND)->willReturn('404 content');
+ $middleware = new NotFound($debug, $this->renderer->reveal());
+
+ $request = $this->createRequestMock();
+ $request->getAttribute(RouteResult::class, false)->willReturn(false);
+ $response = $this->createResponseMock()->reveal();
+ $next = $this->nextShouldNotBeCalled();
+
+ $result = $middleware($request->reveal(), $response, $next);
+
+ $this->assertInstanceOf(HtmlResponse::class, $result);
+ $this->assertEquals(404, $result->getStatusCode());
+ $this->assertEquals('404 content', (string) $result->getBody());
+ }
+
+ public function debugErrorValues()
+ {
+ return [
+ 'true' => [true, ['error' => 'An inner middleware did not return a response']],
+ 'false' => [false, []],
+ ];
+ }
+
+ /**
+ * @dataProvider debugErrorValues
+ */
+ public function testReturnsHtmlResponseWithRenderedErrorTemplateWhenRouteResultPresent($debug, $expectedView)
+ {
+ $this->renderer->render(NotFound::TEMPLATE_ERROR, $expectedView)->willReturn('500 content');
+ $middleware = new NotFound($debug, $this->renderer->reveal());
+
+ $request = $this->createRequestMock();
+ $request->getAttribute(RouteResult::class, false)->willReturn(true);
+ $response = $this->createResponseMock()->reveal();
+ $next = $this->nextShouldNotBeCalled();
+
+ $result = $middleware($request->reveal(), $response, $next);
+
+ $this->assertInstanceOf(HtmlResponse::class, $result);
+ $this->assertEquals(500, $result->getStatusCode());
+ $this->assertEquals('500 content', (string) $result->getBody());
+ }
+}
View
36 test/UnauthorizedTest.php → test/UnauthorizedResponseFactoryTest.php
@@ -6,63 +6,47 @@
namespace MwopTest;
-use Mwop\Unauthorized;
+use Interop\Container\ContainerInterface;
+use Mwop\UnauthorizedResponseFactoryFactory;
use PHPUnit_Framework_TestCase as TestCase;
use Prophecy\Argument;
use Zend\Diactoros\Response\HtmlResponse;
use Zend\Expressive\Template\TemplateRendererInterface;
-class UnauthorizedTest extends TestCase
+class UnauthorizedResponseFactoryTest extends TestCase
{
use HttpMessagesTrait;
public function setUp()
{
$this->renderer = $this->prophesize(TemplateRendererInterface::class);
- $this->middleware = new Unauthorized($this->renderer->reveal());
- }
-
- public function testReturnsNextMiddlewareWhenStatusCodeIsNot401()
- {
- $middleware = $this->middleware;
- $request = $this->createRequestMock();
- $response = $this->createResponseMock();
- $error = 'error';
- $expected = $this->createResponseMock()->reveal();
-
- $response->getStatusCode()->willReturn(200);
- $request->getUri()->shouldNotBeCalled();
- $this->renderer->render(Argument::any(), Argument::any())->shouldNotBeCalled();
-
- $next = $this->nextShouldExpectAndReturn($expected, $request->reveal(), $response->reveal(), $error);
+ $this->container = $this->prophesize(ContainerInterface::class);
+ $this->container->get(TemplateRendererInterface::class)->will([$this->renderer, 'reveal']);
- $result = $middleware($error, $request->reveal(), $response->reveal(), $next);
- $this->assertSame($expected, $result);
+ $factory = new UnauthorizedResponseFactoryFactory();
+ $this->factory = $factory($this->container->reveal());
}
- public function testMiddlewareReturnsHtmlResponseInjectedWithResultsOfRendereringTemplat()
+ public function testReturnsHtmlResponseInjectedWithResultsOfRendereringTemplat()
{
- $middleware = $this->middleware;
+ $factory = $this->factory;
$request = $this->createRequestMock();
$originalRequest = $this->createRequestMock();
$uri = $this->createUriMock();
- $response = $this->createResponseMock();
- $next = $this->nextShouldNotBeCalled();
$error = 'error';
$view = [
'auth_path' => '/auth',
'redirect' => '/foo',
];
- $response->getStatusCode()->willReturn(401);
$request->getUri()->will([$uri, 'reveal']);
$request->getOriginalRequest()->will([$originalRequest, 'reveal']);
$uri->withPath('/auth')->will([$uri, 'reveal']);
$uri->__toString()->willReturn('/auth');
$originalRequest->getUri()->willReturn('/foo');
$this->renderer->render('error::401', $view)->willReturn('unauthorized');
- $result = $middleware($error, $request->reveal(), $response->reveal(), $next);
+ $result = $factory($request->reveal());
$this->assertInstanceOf(HtmlResponse::class, $result);
$this->assertEquals('unauthorized', (string) $result->getBody());

0 comments on commit 03461a4

Please sign in to comment.