From 3c088292050982505726f5136ff4d0f1a918b879 Mon Sep 17 00:00:00 2001 From: Igor Scheller Date: Thu, 18 Jan 2018 19:01:34 +0100 Subject: [PATCH 01/10] Added Http\Response and Psr7{Request,Response} --- composer.json | 4 +- config/app.php | 2 + src/Http/Psr7ServiceProvider.php | 31 +++++++++ src/Http/Response.php | 9 +++ src/Http/ResponseServiceProvider.php | 14 ++++ tests/Unit/Http/Psr7ServiceProviderTest.php | 65 +++++++++++++++++++ tests/Unit/Http/RequestTest.php | 10 +++ .../Unit/Http/ResponseServiceProviderTest.php | 29 +++++++++ tests/Unit/Http/ResponseTest.php | 19 ++++++ 9 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 src/Http/Psr7ServiceProvider.php create mode 100644 src/Http/Response.php create mode 100644 src/Http/ResponseServiceProvider.php create mode 100644 tests/Unit/Http/Psr7ServiceProviderTest.php create mode 100644 tests/Unit/Http/ResponseServiceProviderTest.php create mode 100644 tests/Unit/Http/ResponseTest.php diff --git a/composer.json b/composer.json index 9c0602b6..df9ee57b 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,9 @@ "psr/container": "^1.0", "psr/log": "^1.0", "symfony/http-foundation": "^3.3", - "twbs/bootstrap": "^3.3" + "symfony/psr-http-message-bridge": "^1.0", + "twbs/bootstrap": "^3.3", + "zendframework/zend-diactoros": "^1.7" }, "require-dev": { "filp/whoops": "^2.1", diff --git a/config/app.php b/config/app.php index 74eb2991..480b19d4 100644 --- a/config/app.php +++ b/config/app.php @@ -13,5 +13,7 @@ return [ \Engelsystem\Database\DatabaseServiceProvider::class, \Engelsystem\Http\RequestServiceProvider::class, \Engelsystem\Http\SessionServiceProvider::class, + \Engelsystem\Http\ResponseServiceProvider::class, + \Engelsystem\Http\Psr7ServiceProvider::class, ], ]; diff --git a/src/Http/Psr7ServiceProvider.php b/src/Http/Psr7ServiceProvider.php new file mode 100644 index 00000000..ff7c13ee --- /dev/null +++ b/src/Http/Psr7ServiceProvider.php @@ -0,0 +1,31 @@ +app->make(DiactorosFactory::class); + $this->app->instance('psr7.factory', $psr7Factory); + + /** @var Request $request */ + $request = $this->app->get('request'); + $psr7request = $psr7Factory->createRequest($request); + $this->app->instance('psr7.request', $psr7request); + $this->app->bind(ServerRequestInterface::class, 'psr7.request'); + + /** @var Response $response */ + $response = $this->app->get('response'); + $psr7response = $psr7Factory->createResponse($response); + $this->app->instance('psr7.response', $psr7response); + $this->app->bind(ResponseInterface::class, 'psr7.response'); + } +} diff --git a/src/Http/Response.php b/src/Http/Response.php new file mode 100644 index 00000000..70698fd5 --- /dev/null +++ b/src/Http/Response.php @@ -0,0 +1,9 @@ +app->make(Response::class); + $this->app->instance('response', $response); + } +} diff --git a/tests/Unit/Http/Psr7ServiceProviderTest.php b/tests/Unit/Http/Psr7ServiceProviderTest.php new file mode 100644 index 00000000..b4c3c042 --- /dev/null +++ b/tests/Unit/Http/Psr7ServiceProviderTest.php @@ -0,0 +1,65 @@ +createMock(DiactorosFactory::class); + /** @var MockObject|Request $request */ + $request = $this->createMock(Request::class); + /** @var MockObject|Response $response */ + $response = $this->createMock(Response::class); + /** @var MockObject|RequestInterface $psr7request */ + $psr7request = $this->createMock(Request::class); + /** @var MockObject|ResponseInterface $psr7response */ + $psr7response = $this->createMock(Response::class); + + $app = $this->getApp(['make', 'instance', 'get', 'bind']); + $this->setExpects($app, 'make', [DiactorosFactory::class], $psr7Factory); + + $app->expects($this->atLeastOnce()) + ->method('get') + ->withConsecutive(['request'], ['response']) + ->willReturnOnConsecutiveCalls($request, $response); + $app->expects($this->atLeastOnce()) + ->method('instance') + ->withConsecutive( + ['psr7.factory', $psr7Factory], + ['psr7.request', $psr7request], + ['psr7.response', $psr7response] + ); + $app->expects($this->atLeastOnce()) + ->method('bind') + ->withConsecutive( + [RequestInterface::class, 'psr7.request'], + [ResponseInterface::class, 'psr7.response'] + ); + + $psr7Factory->expects($this->once()) + ->method('createRequest') + ->with($request) + ->willReturn($psr7request); + $psr7Factory->expects($this->once()) + ->method('createResponse') + ->with($response) + ->willReturn($psr7response); + + $serviceProvider = new Psr7ServiceProvider($app); + $serviceProvider->register(); + } +} diff --git a/tests/Unit/Http/RequestTest.php b/tests/Unit/Http/RequestTest.php index a68f8b8f..f8444b84 100644 --- a/tests/Unit/Http/RequestTest.php +++ b/tests/Unit/Http/RequestTest.php @@ -5,9 +5,19 @@ namespace Engelsystem\Test\Unit\Http; use Engelsystem\Http\Request; use PHPUnit\Framework\TestCase; use PHPUnit_Framework_MockObject_MockObject as MockObject; +use Symfony\Component\HttpFoundation\Request as SymfonyRequest; class RequestTest extends TestCase { + /** + * @covers \Engelsystem\Http\Request + */ + public function testCreate() + { + $response = new Request(); + $this->assertInstanceOf(SymfonyRequest::class, $response); + } + /** * @covers \Engelsystem\Http\Request::postData */ diff --git a/tests/Unit/Http/ResponseServiceProviderTest.php b/tests/Unit/Http/ResponseServiceProviderTest.php new file mode 100644 index 00000000..52e95714 --- /dev/null +++ b/tests/Unit/Http/ResponseServiceProviderTest.php @@ -0,0 +1,29 @@ +getMockBuilder(Response::class) + ->getMock(); + + $app = $this->getApp(); + + $this->setExpects($app, 'make', [Response::class], $response); + $this->setExpects($app, 'instance', ['response', $response]); + + $serviceProvider = new ResponseServiceProvider($app); + $serviceProvider->register(); + } +} diff --git a/tests/Unit/Http/ResponseTest.php b/tests/Unit/Http/ResponseTest.php new file mode 100644 index 00000000..6bedf5c1 --- /dev/null +++ b/tests/Unit/Http/ResponseTest.php @@ -0,0 +1,19 @@ +assertInstanceOf(SymfonyResponse::class, $response); + } +} From 79e9714c745c5e611945e30e60f7e563fdc922bc Mon Sep 17 00:00:00 2001 From: Igor Scheller Date: Fri, 19 Jan 2018 23:19:50 +0100 Subject: [PATCH 02/10] Fix for Diactoros in legacy feature tests --- tests/Feature/ApplicationFeatureTest.php | 14 ++++++++++++++ tests/Feature/Logger/EngelsystemLoggerTest.php | 9 ++------- tests/Feature/Model/LogEntriesModelTest.php | 9 ++------- tests/Feature/Model/RoomModelTest.php | 5 ----- 4 files changed, 18 insertions(+), 19 deletions(-) create mode 100644 tests/Feature/ApplicationFeatureTest.php diff --git a/tests/Feature/ApplicationFeatureTest.php b/tests/Feature/ApplicationFeatureTest.php new file mode 100644 index 00000000..fc6216f1 --- /dev/null +++ b/tests/Feature/ApplicationFeatureTest.php @@ -0,0 +1,14 @@ +room_id = Room_create('test', false, null, null); From 864a086900b92233d7cf76747828163346eabc77 Mon Sep 17 00:00:00 2001 From: Igor Scheller Date: Tue, 7 Aug 2018 02:38:41 +0200 Subject: [PATCH 03/10] Prevent object serialization in session --- includes/model/ShiftsFilter.php | 30 +++++++++++++++++++++++++++++- includes/pages/user_shifts.php | 9 +++++---- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/includes/model/ShiftsFilter.php b/includes/model/ShiftsFilter.php index 5ad7a9b3..fe3bfa56 100644 --- a/includes/model/ShiftsFilter.php +++ b/includes/model/ShiftsFilter.php @@ -48,7 +48,7 @@ class ShiftsFilter * @param int[] $rooms * @param int[] $types */ - public function __construct($user_shifts_admin, $rooms, $types) + public function __construct($user_shifts_admin = false, $rooms = [], $types = []) { $this->rooms = $rooms; $this->types = $types; @@ -62,6 +62,34 @@ class ShiftsFilter } } + /** + * @return array + */ + public function sessionExport() + { + return [ + 'userShiftsAdmin' => $this->userShiftsAdmin, + 'filled' => $this->filled, + 'rooms' => $this->rooms, + 'types' => $this->types, + 'startTime' => $this->startTime, + 'endTime' => $this->endTime, + ]; + } + + /** + * @param array $data + */ + public function sessionImport($data) + { + $this->userShiftsAdmin = $data['userShiftsAdmin']; + $this->filled = $data['filled']; + $this->rooms = $data['rooms']; + $this->types = $data['types']; + $this->startTime = $data['startTime']; + $this->endTime = $data['endTime']; + } + /** * @return int unix timestamp */ diff --git a/includes/pages/user_shifts.php b/includes/pages/user_shifts.php index c158ee4a..a620d081 100644 --- a/includes/pages/user_shifts.php +++ b/includes/pages/user_shifts.php @@ -177,18 +177,19 @@ function view_user_shifts() $rooms = load_rooms(); $types = load_types(); - if (!$session->has('ShiftsFilter')) { + if (!$session->has('shifts-filter')) { $room_ids = [ $rooms[0]['id'] ]; $type_ids = array_map('get_ids_from_array', $types); $shiftsFilter = new ShiftsFilter(in_array('user_shifts_admin', $privileges), $room_ids, $type_ids); - $session->set('ShiftsFilter', $shiftsFilter); + $session->set('shifts-filter', $shiftsFilter->sessionExport()); } - /** @var ShiftsFilter $shiftsFilter */ - $shiftsFilter = $session->get('ShiftsFilter'); + $shiftsFilter = new ShiftsFilter(); + $shiftsFilter->sessionImport($session->get('shifts-filter')); update_ShiftsFilter($shiftsFilter, in_array('user_shifts_admin', $privileges), $days); + $session->set('shifts-filter', $shiftsFilter->sessionExport()); $shiftCalendarRenderer = shiftCalendarRendererByShiftFilter($shiftsFilter); From 92c26718fd0799660515d64feabbbc1cd1d71a35 Mon Sep 17 00:00:00 2001 From: Igor Scheller Date: Tue, 7 Aug 2018 03:06:21 +0200 Subject: [PATCH 04/10] exceptions: implemented error rendering return --- src/Exceptions/Handler.php | 15 ++++++++++++++- src/Exceptions/Handlers/Whoops.php | 2 ++ tests/Unit/Exceptions/HandlerTest.php | 14 +++++++++++--- tests/Unit/Exceptions/Handlers/WhoopsTest.php | 8 ++++++++ 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/Exceptions/Handler.php b/src/Exceptions/Handler.php index ee15717a..0503b5b7 100644 --- a/src/Exceptions/Handler.php +++ b/src/Exceptions/Handler.php @@ -54,8 +54,10 @@ class Handler /** * @param Throwable $e + * @param bool $return + * @return string */ - public function exceptionHandler($e) + public function exceptionHandler($e, $return = false) { if (!$this->request instanceof Request) { $this->request = new Request(); @@ -63,8 +65,19 @@ class Handler $handler = $this->handler[$this->environment]; $handler->report($e); + ob_start(); $handler->render($this->request, $e); + + if ($return) { + $output = ob_get_contents(); + ob_end_clean(); + return $output; + } + + http_response_code(500); + ob_end_flush(); $this->die(); + return ''; } /** diff --git a/src/Exceptions/Handlers/Whoops.php b/src/Exceptions/Handlers/Whoops.php index 73352105..630aca1d 100644 --- a/src/Exceptions/Handlers/Whoops.php +++ b/src/Exceptions/Handlers/Whoops.php @@ -34,6 +34,8 @@ class Whoops extends Legacy implements HandlerInterface $whoops = $this->app->make(WhoopsRunner::class); $handler = $this->getPrettyPageHandler($e); $whoops->pushHandler($handler); + $whoops->writeToOutput(false); + $whoops->allowQuit(false); if ($request->isXmlHttpRequest()) { $handler = $this->getJsonResponseHandler(); diff --git a/tests/Unit/Exceptions/HandlerTest.php b/tests/Unit/Exceptions/HandlerTest.php index 40202be8..7987f9d6 100644 --- a/tests/Unit/Exceptions/HandlerTest.php +++ b/tests/Unit/Exceptions/HandlerTest.php @@ -49,15 +49,19 @@ class HandlerTest extends TestCase public function testExceptionHandler() { $exception = new Exception(); + $errorMessage = 'Oh noes, an error!'; /** @var HandlerInterface|Mock $handlerMock */ $handlerMock = $this->getMockForAbstractClass(HandlerInterface::class); - $handlerMock->expects($this->once()) + $handlerMock->expects($this->atLeastOnce()) ->method('report') ->with($exception); - $handlerMock->expects($this->once()) + $handlerMock->expects($this->atLeastOnce()) ->method('render') - ->with($this->isInstanceOf(Request::class), $exception); + ->with($this->isInstanceOf(Request::class), $exception) + ->willReturnCallback(function () use ($errorMessage) { + echo $errorMessage; + }); /** @var Handler|Mock $handler */ $handler = $this->getMockBuilder(Handler::class) @@ -68,7 +72,11 @@ class HandlerTest extends TestCase $handler->setHandler(Handler::ENV_PRODUCTION, $handlerMock); + $this->expectOutputString($errorMessage); $handler->exceptionHandler($exception); + + $return = $handler->exceptionHandler($exception, true); + $this->assertEquals($errorMessage, $return); } /** diff --git a/tests/Unit/Exceptions/Handlers/WhoopsTest.php b/tests/Unit/Exceptions/Handlers/WhoopsTest.php index 261ee83f..4062979b 100644 --- a/tests/Unit/Exceptions/Handlers/WhoopsTest.php +++ b/tests/Unit/Exceptions/Handlers/WhoopsTest.php @@ -72,6 +72,14 @@ class WhoopsTest extends TestCase [$prettyPageHandler], [$jsonResponseHandler] ); + $whoopsRunner + ->expects($this->once()) + ->method('writeToOutput') + ->with(false); + $whoopsRunner + ->expects($this->once()) + ->method('allowQuit') + ->with(false); $whoopsRunner ->expects($this->once()) ->method('handleException') From 20c03a155d2017101a098cefa602116a4a331d71 Mon Sep 17 00:00:00 2001 From: Igor Scheller Date: Tue, 7 Aug 2018 03:18:22 +0200 Subject: [PATCH 05/10] Implemented PSR-15 middleware handler --- composer.json | 2 + locale/de_DE.UTF-8/LC_MESSAGES/default.mo | Bin 44974 -> 44999 bytes locale/de_DE.UTF-8/LC_MESSAGES/default.po | 14 +- public/index.php | 263 ++------------------ src/Middleware/Dispatcher.php | 110 +++++++++ src/Middleware/ExceptionHandler.php | 48 ++++ src/Middleware/LegacyMiddleware.php | 284 ++++++++++++++++++++++ src/Middleware/NotFoundResponse.php | 47 ++++ src/Middleware/SendResponseHandler.php | 45 ++++ src/helpers.php | 30 ++- 10 files changed, 591 insertions(+), 252 deletions(-) create mode 100644 src/Middleware/Dispatcher.php create mode 100644 src/Middleware/ExceptionHandler.php create mode 100644 src/Middleware/LegacyMiddleware.php create mode 100644 src/Middleware/NotFoundResponse.php create mode 100644 src/Middleware/SendResponseHandler.php diff --git a/composer.json b/composer.json index df9ee57b..911df1f5 100644 --- a/composer.json +++ b/composer.json @@ -15,11 +15,13 @@ ], "require": { "php": ">=7.0.0", + "ext-gettext": "*", "erusev/parsedown": "^1.6", "illuminate/container": "5.5.*", "illuminate/database": "5.5.*", "illuminate/support": "^5.5", "psr/container": "^1.0", + "psr/http-server-middleware": "^1.0", "psr/log": "^1.0", "symfony/http-foundation": "^3.3", "symfony/psr-http-message-bridge": "^1.0", diff --git a/locale/de_DE.UTF-8/LC_MESSAGES/default.mo b/locale/de_DE.UTF-8/LC_MESSAGES/default.mo index 3939e87019df710e0047e1cd4bfdc3c7146383f3..8b3b7dcfd07fb338a896d194bee431f0e3de2443 100644 GIT binary patch delta 8170 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 delta 8141 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 WL2+@OmzT6~`&Txu`e=(^(Ek7oZn~5J diff --git a/locale/de_DE.UTF-8/LC_MESSAGES/default.po b/locale/de_DE.UTF-8/LC_MESSAGES/default.po index 7187b32c..49973ca9 100644 --- a/locale/de_DE.UTF-8/LC_MESSAGES/default.po +++ b/locale/de_DE.UTF-8/LC_MESSAGES/default.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: Engelsystem 2.0\n" "POT-Creation-Date: 2017-12-29 19:01+0100\n" -"PO-Revision-Date: 2017-12-29 19:03+0100\n" +"PO-Revision-Date: 2018-08-07 02:00+0200\n" "Last-Translator: msquare \n" "Language-Team: \n" "Language: de_DE\n" @@ -2874,13 +2874,13 @@ msgstr "" "keine Nummer hast, bitte einfach \"-\" angeben." #: /Users/msquare/workspace/projects/engelsystem/public/index.php:218 -msgid "No Access" -msgstr "Kein Zugriff" +msgid "Page not found" +msgstr "Seite nicht gefunden" #: /Users/msquare/workspace/projects/engelsystem/public/index.php:219 msgid "" -"You don't have permission to view this page . You probably have to sign in " -"or register in order to gain access!" +"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!" msgstr "" -"Du hast keinen Zugriff auf diese Seite. Registriere Dich und logge Dich " -"bitte ein, um Zugriff zu erhalten!" +"Diese Seite existiert nicht oder Du hast keinen Zugriff. Melde Dich an um Zugriff " +"zu erhalten!" diff --git a/public/index.php b/public/index.php index db4c4294..88e57252 100644 --- a/public/index.php +++ b/public/index.php @@ -1,252 +1,27 @@ get('psr7.request'); -/** @var Request $request */ -$request = $app->get('request'); -$page = $request->query->get('p'); -if (empty($page)) { - $page = $request->path(); - $page = str_replace('-', '_', $page); -} -if ($page == '/') { - $page = isset($user) ? 'news' : 'login'; -} - -if ( - preg_match('/^\w*$/i', $page) - && ( - in_array($page, $free_pages) - || (isset($privileges) && in_array($page, $privileges)) - ) -) { - $title = $page; - - switch ($page) { - case 'api': - error('Api disabled temporarily.'); - redirect(page_link_to()); - break; - case 'ical': - require_once realpath(__DIR__ . '/../includes/pages/user_ical.php'); - user_ical(); - break; - case 'atom': - require_once realpath(__DIR__ . '/../includes/pages/user_atom.php'); - user_atom(); - break; - case 'shifts_json_export': - require_once realpath(__DIR__ . '/../includes/controller/shifts_controller.php'); - shifts_json_export_controller(); - break; - case 'shifts_json_export_all': - require_once realpath(__DIR__ . '/../includes/controller/shifts_controller.php'); - shifts_json_export_all_controller(); - break; - case 'stats': - require_once realpath(__DIR__ . '/../includes/pages/guest_stats.php'); - guest_stats(); - break; - case 'user_password_recovery': - require_once realpath(__DIR__ . '/../includes/controller/users_controller.php'); - $title = user_password_recovery_title(); - $content = user_password_recovery_controller(); - break; - case 'public_dashboard': - list($title, $content) = public_dashboard_controller(); - break; - case 'angeltypes': - list($title, $content) = angeltypes_controller(); - break; - case 'shift_entries': - list($title, $content) = shift_entries_controller(); - break; - case 'shifts': - list($title, $content) = shifts_controller(); - break; - case 'users': - list($title, $content) = users_controller(); - break; - case 'user_angeltypes': - list($title, $content) = user_angeltypes_controller(); - break; - case 'user_driver_licenses': - list($title, $content) = user_driver_licenses_controller(); - break; - case 'shifttypes': - list($title, $content) = shifttypes_controller(); - break; - case 'admin_event_config': - list($title, $content) = event_config_edit_controller(); - break; - case 'rooms': - list($title, $content) = rooms_controller(); - break; - case 'news': - $title = news_title(); - $content = user_news(); - break; - case 'news_comments': - require_once realpath(__DIR__ . '/../includes/pages/user_news.php'); - $title = user_news_comments_title(); - $content = user_news_comments(); - break; - case 'user_meetings': - $title = meetings_title(); - $content = user_meetings(); - break; - case 'user_myshifts': - $title = myshifts_title(); - $content = user_myshifts(); - break; - case 'user_shifts': - $title = shifts_title(); - $content = user_shifts(); - break; - case 'user_worklog': - list($title, $content) = user_worklog_controller(); - break; - case 'user_messages': - $title = messages_title(); - $content = user_messages(); - break; - case 'user_questions': - $title = questions_title(); - $content = user_questions(); - break; - case 'user_settings': - $title = settings_title(); - $content = user_settings(); - break; - case 'login': - $title = login_title(); - $content = guest_login(); - break; - case 'register': - $title = register_title(); - $content = guest_register(); - break; - case 'logout': - $title = logout_title(); - $content = guest_logout(); - break; - case 'admin_questions': - $title = admin_questions_title(); - $content = admin_questions(); - break; - case 'admin_user': - $title = admin_user_title(); - $content = admin_user(); - break; - case 'admin_arrive': - $title = admin_arrive_title(); - $content = admin_arrive(); - break; - case 'admin_active': - $title = admin_active_title(); - $content = admin_active(); - break; - case 'admin_free': - $title = admin_free_title(); - $content = admin_free(); - break; - case 'admin_news': - require_once realpath(__DIR__ . '/../includes/pages/admin_news.php'); - $content = admin_news(); - break; - case 'admin_rooms': - $title = admin_rooms_title(); - $content = admin_rooms(); - break; - case 'admin_groups': - $title = admin_groups_title(); - $content = admin_groups(); - break; - case 'admin_import': - $title = admin_import_title(); - $content = admin_import(); - break; - case 'admin_shifts': - $title = admin_shifts_title(); - $content = admin_shifts(); - break; - case 'admin_log': - $title = admin_log_title(); - $content = admin_log(); - break; - case 'credits': - require_once realpath(__DIR__ . '/../includes/pages/guest_credits.php'); - $title = credits_title(); - $content = guest_credits(); - break; - default: - require_once realpath(__DIR__ . '/../includes/pages/guest_start.php'); - $content = guest_start(); - break; - } -} else { - // Wenn schon eingeloggt, keine-Berechtigung-Seite anzeigen - if (isset($user)) { - $title = _('No Access'); - $content = _('You don\'t have permission to view this page . You probably have to sign in or register in order to gain access!'); - } else { - // Sonst zur Loginseite leiten - redirect(page_link_to('login')); - } -} - -$event_config = EventConfig(); - -$parameters = [ - 'key' => (isset($user) ? $user['api_key'] : ''), -]; -if ($page == 'user_meetings') { - $parameters['meetings'] = 1; -} - -echo view(__DIR__ . '/../templates/layout.html', [ - 'theme' => isset($user) ? $user['color'] : config('theme'), - 'title' => $title, - 'atom_link' => ($page == 'news' || $page == 'user_meetings') - ? ' ' - : '', - 'start_page_url' => page_link_to('/'), - 'credits_url' => page_link_to('credits'), - 'menu' => make_menu(), - 'content' => msg() . $content, - 'header_toolbar' => header_toolbar(), - 'faq_url' => config('faq_url'), - 'contact_email' => config('contact_email'), - 'locale' => locale(), - 'event_info' => EventConfig_info($event_config) . '
' +$dispatcher = new Dispatcher([ + SendResponseHandler::class, + ExceptionHandler::class, + LegacyMiddleware::class, + NotFoundResponse::class, ]); +$dispatcher->setContainer($app); + +$dispatcher->handle($request); diff --git a/src/Middleware/Dispatcher.php b/src/Middleware/Dispatcher.php new file mode 100644 index 00000000..774040fb --- /dev/null +++ b/src/Middleware/Dispatcher.php @@ -0,0 +1,110 @@ +stack = $stack; + $this->container = $container; + } + + /** + * Process an incoming server request and return a response, optionally delegating + * response creation to a handler. + * + * Could be used to group middleware + * + * @param ServerRequestInterface $request + * @param RequestHandlerInterface $handler + * @return ResponseInterface + */ + public function process( + ServerRequestInterface $request, + RequestHandlerInterface $handler + ): ResponseInterface { + $this->next = $handler; + + return $this->handle($request); + } + + /** + * Handle the request and return a response. + * + * It calls all configured middleware and handles their response + * + * @param ServerRequestInterface $request + * @return ResponseInterface + */ + public function handle(ServerRequestInterface $request): ResponseInterface + { + $middleware = array_shift($this->stack); + + if (!$middleware) { + if ($this->next) { + return $this->next->handle($request); + } + + throw new LogicException('Middleware queue is empty'); + } + + if (is_string($middleware)) { + $middleware = $this->resolveMiddleware($middleware); + } + + if (!$middleware instanceof MiddlewareInterface) { + throw new InvalidArgumentException('Middleware is no instance of ' . MiddlewareInterface::class); + } + + 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 + */ + public function setContainer(Application $container) + { + $this->container = $container; + } +} diff --git a/src/Middleware/ExceptionHandler.php b/src/Middleware/ExceptionHandler.php new file mode 100644 index 00000000..a5db0337 --- /dev/null +++ b/src/Middleware/ExceptionHandler.php @@ -0,0 +1,48 @@ +container = $container; + } + + /** + * Handles any exceptions that occurred inside other middleware while returning it to the default response handler + * + * Should be added at the beginning + * + * @param ServerRequestInterface $request + * @param RequestHandlerInterface $handler + * @return ResponseInterface + */ + public function process( + ServerRequestInterface $request, + RequestHandlerInterface $handler + ): ResponseInterface { + try { + return $handler->handle($request); + } catch (\Throwable $e) { + /** @var ExceptionsHandler $handler */ + $handler = $this->container->get('error.handler'); + $content = $handler->exceptionHandler($e, true); + + return response($content, 500); + } + } +} diff --git a/src/Middleware/LegacyMiddleware.php b/src/Middleware/LegacyMiddleware.php new file mode 100644 index 00000000..41b2e471 --- /dev/null +++ b/src/Middleware/LegacyMiddleware.php @@ -0,0 +1,284 @@ +container = $container; + } + + /** + * Handle the request the old way + * + * Should be used before a 404 is send + * + * @param ServerRequestInterface $request + * @param RequestHandlerInterface $handler + * @return ResponseInterface + */ + public function process( + ServerRequestInterface $request, + RequestHandlerInterface $handler + ): ResponseInterface { + global $user; + global $privileges; + + /** @var Request $appRequest */ + $appRequest = $this->container->get('request'); + + // Default page content + $content = ''; + + $page = $appRequest->query->get('p'); + if (empty($page)) { + $page = $appRequest->path(); + $page = str_replace('-', '_', $page); + } + if ($page == '/') { + $page = isset($user) ? 'news' : 'login'; + } + + if ( + preg_match('/^\w+$/i', $page) + && ( + in_array($page, $this->free_pages) + || (isset($privileges) && in_array($page, $privileges)) + ) + ) { + $title = $page; + + switch ($page) { + case 'api': + error('Api disabled temporarily.'); + redirect(page_link_to()); + break; + case 'ical': + require_once realpath(__DIR__ . '/../includes/pages/user_ical.php'); + user_ical(); + break; + case 'atom': + require_once realpath(__DIR__ . '/../includes/pages/user_atom.php'); + user_atom(); + break; + case 'shifts_json_export': + require_once realpath(__DIR__ . '/../includes/controller/shifts_controller.php'); + shifts_json_export_controller(); + break; + case 'shifts_json_export_all': + require_once realpath(__DIR__ . '/../includes/controller/shifts_controller.php'); + shifts_json_export_all_controller(); + break; + case 'stats': + require_once realpath(__DIR__ . '/../includes/pages/guest_stats.php'); + guest_stats(); + break; + case 'user_password_recovery': + require_once realpath(__DIR__ . '/../includes/controller/users_controller.php'); + $title = user_password_recovery_title(); + $content = user_password_recovery_controller(); + break; + case 'public_dashboard': + list($title, $content) = public_dashboard_controller(); + break; + case 'angeltypes': + list($title, $content) = angeltypes_controller(); + break; + case 'shift_entries': + list($title, $content) = shift_entries_controller(); + break; + case 'shifts': + list($title, $content) = shifts_controller(); + break; + case 'users': + list($title, $content) = users_controller(); + break; + case 'user_angeltypes': + list($title, $content) = user_angeltypes_controller(); + break; + case 'user_driver_licenses': + list($title, $content) = user_driver_licenses_controller(); + break; + case 'shifttypes': + list($title, $content) = shifttypes_controller(); + break; + case 'admin_event_config': + list($title, $content) = event_config_edit_controller(); + break; + case 'rooms': + list($title, $content) = rooms_controller(); + break; + case 'news': + $title = news_title(); + $content = user_news(); + break; + case 'news_comments': + require_once realpath(__DIR__ . '/../includes/pages/user_news.php'); + $title = user_news_comments_title(); + $content = user_news_comments(); + break; + case 'user_meetings': + $title = meetings_title(); + $content = user_meetings(); + break; + case 'user_myshifts': + $title = myshifts_title(); + $content = user_myshifts(); + break; + case 'user_shifts': + $title = shifts_title(); + $content = user_shifts(); + break; + case 'user_worklog': + list($title, $content) = user_worklog_controller(); + break; + case 'user_messages': + $title = messages_title(); + $content = user_messages(); + break; + case 'user_questions': + $title = questions_title(); + $content = user_questions(); + break; + case 'user_settings': + $title = settings_title(); + $content = user_settings(); + break; + case 'login': + $title = login_title(); + $content = guest_login(); + break; + case 'register': + $title = register_title(); + $content = guest_register(); + break; + case 'logout': + $title = logout_title(); + $content = guest_logout(); + break; + case 'admin_questions': + $title = admin_questions_title(); + $content = admin_questions(); + break; + case 'admin_user': + $title = admin_user_title(); + $content = admin_user(); + break; + case 'admin_arrive': + $title = admin_arrive_title(); + $content = admin_arrive(); + break; + case 'admin_active': + $title = admin_active_title(); + $content = admin_active(); + break; + case 'admin_free': + $title = admin_free_title(); + $content = admin_free(); + break; + case 'admin_news': + require_once realpath(__DIR__ . '/../includes/pages/admin_news.php'); + $content = admin_news(); + break; + case 'admin_rooms': + $title = admin_rooms_title(); + $content = admin_rooms(); + break; + case 'admin_groups': + $title = admin_groups_title(); + $content = admin_groups(); + break; + case 'admin_import': + $title = admin_import_title(); + $content = admin_import(); + break; + case 'admin_shifts': + $title = admin_shifts_title(); + $content = admin_shifts(); + break; + case 'admin_log': + $title = admin_log_title(); + $content = admin_log(); + break; + case 'credits': + require_once realpath(__DIR__ . '/../includes/pages/guest_credits.php'); + $title = credits_title(); + $content = guest_credits(); + break; + default: + require_once realpath(__DIR__ . '/../includes/pages/guest_start.php'); + $content = guest_start(); + break; + } + } else { + return $handler->handle($request); + } + + if (empty($title) and empty($content)) { + return $handler->handle($request); + } + + $event_config = EventConfig(); + + $parameters = [ + 'key' => (isset($user) ? $user['api_key'] : ''), + ]; + if ($page == 'user_meetings') { + $parameters['meetings'] = 1; + } + + return response(view(__DIR__ . '/../../templates/layout.html', [ + 'theme' => isset($user) ? $user['color'] : config('theme'), + 'title' => $title, + 'atom_link' => ($page == 'news' || $page == 'user_meetings') + ? ' ' + : '', + 'start_page_url' => page_link_to('/'), + 'credits_url' => page_link_to('credits'), + 'menu' => make_menu(), + 'content' => msg() . $content, + 'header_toolbar' => header_toolbar(), + 'faq_url' => config('faq_url'), + 'contact_email' => config('contact_email'), + 'locale' => locale(), + 'event_info' => EventConfig_info($event_config) . '
' + ])); + } +} diff --git a/src/Middleware/NotFoundResponse.php b/src/Middleware/NotFoundResponse.php new file mode 100644 index 00000000..c5d51d2d --- /dev/null +++ b/src/Middleware/NotFoundResponse.php @@ -0,0 +1,47 @@ + 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() . $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/SendResponseHandler.php b/src/Middleware/SendResponseHandler.php new file mode 100644 index 00000000..06406fe0 --- /dev/null +++ b/src/Middleware/SendResponseHandler.php @@ -0,0 +1,45 @@ +handle($request); + + if (!headers_sent()) { + header(sprintf( + 'HTTP/%s %s %s', + $response->getProtocolVersion(), + $response->getStatusCode(), + $response->getReasonPhrase() + ), true, $response->getStatusCode()); + + foreach ($response->getHeaders() as $name => $values) { + foreach ($values as $value) { + header($name . ': ' . $value, false); + } + } + } + + echo $response->getBody(); + return $response; + } +} diff --git a/src/helpers.php b/src/helpers.php index c3c727ec..2a90dcde 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -6,13 +6,15 @@ use Engelsystem\Config\Config; use Engelsystem\Http\Request; use Engelsystem\Renderer\Renderer; use Engelsystem\Routing\UrlGenerator; +use Psr\Http\Message\ResponseInterface; use Symfony\Component\HttpFoundation\Session\SessionInterface; +use Zend\Diactoros\Stream; /** * Get the global app instance * * @param string $id - * @return mixed + * @return mixed|Application */ function app($id = null) { @@ -80,6 +82,32 @@ function request($key = null, $default = null) return $request->input($key, $default); } +/** + * @param string $content + * @param int $status + * @param array $headers + * @return ResponseInterface + */ +function response($content = '', $status = 200, $headers = []) +{ + /** @var ResponseInterface $response */ + $response = app('psr7.response'); + + /** @var Stream $stream */ + $stream = app()->make(Stream::class, ['stream' => 'php://memory', 'mode' => 'wb+']); + $stream->write($content); + $stream->rewind(); + + $response = $response + ->withBody($stream) + ->withStatus($status); + foreach ($headers as $key => $value) { + $response = $response->withAddedHeader($key, $value); + } + + return $response; +} + /** * @param string $key * @param mixed $default From f3f05f6cc4bef3338dbfb6eb340da4fb1c5ba1e1 Mon Sep 17 00:00:00 2001 From: Igor Scheller Date: Sat, 11 Aug 2018 15:05:55 +0200 Subject: [PATCH 06/10] Make Engelsystem\Http\Response PSR-7 compatible --- src/Http/MessageTrait.php | 235 ++++++++++++++++++ src/Http/Psr7ServiceProvider.php | 3 +- src/Http/Response.php | 68 ++++- src/helpers.php | 16 +- tests/Unit/HelpersTest.php | 28 +++ tests/Unit/Http/MessageTraitTest.php | 159 ++++++++++++ tests/Unit/Http/Psr7ServiceProviderTest.php | 4 - tests/Unit/Http/ResponseTest.php | 30 +++ .../Http/Stub/MessageTraitImplementation.php | 12 + 9 files changed, 537 insertions(+), 18 deletions(-) create mode 100644 src/Http/MessageTrait.php create mode 100644 tests/Unit/Http/MessageTraitTest.php create mode 100644 tests/Unit/Http/Stub/MessageTraitImplementation.php diff --git a/src/Http/MessageTrait.php b/src/Http/MessageTrait.php new file mode 100644 index 00000000..fa3a1459 --- /dev/null +++ b/src/Http/MessageTrait.php @@ -0,0 +1,235 @@ +setProtocolVersion($version); + return $new; + } + + /** + * Retrieves all message header values. + * + * The keys represent the header name as it will be sent over the wire, and + * each value is an array of strings associated with the header. + * + * // Represent the headers as a string + * foreach ($message->getHeaders() as $name => $values) { + * echo $name . ": " . implode(", ", $values); + * } + * + * // Emit headers iteratively: + * foreach ($message->getHeaders() as $name => $values) { + * foreach ($values as $value) { + * header(sprintf('%s: %s', $name, $value), false); + * } + * } + * + * While header names are not case-sensitive, getHeaders() will preserve the + * exact case in which headers were originally specified. + * + * @return string[][] Returns an associative array of the message's headers. Each + * key MUST be a header name, and each value MUST be an array of strings + * for that header. + */ + public function getHeaders() + { + return $this->headers->allPreserveCase(); + } + + /** + * Checks if a header exists by the given case-insensitive name. + * + * @param string $name Case-insensitive header field name. + * @return bool Returns true if any header names match the given header + * name using a case-insensitive string comparison. Returns false if + * no matching header name is found in the message. + */ + public function hasHeader($name) + { + return $this->headers->has($name); + } + + /** + * Retrieves a message header value by the given case-insensitive name. + * + * This method returns an array of all the header values of the given + * case-insensitive header name. + * + * If the header does not appear in the message, this method MUST return an + * empty array. + * + * @param string $name Case-insensitive header field name. + * @return string[] An array of string values as provided for the given + * header. If the header does not appear in the message, this method MUST + * return an empty array. + */ + public function getHeader($name) + { + return $this->headers->get($name, null, false); + } + + /** + * Retrieves a comma-separated string of the values for a single header. + * + * This method returns all of the header values of the given + * case-insensitive header name as a string concatenated together using + * a comma. + * + * NOTE: Not all header values may be appropriately represented using + * comma concatenation. For such headers, use getHeader() instead + * and supply your own delimiter when concatenating. + * + * If the header does not appear in the message, this method MUST return + * an empty string. + * + * @param string $name Case-insensitive header field name. + * @return string A string of values as provided for the given header + * concatenated together using a comma. If the header does not appear in + * the message, this method MUST return an empty string. + */ + public function getHeaderLine($name) + { + return implode(',', $this->getHeader($name)); + } + + /** + * Return an instance with the provided value replacing the specified header. + * + * While header names are case-insensitive, the casing of the header will + * be preserved by this function, and returned from getHeaders(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * new and/or updated header and value. + * + * @param string $name Case-insensitive header field name. + * @param string|string[] $value Header value(s). + * @return static + * @throws \InvalidArgumentException for invalid header names or values. + */ + public function withHeader($name, $value) + { + $new = clone $this; + $new->headers->set($name, $value); + + return $new; + } + + /** + * Return an instance with the specified header appended with the given value. + * + * Existing values for the specified header will be maintained. The new + * value(s) will be appended to the existing list. If the header did not + * exist previously, it will be added. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * new header and/or value. + * + * @param string $name Case-insensitive header field name to add. + * @param string|string[] $value Header value(s). + * @return static + * @throws \InvalidArgumentException for invalid header names or values. + */ + public function withAddedHeader($name, $value) + { + $new = clone $this; + $new->headers->set($name, $value, false); + + return $new; + } + + /** + * Return an instance without the specified header. + * + * Header resolution MUST be done without case-sensitivity. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that removes + * the named header. + * + * @param string $name Case-insensitive header field name to remove. + * @return static + */ + public function withoutHeader($name) + { + $new = clone $this; + $new->headers->remove($name); + + return $new; + } + + /** + * Gets the body of the message. + * + * @return StreamInterface Returns the body as a stream. + */ + public function getBody() + { + $stream = new Stream('php://memory', 'wb+'); + $stream->write($this->getContent()); + $stream->rewind(); + + return $stream; + } + + /** + * Return an instance with the specified message body. + * + * The body MUST be a StreamInterface object. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return a new instance that has the + * new body stream. + * + * @param StreamInterface $body Body. + * @return static + * @throws \InvalidArgumentException When the body is not valid. + */ + public function withBody(StreamInterface $body) + { + $new = clone $this; + $new->setContent($body); + + return $new; + } +} diff --git a/src/Http/Psr7ServiceProvider.php b/src/Http/Psr7ServiceProvider.php index ff7c13ee..4a3c6583 100644 --- a/src/Http/Psr7ServiceProvider.php +++ b/src/Http/Psr7ServiceProvider.php @@ -24,8 +24,7 @@ class Psr7ServiceProvider extends ServiceProvider /** @var Response $response */ $response = $this->app->get('response'); - $psr7response = $psr7Factory->createResponse($response); - $this->app->instance('psr7.response', $psr7response); + $this->app->instance('psr7.response', $response); $this->app->bind(ResponseInterface::class, 'psr7.response'); } } diff --git a/src/Http/Response.php b/src/Http/Response.php index 70698fd5..9db6fa83 100644 --- a/src/Http/Response.php +++ b/src/Http/Response.php @@ -2,8 +2,74 @@ namespace Engelsystem\Http; +use Psr\Http\Message\ResponseInterface; use Symfony\Component\HttpFoundation\Response as SymfonyResponse; -class Response extends SymfonyResponse +class Response extends SymfonyResponse implements ResponseInterface { + use MessageTrait; + + /** + * Return an instance with the specified status code and, optionally, reason phrase. + * + * If no reason phrase is specified, implementations MAY choose to default + * to the RFC 7231 or IANA recommended reason phrase for the response's + * status code. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated status and reason phrase. + * + * @link http://tools.ietf.org/html/rfc7231#section-6 + * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml + * @param int $code The 3-digit integer result code to set. + * @param string $reasonPhrase The reason phrase to use with the + * provided status code; if none is provided, implementations MAY + * use the defaults as suggested in the HTTP specification. + * @return static + * @throws \InvalidArgumentException For invalid status code arguments. + */ + public function withStatus($code, $reasonPhrase = '') + { + $new = clone $this; + $new->setStatusCode($code, !empty($reasonPhrase) ? $reasonPhrase : null); + + return $new; + } + + /** + * Gets the response reason phrase associated with the status code. + * + * Because a reason phrase is not a required element in a response + * status line, the reason phrase value MAY be null. Implementations MAY + * choose to return the default RFC 7231 recommended reason phrase (or those + * listed in the IANA HTTP Status Code Registry) for the response's + * status code. + * + * @link http://tools.ietf.org/html/rfc7231#section-6 + * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml + * @return string Reason phrase; must return an empty string if none present. + */ + public function getReasonPhrase() + { + return $this->statusText; + } + + /** + * Return an instance with the specified content. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated status and reason phrase. + * + * @param mixed $content Content that can be cast to string + * @return static + */ + public function withContent($content) + { + $new = clone $this; + $new->setContent($content); + + return $new; + } } diff --git a/src/helpers.php b/src/helpers.php index 2a90dcde..01fb10bd 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -4,11 +4,10 @@ use Engelsystem\Application; use Engelsystem\Config\Config; use Engelsystem\Http\Request; +use Engelsystem\Http\Response; use Engelsystem\Renderer\Renderer; use Engelsystem\Routing\UrlGenerator; -use Psr\Http\Message\ResponseInterface; use Symfony\Component\HttpFoundation\Session\SessionInterface; -use Zend\Diactoros\Stream; /** * Get the global app instance @@ -86,21 +85,16 @@ function request($key = null, $default = null) * @param string $content * @param int $status * @param array $headers - * @return ResponseInterface + * @return Response */ function response($content = '', $status = 200, $headers = []) { - /** @var ResponseInterface $response */ + /** @var Response $response */ $response = app('psr7.response'); - - /** @var Stream $stream */ - $stream = app()->make(Stream::class, ['stream' => 'php://memory', 'mode' => 'wb+']); - $stream->write($content); - $stream->rewind(); - $response = $response - ->withBody($stream) + ->withContent($content) ->withStatus($status); + foreach ($headers as $key => $value) { $response = $response->withAddedHeader($key, $value); } diff --git a/tests/Unit/HelpersTest.php b/tests/Unit/HelpersTest.php index 43c29c84..82030169 100644 --- a/tests/Unit/HelpersTest.php +++ b/tests/Unit/HelpersTest.php @@ -6,6 +6,7 @@ use Engelsystem\Application; use Engelsystem\Config\Config; use Engelsystem\Container\Container; use Engelsystem\Http\Request; +use Engelsystem\Http\Response; use Engelsystem\Renderer\Renderer; use Engelsystem\Routing\UrlGenerator; use PHPUnit\Framework\TestCase; @@ -126,6 +127,33 @@ class HelpersTest extends TestCase $this->assertEquals('requestValue', request('requestKey')); } + /** + * @covers \response + */ + public function testResponse() + { + /** @var MockObject|Response $response */ + $response = $this->getMockBuilder(Response::class)->getMock(); + $this->getAppMock('psr7.response', $response); + + $response->expects($this->once()) + ->method('withContent') + ->with('Lorem Ipsum?') + ->willReturn($response); + + $response->expects($this->once()) + ->method('withStatus') + ->with(501) + ->willReturn($response); + + $response->expects($this->exactly(2)) + ->method('withAddedHeader') + ->withConsecutive(['lor', 'em'], ['foo', 'bar']) + ->willReturn($response); + + $this->assertEquals($response, response('Lorem Ipsum?', 501, ['lor' => 'em', 'foo' => 'bar',])); + } + /** * @covers \session */ diff --git a/tests/Unit/Http/MessageTraitTest.php b/tests/Unit/Http/MessageTraitTest.php new file mode 100644 index 00000000..46076a67 --- /dev/null +++ b/tests/Unit/Http/MessageTraitTest.php @@ -0,0 +1,159 @@ +assertInstanceOf(MessageInterface::class, $message); + $this->assertInstanceOf(SymfonyResponse::class, $message); + } + + /** + * @covers \Engelsystem\Http\MessageTrait::getProtocolVersion + * @covers \Engelsystem\Http\MessageTrait::withProtocolVersion + */ + public function testGetProtocolVersion() + { + $message = new MessageTraitImplementation(); + $newMessage = $message->withProtocolVersion('0.1'); + $this->assertNotEquals($message, $newMessage); + $this->assertEquals('0.1', $newMessage->getProtocolVersion()); + } + + /** + * @covers \Engelsystem\Http\MessageTrait::getHeaders + */ + public function testGetHeaders() + { + $message = new MessageTraitImplementation(); + $newMessage = $message->withHeader('Foo', 'bar'); + + $this->assertNotEquals($message, $newMessage); + $this->assertArraySubset(['Foo' => ['bar']], $newMessage->getHeaders()); + + $newMessage = $message->withHeader('lorem', ['ipsum', 'dolor']); + $this->assertArraySubset(['lorem' => ['ipsum', 'dolor']], $newMessage->getHeaders()); + } + + /** + * @covers \Engelsystem\Http\MessageTrait::hasHeader + */ + public function testHasHeader() + { + $message = new MessageTraitImplementation(); + $this->assertFalse($message->hasHeader('test')); + + $newMessage = $message->withHeader('test', '12345'); + $this->assertTrue($newMessage->hasHeader('Test')); + $this->assertTrue($newMessage->hasHeader('test')); + } + + /** + * @covers \Engelsystem\Http\MessageTrait::getHeader + */ + public function testGetHeader() + { + $message = new MessageTraitImplementation(); + $newMessage = $message->withHeader('foo', 'bar'); + + $this->assertEquals(['bar'], $newMessage->getHeader('Foo')); + $this->assertEquals([], $newMessage->getHeader('LoremIpsum')); + } + + /** + * @covers \Engelsystem\Http\MessageTrait::getHeaderLine + */ + public function testGetHeaderLine() + { + $message = new MessageTraitImplementation(); + $newMessage = $message->withHeader('foo', ['bar', 'bla']); + + $this->assertEquals('', $newMessage->getHeaderLine('Lorem-Ipsum')); + $this->assertEquals('bar,bla', $newMessage->getHeaderLine('Foo')); + } + + /** + * @covers \Engelsystem\Http\MessageTrait::withHeader + */ + public function testWithHeader() + { + $message = new MessageTraitImplementation(); + $newMessage = $message->withHeader('foo', 'bar'); + + $this->assertNotEquals($message, $newMessage); + $this->assertArraySubset(['foo' => ['bar']], $newMessage->getHeaders()); + + $newMessage = $newMessage->withHeader('Foo', ['lorem', 'ipsum']); + $this->assertArraySubset(['Foo' => ['lorem', 'ipsum']], $newMessage->getHeaders()); + } + + /** + * @covers \Engelsystem\Http\MessageTrait::withAddedHeader + */ + public function testWithAddedHeader() + { + $message = new MessageTraitImplementation(); + $newMessage = $message->withHeader('foo', 'bar'); + + $this->assertNotEquals($message, $newMessage); + $this->assertArraySubset(['foo' => ['bar']], $newMessage->getHeaders()); + + $newMessage = $newMessage->withAddedHeader('Foo', ['lorem', 'ipsum']); + $this->assertArraySubset(['Foo' => ['bar', 'lorem', 'ipsum']], $newMessage->getHeaders()); + } + + /** + * @covers \Engelsystem\Http\MessageTrait::withoutHeader + */ + public function testWithoutHeader() + { + $message = (new MessageTraitImplementation())->withHeader('foo', 'bar'); + $this->assertTrue($message->hasHeader('foo')); + + $newMessage = $message->withoutHeader('Foo'); + $this->assertNotEquals($message, $newMessage); + $this->assertFalse($newMessage->hasHeader('foo')); + } + + /** + * @covers \Engelsystem\Http\MessageTrait::getBody + */ + public function testGetBody() + { + $message = (new MessageTraitImplementation())->setContent('Foo bar!'); + $body = $message->getBody(); + + $this->assertInstanceOf(StreamInterface::class, $body); + $this->assertEquals('Foo bar!', $body->getContents()); + } + + /** + * @covers \Engelsystem\Http\MessageTrait::withBody + */ + public function testWithBody() + { + /** @var Stream $stream */ + $stream = new Stream('php://memory', 'wb+'); + $stream->write('Test content'); + $stream->rewind(); + + $message = new MessageTraitImplementation(); + $newMessage = $message->withBody($stream); + + $this->assertNotEquals($message, $newMessage); + $this->assertEquals('Test content', $newMessage->getContent()); + } +} diff --git a/tests/Unit/Http/Psr7ServiceProviderTest.php b/tests/Unit/Http/Psr7ServiceProviderTest.php index b4c3c042..a09e9572 100644 --- a/tests/Unit/Http/Psr7ServiceProviderTest.php +++ b/tests/Unit/Http/Psr7ServiceProviderTest.php @@ -54,10 +54,6 @@ class Psr7ServiceProviderTest extends ServiceProviderTest ->method('createRequest') ->with($request) ->willReturn($psr7request); - $psr7Factory->expects($this->once()) - ->method('createResponse') - ->with($response) - ->willReturn($psr7response); $serviceProvider = new Psr7ServiceProvider($app); $serviceProvider->register(); diff --git a/tests/Unit/Http/ResponseTest.php b/tests/Unit/Http/ResponseTest.php index 6bedf5c1..f6c24767 100644 --- a/tests/Unit/Http/ResponseTest.php +++ b/tests/Unit/Http/ResponseTest.php @@ -4,6 +4,7 @@ namespace Engelsystem\Test\Unit\Http; use Engelsystem\Http\Response; use PHPUnit\Framework\TestCase; +use Psr\Http\Message\ResponseInterface; use Symfony\Component\HttpFoundation\Response as SymfonyResponse; class ResponseTest extends TestCase @@ -15,5 +16,34 @@ class ResponseTest extends TestCase { $response = new Response(); $this->assertInstanceOf(SymfonyResponse::class, $response); + $this->assertInstanceOf(ResponseInterface::class, $response); + } + + /** + * @covers \Engelsystem\Http\Response::withStatus + * @covers \Engelsystem\Http\Response::getReasonPhrase + */ + public function testWithStatus() + { + $response = new Response(); + $newResponse = $response->withStatus(503); + $this->assertNotEquals($response, $newResponse); + $this->assertNotEquals('', $newResponse->getReasonPhrase()); + $this->assertEquals(503, $newResponse->getStatusCode()); + + $newResponse = $response->withStatus(503, 'Foo'); + $this->assertEquals('Foo', $newResponse->getReasonPhrase()); + } + + /** + * @covers \Engelsystem\Http\Response::withContent + */ + public function testWithContent() + { + $response = new Response(); + $newResponse = $response->withContent('Lorem Ipsum?'); + + $this->assertNotEquals($response, $newResponse); + $this->assertEquals('Lorem Ipsum?', $newResponse->getContent()); } } diff --git a/tests/Unit/Http/Stub/MessageTraitImplementation.php b/tests/Unit/Http/Stub/MessageTraitImplementation.php new file mode 100644 index 00000000..d78fd0b2 --- /dev/null +++ b/tests/Unit/Http/Stub/MessageTraitImplementation.php @@ -0,0 +1,12 @@ + Date: Sat, 11 Aug 2018 23:46:28 +0200 Subject: [PATCH 07/10] Added middleware tests --- composer.json | 2 + includes/helper/message_helper.php | 3 +- src/Middleware/Dispatcher.php | 6 +- src/Middleware/LegacyMiddleware.php | 356 +++++++++--------- src/Middleware/NotFoundResponse.php | 19 +- src/Middleware/SendResponseHandler.php | 30 +- tests/Unit/Middleware/DispatcherTest.php | 230 +++++++++++ .../Unit/Middleware/ExceptionHandlerTest.php | 58 +++ .../Unit/Middleware/LegacyMiddlewareTest.php | 85 +++++ .../Unit/Middleware/NotFoundResponseTest.php | 39 ++ .../Middleware/SendResponseHandlerTest.php | 71 ++++ .../Stub/ExceptionMiddlewareHandler.php | 23 ++ .../Middleware/Stub/NotARealMiddleware.php | 8 + .../Stub/ReturnResponseMiddleware.php | 36 ++ .../Stub/ReturnResponseMiddlewareHandler.php | 30 ++ 15 files changed, 811 insertions(+), 185 deletions(-) create mode 100644 tests/Unit/Middleware/DispatcherTest.php create mode 100644 tests/Unit/Middleware/ExceptionHandlerTest.php create mode 100644 tests/Unit/Middleware/LegacyMiddlewareTest.php create mode 100644 tests/Unit/Middleware/NotFoundResponseTest.php create mode 100644 tests/Unit/Middleware/SendResponseHandlerTest.php create mode 100644 tests/Unit/Middleware/Stub/ExceptionMiddlewareHandler.php create mode 100644 tests/Unit/Middleware/Stub/NotARealMiddleware.php create mode 100644 tests/Unit/Middleware/Stub/ReturnResponseMiddleware.php create mode 100644 tests/Unit/Middleware/Stub/ReturnResponseMiddlewareHandler.php diff --git a/composer.json b/composer.json index 911df1f5..283bc30a 100644 --- a/composer.json +++ b/composer.json @@ -16,6 +16,8 @@ "require": { "php": ">=7.0.0", "ext-gettext": "*", + "ext-json": "*", + "ext-PDO": "*", "erusev/parsedown": "^1.6", "illuminate/container": "5.5.*", "illuminate/database": "5.5.*", diff --git a/includes/helper/message_helper.php b/includes/helper/message_helper.php index 3d2b663a..388a878c 100644 --- a/includes/helper/message_helper.php +++ b/includes/helper/message_helper.php @@ -73,8 +73,6 @@ function success($msg, $immediately = false) */ function alert($class, $msg, $immediately = false) { - $session = session(); - if (empty($msg)) { return ''; } @@ -83,6 +81,7 @@ function alert($class, $msg, $immediately = false) return '
' . $msg . '
'; } + $session = session(); $message = $session->get('msg', ''); $message .= alert($class, $msg, true); $session->set('msg', $message); diff --git a/src/Middleware/Dispatcher.php b/src/Middleware/Dispatcher.php index 774040fb..f2a5b5d5 100644 --- a/src/Middleware/Dispatcher.php +++ b/src/Middleware/Dispatcher.php @@ -12,7 +12,7 @@ use Psr\Http\Server\RequestHandlerInterface; class Dispatcher implements MiddlewareInterface, RequestHandlerInterface { - /** @var MiddlewareInterface[] */ + /** @var MiddlewareInterface[]|string[] */ protected $stack; /** @var Application */ @@ -22,8 +22,8 @@ class Dispatcher implements MiddlewareInterface, RequestHandlerInterface protected $next; /** - * @param MiddlewareInterface[] $stack - * @param Application|null $container + * @param MiddlewareInterface[]|string[] $stack + * @param Application|null $container */ public function __construct($stack = [], Application $container = null) { diff --git a/src/Middleware/LegacyMiddleware.php b/src/Middleware/LegacyMiddleware.php index 41b2e471..714141de 100644 --- a/src/Middleware/LegacyMiddleware.php +++ b/src/Middleware/LegacyMiddleware.php @@ -3,6 +3,7 @@ namespace Engelsystem\Middleware; use Engelsystem\Http\Request; +use Engelsystem\Http\Response; use Psr\Container\ContainerInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -61,10 +62,6 @@ class LegacyMiddleware implements MiddlewareInterface /** @var Request $appRequest */ $appRequest = $this->container->get('request'); - - // Default page content - $content = ''; - $page = $appRequest->query->get('p'); if (empty($page)) { $page = $appRequest->path(); @@ -74,187 +71,202 @@ class LegacyMiddleware implements MiddlewareInterface $page = isset($user) ? 'news' : 'login'; } + $title = $content = ''; if ( - preg_match('/^\w+$/i', $page) + preg_match('~^\w+$~i', $page) && ( in_array($page, $this->free_pages) || (isset($privileges) && in_array($page, $privileges)) ) ) { - $title = $page; - - switch ($page) { - case 'api': - error('Api disabled temporarily.'); - redirect(page_link_to()); - break; - case 'ical': - require_once realpath(__DIR__ . '/../includes/pages/user_ical.php'); - user_ical(); - break; - case 'atom': - require_once realpath(__DIR__ . '/../includes/pages/user_atom.php'); - user_atom(); - break; - case 'shifts_json_export': - require_once realpath(__DIR__ . '/../includes/controller/shifts_controller.php'); - shifts_json_export_controller(); - break; - case 'shifts_json_export_all': - require_once realpath(__DIR__ . '/../includes/controller/shifts_controller.php'); - shifts_json_export_all_controller(); - break; - case 'stats': - require_once realpath(__DIR__ . '/../includes/pages/guest_stats.php'); - guest_stats(); - break; - case 'user_password_recovery': - require_once realpath(__DIR__ . '/../includes/controller/users_controller.php'); - $title = user_password_recovery_title(); - $content = user_password_recovery_controller(); - break; - case 'public_dashboard': - list($title, $content) = public_dashboard_controller(); - break; - case 'angeltypes': - list($title, $content) = angeltypes_controller(); - break; - case 'shift_entries': - list($title, $content) = shift_entries_controller(); - break; - case 'shifts': - list($title, $content) = shifts_controller(); - break; - case 'users': - list($title, $content) = users_controller(); - break; - case 'user_angeltypes': - list($title, $content) = user_angeltypes_controller(); - break; - case 'user_driver_licenses': - list($title, $content) = user_driver_licenses_controller(); - break; - case 'shifttypes': - list($title, $content) = shifttypes_controller(); - break; - case 'admin_event_config': - list($title, $content) = event_config_edit_controller(); - break; - case 'rooms': - list($title, $content) = rooms_controller(); - break; - case 'news': - $title = news_title(); - $content = user_news(); - break; - case 'news_comments': - require_once realpath(__DIR__ . '/../includes/pages/user_news.php'); - $title = user_news_comments_title(); - $content = user_news_comments(); - break; - case 'user_meetings': - $title = meetings_title(); - $content = user_meetings(); - break; - case 'user_myshifts': - $title = myshifts_title(); - $content = user_myshifts(); - break; - case 'user_shifts': - $title = shifts_title(); - $content = user_shifts(); - break; - case 'user_worklog': - list($title, $content) = user_worklog_controller(); - break; - case 'user_messages': - $title = messages_title(); - $content = user_messages(); - break; - case 'user_questions': - $title = questions_title(); - $content = user_questions(); - break; - case 'user_settings': - $title = settings_title(); - $content = user_settings(); - break; - case 'login': - $title = login_title(); - $content = guest_login(); - break; - case 'register': - $title = register_title(); - $content = guest_register(); - break; - case 'logout': - $title = logout_title(); - $content = guest_logout(); - break; - case 'admin_questions': - $title = admin_questions_title(); - $content = admin_questions(); - break; - case 'admin_user': - $title = admin_user_title(); - $content = admin_user(); - break; - case 'admin_arrive': - $title = admin_arrive_title(); - $content = admin_arrive(); - break; - case 'admin_active': - $title = admin_active_title(); - $content = admin_active(); - break; - case 'admin_free': - $title = admin_free_title(); - $content = admin_free(); - break; - case 'admin_news': - require_once realpath(__DIR__ . '/../includes/pages/admin_news.php'); - $content = admin_news(); - break; - case 'admin_rooms': - $title = admin_rooms_title(); - $content = admin_rooms(); - break; - case 'admin_groups': - $title = admin_groups_title(); - $content = admin_groups(); - break; - case 'admin_import': - $title = admin_import_title(); - $content = admin_import(); - break; - case 'admin_shifts': - $title = admin_shifts_title(); - $content = admin_shifts(); - break; - case 'admin_log': - $title = admin_log_title(); - $content = admin_log(); - break; - case 'credits': - require_once realpath(__DIR__ . '/../includes/pages/guest_credits.php'); - $title = credits_title(); - $content = guest_credits(); - break; - default: - require_once realpath(__DIR__ . '/../includes/pages/guest_start.php'); - $content = guest_start(); - break; - } - } else { - return $handler->handle($request); + list($title, $content) = $this->loadPage($page); } if (empty($title) and empty($content)) { return $handler->handle($request); } - $event_config = EventConfig(); + return $this->renderPage($page, $title, $content); + } + /** + * Get the legacy page content and title + * + * @param string $page + * @return array ['title', 'content'] + * @codeCoverageIgnore + */ + protected function loadPage($page) + { + $title = ucfirst($page); + switch ($page) { + /** @noinspection PhpMissingBreakStatementInspection */ + case 'api': + error('Api disabled temporarily.'); + redirect(page_link_to()); + /** @noinspection PhpMissingBreakStatementInspection */ + case 'ical': + require_once realpath(__DIR__ . '/../../includes/pages/user_ical.php'); + user_ical(); + /** @noinspection PhpMissingBreakStatementInspection */ + case 'atom': + require_once realpath(__DIR__ . '/../../includes/pages/user_atom.php'); + user_atom(); + /** @noinspection PhpMissingBreakStatementInspection */ + case 'shifts_json_export': + require_once realpath(__DIR__ . '/../../includes/controller/shifts_controller.php'); + shifts_json_export_controller(); + /** @noinspection PhpMissingBreakStatementInspection */ + case 'shifts_json_export_all': + require_once realpath(__DIR__ . '/../../includes/controller/shifts_controller.php'); + shifts_json_export_all_controller(); + /** @noinspection PhpMissingBreakStatementInspection */ + case 'stats': + require_once realpath(__DIR__ . '/../../includes/pages/guest_stats.php'); + guest_stats(); + case 'user_password_recovery': + require_once realpath(__DIR__ . '/../../includes/controller/users_controller.php'); + $title = user_password_recovery_title(); + $content = user_password_recovery_controller(); + return [$title, $content]; + case 'public_dashboard': + return public_dashboard_controller(); + case 'angeltypes': + return angeltypes_controller(); + case 'shift_entries': + return shift_entries_controller(); + case 'shifts': + return shifts_controller(); + case 'users': + return users_controller(); + case 'user_angeltypes': + return user_angeltypes_controller(); + case 'user_driver_licenses': + return user_driver_licenses_controller(); + case 'shifttypes': + list($title, $content) = shifttypes_controller(); + return [$title, $content]; + case 'admin_event_config': + list($title, $content) = event_config_edit_controller(); + return [$title, $content]; + case 'rooms': + return rooms_controller(); + case 'news': + $title = news_title(); + $content = user_news(); + return [$title, $content]; + case 'news_comments': + require_once realpath(__DIR__ . '/../../includes/pages/user_news.php'); + $title = user_news_comments_title(); + $content = user_news_comments(); + return [$title, $content]; + case 'user_meetings': + $title = meetings_title(); + $content = user_meetings(); + return [$title, $content]; + case 'user_myshifts': + $title = myshifts_title(); + $content = user_myshifts(); + return [$title, $content]; + case 'user_shifts': + $title = shifts_title(); + $content = user_shifts(); + return [$title, $content]; + case 'user_worklog': + return user_worklog_controller(); + case 'user_messages': + $title = messages_title(); + $content = user_messages(); + return [$title, $content]; + case 'user_questions': + $title = questions_title(); + $content = user_questions(); + return [$title, $content]; + case 'user_settings': + $title = settings_title(); + $content = user_settings(); + return [$title, $content]; + case 'login': + $title = login_title(); + $content = guest_login(); + return [$title, $content]; + case 'register': + $title = register_title(); + $content = guest_register(); + return [$title, $content]; + case 'logout': + $title = logout_title(); + $content = guest_logout(); + return [$title, $content]; + case 'admin_questions': + $title = admin_questions_title(); + $content = admin_questions(); + return [$title, $content]; + case 'admin_user': + $title = admin_user_title(); + $content = admin_user(); + return [$title, $content]; + case 'admin_arrive': + $title = admin_arrive_title(); + $content = admin_arrive(); + return [$title, $content]; + case 'admin_active': + $title = admin_active_title(); + $content = admin_active(); + return [$title, $content]; + case 'admin_free': + $title = admin_free_title(); + $content = admin_free(); + return [$title, $content]; + case 'admin_news': + require_once realpath(__DIR__ . '/../../includes/pages/admin_news.php'); + $content = admin_news(); + return [$title, $content]; + case 'admin_rooms': + $title = admin_rooms_title(); + $content = admin_rooms(); + return [$title, $content]; + case 'admin_groups': + $title = admin_groups_title(); + $content = admin_groups(); + return [$title, $content]; + case 'admin_import': + $title = admin_import_title(); + $content = admin_import(); + return [$title, $content]; + case 'admin_shifts': + $title = admin_shifts_title(); + $content = admin_shifts(); + return [$title, $content]; + case 'admin_log': + $title = admin_log_title(); + $content = admin_log(); + return [$title, $content]; + case 'credits': + require_once realpath(__DIR__ . '/../../includes/pages/guest_credits.php'); + $title = credits_title(); + $content = guest_credits(); + return [$title, $content]; + } + + require_once realpath(__DIR__ . '/../../includes/pages/guest_start.php'); + $content = guest_start(); + return [$title, $content]; + } + + /** + * Render the template + * + * @param string $page + * @param string $title + * @param string $content + * @return Response + * @codeCoverageIgnore + */ + protected function renderPage($page, $title, $content) + { + global $user; + $event_config = EventConfig(); $parameters = [ 'key' => (isset($user) ? $user['api_key'] : ''), ]; diff --git a/src/Middleware/NotFoundResponse.php b/src/Middleware/NotFoundResponse.php index c5d51d2d..f9431c1d 100644 --- a/src/Middleware/NotFoundResponse.php +++ b/src/Middleware/NotFoundResponse.php @@ -2,6 +2,7 @@ namespace Engelsystem\Middleware; +use Engelsystem\Http\Response; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; @@ -22,12 +23,20 @@ class NotFoundResponse implements MiddlewareInterface ServerRequestInterface $request, RequestHandlerInterface $handler ): ResponseInterface { + $info = _('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($info); + } + + /** + * @param string $content + * @return Response + * @codeCoverageIgnore + */ + protected function renderPage($content) + { global $user; $event_config = EventConfig(); - $content = info( - _('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!'), - true - ); return response(view(__DIR__ . '/../../templates/layout.html', [ 'theme' => isset($user) ? $user['color'] : config('theme'), @@ -36,7 +45,7 @@ class NotFoundResponse implements MiddlewareInterface 'start_page_url' => page_link_to('/'), 'credits_url' => page_link_to('credits'), 'menu' => make_menu(), - 'content' => msg() . $content, + 'content' => msg() . info($content), 'header_toolbar' => header_toolbar(), 'faq_url' => config('faq_url'), 'contact_email' => config('contact_email'), diff --git a/src/Middleware/SendResponseHandler.php b/src/Middleware/SendResponseHandler.php index 06406fe0..34e70a87 100644 --- a/src/Middleware/SendResponseHandler.php +++ b/src/Middleware/SendResponseHandler.php @@ -24,8 +24,8 @@ class SendResponseHandler implements MiddlewareInterface ): ResponseInterface { $response = $handler->handle($request); - if (!headers_sent()) { - header(sprintf( + if (!$this->headersSent()) { + $this->sendHeader(sprintf( 'HTTP/%s %s %s', $response->getProtocolVersion(), $response->getStatusCode(), @@ -34,7 +34,7 @@ class SendResponseHandler implements MiddlewareInterface foreach ($response->getHeaders() as $name => $values) { foreach ($values as $value) { - header($name . ': ' . $value, false); + $this->sendHeader($name . ': ' . $value, false); } } } @@ -42,4 +42,28 @@ class SendResponseHandler implements MiddlewareInterface echo $response->getBody(); return $response; } + + /** + * Checks if headers have been sent + * + * @return bool + * @codeCoverageIgnore + */ + protected function headersSent() + { + return headers_sent(); + } + + /** + * Send a raw HTTP header + * + * @param string $content + * @param bool $replace + * @param int $code + * @codeCoverageIgnore + */ + protected function sendHeader($content, $replace = true, $code = null) + { + header($content, $replace, $code); + } } diff --git a/tests/Unit/Middleware/DispatcherTest.php b/tests/Unit/Middleware/DispatcherTest.php new file mode 100644 index 00000000..c01c5029 --- /dev/null +++ b/tests/Unit/Middleware/DispatcherTest.php @@ -0,0 +1,230 @@ +createMock(Application::class); + + $dispatcher = new Dispatcher([], $container); + $this->assertInstanceOf(MiddlewareInterface::class, $dispatcher); + $this->assertInstanceOf(RequestHandlerInterface::class, $dispatcher); + + $reflection = new Reflection(get_class($dispatcher)); + $property = $reflection->getProperty('container'); + $property->setAccessible(true); + $this->assertEquals($container, $property->getValue($dispatcher)); + } + + /** + * @covers \Engelsystem\Middleware\Dispatcher::process + */ + public function testProcess() + { + /** @var ServerRequestInterface|MockObject $request */ + $request = $this->createMock(ServerRequestInterface::class); + /** @var ResponseInterface|MockObject $response */ + $response = $this->createMock(ResponseInterface::class); + /** @var RequestHandlerInterface|MockObject $handler */ + $handler = $this->createMock(RequestHandlerInterface::class); + + /** @var Dispatcher|MockObject $dispatcher */ + $dispatcher = $this->getMockBuilder(Dispatcher::class) + ->setMethods(['handle']) + ->getMock(); + + $dispatcher->expects($this->once()) + ->method('handle') + ->willReturn($response); + + $return = $dispatcher->process($request, $handler); + $this->assertEquals($response, $return); + + $reflection = new Reflection(get_class($dispatcher)); + $property = $reflection->getProperty('next'); + $property->setAccessible(true); + + $this->assertEquals($handler, $property->getValue($dispatcher)); + } + + /** + * @covers \Engelsystem\Middleware\Dispatcher::handle + */ + public function testHandle() + { + /** @var ServerRequestInterface|MockObject $request */ + $request = $this->createMock(ServerRequestInterface::class); + /** @var ResponseInterface|MockObject $response */ + $response = $this->createMock(ResponseInterface::class); + /** @var MiddlewareInterface|MockObject $middleware */ + $middleware = $this->createMock(MiddlewareInterface::class); + + $dispatcher = new Dispatcher([$middleware]); + $middleware->expects($this->once()) + ->method('process') + ->with($request, $dispatcher) + ->willReturn($response); + + $return = $dispatcher->handle($request); + $this->assertEquals($response, $return); + } + + /** + * @covers \Engelsystem\Middleware\Dispatcher::handle + */ + public function testHandleNext() + { + /** @var ServerRequestInterface|MockObject $request */ + $request = $this->createMock(ServerRequestInterface::class); + /** @var ResponseInterface|MockObject $response */ + $response = $this->createMock(ResponseInterface::class); + /** @var RequestHandlerInterface|MockObject $handler */ + $handler = $this->createMock(RequestHandlerInterface::class); + + $dispatcher = new Dispatcher(); + $handler->expects($this->once()) + ->method('handle') + ->with($request) + ->willReturn($response); + + $reflection = new Reflection(get_class($dispatcher)); + $property = $reflection->getProperty('next'); + $property->setAccessible(true); + $property->setValue($dispatcher, $handler); + + $return = $dispatcher->handle($request); + $this->assertEquals($response, $return); + } + + /** + * @covers \Engelsystem\Middleware\Dispatcher::handle + */ + public function testHandleNoMiddleware() + { + /** @var ServerRequestInterface|MockObject $request */ + $request = $this->createMock(ServerRequestInterface::class); + + $this->expectException(LogicException::class); + + $dispatcher = new Dispatcher(); + $dispatcher->handle($request); + } + + /** + * @covers \Engelsystem\Middleware\Dispatcher::handle + */ + public function testHandleNoRealMiddleware() + { + /** @var ServerRequestInterface|MockObject $request */ + $request = $this->createMock(ServerRequestInterface::class); + + $this->expectException(InvalidArgumentException::class); + + $dispatcher = new Dispatcher([new NotARealMiddleware()]); + $dispatcher->handle($request); + } + + /** + * @covers \Engelsystem\Middleware\Dispatcher::handle + */ + public function testHandleCallResolve() + { + /** @var ServerRequestInterface|MockObject $request */ + $request = $this->createMock(ServerRequestInterface::class); + /** @var ResponseInterface|MockObject $response */ + $response = $this->createMock(ResponseInterface::class); + /** @var MiddlewareInterface|MockObject $middleware */ + $middleware = $this->createMock(MiddlewareInterface::class); + + /** @var Dispatcher|MockObject $dispatcher */ + $dispatcher = $this->getMockBuilder(Dispatcher::class) + ->setConstructorArgs([[MiddlewareInterface::class]]) + ->setMethods(['resolveMiddleware']) + ->getMock(); + + $dispatcher->expects($this->once()) + ->method('resolveMiddleware') + ->with(MiddlewareInterface::class) + ->willReturn($middleware); + + $middleware->expects($this->once()) + ->method('process') + ->with($request, $dispatcher) + ->willReturn($response); + + $return = $dispatcher->handle($request); + $this->assertEquals($response, $return); + } + + /** + * @covers \Engelsystem\Middleware\Dispatcher::resolveMiddleware + * @covers \Engelsystem\Middleware\Dispatcher::setContainer + */ + public function testResolveMiddleware() + { + /** @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); + } + + /** + * @covers \Engelsystem\Middleware\Dispatcher::resolveMiddleware + */ + public function testResolveMiddlewareNoContainer() + { + /** @var ServerRequestInterface|MockObject $request */ + $request = $this->createMock(ServerRequestInterface::class); + + $this->expectException(InvalidArgumentException::class); + + $dispatcher = new Dispatcher([ReturnResponseMiddleware::class]); + $dispatcher->handle($request); + } +} diff --git a/tests/Unit/Middleware/ExceptionHandlerTest.php b/tests/Unit/Middleware/ExceptionHandlerTest.php new file mode 100644 index 00000000..6d2a20e6 --- /dev/null +++ b/tests/Unit/Middleware/ExceptionHandlerTest.php @@ -0,0 +1,58 @@ +getMockForAbstractClass(ContainerInterface::class); + /** @var MockObject|ServerRequestInterface $request */ + $request = $this->getMockBuilder(ServerRequestInterface::class)->getMock(); + /** @var MockObject|ResponseInterface $response */ + $response = $this->getMockBuilder(Response::class)->getMock(); + /** @var MockObject|Handler $errorHandler */ + $errorHandler = $this->getMockBuilder(Handler::class)->getMock(); + $returnResponseHandler = new ReturnResponseMiddlewareHandler($response); + $throwExceptionHandler = new ExceptionMiddlewareHandler(); + + Application::setInstance($container); + + $container->expects($this->exactly(2)) + ->method('get') + ->withConsecutive(['error.handler'], ['psr7.response']) + ->willReturnOnConsecutiveCalls($errorHandler, $response); + + $response->expects($this->once()) + ->method('withContent') + ->willReturn($response); + $response->expects($this->once()) + ->method('withStatus') + ->with(500) + ->willReturn($response); + + $handler = new ExceptionHandler($container); + $return = $handler->process($request, $returnResponseHandler); + $this->assertEquals($response, $return); + + $return = $handler->process($request, $throwExceptionHandler); + $this->assertEquals($response, $return); + } +} diff --git a/tests/Unit/Middleware/LegacyMiddlewareTest.php b/tests/Unit/Middleware/LegacyMiddlewareTest.php new file mode 100644 index 00000000..34e60b60 --- /dev/null +++ b/tests/Unit/Middleware/LegacyMiddlewareTest.php @@ -0,0 +1,85 @@ +getMockForAbstractClass(ContainerInterface::class); + /** @var LegacyMiddleware|MockObject $middleware */ + $middleware = $this->getMockBuilder(LegacyMiddleware::class) + ->setConstructorArgs([$container]) + ->setMethods(['loadPage', 'renderPage']) + ->getMock(); + /** @var Request|MockObject $defaultRequest */ + $defaultRequest = $this->createMock(Request::class); + /** @var ParameterBag|MockObject $parameters */ + $parameters = $this->createMock(ParameterBag::class); + /** @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->exactly(2)) + ->method('loadPage') + ->withConsecutive(['user_worklog'], ['login']) + ->willReturnOnConsecutiveCalls( + ['title', 'content'], + ['title2', 'content2'] + ); + + $middleware->expects($this->exactly(2)) + ->method('renderPage') + ->withConsecutive( + ['user_worklog', 'title', 'content'], + ['login', 'title2', 'content2'] + ) + ->willReturn($response); + + $container->expects($this->atLeastOnce()) + ->method('get') + ->with('request') + ->willReturn($defaultRequest); + + $defaultRequest->query = $parameters; + $defaultRequest->expects($this->once()) + ->method('path') + ->willReturn('user-worklog'); + + $parameters->expects($this->exactly(3)) + ->method('get') + ->with('p') + ->willReturnOnConsecutiveCalls( + null, + 'foo', + '/' + ); + + $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 new file mode 100644 index 00000000..9279e81d --- /dev/null +++ b/tests/Unit/Middleware/NotFoundResponseTest.php @@ -0,0 +1,39 @@ +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/SendResponseHandlerTest.php b/tests/Unit/Middleware/SendResponseHandlerTest.php new file mode 100644 index 00000000..7431299e --- /dev/null +++ b/tests/Unit/Middleware/SendResponseHandlerTest.php @@ -0,0 +1,71 @@ +getMockBuilder(SendResponseHandler::class) + ->setMethods(['headersSent', 'sendHeader']) + ->getMock(); + /** @var ServerRequestInterface|MockObject $request */ + $request = $this->getMockForAbstractClass(ServerRequestInterface::class); + /** @var ResponseInterface|MockObject $response */ + $response = $this->getMockForAbstractClass(ResponseInterface::class); + /** @var RequestHandlerInterface|MockObject $handler */ + $handler = $this->getMockForAbstractClass(RequestHandlerInterface::class); + + $middleware->expects($this->atLeastOnce()) + ->method('headersSent') + ->willReturnOnConsecutiveCalls(true, false); + + $middleware->expects($this->exactly(4)) + ->method('sendHeader') + ->withConsecutive( + ['HTTP/0.7 505 Something went wrong!', true, 505], + ['Foo: bar', false], + ['lorem: ipsum', false], + ['lorem: dolor', false] + ); + + $handler->expects($this->exactly(2)) + ->method('handle') + ->with($request) + ->willReturn($response); + + $response->expects($this->exactly(2)) + ->method('getBody') + ->willReturn('Lorem Ipsum!'); + + $response->expects($this->atLeastOnce()) + ->method('getProtocolVersion') + ->willReturn('0.7'); + + $response->expects($this->atLeastOnce()) + ->method('getStatusCode') + ->willReturn(505); + + $response->expects($this->once()) + ->method('getReasonPhrase') + ->willReturn('Something went wrong!'); + $response->expects($this->once()) + ->method('getHeaders') + ->willReturn(['Foo' => ['bar'], 'lorem' => ['ipsum', 'dolor']]); + + $this->expectOutputString('Lorem Ipsum!Lorem Ipsum!'); + $middleware->process($request, $handler); + $middleware->process($request, $handler); + } +} diff --git a/tests/Unit/Middleware/Stub/ExceptionMiddlewareHandler.php b/tests/Unit/Middleware/Stub/ExceptionMiddlewareHandler.php new file mode 100644 index 00000000..5e374bea --- /dev/null +++ b/tests/Unit/Middleware/Stub/ExceptionMiddlewareHandler.php @@ -0,0 +1,23 @@ +response = $response; + } + + /** + * Process an incoming server request and return a response, optionally delegating + * response creation to a handler. + * + * Could be used to group middleware + * + * @param ServerRequestInterface $request + * @param RequestHandlerInterface $handler + * @return ResponseInterface + */ + public function process( + ServerRequestInterface $request, + RequestHandlerInterface $handler + ): ResponseInterface { + return $this->response; + } +} diff --git a/tests/Unit/Middleware/Stub/ReturnResponseMiddlewareHandler.php b/tests/Unit/Middleware/Stub/ReturnResponseMiddlewareHandler.php new file mode 100644 index 00000000..323e07b4 --- /dev/null +++ b/tests/Unit/Middleware/Stub/ReturnResponseMiddlewareHandler.php @@ -0,0 +1,30 @@ +response = $response; + } + + /** + * Returns a given response + * + * @param ServerRequestInterface $request + * @return ResponseInterface + * @throws \Exception + */ + public function handle(ServerRequestInterface $request): ResponseInterface + { + return $this->response; + } +} From 18fd73a899602a473044013854a354254062ebd4 Mon Sep 17 00:00:00 2001 From: Igor Scheller Date: Sun, 12 Aug 2018 00:08:52 +0200 Subject: [PATCH 08/10] Moved middleware to application config --- config/app.php | 10 +++++++++- public/index.php | 12 ++---------- src/Application.php | 14 ++++++++++++++ tests/Unit/ApplicationTest.php | 10 +++++++--- 4 files changed, 32 insertions(+), 14 deletions(-) diff --git a/config/app.php b/config/app.php index 480b19d4..bb405fde 100644 --- a/config/app.php +++ b/config/app.php @@ -4,7 +4,7 @@ return [ // Service providers - 'providers' => [ + 'providers' => [ \Engelsystem\Logger\LoggerServiceProvider::class, \Engelsystem\Exceptions\ExceptionsServiceProvider::class, \Engelsystem\Config\ConfigServiceProvider::class, @@ -16,4 +16,12 @@ return [ \Engelsystem\Http\ResponseServiceProvider::class, \Engelsystem\Http\Psr7ServiceProvider::class, ], + + // Application middleware + 'middleware' => [ + \Engelsystem\Middleware\SendResponseHandler::class, + \Engelsystem\Middleware\ExceptionHandler::class, + \Engelsystem\Middleware\LegacyMiddleware::class, + \Engelsystem\Middleware\NotFoundResponse::class, + ], ]; diff --git a/public/index.php b/public/index.php index 88e57252..4e345a7c 100644 --- a/public/index.php +++ b/public/index.php @@ -2,10 +2,6 @@ use Engelsystem\Application; use Engelsystem\Middleware\Dispatcher; -use Engelsystem\Middleware\ExceptionHandler; -use Engelsystem\Middleware\LegacyMiddleware; -use Engelsystem\Middleware\NotFoundResponse; -use Engelsystem\Middleware\SendResponseHandler; use Psr\Http\Message\ServerRequestInterface; require_once realpath(__DIR__ . '/../includes/engelsystem.php'); @@ -15,13 +11,9 @@ $app = app(); /** @var ServerRequestInterface $request */ $request = $app->get('psr7.request'); +$middleware = $app->getMiddleware(); -$dispatcher = new Dispatcher([ - SendResponseHandler::class, - ExceptionHandler::class, - LegacyMiddleware::class, - NotFoundResponse::class, -]); +$dispatcher = new Dispatcher($middleware); $dispatcher->setContainer($app); $dispatcher->handle($request); diff --git a/src/Application.php b/src/Application.php index 68ce9e33..6644a6cf 100644 --- a/src/Application.php +++ b/src/Application.php @@ -7,6 +7,7 @@ use Engelsystem\Container\Container; use Engelsystem\Container\ServiceProvider; use Illuminate\Container\Container as IlluminateContainer; use Psr\Container\ContainerInterface; +use Psr\Http\Server\MiddlewareInterface; class Application extends Container { @@ -16,6 +17,9 @@ class Application extends Container /** @var bool */ protected $isBootstrapped = false; + /** @var MiddlewareInterface[]|string[] */ + protected $middleware; + /** * Registered service providers * @@ -85,6 +89,8 @@ class Application extends Container foreach ($config->get('providers', []) as $provider) { $this->register($provider); } + + $this->middleware = $config->get('middleware', []); } foreach ($this->serviceProviders as $provider) { @@ -136,4 +142,12 @@ class Application extends Container { return $this->isBootstrapped; } + + /** + * @return MiddlewareInterface[]|string[] + */ + public function getMiddleware() + { + return $this->middleware; + } } diff --git a/tests/Unit/ApplicationTest.php b/tests/Unit/ApplicationTest.php index f58483ea..866eb957 100644 --- a/tests/Unit/ApplicationTest.php +++ b/tests/Unit/ApplicationTest.php @@ -9,6 +9,7 @@ use Engelsystem\Container\ServiceProvider; use PHPUnit\Framework\TestCase; use PHPUnit_Framework_MockObject_MockObject; use Psr\Container\ContainerInterface; +use Psr\Http\Server\MiddlewareInterface; use ReflectionClass; class ApplicationTest extends TestCase @@ -118,6 +119,7 @@ class ApplicationTest extends TestCase /** * @covers \Engelsystem\Application::bootstrap * @covers \Engelsystem\Application::isBooted + * @covers \Engelsystem\Application::getMiddleware */ public function testBootstrap() { @@ -137,10 +139,11 @@ class ApplicationTest extends TestCase $config = $this->getMockBuilder(Config::class) ->getMock(); - $config->expects($this->once()) + $middleware = [MiddlewareInterface::class]; + $config->expects($this->exactly(2)) ->method('get') - ->with('providers') - ->willReturn([$serviceProvider]); + ->withConsecutive(['providers'], ['middleware']) + ->willReturnOnConsecutiveCalls([$serviceProvider], $middleware); $property = (new ReflectionClass($app))->getProperty('serviceProviders'); $property->setAccessible(true); @@ -149,6 +152,7 @@ class ApplicationTest extends TestCase $app->bootstrap($config); $this->assertTrue($app->isBooted()); + $this->assertEquals($middleware, $app->getMiddleware()); // Run bootstrap another time to ensure that providers are registered only once $app->bootstrap($config); From 5427ee385dc2ff117a1130eb606f78e708949c8f Mon Sep 17 00:00:00 2001 From: Igor Scheller Date: Thu, 16 Aug 2018 18:13:53 +0200 Subject: [PATCH 09/10] Made Engelsystem\Http\Request PSR-7 RequestInterface compatible --- src/Http/MessageTrait.php | 21 ++- src/Http/Request.php | 131 +++++++++++++++++- tests/Unit/Http/MessageTraitRequestTest.php | 50 +++++++ ...tTest.php => MessageTraitResponseTest.php} | 26 ++-- tests/Unit/Http/RequestTest.php | 90 ++++++++++++ .../MessageTraitRequestImplementation.php | 12 ++ ...=> MessageTraitResponseImplementation.php} | 2 +- 7 files changed, 313 insertions(+), 19 deletions(-) create mode 100644 tests/Unit/Http/MessageTraitRequestTest.php rename tests/Unit/Http/{MessageTraitTest.php => MessageTraitResponseTest.php} (84%) create mode 100644 tests/Unit/Http/Stub/MessageTraitRequestImplementation.php rename tests/Unit/Http/Stub/{MessageTraitImplementation.php => MessageTraitResponseImplementation.php} (69%) diff --git a/src/Http/MessageTrait.php b/src/Http/MessageTrait.php index fa3a1459..e46d291e 100644 --- a/src/Http/MessageTrait.php +++ b/src/Http/MessageTrait.php @@ -4,7 +4,6 @@ namespace Engelsystem\Http; use Psr\Http\Message\StreamInterface; -use Symfony\Component\HttpFoundation\ResponseHeaderBag; use Zend\Diactoros\Stream; /** @@ -41,7 +40,12 @@ trait MessageTrait public function withProtocolVersion($version) { $new = clone $this; - $new->setProtocolVersion($version); + if (method_exists($new, 'setProtocolVersion')) { + $new->setProtocolVersion($version); + } else { + $new->server->set('SERVER_PROTOCOL', $version); + } + return $new; } @@ -72,7 +76,11 @@ trait MessageTrait */ public function getHeaders() { - return $this->headers->allPreserveCase(); + if (method_exists($this->headers, 'allPreserveCase')) { + return $this->headers->allPreserveCase(); + } + + return $this->headers->all(); } /** @@ -228,7 +236,12 @@ trait MessageTrait public function withBody(StreamInterface $body) { $new = clone $this; - $new->setContent($body); + + if (method_exists($new, 'setContent')) { + $new->setContent($body); + } else { + $new->content = $body; + } return $new; } diff --git a/src/Http/Request.php b/src/Http/Request.php index c6a9e5ad..fd3bff42 100644 --- a/src/Http/Request.php +++ b/src/Http/Request.php @@ -2,10 +2,15 @@ namespace Engelsystem\Http; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\UriInterface; use Symfony\Component\HttpFoundation\Request as SymfonyRequest; +use Zend\Diactoros\Uri; -class Request extends SymfonyRequest +class Request extends SymfonyRequest implements RequestInterface { + use MessageTrait; + /** * Get POST input * @@ -64,4 +69,128 @@ class Request extends SymfonyRequest { return rtrim(preg_replace('/\?.*/', '', $this->getUri()), '/'); } + + /** + * Retrieves the message's request target. + * + * + * Retrieves the message's request-target either as it will appear (for + * clients), as it appeared at request (for servers), or as it was + * specified for the instance (see withRequestTarget()). + * + * In most cases, this will be the origin-form of the composed URI, + * unless a value was provided to the concrete implementation (see + * withRequestTarget() below). + * + * If no URI is available, and no request-target has been specifically + * provided, this method MUST return the string "/". + * + * @return string + */ + public function getRequestTarget() + { + $query = $this->getQueryString(); + return '/' . $this->path() . (!empty($query) ? '?' . $query : ''); + } + + /** + * Return an instance with the specific request-target. + * + * If the request needs a non-origin-form request-target — e.g., for + * specifying an absolute-form, authority-form, or asterisk-form — + * this method may be used to create an instance with the specified + * request-target, verbatim. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * changed request target. + * + * @link http://tools.ietf.org/html/rfc7230#section-5.3 (for the various + * request-target forms allowed in request messages) + * @param mixed $requestTarget + * @return static + */ + public function withRequestTarget($requestTarget) + { + return $this->create($requestTarget); + } + + /** + * Return an instance with the provided HTTP method. + * + * While HTTP method names are typically all uppercase characters, HTTP + * method names are case-sensitive and thus implementations SHOULD NOT + * modify the given string. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * changed request method. + * + * @param string $method Case-sensitive method. + * @return static + * @throws \InvalidArgumentException for invalid HTTP methods. + */ + public function withMethod($method) + { + $new = clone $this; + $new->setMethod($method); + + return $new; + } + + /** + * Returns an instance with the provided URI. + * + * This method MUST update the Host header of the returned request by + * default if the URI contains a host component. If the URI does not + * contain a host component, any pre-existing Host header MUST be carried + * over to the returned request. + * + * You can opt-in to preserving the original state of the Host header by + * setting `$preserveHost` to `true`. When `$preserveHost` is set to + * `true`, this method interacts with the Host header in the following ways: + * + * - If the Host header is missing or empty, and the new URI contains + * a host component, this method MUST update the Host header in the returned + * request. + * - If the Host header is missing or empty, and the new URI does not contain a + * host component, this method MUST NOT update the Host header in the returned + * request. + * - If a Host header is present and non-empty, this method MUST NOT update + * the Host header in the returned request. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * new UriInterface instance. + * + * @link http://tools.ietf.org/html/rfc3986#section-4.3 + * @param UriInterface $uri New request URI to use. + * @param bool $preserveHost Preserve the original state of the Host header. + * @return static + */ + public function withUri(UriInterface $uri, $preserveHost = false) + { + $new = $this->create($uri); + if ($preserveHost) { + $new->headers->set('HOST', $this->getHost()); + } + + return $new; + } + + /** + * Retrieves the URI instance. + * + * This method MUST return a UriInterface instance. + * + * @link http://tools.ietf.org/html/rfc3986#section-4.3 + * @return string|UriInterface Returns a UriInterface instance + * representing the URI of the request. + */ + public function getUri() + { + $uri = parent::getUri(); + + return new Uri($uri); + } } diff --git a/tests/Unit/Http/MessageTraitRequestTest.php b/tests/Unit/Http/MessageTraitRequestTest.php new file mode 100644 index 00000000..7430b5d7 --- /dev/null +++ b/tests/Unit/Http/MessageTraitRequestTest.php @@ -0,0 +1,50 @@ +withProtocolVersion('0.1'); + $this->assertNotEquals($message, $newMessage); + $this->assertEquals('0.1', $newMessage->getProtocolVersion()); + } + + /** + * @covers \Engelsystem\Http\MessageTrait::getHeaders + */ + public function testGetHeaders() + { + $message = new MessageTraitRequestImplementation(); + $newMessage = $message->withHeader('lorem', 'ipsum'); + + $this->assertNotEquals($message, $newMessage); + $this->assertArraySubset(['lorem' => ['ipsum']], $newMessage->getHeaders()); + } + + /** + * @covers \Engelsystem\Http\MessageTrait::withBody + */ + public function testWithBody() + { + /** @var Stream $stream */ + $stream = new Stream('php://memory', 'wb+'); + $stream->write('Test content'); + $stream->rewind(); + + $message = new MessageTraitRequestImplementation(); + $newMessage = $message->withBody($stream); + + $this->assertNotEquals($message, $newMessage); + $this->assertEquals('Test content', $newMessage->getContent()); + } +} diff --git a/tests/Unit/Http/MessageTraitTest.php b/tests/Unit/Http/MessageTraitResponseTest.php similarity index 84% rename from tests/Unit/Http/MessageTraitTest.php rename to tests/Unit/Http/MessageTraitResponseTest.php index 46076a67..f60360a3 100644 --- a/tests/Unit/Http/MessageTraitTest.php +++ b/tests/Unit/Http/MessageTraitResponseTest.php @@ -2,21 +2,21 @@ namespace Engelsystem\Test\Unit\Http; -use Engelsystem\Test\Unit\Http\Stub\MessageTraitImplementation; +use Engelsystem\Test\Unit\Http\Stub\MessageTraitResponseImplementation; use PHPUnit\Framework\TestCase; use Psr\Http\Message\MessageInterface; use Psr\Http\Message\StreamInterface; use Symfony\Component\HttpFoundation\Response as SymfonyResponse; use Zend\Diactoros\Stream; -class MessageTraitTest extends TestCase +class MessageTraitResponseTest extends TestCase { /** * @covers \Engelsystem\Http\MessageTrait */ public function testCreate() { - $message = new MessageTraitImplementation(); + $message = new MessageTraitResponseImplementation(); $this->assertInstanceOf(MessageInterface::class, $message); $this->assertInstanceOf(SymfonyResponse::class, $message); } @@ -27,7 +27,7 @@ class MessageTraitTest extends TestCase */ public function testGetProtocolVersion() { - $message = new MessageTraitImplementation(); + $message = new MessageTraitResponseImplementation(); $newMessage = $message->withProtocolVersion('0.1'); $this->assertNotEquals($message, $newMessage); $this->assertEquals('0.1', $newMessage->getProtocolVersion()); @@ -38,7 +38,7 @@ class MessageTraitTest extends TestCase */ public function testGetHeaders() { - $message = new MessageTraitImplementation(); + $message = new MessageTraitResponseImplementation(); $newMessage = $message->withHeader('Foo', 'bar'); $this->assertNotEquals($message, $newMessage); @@ -53,7 +53,7 @@ class MessageTraitTest extends TestCase */ public function testHasHeader() { - $message = new MessageTraitImplementation(); + $message = new MessageTraitResponseImplementation(); $this->assertFalse($message->hasHeader('test')); $newMessage = $message->withHeader('test', '12345'); @@ -66,7 +66,7 @@ class MessageTraitTest extends TestCase */ public function testGetHeader() { - $message = new MessageTraitImplementation(); + $message = new MessageTraitResponseImplementation(); $newMessage = $message->withHeader('foo', 'bar'); $this->assertEquals(['bar'], $newMessage->getHeader('Foo')); @@ -78,7 +78,7 @@ class MessageTraitTest extends TestCase */ public function testGetHeaderLine() { - $message = new MessageTraitImplementation(); + $message = new MessageTraitResponseImplementation(); $newMessage = $message->withHeader('foo', ['bar', 'bla']); $this->assertEquals('', $newMessage->getHeaderLine('Lorem-Ipsum')); @@ -90,7 +90,7 @@ class MessageTraitTest extends TestCase */ public function testWithHeader() { - $message = new MessageTraitImplementation(); + $message = new MessageTraitResponseImplementation(); $newMessage = $message->withHeader('foo', 'bar'); $this->assertNotEquals($message, $newMessage); @@ -105,7 +105,7 @@ class MessageTraitTest extends TestCase */ public function testWithAddedHeader() { - $message = new MessageTraitImplementation(); + $message = new MessageTraitResponseImplementation(); $newMessage = $message->withHeader('foo', 'bar'); $this->assertNotEquals($message, $newMessage); @@ -120,7 +120,7 @@ class MessageTraitTest extends TestCase */ public function testWithoutHeader() { - $message = (new MessageTraitImplementation())->withHeader('foo', 'bar'); + $message = (new MessageTraitResponseImplementation())->withHeader('foo', 'bar'); $this->assertTrue($message->hasHeader('foo')); $newMessage = $message->withoutHeader('Foo'); @@ -133,7 +133,7 @@ class MessageTraitTest extends TestCase */ public function testGetBody() { - $message = (new MessageTraitImplementation())->setContent('Foo bar!'); + $message = (new MessageTraitResponseImplementation())->setContent('Foo bar!'); $body = $message->getBody(); $this->assertInstanceOf(StreamInterface::class, $body); @@ -150,7 +150,7 @@ class MessageTraitTest extends TestCase $stream->write('Test content'); $stream->rewind(); - $message = new MessageTraitImplementation(); + $message = new MessageTraitResponseImplementation(); $newMessage = $message->withBody($stream); $this->assertNotEquals($message, $newMessage); diff --git a/tests/Unit/Http/RequestTest.php b/tests/Unit/Http/RequestTest.php index f8444b84..f7d69aff 100644 --- a/tests/Unit/Http/RequestTest.php +++ b/tests/Unit/Http/RequestTest.php @@ -5,6 +5,8 @@ namespace Engelsystem\Test\Unit\Http; use Engelsystem\Http\Request; use PHPUnit\Framework\TestCase; use PHPUnit_Framework_MockObject_MockObject as MockObject; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\UriInterface; use Symfony\Component\HttpFoundation\Request as SymfonyRequest; class RequestTest extends TestCase @@ -16,6 +18,7 @@ class RequestTest extends TestCase { $response = new Request(); $this->assertInstanceOf(SymfonyRequest::class, $response); + $this->assertInstanceOf(RequestInterface::class, $response); } /** @@ -106,4 +109,91 @@ class RequestTest extends TestCase $this->assertEquals('http://foo.bar/bla/foo', $request->url()); $this->assertEquals('https://lorem.ipsum/dolor/sit', $request->url()); } + + /** + * @covers \Engelsystem\Http\Request::getRequestTarget + */ + public function testGetRequestTarget() + { + /** @var Request|MockObject $request */ + $request = $this + ->getMockBuilder(Request::class) + ->setMethods(['getQueryString', 'path']) + ->getMock(); + + $request->expects($this->exactly(2)) + ->method('getQueryString') + ->willReturnOnConsecutiveCalls(null, 'foo=bar&lorem=ipsum'); + $request->expects($this->exactly(2)) + ->method('path') + ->willReturn('foo/bar'); + + $this->assertEquals('/foo/bar', $request->getRequestTarget()); + $this->assertEquals('/foo/bar?foo=bar&lorem=ipsum', $request->getRequestTarget()); + } + + /** + * @covers \Engelsystem\Http\Request::withRequestTarget + */ + public function testWithRequestTarget() + { + $request = new Request(); + foreach ( + [ + '*', + '/foo/bar', + 'https://lorem.ipsum/test?lor=em' + ] as $target + ) { + $new = $request->withRequestTarget($target); + $this->assertNotEquals($request, $new); + } + } + + /** + * @covers \Engelsystem\Http\Request::withMethod + */ + public function testWithMethod() + { + $request = new Request(); + + $new = $request->withMethod('PUT'); + + $this->assertNotEquals($request, $new); + $this->assertEquals('PUT', $new->getMethod()); + } + + /** + * @covers \Engelsystem\Http\Request::withUri + */ + public function testWithUri() + { + /** @var UriInterface|MockObject $uri */ + $uri = $this->getMockForAbstractClass(UriInterface::class); + + $uri->expects($this->atLeastOnce()) + ->method('__toString') + ->willReturn('http://foo.bar/bla?foo=bar'); + + $request = Request::create('http://lor.em/'); + + $new = $request->withUri($uri); + $this->assertNotEquals($request, $new); + $this->assertEquals('http://foo.bar/bla?foo=bar', (string)$new->getUri()); + + $new = $request->withUri($uri, true); + $this->assertEquals('http://lor.em/bla?foo=bar', (string)$new->getUri()); + } + + /** + * @covers \Engelsystem\Http\Request::getUri + */ + public function testGetUri() + { + $request = Request::create('http://lor.em/test?bla=foo'); + + $uri = $request->getUri(); + $this->assertInstanceOf(UriInterface::class, $uri); + $this->assertEquals('http://lor.em/test?bla=foo', (string)$uri); + } } diff --git a/tests/Unit/Http/Stub/MessageTraitRequestImplementation.php b/tests/Unit/Http/Stub/MessageTraitRequestImplementation.php new file mode 100644 index 00000000..04d08913 --- /dev/null +++ b/tests/Unit/Http/Stub/MessageTraitRequestImplementation.php @@ -0,0 +1,12 @@ + Date: Mon, 20 Aug 2018 17:35:07 +0200 Subject: [PATCH 10/10] Made Engelsystem\Http\Request PSR-7 ServerRequestInterface compatible --- src/Http/Psr7ServiceProvider.php | 3 +- src/Http/Request.php | 320 +++++++++++++++++++- tests/Unit/Http/Psr7ServiceProviderTest.php | 5 - tests/Unit/Http/RequestTest.php | 183 +++++++++++ 4 files changed, 502 insertions(+), 9 deletions(-) diff --git a/src/Http/Psr7ServiceProvider.php b/src/Http/Psr7ServiceProvider.php index 4a3c6583..72fdef8e 100644 --- a/src/Http/Psr7ServiceProvider.php +++ b/src/Http/Psr7ServiceProvider.php @@ -18,8 +18,7 @@ class Psr7ServiceProvider extends ServiceProvider /** @var Request $request */ $request = $this->app->get('request'); - $psr7request = $psr7Factory->createRequest($request); - $this->app->instance('psr7.request', $psr7request); + $this->app->instance('psr7.request', $request); $this->app->bind(ServerRequestInterface::class, 'psr7.request'); /** @var Response $response */ diff --git a/src/Http/Request.php b/src/Http/Request.php index fd3bff42..4729606f 100644 --- a/src/Http/Request.php +++ b/src/Http/Request.php @@ -2,12 +2,15 @@ namespace Engelsystem\Http; -use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\UploadedFileInterface; use Psr\Http\Message\UriInterface; +use Symfony\Component\HttpFoundation\File\UploadedFile as SymfonyFile; use Symfony\Component\HttpFoundation\Request as SymfonyRequest; +use Zend\Diactoros\UploadedFile; use Zend\Diactoros\Uri; -class Request extends SymfonyRequest implements RequestInterface +class Request extends SymfonyRequest implements ServerRequestInterface { use MessageTrait; @@ -193,4 +196,317 @@ class Request extends SymfonyRequest implements RequestInterface return new Uri($uri); } + + /** + * Retrieve server parameters. + * + * Retrieves data related to the incoming request environment, + * typically derived from PHP's $_SERVER superglobal. The data IS NOT + * REQUIRED to originate from $_SERVER. + * + * @return array + */ + public function getServerParams() + { + return $this->server->all(); + } + + /** + * Retrieve cookies. + * + * Retrieves cookies sent by the client to the server. + * + * The data MUST be compatible with the structure of the $_COOKIE + * superglobal. + * + * @return array + */ + public function getCookieParams() + { + return $this->cookies->all(); + } + + /** + * Return an instance with the specified cookies. + * + * The data IS NOT REQUIRED to come from the $_COOKIE superglobal, but MUST + * be compatible with the structure of $_COOKIE. Typically, this data will + * be injected at instantiation. + * + * This method MUST NOT update the related Cookie header of the request + * instance, nor related values in the server params. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated cookie values. + * + * @param array $cookies Array of key/value pairs representing cookies. + * @return static + */ + public function withCookieParams(array $cookies) + { + $new = clone $this; + $new->cookies = clone $this->cookies; + $new->cookies->replace($cookies); + + return $new; + } + + /** + * Retrieve query string arguments. + * + * Retrieves the deserialized query string arguments, if any. + * + * Note: the query params might not be in sync with the URI or server + * params. If you need to ensure you are only getting the original + * values, you may need to parse the query string from `getUri()->getQuery()` + * or from the `QUERY_STRING` server param. + * + * @return array + */ + public function getQueryParams() + { + return $this->query->all(); + } + + /** + * Return an instance with the specified query string arguments. + * + * These values SHOULD remain immutable over the course of the incoming + * request. They MAY be injected during instantiation, such as from PHP's + * $_GET superglobal, or MAY be derived from some other value such as the + * URI. In cases where the arguments are parsed from the URI, the data + * MUST be compatible with what PHP's parse_str() would return for + * purposes of how duplicate query parameters are handled, and how nested + * sets are handled. + * + * Setting query string arguments MUST NOT change the URI stored by the + * request, nor the values in the server params. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated query string arguments. + * + * @param array $query Array of query string arguments, typically from + * $_GET. + * @return static + */ + public function withQueryParams(array $query) + { + $new = clone $this; + $new->query = clone $this->query; + $new->query->replace($query); + + return $new; + } + + /** + * Retrieve normalized file upload data. + * + * This method returns upload metadata in a normalized tree, with each leaf + * an instance of Psr\Http\Message\UploadedFileInterface. + * + * These values MAY be prepared from $_FILES or the message body during + * instantiation, or MAY be injected via withUploadedFiles(). + * + * @return array An array tree of UploadedFileInterface instances; an empty + * array MUST be returned if no data is present. + */ + public function getUploadedFiles() + { + $files = []; + foreach ($this->files as $file) { + /** @var SymfonyFile $file */ + + $files[] = new UploadedFile( + $file->getPath(), + $file->getSize(), + $file->getError(), + $file->getClientOriginalName(), + $file->getClientMimeType() + ); + } + + return $files; + } + + /** + * Create a new instance with the specified uploaded files. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated body parameters. + * + * @param array $uploadedFiles An array tree of UploadedFileInterface instances. + * @return static + * @throws \InvalidArgumentException if an invalid structure is provided. + */ + public function withUploadedFiles(array $uploadedFiles) + { + $new = clone $this; + $new->files = clone $this->files; + + $files = []; + foreach ($uploadedFiles as $file) { + /** @var UploadedFileInterface $file */ + $filename = tempnam(sys_get_temp_dir(), 'upload'); + $handle = fopen($filename, "w"); + fwrite($handle, $file->getStream()->getContents()); + fclose($handle); + + $files[] = new SymfonyFile( + $filename, + $file->getClientFilename(), + $file->getClientMediaType(), + $file->getSize(), + $file->getError() + ); + } + $new->files->add($files); + + return $new; + } + + /** + * Retrieve any parameters provided in the request body. + * + * If the request Content-Type is either application/x-www-form-urlencoded + * or multipart/form-data, and the request method is POST, this method MUST + * return the contents of $_POST. + * + * Otherwise, this method may return any results of deserializing + * the request body content; as parsing returns structured content, the + * potential types MUST be arrays or objects only. A null value indicates + * the absence of body content. + * + * @return null|array|object The deserialized body parameters, if any. + * These will typically be an array or object. + */ + public function getParsedBody() + { + return $this->request->all(); + } + + /** + * Return an instance with the specified body parameters. + * + * These MAY be injected during instantiation. + * + * If the request Content-Type is either application/x-www-form-urlencoded + * or multipart/form-data, and the request method is POST, use this method + * ONLY to inject the contents of $_POST. + * + * The data IS NOT REQUIRED to come from $_POST, but MUST be the results of + * deserializing the request body content. Deserialization/parsing returns + * structured data, and, as such, this method ONLY accepts arrays or objects, + * or a null value if nothing was available to parse. + * + * As an example, if content negotiation determines that the request data + * is a JSON payload, this method could be used to create a request + * instance with the deserialized parameters. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated body parameters. + * + * @param null|array|object $data The deserialized body data. This will + * typically be in an array or object. + * @return static + * @throws \InvalidArgumentException if an unsupported argument type is + * provided. + */ + public function withParsedBody($data) + { + $new = clone $this; + $new->request = clone $this->request; + + $new->request->replace($data); + + return $new; + } + + /** + * Retrieve attributes derived from the request. + * + * The request "attributes" may be used to allow injection of any + * parameters derived from the request: e.g., the results of path + * match operations; the results of decrypting cookies; the results of + * deserializing non-form-encoded message bodies; etc. Attributes + * will be application and request specific, and CAN be mutable. + * + * @return array Attributes derived from the request. + */ + public function getAttributes() + { + return $this->attributes->all(); + } + + /** + * Retrieve a single derived request attribute. + * + * Retrieves a single derived request attribute as described in + * getAttributes(). If the attribute has not been previously set, returns + * the default value as provided. + * + * This method obviates the need for a hasAttribute() method, as it allows + * specifying a default value to return if the attribute is not found. + * + * @see getAttributes() + * @param string $name The attribute name. + * @param mixed $default Default value to return if the attribute does not exist. + * @return mixed + */ + public function getAttribute($name, $default = null) + { + return $this->attributes->get($name, $default); + } + + /** + * Return an instance with the specified derived request attribute. + * + * This method allows setting a single derived request attribute as + * described in getAttributes(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated attribute. + * + * @see getAttributes() + * @param string $name The attribute name. + * @param mixed $value The value of the attribute. + * @return static + */ + public function withAttribute($name, $value) + { + $new = clone $this; + $new->attributes = clone $this->attributes; + + $new->attributes->set($name, $value); + + return $new; + } + + /** + * Return an instance that removes the specified derived request attribute. + * + * This method allows removing a single derived request attribute as + * described in getAttributes(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that removes + * the attribute. + * + * @see getAttributes() + * @param string $name The attribute name. + * @return static + */ + public function withoutAttribute($name) + { + $new = clone $this; + $new->attributes = clone $this->attributes; + + $new->attributes->remove($name); + + return $new; + } } diff --git a/tests/Unit/Http/Psr7ServiceProviderTest.php b/tests/Unit/Http/Psr7ServiceProviderTest.php index a09e9572..e14daf2a 100644 --- a/tests/Unit/Http/Psr7ServiceProviderTest.php +++ b/tests/Unit/Http/Psr7ServiceProviderTest.php @@ -50,11 +50,6 @@ class Psr7ServiceProviderTest extends ServiceProviderTest [ResponseInterface::class, 'psr7.response'] ); - $psr7Factory->expects($this->once()) - ->method('createRequest') - ->with($request) - ->willReturn($psr7request); - $serviceProvider = new Psr7ServiceProvider($app); $serviceProvider->register(); } diff --git a/tests/Unit/Http/RequestTest.php b/tests/Unit/Http/RequestTest.php index f7d69aff..916aac35 100644 --- a/tests/Unit/Http/RequestTest.php +++ b/tests/Unit/Http/RequestTest.php @@ -6,7 +6,9 @@ use Engelsystem\Http\Request; use PHPUnit\Framework\TestCase; use PHPUnit_Framework_MockObject_MockObject as MockObject; use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\UploadedFileInterface; use Psr\Http\Message\UriInterface; +use Symfony\Component\HttpFoundation\File\UploadedFile as SymfonyFile; use Symfony\Component\HttpFoundation\Request as SymfonyRequest; class RequestTest extends TestCase @@ -196,4 +198,185 @@ class RequestTest extends TestCase $this->assertInstanceOf(UriInterface::class, $uri); $this->assertEquals('http://lor.em/test?bla=foo', (string)$uri); } + + /** + * @covers \Engelsystem\Http\Request::getServerParams + */ + public function testGetServerParams() + { + $server = ['foo' => 'bar']; + $request = new Request([], [], [], [], [], $server); + + $this->assertEquals($server, $request->getServerParams()); + } + + /** + * @covers \Engelsystem\Http\Request::getCookieParams + */ + public function testGetCookieParams() + { + $cookies = ['session' => 'LoremIpsumDolorSit']; + $request = new Request([], [], [], $cookies); + + $this->assertEquals($cookies, $request->getCookieParams()); + } + + /** + * @covers \Engelsystem\Http\Request::withCookieParams + */ + public function testWithCookieParams() + { + $cookies = ['lor' => 'em']; + $request = new Request(); + + $new = $request->withCookieParams($cookies); + + $this->assertNotEquals($request, $new); + $this->assertEquals($cookies, $new->getCookieParams()); + } + + /** + * @covers \Engelsystem\Http\Request::getQueryParams + */ + public function testGetQueryParams() + { + $params = ['foo' => 'baz']; + $request = new Request($params); + + $this->assertEquals($params, $request->getQueryParams()); + } + + /** + * @covers \Engelsystem\Http\Request::withQueryParams + */ + public function testWithQueryParams() + { + $params = ['test' => 'ing']; + $request = new Request(); + + $new = $request->withQueryParams($params); + + $this->assertNotEquals($request, $new); + $this->assertEquals($params, $new->getQueryParams()); + } + + /** + * @covers \Engelsystem\Http\Request::getUploadedFiles + */ + public function testGetUploadedFiles() + { + $filename = tempnam(sys_get_temp_dir(), 'test'); + file_put_contents($filename, 'LoremIpsum!'); + $files = [new SymfonyFile($filename, 'foo.html', 'text/html', 11)]; + $request = new Request([], [], [], [], $files); + + $uploadedFiles = $request->getUploadedFiles(); + $this->assertNotEmpty($uploadedFiles); + + /** @var UploadedFileInterface $file */ + $file = $uploadedFiles[0]; + $this->assertInstanceOf(UploadedFileInterface::class, $file); + $this->assertEquals('foo.html', $file->getClientFilename()); + $this->assertEquals('text/html', $file->getClientMediaType()); + $this->assertEquals(11, $file->getSize()); + } + + /** + * @covers \Engelsystem\Http\Request::withUploadedFiles + */ + public function testWithUploadedFiles() + { + $filename = tempnam(sys_get_temp_dir(), 'test'); + file_put_contents($filename, 'LoremIpsum!'); + $file = new \Zend\Diactoros\UploadedFile($filename, 11, UPLOAD_ERR_OK, 'test.txt', 'text/plain'); + + $request = new Request(); + $new = $request->withUploadedFiles([$file]); + $uploadedFiles = $new->getUploadedFiles(); + $this->assertNotEquals($request, $new); + $this->assertNotEmpty($uploadedFiles); + + /** @var UploadedFileInterface $file */ + $file = $uploadedFiles[0]; + $this->assertEquals('test.txt', $file->getClientFilename()); + $this->assertEquals('text/plain', $file->getClientMediaType()); + $this->assertEquals(11, $file->getSize()); + } + + /** + * @covers \Engelsystem\Http\Request::getParsedBody + */ + public function testGetParsedBody() + { + $body = ['foo' => 'lorem']; + $request = new Request(); + $request->request->add($body); + + $this->assertEquals($body, $request->getParsedBody()); + } + + /** + * @covers \Engelsystem\Http\Request::withParsedBody + */ + public function testWithParsedBody() + { + $data = ['test' => 'er']; + $request = new Request(); + + $new = $request->withParsedBody($data); + + $this->assertNotEquals($request, $new); + $this->assertEquals($data, $new->getParsedBody()); + } + + /** + * @covers \Engelsystem\Http\Request::getAttributes + */ + public function testGetAttributes() + { + $attributes = ['foo' => 'lorem', 'ipsum' => 'dolor']; + $request = new Request([], [], $attributes); + + $this->assertEquals($attributes, $request->getAttributes()); + } + + /** + * @covers \Engelsystem\Http\Request::getAttribute + */ + public function testGetAttribute() + { + $attributes = ['foo' => 'lorem', 'ipsum' => 'dolor']; + $request = new Request([], [], $attributes); + + $this->assertEquals($attributes['ipsum'], $request->getAttribute('ipsum')); + $this->assertEquals(null, $request->getAttribute('dolor')); + $this->assertEquals(1234, $request->getAttribute('test', 1234)); + } + + /** + * @covers \Engelsystem\Http\Request::withAttribute + */ + public function testWithAttribute() + { + $request = new Request(); + + $new = $request->withAttribute('lorem', 'ipsum'); + + $this->assertNotEquals($request, $new); + $this->assertEquals('ipsum', $new->getAttribute('lorem')); + } + + /** + * @covers \Engelsystem\Http\Request::withoutAttribute + */ + public function testWithoutAttribute() + { + $attributes = ['foo' => 'lorem', 'ipsum' => 'dolor']; + $request = new Request([], [], $attributes); + + $new = $request->withoutAttribute('ipsum'); + + $this->assertNotEquals($request, $new); + $this->assertEquals(['foo' => 'lorem'], $new->getAttributes()); + } }