From a1bc763a16ee8be109de5c9053fbc5eded53824e Mon Sep 17 00:00:00 2001 From: Igor Scheller Date: Sat, 25 Aug 2018 21:16:20 +0200 Subject: [PATCH] Added nikic/fast-route as routing dispatcher --- composer.json | 1 + config/app.php | 6 +- config/routes.php | 14 ++ src/Middleware/CallableHandler.php | 77 +++++++++ src/Middleware/Dispatcher.php | 26 +-- src/Middleware/LegacyMiddleware.php | 13 +- src/Middleware/NotFoundResponse.php | 56 ------- src/Middleware/RequestHandler.php | 50 ++++++ .../RequestHandlerServiceProvider.php | 17 ++ src/Middleware/ResolvesMiddlewareTrait.php | 56 +++++++ src/Middleware/RouteDispatcher.php | 75 +++++++++ .../RouteDispatcherServiceProvider.php | 41 +++++ tests/Unit/Middleware/CallableHandlerTest.php | 141 +++++++++++++++++ tests/Unit/Middleware/DispatcherTest.php | 58 ++----- .../Unit/Middleware/LegacyMiddlewareTest.php | 8 +- .../Unit/Middleware/NotFoundResponseTest.php | 39 ----- .../RequestHandlerServiceProviderTest.php | 36 +++++ tests/Unit/Middleware/RequestHandlerTest.php | 89 +++++++++++ .../ResolvesMiddlewareTraitTest.php | 67 ++++++++ .../RouteDispatcherServiceProviderTest.php | 65 ++++++++ tests/Unit/Middleware/RouteDispatcherTest.php | 148 ++++++++++++++++++ .../Unit/Middleware/Stub/HasStaticMethod.php | 8 + .../ResolvesMiddlewareTraitImplementation.php | 35 +++++ 23 files changed, 953 insertions(+), 173 deletions(-) create mode 100644 config/routes.php create mode 100644 src/Middleware/CallableHandler.php delete mode 100644 src/Middleware/NotFoundResponse.php create mode 100644 src/Middleware/RequestHandler.php create mode 100644 src/Middleware/RequestHandlerServiceProvider.php create mode 100644 src/Middleware/ResolvesMiddlewareTrait.php create mode 100644 src/Middleware/RouteDispatcher.php create mode 100644 src/Middleware/RouteDispatcherServiceProvider.php create mode 100644 tests/Unit/Middleware/CallableHandlerTest.php delete mode 100644 tests/Unit/Middleware/NotFoundResponseTest.php create mode 100644 tests/Unit/Middleware/RequestHandlerServiceProviderTest.php create mode 100644 tests/Unit/Middleware/RequestHandlerTest.php create mode 100644 tests/Unit/Middleware/ResolvesMiddlewareTraitTest.php create mode 100644 tests/Unit/Middleware/RouteDispatcherServiceProviderTest.php create mode 100644 tests/Unit/Middleware/RouteDispatcherTest.php create mode 100644 tests/Unit/Middleware/Stub/HasStaticMethod.php create mode 100644 tests/Unit/Middleware/Stub/ResolvesMiddlewareTraitImplementation.php diff --git a/composer.json b/composer.json index 283bc30a..f38bb972 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,7 @@ "illuminate/container": "5.5.*", "illuminate/database": "5.5.*", "illuminate/support": "^5.5", + "nikic/fast-route": "^1.3", "psr/container": "^1.0", "psr/http-server-middleware": "^1.0", "psr/log": "^1.0", diff --git a/config/app.php b/config/app.php index 6278f193..9af35eb4 100644 --- a/config/app.php +++ b/config/app.php @@ -15,13 +15,15 @@ return [ \Engelsystem\Http\SessionServiceProvider::class, \Engelsystem\Http\ResponseServiceProvider::class, \Engelsystem\Http\Psr7ServiceProvider::class, + \Engelsystem\Middleware\RouteDispatcherServiceProvider::class, + \Engelsystem\Middleware\RequestHandlerServiceProvider::class, ], // Application middleware 'middleware' => [ \Engelsystem\Middleware\SendResponseHandler::class, \Engelsystem\Middleware\ExceptionHandler::class, - \Engelsystem\Middleware\LegacyMiddleware::class, - \Engelsystem\Middleware\NotFoundResponse::class, + \Engelsystem\Middleware\RouteDispatcher::class, + \Engelsystem\Middleware\RequestHandler::class, ], ]; diff --git a/config/routes.php b/config/routes.php new file mode 100644 index 00000000..5296dbc7 --- /dev/null +++ b/config/routes.php @@ -0,0 +1,14 @@ +addRoute('GET', '/hello/{name}', function ($request) { + /** @var ServerRequestInterface $request */ + $name = $request->getAttribute('name'); + + return response(sprintf('Hello %s!', htmlspecialchars($name))); +}); diff --git a/src/Middleware/CallableHandler.php b/src/Middleware/CallableHandler.php new file mode 100644 index 00000000..eb493bf1 --- /dev/null +++ b/src/Middleware/CallableHandler.php @@ -0,0 +1,77 @@ +callable = $callable; + $this->container = $container; + } + + /** + * Process an incoming server request and return a response, optionally delegating + * response creation to a handler. + * + * @param ServerRequestInterface $request + * @param RequestHandlerInterface $handler + * @return ResponseInterface + */ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + return $this->execute([$request, $handler]); + } + + /** + * Handle the request and return a response. + * + * @param ServerRequestInterface $request + * @return ResponseInterface + */ + public function handle(ServerRequestInterface $request): ResponseInterface + { + return $this->execute([$request]); + } + + /** + * Execute the callable and return a response + * + * @param array $arguments + * @return ResponseInterface + */ + protected function execute(array $arguments = []): ResponseInterface + { + $return = call_user_func_array($this->callable, $arguments); + + if ($return instanceof ResponseInterface) { + return $return; + } + + if (!$this->container instanceof Container) { + throw new InvalidArgumentException('Unable to resolve response'); + } + + /** @var Response $response */ + $response = $this->container->get('response'); + return $response->withContent($return); + } +} diff --git a/src/Middleware/Dispatcher.php b/src/Middleware/Dispatcher.php index f2a5b5d5..48eb0948 100644 --- a/src/Middleware/Dispatcher.php +++ b/src/Middleware/Dispatcher.php @@ -12,6 +12,8 @@ use Psr\Http\Server\RequestHandlerInterface; class Dispatcher implements MiddlewareInterface, RequestHandlerInterface { + use ResolvesMiddlewareTrait; + /** @var MiddlewareInterface[]|string[] */ protected $stack; @@ -70,10 +72,7 @@ class Dispatcher implements MiddlewareInterface, RequestHandlerInterface throw new LogicException('Middleware queue is empty'); } - if (is_string($middleware)) { - $middleware = $this->resolveMiddleware($middleware); - } - + $middleware = $this->resolveMiddleware($middleware); if (!$middleware instanceof MiddlewareInterface) { throw new InvalidArgumentException('Middleware is no instance of ' . MiddlewareInterface::class); } @@ -81,25 +80,6 @@ class Dispatcher implements MiddlewareInterface, RequestHandlerInterface return $middleware->process($request, $this); } - /** - * Resolve the middleware with the container - * - * @param string $middleware - * @return MiddlewareInterface - */ - protected function resolveMiddleware($middleware) - { - if (!$this->container instanceof Application) { - throw new InvalidArgumentException('Unable to resolve middleware ' . $middleware); - } - - if ($this->container->has($middleware)) { - return $this->container->get($middleware); - } - - return $this->container->make($middleware); - } - /** * @param Application $container */ diff --git a/src/Middleware/LegacyMiddleware.php b/src/Middleware/LegacyMiddleware.php index 714141de..276fb3ee 100644 --- a/src/Middleware/LegacyMiddleware.php +++ b/src/Middleware/LegacyMiddleware.php @@ -83,7 +83,9 @@ class LegacyMiddleware implements MiddlewareInterface } if (empty($title) and empty($content)) { - return $handler->handle($request); + $page = '404'; + $title = _('Page not found'); + $content = _('This page could not be found or you don\'t have permission to view it. You probably have to sign in or register in order to gain access!'); } return $this->renderPage($page, $title, $content); @@ -270,10 +272,17 @@ class LegacyMiddleware implements MiddlewareInterface $parameters = [ 'key' => (isset($user) ? $user['api_key'] : ''), ]; + if ($page == 'user_meetings') { $parameters['meetings'] = 1; } + $status = 200; + if ($page == '404') { + $status = 404; + $content = info($content, true); + } + return response(view(__DIR__ . '/../../templates/layout.html', [ 'theme' => isset($user) ? $user['color'] : config('theme'), 'title' => $title, @@ -291,6 +300,6 @@ class LegacyMiddleware implements MiddlewareInterface 'contact_email' => config('contact_email'), 'locale' => locale(), 'event_info' => EventConfig_info($event_config) . '
' - ])); + ]), $status); } } diff --git a/src/Middleware/NotFoundResponse.php b/src/Middleware/NotFoundResponse.php deleted file mode 100644 index f9431c1d..00000000 --- a/src/Middleware/NotFoundResponse.php +++ /dev/null @@ -1,56 +0,0 @@ -renderPage($info); - } - - /** - * @param string $content - * @return Response - * @codeCoverageIgnore - */ - protected function renderPage($content) - { - global $user; - $event_config = EventConfig(); - - return response(view(__DIR__ . '/../../templates/layout.html', [ - 'theme' => isset($user) ? $user['color'] : config('theme'), - 'title' => _('Page not found'), - 'atom_link' => '', - 'start_page_url' => page_link_to('/'), - 'credits_url' => page_link_to('credits'), - 'menu' => make_menu(), - 'content' => msg() . info($content), - 'header_toolbar' => header_toolbar(), - 'faq_url' => config('faq_url'), - 'contact_email' => config('contact_email'), - 'locale' => locale(), - 'event_info' => EventConfig_info($event_config) . '
' - ]), 404); - } -} diff --git a/src/Middleware/RequestHandler.php b/src/Middleware/RequestHandler.php new file mode 100644 index 00000000..e1381abf --- /dev/null +++ b/src/Middleware/RequestHandler.php @@ -0,0 +1,50 @@ +container = $container; + } + + /** + * Process an incoming server request and return a response, optionally delegating + * response creation to a handler. + * + * @param ServerRequestInterface $request + * @param RequestHandlerInterface $handler + * @return ResponseInterface + */ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + $requestHandler = $request->getAttribute('route-request-handler'); + $requestHandler = $this->resolveMiddleware($requestHandler); + + if ($requestHandler instanceof MiddlewareInterface) { + return $requestHandler->process($request, $handler); + } + + if ($requestHandler instanceof RequestHandlerInterface) { + return $requestHandler->handle($request); + } + + throw new InvalidArgumentException('Unable to process request handler of type ' . gettype($requestHandler)); + } +} diff --git a/src/Middleware/RequestHandlerServiceProvider.php b/src/Middleware/RequestHandlerServiceProvider.php new file mode 100644 index 00000000..c6488118 --- /dev/null +++ b/src/Middleware/RequestHandlerServiceProvider.php @@ -0,0 +1,17 @@ +app->make(RequestHandler::class); + + $this->app->instance('request.handler', $requestHandler); + $this->app->bind(RequestHandler::class, 'request.handler'); + } +} diff --git a/src/Middleware/ResolvesMiddlewareTrait.php b/src/Middleware/ResolvesMiddlewareTrait.php new file mode 100644 index 00000000..76557ce6 --- /dev/null +++ b/src/Middleware/ResolvesMiddlewareTrait.php @@ -0,0 +1,56 @@ +isMiddleware($middleware)) { + return $middleware; + } + + if (!property_exists($this, 'container') || !$this->container instanceof Application) { + throw new InvalidArgumentException('Unable to resolve middleware'); + } + + /** @var Application $container */ + $container = $this->container; + + if (is_string($middleware)) { + $middleware = $container->make($middleware); + } + + if (is_callable($middleware)) { + $middleware = $container->make(CallableHandler::class, ['callable' => $middleware]); + } + + if ($this->isMiddleware($middleware)) { + return $middleware; + } + + throw new InvalidArgumentException('Unable to resolve middleware'); + } + + /** + * Checks if the given object is a middleware or middleware or request handler + * + * @param mixed $middleware + * @return bool + */ + protected function isMiddleware($middleware) + { + return ($middleware instanceof MiddlewareInterface || $middleware instanceof RequestHandlerInterface); + } +} diff --git a/src/Middleware/RouteDispatcher.php b/src/Middleware/RouteDispatcher.php new file mode 100644 index 00000000..f14faea8 --- /dev/null +++ b/src/Middleware/RouteDispatcher.php @@ -0,0 +1,75 @@ +dispatcher = $dispatcher; + $this->response = $response; + $this->notFound = $notFound; + } + + /** + * Process an incoming server request and return a response, optionally delegating + * response creation to a handler. + * + * @param ServerRequestInterface $request + * @param RequestHandlerInterface $handler + * @return ResponseInterface + */ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + $route = $this->dispatcher->dispatch($request->getMethod(), urldecode($request->getUri()->getPath())); + + $status = $route[0]; + if ($status == FastRouteDispatcher::NOT_FOUND) { + if ($this->notFound instanceof MiddlewareInterface) { + return $this->notFound->process($request, $handler); + } + + return $this->response->withStatus(404); + } + + if ($status == FastRouteDispatcher::METHOD_NOT_ALLOWED) { + $methods = $route[1]; + return $this->response + ->withStatus(405) + ->withHeader('Allow', implode(', ', $methods)); + } + + $routeHandler = $route[1]; + $request = $request->withAttribute('route-request-handler', $routeHandler); + + $vars = $route[2]; + foreach ($vars as $name => $value) { + $request = $request->withAttribute($name, $value); + } + + return $handler->handle($request); + } +} diff --git a/src/Middleware/RouteDispatcherServiceProvider.php b/src/Middleware/RouteDispatcherServiceProvider.php new file mode 100644 index 00000000..3b4fa183 --- /dev/null +++ b/src/Middleware/RouteDispatcherServiceProvider.php @@ -0,0 +1,41 @@ +app->alias(RouteDispatcher::class, 'route.dispatcher'); + + $this->app + ->when(RouteDispatcher::class) + ->needs(FastRouteDispatcher::class) + ->give(function () { + return $this->generateRouting(); + }); + + $this->app + ->when(RouteDispatcher::class) + ->needs(MiddlewareInterface::class) + ->give(LegacyMiddleware::class); + } + + /** + * Includes the routes.php file + * + * @return FastRouteDispatcher + * @codeCoverageIgnore + */ + function generateRouting() + { + return \FastRoute\simpleDispatcher(function (RouteCollector $route) { + require config_path('routes.php'); + }); + } +} diff --git a/tests/Unit/Middleware/CallableHandlerTest.php b/tests/Unit/Middleware/CallableHandlerTest.php new file mode 100644 index 00000000..6e6dab58 --- /dev/null +++ b/tests/Unit/Middleware/CallableHandlerTest.php @@ -0,0 +1,141 @@ +getProperty('callable'); + $property->setAccessible(true); + + $this->assertEquals($callable, $property->getValue($handler)); + } + + /** + * @covers \Engelsystem\Middleware\CallableHandler::process + */ + public function testProcess() + { + /** @var ServerRequestInterface|MockObject $request */ + /** @var ResponseInterface|MockObject $response */ + /** @var callable|MockObject $callable */ + /** @var RequestHandlerInterface|MockObject $handler */ + list($request, $response, $callable, $handler) = $this->getMocks(); + + $callable->expects($this->once()) + ->method('__invoke') + ->with($request, $handler) + ->willReturn($response); + + $middleware = new CallableHandler($callable); + $middleware->process($request, $handler); + } + + /** + * @covers \Engelsystem\Middleware\CallableHandler::handle + */ + public function testHandler() + { + /** @var ServerRequestInterface|MockObject $request */ + /** @var ResponseInterface|MockObject $response */ + /** @var callable|MockObject $callable */ + list($request, $response, $callable) = $this->getMocks(); + + $callable->expects($this->once()) + ->method('__invoke') + ->with($request) + ->willReturn($response); + + $middleware = new CallableHandler($callable); + $middleware->handle($request); + } + + /** + * @covers \Engelsystem\Middleware\CallableHandler::execute + */ + public function testExecute() + { + /** @var ServerRequestInterface|MockObject $request */ + /** @var Response|MockObject $response */ + /** @var callable|MockObject $callable */ + list($request, $response, $callable) = $this->getMocks(); + /** @var Container|MockObject $container */ + $container = $this->createMock(Container::class); + + $callable->expects($this->exactly(3)) + ->method('__invoke') + ->with($request) + ->willReturnOnConsecutiveCalls($response, 'Lorem ipsum?', 'I\'m not an exception!'); + + $container->expects($this->once()) + ->method('get') + ->with('response') + ->willReturn($response); + + $response->expects($this->once()) + ->method('withContent') + ->with('Lorem ipsum?') + ->willReturn($response); + + $middleware = new CallableHandler($callable, $container); + $return = $middleware->handle($request); + $this->assertInstanceOf(ResponseInterface::class, $return); + $this->assertEquals($response, $return); + + $return = $middleware->handle($request); + $this->assertInstanceOf(ResponseInterface::class, $return); + $this->assertEquals($response, $return); + + $middleware = new CallableHandler($callable); + $this->expectException(\InvalidArgumentException::class); + $middleware->handle($request); + } + + /** + * @return array + */ + protected function getMocks(): array + { + /** @var ServerRequestInterface|MockObject $request */ + $request = $this->getMockForAbstractClass(ServerRequestInterface::class); + /** @var RequestHandlerInterface|MockObject $handler */ + $handler = $this->getMockForAbstractClass(RequestHandlerInterface::class); + /** @var Response|MockObject $response */ + $response = $this->createMock(Response::class); + /** @var callable|MockObject $callable */ + $callable = $this->getMockBuilder(stdClass::class) + ->setMethods(['__invoke']) + ->getMock(); + return array($request, $response, $callable, $handler); + } +} diff --git a/tests/Unit/Middleware/DispatcherTest.php b/tests/Unit/Middleware/DispatcherTest.php index c01c5029..4e1c51a7 100644 --- a/tests/Unit/Middleware/DispatcherTest.php +++ b/tests/Unit/Middleware/DispatcherTest.php @@ -5,7 +5,6 @@ namespace Engelsystem\Test\Unit\Middleware; use Engelsystem\Application; use Engelsystem\Middleware\Dispatcher; use Engelsystem\Test\Unit\Middleware\Stub\NotARealMiddleware; -use Engelsystem\Test\Unit\Middleware\Stub\ReturnResponseMiddleware; use InvalidArgumentException; use LogicException; use PHPUnit\Framework\TestCase; @@ -158,14 +157,14 @@ class DispatcherTest extends TestCase /** @var Dispatcher|MockObject $dispatcher */ $dispatcher = $this->getMockBuilder(Dispatcher::class) - ->setConstructorArgs([[MiddlewareInterface::class]]) + ->setConstructorArgs([[MiddlewareInterface::class, MiddlewareInterface::class]]) ->setMethods(['resolveMiddleware']) ->getMock(); - $dispatcher->expects($this->once()) + $dispatcher->expects($this->exactly(2)) ->method('resolveMiddleware') ->with(MiddlewareInterface::class) - ->willReturn($middleware); + ->willReturnOnConsecutiveCalls($middleware, null); $middleware->expects($this->once()) ->method('process') @@ -174,57 +173,26 @@ class DispatcherTest extends TestCase $return = $dispatcher->handle($request); $this->assertEquals($response, $return); + + $this->expectException(InvalidArgumentException::class); + $dispatcher->handle($request); } /** - * @covers \Engelsystem\Middleware\Dispatcher::resolveMiddleware * @covers \Engelsystem\Middleware\Dispatcher::setContainer */ - public function testResolveMiddleware() + public function testSetContainer() { /** @var Application|MockObject $container */ $container = $this->createMock(Application::class); - /** @var ServerRequestInterface|MockObject $request */ - $request = $this->createMock(ServerRequestInterface::class); - /** @var ResponseInterface|MockObject $response */ - $response = $this->createMock(ResponseInterface::class); - - $returnResponseMiddleware = new ReturnResponseMiddleware($response); - - $container->expects($this->exactly(2)) - ->method('has') - ->withConsecutive([ReturnResponseMiddleware::class], ['middleware']) - ->willReturnOnConsecutiveCalls(false, true); - - $container->expects($this->once()) - ->method('make') - ->with(ReturnResponseMiddleware::class) - ->willReturn($returnResponseMiddleware); - - $container->expects($this->once()) - ->method('get') - ->with('middleware') - ->willReturn($returnResponseMiddleware); - - $dispatcher = new Dispatcher([ReturnResponseMiddleware::class]); - $dispatcher->setContainer($container); - $dispatcher->handle($request); - $dispatcher = new Dispatcher(['middleware'], $container); - $dispatcher->handle($request); - } + $middleware = new Dispatcher(); + $middleware->setContainer($container); - /** - * @covers \Engelsystem\Middleware\Dispatcher::resolveMiddleware - */ - public function testResolveMiddlewareNoContainer() - { - /** @var ServerRequestInterface|MockObject $request */ - $request = $this->createMock(ServerRequestInterface::class); - - $this->expectException(InvalidArgumentException::class); + $reflection = new Reflection(get_class($middleware)); + $property = $reflection->getProperty('container'); + $property->setAccessible(true); - $dispatcher = new Dispatcher([ReturnResponseMiddleware::class]); - $dispatcher->handle($request); + $this->assertEquals($container, $property->getValue($middleware)); } } diff --git a/tests/Unit/Middleware/LegacyMiddlewareTest.php b/tests/Unit/Middleware/LegacyMiddlewareTest.php index 34e60b60..ed9a5a74 100644 --- a/tests/Unit/Middleware/LegacyMiddlewareTest.php +++ b/tests/Unit/Middleware/LegacyMiddlewareTest.php @@ -46,10 +46,11 @@ class LegacyMiddlewareTest extends TestCase ['title2', 'content2'] ); - $middleware->expects($this->exactly(2)) + $middleware->expects($this->exactly(3)) ->method('renderPage') ->withConsecutive( ['user_worklog', 'title', 'content'], + ['404', 'Page not found'], ['login', 'title2', 'content2'] ) ->willReturn($response); @@ -73,11 +74,6 @@ class LegacyMiddlewareTest extends TestCase '/' ); - $handler->expects($this->once()) - ->method('handle') - ->with($request) - ->willReturn($response); - $middleware->process($request, $handler); $middleware->process($request, $handler); $middleware->process($request, $handler); diff --git a/tests/Unit/Middleware/NotFoundResponseTest.php b/tests/Unit/Middleware/NotFoundResponseTest.php deleted file mode 100644 index 9279e81d..00000000 --- a/tests/Unit/Middleware/NotFoundResponseTest.php +++ /dev/null @@ -1,39 +0,0 @@ -getMockBuilder(NotFoundResponse::class) - ->setMethods(['renderPage']) - ->getMock(); - /** @var ResponseInterface|MockObject $response */ - $response = $this->getMockForAbstractClass(ResponseInterface::class); - /** @var RequestHandlerInterface|MockObject $handler */ - $handler = $this->getMockForAbstractClass(RequestHandlerInterface::class); - /** @var ServerRequestInterface|MockObject $request */ - $request = $this->getMockForAbstractClass(ServerRequestInterface::class); - - $middleware->expects($this->once()) - ->method('renderPage') - ->willReturn($response); - - $handler->expects($this->never()) - ->method('handle'); - - $middleware->process($request, $handler); - } -} diff --git a/tests/Unit/Middleware/RequestHandlerServiceProviderTest.php b/tests/Unit/Middleware/RequestHandlerServiceProviderTest.php new file mode 100644 index 00000000..281016b5 --- /dev/null +++ b/tests/Unit/Middleware/RequestHandlerServiceProviderTest.php @@ -0,0 +1,36 @@ +createMock(RequestHandler::class); + + $app = $this->getApp(['make', 'instance', 'bind']); + + $app->expects($this->once()) + ->method('make') + ->with(RequestHandler::class) + ->willReturn($requestHandler); + $app->expects($this->once()) + ->method('instance') + ->with('request.handler', $requestHandler); + $app->expects($this->once()) + ->method('bind') + ->with(RequestHandler::class, 'request.handler'); + + $serviceProvider = new RequestHandlerServiceProvider($app); + $serviceProvider->register(); + } +} diff --git a/tests/Unit/Middleware/RequestHandlerTest.php b/tests/Unit/Middleware/RequestHandlerTest.php new file mode 100644 index 00000000..896b55c3 --- /dev/null +++ b/tests/Unit/Middleware/RequestHandlerTest.php @@ -0,0 +1,89 @@ +createMock(Application::class); + + $handler = new RequestHandler($container); + + $reflection = new Reflection(get_class($handler)); + $property = $reflection->getProperty('container'); + $property->setAccessible(true); + + $this->assertEquals($container, $property->getValue($handler)); + } + + /** + * @covers \Engelsystem\Middleware\RequestHandler::process + */ + public function testProcess() + { + /** @var Application|MockObject $container */ + $container = $this->createMock(Application::class); + /** @var ServerRequestInterface|MockObject $request */ + $request = $this->getMockForAbstractClass(ServerRequestInterface::class); + /** @var RequestHandlerInterface|MockObject $handler */ + $handler = $this->getMockForAbstractClass(RequestHandlerInterface::class); + /** @var ResponseInterface|MockObject $response */ + $response = $this->getMockForAbstractClass(ResponseInterface::class); + + $middlewareInterface = $this->getMockForAbstractClass(MiddlewareInterface::class); + $requestHandlerInterface = $this->getMockForAbstractClass(RequestHandlerInterface::class); + + $request->expects($this->exactly(3)) + ->method('getAttribute') + ->with('route-request-handler') + ->willReturn('FooBarClass'); + + /** @var RequestHandler|MockObject $middleware */ + $middleware = $this->getMockBuilder(RequestHandler::class) + ->setConstructorArgs([$container]) + ->setMethods(['resolveMiddleware']) + ->getMock(); + $middleware->expects($this->exactly(3)) + ->method('resolveMiddleware') + ->with('FooBarClass') + ->willReturnOnConsecutiveCalls( + $middlewareInterface, + $requestHandlerInterface, + null + ); + + $middlewareInterface->expects($this->once()) + ->method('process') + ->with($request, $handler) + ->willReturn($response); + $requestHandlerInterface->expects($this->once()) + ->method('handle') + ->with($request) + ->willReturn($response); + + $return = $middleware->process($request, $handler); + $this->assertEquals($return, $response); + + $middleware->process($request, $handler); + $this->assertEquals($return, $response); + + $this->expectException(InvalidArgumentException::class); + $middleware->process($request, $handler); + } +} diff --git a/tests/Unit/Middleware/ResolvesMiddlewareTraitTest.php b/tests/Unit/Middleware/ResolvesMiddlewareTraitTest.php new file mode 100644 index 00000000..320a6d6b --- /dev/null +++ b/tests/Unit/Middleware/ResolvesMiddlewareTraitTest.php @@ -0,0 +1,67 @@ +createMock(Application::class); + $middlewareInterface = $this->getMockForAbstractClass(MiddlewareInterface::class); + $callable = [HasStaticMethod::class, 'foo']; + + $container->expects($this->exactly(3)) + ->method('make') + ->withConsecutive( + ['FooBarClass'], + [CallableHandler::class, ['callable' => $callable]], + ['UnresolvableClass'] + ) + ->willReturnOnConsecutiveCalls( + $middlewareInterface, + $middlewareInterface, + null + ); + + $middleware = new ResolvesMiddlewareTraitImplementation($container); + + $return = $middleware->callResolveMiddleware('FooBarClass'); + $this->assertEquals($middlewareInterface, $return); + + $return = $middleware->callResolveMiddleware($callable); + $this->assertEquals($middlewareInterface, $return); + + $this->expectException(InvalidArgumentException::class); + $middleware->callResolveMiddleware('UnresolvableClass'); + } + + /** + * @covers \Engelsystem\Middleware\ResolvesMiddlewareTrait::resolveMiddleware + */ + public function testResolveMiddlewareNoContainer() + { + $middlewareInterface = $this->getMockForAbstractClass(MiddlewareInterface::class); + + $middleware = new ResolvesMiddlewareTraitImplementation(); + $return = $middleware->callResolveMiddleware($middlewareInterface); + + $this->assertEquals($middlewareInterface, $return); + + $this->expectException(InvalidArgumentException::class); + $middleware->callResolveMiddleware('FooBarClass'); + } +} diff --git a/tests/Unit/Middleware/RouteDispatcherServiceProviderTest.php b/tests/Unit/Middleware/RouteDispatcherServiceProviderTest.php new file mode 100644 index 00000000..ca784c73 --- /dev/null +++ b/tests/Unit/Middleware/RouteDispatcherServiceProviderTest.php @@ -0,0 +1,65 @@ +createMock(ContextualBindingBuilder::class); + $routeDispatcher = $this->getMockForAbstractClass(FastRouteDispatcher::class); + + $app = $this->getApp(['alias', 'when']); + + $app->expects($this->once()) + ->method('alias') + ->with(RouteDispatcher::class, 'route.dispatcher'); + + $app->expects($this->exactly(2)) + ->method('when') + ->with(RouteDispatcher::class) + ->willReturn($bindingBuilder); + + $bindingBuilder->expects($this->exactly(2)) + ->method('needs') + ->withConsecutive( + [FastRouteDispatcher::class], + [MiddlewareInterface::class] + ) + ->willReturn($bindingBuilder); + + $bindingBuilder->expects($this->exactly(2)) + ->method('give') + ->with($this->callback(function ($subject) { + if (is_callable($subject)) { + $subject(); + } + + return is_callable($subject) || $subject == LegacyMiddleware::class; + })); + + /** @var RouteDispatcherServiceProvider|MockObject $serviceProvider */ + $serviceProvider = $this->getMockBuilder(RouteDispatcherServiceProvider::class) + ->setConstructorArgs([$app]) + ->setMethods(['generateRouting']) + ->getMock(); + + $serviceProvider->expects($this->once()) + ->method('generateRouting') + ->willReturn($routeDispatcher); + + $serviceProvider->register(); + } +} diff --git a/tests/Unit/Middleware/RouteDispatcherTest.php b/tests/Unit/Middleware/RouteDispatcherTest.php new file mode 100644 index 00000000..edb2f158 --- /dev/null +++ b/tests/Unit/Middleware/RouteDispatcherTest.php @@ -0,0 +1,148 @@ +getMocks(); + + $dispatcher->expects($this->once()) + ->method('dispatch') + ->with('HEAD', '/foo!bar') + ->willReturn([FastRouteDispatcher::FOUND, $handler, ['foo' => 'bar', 'lorem' => 'ipsum']]); + + $request->expects($this->exactly(3)) + ->method('withAttribute') + ->withConsecutive( + ['route-request-handler', $handler], + ['foo', 'bar'], + ['lorem', 'ipsum'] + ) + ->willReturn($request); + + $handler->expects($this->once()) + ->method('handle') + ->with($request) + ->willReturn($response); + + $middleware = new RouteDispatcher($dispatcher, $response); + $return = $middleware->process($request, $handler); + $this->assertEquals($response, $return); + } + + /** + * @covers \Engelsystem\Middleware\RouteDispatcher::process + */ + public function testProcessNotFound() + { + /** @var FastRouteDispatcher|MockObject $dispatcher */ + /** @var ResponseInterface|MockObject $response */ + /** @var ServerRequestInterface|MockObject $request */ + /** @var RequestHandlerInterface|MockObject $handler */ + list($dispatcher, $response, $request, $handler) = $this->getMocks(); + /** @var MiddlewareInterface|MockObject $notFound */ + $notFound = $this->createMock(MiddlewareInterface::class); + + $dispatcher->expects($this->exactly(2)) + ->method('dispatch') + ->with('HEAD', '/foo!bar') + ->willReturn([FastRouteDispatcher::NOT_FOUND]); + + $response->expects($this->once()) + ->method('withStatus') + ->with(404) + ->willReturn($response); + + $notFound->expects($this->once()) + ->method('process') + ->with($request, $handler) + ->willReturn($response); + + $middleware = new RouteDispatcher($dispatcher, $response, $notFound); + $return = $middleware->process($request, $handler); + $this->assertEquals($response, $return); + + $middleware = new RouteDispatcher($dispatcher, $response); + $return = $middleware->process($request, $handler); + $this->assertEquals($response, $return); + } + + /** + * @covers \Engelsystem\Middleware\RouteDispatcher::process + */ + public function testProcessNotAllowed() + { + /** @var FastRouteDispatcher|MockObject $dispatcher */ + /** @var ResponseInterface|MockObject $response */ + /** @var ServerRequestInterface|MockObject $request */ + /** @var RequestHandlerInterface|MockObject $handler */ + list($dispatcher, $response, $request, $handler) = $this->getMocks(); + + $dispatcher->expects($this->once()) + ->method('dispatch') + ->with('HEAD', '/foo!bar') + ->willReturn([FastRouteDispatcher::METHOD_NOT_ALLOWED, ['POST', 'TEST']]); + + $response->expects($this->once()) + ->method('withStatus') + ->with(405) + ->willReturn($response); + $response->expects($this->once()) + ->method('withHeader') + ->with('Allow', 'POST, TEST') + ->willReturn($response); + + $middleware = new RouteDispatcher($dispatcher, $response); + $return = $middleware->process($request, $handler); + $this->assertEquals($response, $return); + } + + /** + * @return array + */ + protected function getMocks(): array + { + /** @var FastRouteDispatcher|MockObject $dispatcher */ + $dispatcher = $this->getMockForAbstractClass(FastRouteDispatcher::class); + /** @var ResponseInterface|MockObject $response */ + $response = $this->getMockForAbstractClass(ResponseInterface::class); + /** @var ServerRequestInterface|MockObject $request */ + $request = $this->getMockForAbstractClass(ServerRequestInterface::class); + /** @var RequestHandlerInterface|MockObject $handler */ + $handler = $this->getMockForAbstractClass(RequestHandlerInterface::class); + /** @var UriInterface|MockObject $uriInterface */ + $uriInterface = $this->getMockForAbstractClass(UriInterface::class); + + $request->expects($this->atLeastOnce()) + ->method('getMethod') + ->willReturn('HEAD'); + $request->expects($this->atLeastOnce()) + ->method('getUri') + ->willReturn($uriInterface); + $uriInterface->expects($this->atLeastOnce()) + ->method('getPath') + ->willReturn('/foo%21bar'); + + return array($dispatcher, $response, $request, $handler); + } +} diff --git a/tests/Unit/Middleware/Stub/HasStaticMethod.php b/tests/Unit/Middleware/Stub/HasStaticMethod.php new file mode 100644 index 00000000..5ca2670e --- /dev/null +++ b/tests/Unit/Middleware/Stub/HasStaticMethod.php @@ -0,0 +1,8 @@ +container = $container; + } + + /** + * @param string|callable|MiddlewareInterface|RequestHandlerInterface $middleware + * @return MiddlewareInterface|RequestHandlerInterface + * @throws InvalidArgumentException + */ + public function callResolveMiddleware($middleware) + { + return $this->resolveMiddleware($middleware); + } +}