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 bb405fde..9af35eb4 100644 --- a/config/app.php +++ b/config/app.php @@ -8,20 +8,22 @@ return [ \Engelsystem\Logger\LoggerServiceProvider::class, \Engelsystem\Exceptions\ExceptionsServiceProvider::class, \Engelsystem\Config\ConfigServiceProvider::class, - \Engelsystem\Routing\RoutingServiceProvider::class, + \Engelsystem\Http\UrlGeneratorServiceProvider::class, \Engelsystem\Renderer\RendererServiceProvider::class, \Engelsystem\Database\DatabaseServiceProvider::class, \Engelsystem\Http\RequestServiceProvider::class, \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/Routing/LegacyUrlGenerator.php b/src/Http/LegacyUrlGenerator.php similarity index 83% rename from src/Routing/LegacyUrlGenerator.php rename to src/Http/LegacyUrlGenerator.php index fdac4f96..b9f8b7f1 100644 --- a/src/Routing/LegacyUrlGenerator.php +++ b/src/Http/LegacyUrlGenerator.php @@ -1,6 +1,6 @@ /index.php?p=& */ - public function linkTo($path, $parameters = []) + public function to($path, $parameters = []) { $page = ltrim($path, '/'); if (!empty($page)) { @@ -22,7 +22,7 @@ class LegacyUrlGenerator extends UrlGenerator $parameters = array_merge(['p' => $page], $parameters); } - $uri = parent::linkTo('index.php', $parameters); + $uri = parent::to('index.php', $parameters); $uri = preg_replace('~(/index\.php)+~', '/index.php', $uri); $uri = preg_replace('~(/index\.php)$~', '/', $uri); diff --git a/src/Routing/UrlGenerator.php b/src/Http/UrlGenerator.php similarity index 88% rename from src/Routing/UrlGenerator.php rename to src/Http/UrlGenerator.php index 188bac3b..7ced769e 100644 --- a/src/Routing/UrlGenerator.php +++ b/src/Http/UrlGenerator.php @@ -1,6 +1,6 @@ app->make(UrlGenerator::class); + $this->app->instance('http.urlGenerator', $urlGenerator); + } +} 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/src/Routing/RoutingServiceProvider.php b/src/Routing/RoutingServiceProvider.php deleted file mode 100644 index beaa6a94..00000000 --- a/src/Routing/RoutingServiceProvider.php +++ /dev/null @@ -1,24 +0,0 @@ -app->get('config'); - $class = UrlGenerator::class; - if (! $config->get('rewrite_urls', true)) { - $class = LegacyUrlGenerator::class; - } - - $urlGenerator = $this->app->make($class); - $this->app->instance('routing.urlGenerator', $urlGenerator); - $this->app->bind(UrlGeneratorInterface::class, 'routing.urlGenerator'); - } -} diff --git a/src/helpers.php b/src/helpers.php index 50c5c837..95571a40 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -6,7 +6,7 @@ use Engelsystem\Config\Config; use Engelsystem\Http\Request; use Engelsystem\Http\Response; use Engelsystem\Renderer\Renderer; -use Engelsystem\Routing\UrlGeneratorInterface; +use Engelsystem\Http\UrlGenerator; use Symfony\Component\HttpFoundation\Session\SessionInterface; /** @@ -125,13 +125,13 @@ function session($key = null, $default = null) */ function url($path = null, $parameters = []) { - $urlGenerator = app('routing.urlGenerator'); + $urlGenerator = app('http.urlGenerator'); if (is_null($path)) { return $urlGenerator; } - return $urlGenerator->linkTo($path, $parameters); + return $urlGenerator->to($path, $parameters); } /** diff --git a/tests/Unit/HelpersTest.php b/tests/Unit/HelpersTest.php index cd001df9..fb9e6f00 100644 --- a/tests/Unit/HelpersTest.php +++ b/tests/Unit/HelpersTest.php @@ -8,7 +8,8 @@ use Engelsystem\Container\Container; use Engelsystem\Http\Request; use Engelsystem\Http\Response; use Engelsystem\Renderer\Renderer; -use Engelsystem\Routing\UrlGeneratorInterface; +use Engelsystem\Http\UrlGenerator; +use Engelsystem\Http\UrlGeneratorInterface; use PHPUnit\Framework\TestCase; use PHPUnit_Framework_MockObject_MockObject as MockObject; use Symfony\Component\HttpFoundation\Session\Session; @@ -201,11 +202,11 @@ class HelpersTest extends TestCase { $urlGeneratorMock = $this->getMockForAbstractClass(UrlGeneratorInterface::class); - $this->getAppMock('routing.urlGenerator', $urlGeneratorMock); + $this->getAppMock('http.urlGenerator', $urlGeneratorMock); $this->assertEquals($urlGeneratorMock, url()); $urlGeneratorMock->expects($this->once()) - ->method('linkTo') + ->method('to') ->with('foo/bar', ['param' => 'value']) ->willReturn('http://lorem.ipsum/foo/bar?param=value'); diff --git a/tests/Unit/Routing/LegacyUrlGeneratorTest.php b/tests/Unit/Http/LegacyUrlGeneratorTest.php similarity index 80% rename from tests/Unit/Routing/LegacyUrlGeneratorTest.php rename to tests/Unit/Http/LegacyUrlGeneratorTest.php index 3d42afbd..2c087f8f 100644 --- a/tests/Unit/Routing/LegacyUrlGeneratorTest.php +++ b/tests/Unit/Http/LegacyUrlGeneratorTest.php @@ -1,12 +1,12 @@ assertInstanceOf(UrlGeneratorInterface::class, $urlGenerator); - $url = $urlGenerator->linkTo($urlToPath, $arguments); + $url = $urlGenerator->to($urlToPath, $arguments); $this->assertEquals($expectedUrl, $url); } } diff --git a/tests/Unit/Http/UrlGeneratorServiceProviderTest.php b/tests/Unit/Http/UrlGeneratorServiceProviderTest.php new file mode 100644 index 00000000..874268b0 --- /dev/null +++ b/tests/Unit/Http/UrlGeneratorServiceProviderTest.php @@ -0,0 +1,29 @@ +getMockBuilder(UrlGenerator::class) + ->getMock(); + + $app = $this->getApp(); + + $this->setExpects($app, 'make', [UrlGenerator::class], $urlGenerator); + $this->setExpects($app, 'instance', ['http.urlGenerator', $urlGenerator]); + + $serviceProvider = new UrlGeneratorServiceProvider($app); + $serviceProvider->register(); + } +} diff --git a/tests/Unit/Routing/UrlGeneratorTest.php b/tests/Unit/Http/UrlGeneratorTest.php similarity index 71% rename from tests/Unit/Routing/UrlGeneratorTest.php rename to tests/Unit/Http/UrlGeneratorTest.php index e128bfe7..fa2ec36e 100644 --- a/tests/Unit/Routing/UrlGeneratorTest.php +++ b/tests/Unit/Http/UrlGeneratorTest.php @@ -1,12 +1,11 @@ 'abc', 'bla' => 'foo'], 'http://f.b/foo?test=abc&bla=foo'], @@ -23,7 +21,7 @@ class UrlGeneratorTest extends TestCase /** * @dataProvider provideLinksTo - * @covers \Engelsystem\Routing\UrlGenerator::linkTo + * @covers \Engelsystem\Http\UrlGenerator::to * * @param string $path * @param string $willReturn @@ -31,9 +29,10 @@ class UrlGeneratorTest extends TestCase * @param string[] $arguments * @param string $expectedUrl */ - public function testLinkTo($urlToPath, $path, $willReturn, $arguments, $expectedUrl) + public function testTo($urlToPath, $path, $willReturn, $arguments, $expectedUrl) { $app = new Container(); + $urlGenerator = new UrlGenerator(); Application::setInstance($app); $request = $this->getMockBuilder(Request::class) @@ -46,10 +45,7 @@ class UrlGeneratorTest extends TestCase $app->instance('request', $request); - $urlGenerator = new UrlGenerator(); - $this->assertInstanceOf(UrlGeneratorInterface::class, $urlGenerator); - - $url = $urlGenerator->linkTo($urlToPath, $arguments); + $url = $urlGenerator->to($urlToPath, $arguments); $this->assertEquals($expectedUrl, $url); } } diff --git a/tests/Unit/Middleware/CallableHandlerTest.php b/tests/Unit/Middleware/CallableHandlerTest.php new file mode 100644 index 00000000..29424480 --- /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 [$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..1e920f06 --- /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 [$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); + } +} diff --git a/tests/Unit/Routing/RoutingServiceProviderTest.php b/tests/Unit/Routing/RoutingServiceProviderTest.php deleted file mode 100644 index ce3d7290..00000000 --- a/tests/Unit/Routing/RoutingServiceProviderTest.php +++ /dev/null @@ -1,64 +0,0 @@ -getApp(['make', 'instance', 'bind', 'get']); - /** @var MockObject|Config $config */ - $config = $this->getMockBuilder(Config::class)->getMock(); - /** @var MockObject|UrlGeneratorInterface $urlGenerator */ - $urlGenerator = $this->getMockForAbstractClass(UrlGeneratorInterface::class); - /** @var MockObject|UrlGeneratorInterface $legacyUrlGenerator */ - $legacyUrlGenerator = $this->getMockForAbstractClass(UrlGeneratorInterface::class); - - $config->expects($this->atLeastOnce()) - ->method('get') - ->with('rewrite_urls') - ->willReturnOnConsecutiveCalls( - true, - false - ); - - $this->setExpects($app, 'get', ['config'], $config, $this->atLeastOnce()); - - $app->expects($this->atLeastOnce()) - ->method('make') - ->withConsecutive( - [UrlGenerator::class], - [LegacyUrlGenerator::class] - ) - ->willReturnOnConsecutiveCalls( - $urlGenerator, - $legacyUrlGenerator - ); - $app->expects($this->atLeastOnce()) - ->method('instance') - ->withConsecutive( - ['routing.urlGenerator', $urlGenerator], - ['routing.urlGenerator', $legacyUrlGenerator] - ); - $this->setExpects( - $app, 'bind', - [UrlGeneratorInterface::class, 'routing.urlGenerator'], null, - $this->atLeastOnce() - ); - - $serviceProvider = new RoutingServiceProvider($app); - $serviceProvider->register(); - $serviceProvider->register(); - } -}