From 86dd6d7ae425df143a1a3930067c4285fc5c0122 Mon Sep 17 00:00:00 2001 From: Gustavo Freze Date: Thu, 19 Dec 2024 11:31:12 -0300 Subject: [PATCH] Fixes header construction. --- composer.json | 7 +- src/CacheControl.php | 2 +- src/ContentType.php | 10 +-- src/Internal/Response/ResponseHeaders.php | 6 +- tests/Drivers/Laminas/LaminasTest.php | 86 +++++++++++++++++++++++ tests/Drivers/Slim/RequestFactory.php | 41 ----------- tests/Drivers/Slim/SlimTest.php | 42 +++++++++-- tests/Response/HeadersTest.php | 37 +++++++--- tests/ResponseTest.php | 18 ++--- 9 files changed, 169 insertions(+), 80 deletions(-) create mode 100644 tests/Drivers/Laminas/LaminasTest.php delete mode 100644 tests/Drivers/Slim/RequestFactory.php diff --git a/composer.json b/composer.json index 795d6e1..e8a57de 100644 --- a/composer.json +++ b/composer.json @@ -51,13 +51,14 @@ "ext-mbstring": "*" }, "require-dev": { - "slim/psr7": "^1.7", - "slim/slim": "^4.14", + "slim/psr7": "^1", + "slim/slim": "^4", "phpmd/phpmd": "^2.15", "phpstan/phpstan": "^1", "phpunit/phpunit": "^11", "infection/infection": "^0", - "squizlabs/php_codesniffer": "^3.11" + "squizlabs/php_codesniffer": "^3.11", + "laminas/laminas-httphandlerrunner": "^2" }, "suggest": { "ext-mbstring": "Provides multibyte-specific string functions that help us deal with multibyte encodings in PHP." diff --git a/src/CacheControl.php b/src/CacheControl.php index 6f474c7..bb8e53c 100644 --- a/src/CacheControl.php +++ b/src/CacheControl.php @@ -24,6 +24,6 @@ public static function fromResponseDirectives(ResponseCacheDirectives ...$direct public function toArray(): array { - return ['Cache-Control' => implode(', ', $this->directives)]; + return ['Cache-Control' => [implode(', ', $this->directives)]]; } } diff --git a/src/ContentType.php b/src/ContentType.php index e5b3d1d..221ed9f 100644 --- a/src/ContentType.php +++ b/src/ContentType.php @@ -48,10 +48,10 @@ public static function applicationFormUrlencoded(?Charset $charset = null): Cont public function toArray(): array { - return [ - 'Content-Type' => $this->charset - ? sprintf('%s; %s', $this->mimeType->value, $this->charset->toString()) - : $this->mimeType->value - ]; + $value = $this->charset + ? sprintf('%s; %s', $this->mimeType->value, $this->charset->toString()) + : $this->mimeType->value; + + return ['Content-Type' => [$value]]; } } diff --git a/src/Internal/Response/ResponseHeaders.php b/src/Internal/Response/ResponseHeaders.php index fb5e008..da08f65 100644 --- a/src/Internal/Response/ResponseHeaders.php +++ b/src/Internal/Response/ResponseHeaders.php @@ -25,15 +25,14 @@ public static function fromOrDefault(Headers ...$headers): ResponseHeaders public static function fromNameAndValue(string $name, mixed $value): ResponseHeaders { - return new ResponseHeaders(headers: [$name => $value]); + return new ResponseHeaders(headers: [$name => [$value]]); } public function getByName(string $name): array { $headers = array_change_key_case($this->headers); - $value = $headers[strtolower($name)] ?? []; - return is_array($value) ? $value : [$value]; + return $headers[strtolower($name)] ?? []; } public function hasHeader(string $name): bool @@ -53,7 +52,6 @@ public function removeByName(string $name): ResponseHeaders return new ResponseHeaders(headers: $headers); } - public function toArray(): array { return $this->headers; diff --git a/tests/Drivers/Laminas/LaminasTest.php b/tests/Drivers/Laminas/LaminasTest.php new file mode 100644 index 0000000..53360bf --- /dev/null +++ b/tests/Drivers/Laminas/LaminasTest.php @@ -0,0 +1,86 @@ +emitter = new SapiEmitter(); + $this->middleware = new Middleware(); + } + + /** + * @throws Exception + */ + public function testSuccessfulRequestProcessingWithLaminas(): void + { + /** @Given a valid request */ + $request = $this->createMock(ServerRequestInterface::class); + + /** @And the Content-Type for the response is set to application/json with UTF-8 charset */ + $contentType = ContentType::applicationJson(charset: Charset::UTF_8); + + /** @And a Cache-Control header is set with no-cache directive */ + $cacheControl = CacheControl::fromResponseDirectives(noCache: ResponseCacheDirectives::noCache()); + + /** @And an HTTP response is created with a 200 OK status and a body containing the creation timestamp */ + $response = Response::ok(['createdAt' => date(DateTimeInterface::ATOM)], $contentType, $cacheControl); + + /** @When the request is processed by the handler */ + $actual = $this->middleware->process(request: $request, handler: new Endpoint(response: $response)); + + /** @Then the response status should indicate success */ + self::assertSame(Code::OK->value, $actual->getStatusCode()); + + /** @And the response body should match the expected body */ + self::assertSame($response->getBody()->getContents(), $actual->getBody()->getContents()); + + /** @And the response headers should match the expected headers */ + self::assertSame($response->getHeaders(), $actual->getHeaders()); + } + + public function testResponseEmissionWithLaminas(): void + { + /** @Given the Content-Type for the response is set to application/json with UTF-8 charset */ + $contentType = ContentType::applicationJson(charset: Charset::UTF_8); + + /** @And a Cache-Control header is set with no-cache directive */ + $cacheControl = CacheControl::fromResponseDirectives(noCache: ResponseCacheDirectives::noCache()); + + /** @And an HTTP response is created with a 200 OK status and a body containing the creation timestamp */ + $response = Response::ok( + ['createdAt' => date(DateTimeInterface::ATOM)], + $contentType, + $cacheControl + )->withHeader(name: 'X-Request-ID', value: '123456'); + + /** @When the response is emitted */ + ob_start(); + $this->emitter->emit($response); + $actual = ob_get_clean(); + + /** @Then the emitted response content should match the response body */ + self::assertSame($response->getBody()->__toString(), $actual); + } +} diff --git a/tests/Drivers/Slim/RequestFactory.php b/tests/Drivers/Slim/RequestFactory.php deleted file mode 100644 index 566ee8d..0000000 --- a/tests/Drivers/Slim/RequestFactory.php +++ /dev/null @@ -1,41 +0,0 @@ -createUri() - ->withScheme('https') - ->withHost('localhost') - ->withPath('/'); - - /** @var resource $stream */ - $stream = fopen('php://temp', 'r+'); - - fwrite($stream, json_encode($payload)); - rewind($stream); - - $body = new Stream($stream); - $headers = new Headers(['Content-Type' => 'application/json']); - - return new SlimRequest( - method: 'POST', - uri: $uri, - headers: $headers, - cookies: [], - serverParams: [], - body: $body - ); - } -} diff --git a/tests/Drivers/Slim/SlimTest.php b/tests/Drivers/Slim/SlimTest.php index 8cd39ae..0263604 100644 --- a/tests/Drivers/Slim/SlimTest.php +++ b/tests/Drivers/Slim/SlimTest.php @@ -5,7 +5,10 @@ namespace TinyBlocks\Http\Drivers\Slim; use DateTimeInterface; +use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\TestCase; +use Psr\Http\Message\ServerRequestInterface; +use Slim\ResponseEmitter; use TinyBlocks\Http\CacheControl; use TinyBlocks\Http\Charset; use TinyBlocks\Http\Code; @@ -17,20 +20,23 @@ final class SlimTest extends TestCase { + private ResponseEmitter $emitter; + private Middleware $middleware; protected function setUp(): void { + $this->emitter = new ResponseEmitter(); $this->middleware = new Middleware(); } - public function testSuccessfulResponse(): void + /** + * @throws Exception + */ + public function testSuccessfulRequestProcessingWithSlim(): void { - /** @Given valid data */ - $payload = ['id' => PHP_INT_MAX, 'name' => 'Drakkor Emberclaw']; - - /** @And this data is used to create a request */ - $request = RequestFactory::postFrom(payload: $payload); + /** @Given a valid request */ + $request = $this->createMock(ServerRequestInterface::class); /** @And the Content-Type for the response is set to application/json with UTF-8 charset */ $contentType = ContentType::applicationJson(charset: Charset::UTF_8); @@ -53,4 +59,28 @@ public function testSuccessfulResponse(): void /** @And the response headers should match the expected headers */ self::assertSame($response->getHeaders(), $actual->getHeaders()); } + + public function testResponseEmissionWithSlim(): void + { + /** @Given the Content-Type for the response is set to application/json with UTF-8 charset */ + $contentType = ContentType::applicationJson(charset: Charset::UTF_8); + + /** @And a Cache-Control header is set with no-cache directive */ + $cacheControl = CacheControl::fromResponseDirectives(noCache: ResponseCacheDirectives::noCache()); + + /** @And an HTTP response is created with a 200 OK status and a body containing the creation timestamp */ + $response = Response::ok( + ['createdAt' => date(DateTimeInterface::ATOM)], + $contentType, + $cacheControl + )->withHeader(name: 'X-Request-ID', value: '123456'); + + /** @When the response is emitted */ + ob_start(); + $this->emitter->emit($response); + $actual = ob_get_clean(); + + /** @Then the emitted response content should match the response body */ + self::assertSame($response->getBody()->__toString(), $actual); + } } diff --git a/tests/Response/HeadersTest.php b/tests/Response/HeadersTest.php index ca9c0b2..430ce06 100644 --- a/tests/Response/HeadersTest.php +++ b/tests/Response/HeadersTest.php @@ -18,7 +18,7 @@ public function testResponseWithCustomHeaders(): void $response = Response::noContent(); /** @And by default, the response contains the 'Content-Type' header set to 'application/json; charset=utf-8' */ - self::assertSame(['Content-Type' => 'application/json; charset=utf-8'], $response->getHeaders()); + self::assertSame(['Content-Type' => ['application/json; charset=utf-8']], $response->getHeaders()); /** @When we add custom headers to the response */ $actual = $response @@ -27,7 +27,7 @@ public function testResponseWithCustomHeaders(): void /** @Then the response should contain the correct headers */ self::assertSame( - ['Content-Type' => 'application/json; charset=utf-8', 'X-ID' => 100, 'X-NAME' => 'Xpto'], + ['Content-Type' => ['application/json; charset=utf-8'], 'X-ID' => [100], 'X-NAME' => ['Xpto']], $actual->getHeaders() ); @@ -37,7 +37,7 @@ public function testResponseWithCustomHeaders(): void /** @Then the response should contain the updated 'X-ID' header value */ self::assertSame('200', $actual->withAddedHeader(name: 'X-ID', value: 200)->getHeaderLine(name: 'X-ID')); self::assertSame( - ['Content-Type' => 'application/json; charset=utf-8', 'X-ID' => 200, 'X-NAME' => 'Xpto'], + ['Content-Type' => ['application/json; charset=utf-8'], 'X-ID' => [200], 'X-NAME' => ['Xpto']], $actual->getHeaders() ); @@ -45,7 +45,10 @@ public function testResponseWithCustomHeaders(): void $actual = $actual->withoutHeader(name: 'X-NAME'); /** @Then the response should contain only the 'X-ID' header and the default 'Content-Type' header */ - self::assertSame(['Content-Type' => 'application/json; charset=utf-8', 'X-ID' => 200], $actual->getHeaders()); + self::assertSame( + ['Content-Type' => ['application/json; charset=utf-8'], 'X-ID' => [200]], + $actual->getHeaders() + ); } public function testResponseWithDuplicatedHeader(): void @@ -62,7 +65,19 @@ public function testResponseWithDuplicatedHeader(): void self::assertSame('application/json; charset=ISO-8859-1', $actual->getHeaderLine(name: 'Content-Type')); /** @And the headers should only contain the last 'Content-Type' value */ - self::assertSame(['Content-Type' => 'application/json; charset=ISO-8859-1'], $actual->getHeaders()); + self::assertSame(['Content-Type' => ['application/json; charset=ISO-8859-1']], $actual->getHeaders()); + } + + public function testResponseHeadersWithNoCustomHeader(): void + { + /** @Given an HTTP response with no custom headers */ + $response = Response::noContent(); + + /** @When we retrieve the header that doesn't exist */ + $actual = $response->getHeader(name: 'Non-Existent-Header'); + + /** @Then the header should return an empty array */ + self::assertSame([], $actual); } public function testResponseWithCacheControl(): void @@ -108,7 +123,7 @@ public function testResponseWithContentTypePDF(): void self::assertSame($expected, $actual->getHeaderLine(name: 'Content-Type')); self::assertSame([$expected], $actual->getHeader(name: 'Content-Type')); - self::assertSame(['Content-Type' => $expected], $actual->getHeaders()); + self::assertSame(['Content-Type' => [$expected]], $actual->getHeaders()); } public function testResponseWithContentTypeHTML(): void @@ -127,7 +142,7 @@ public function testResponseWithContentTypeHTML(): void self::assertSame($expected, $actual->getHeaderLine(name: 'Content-Type')); self::assertSame([$expected], $actual->getHeader(name: 'Content-Type')); - self::assertSame(['Content-Type' => $expected], $actual->getHeaders()); + self::assertSame(['Content-Type' => [$expected]], $actual->getHeaders()); } public function testResponseWithContentTypeJSON(): void @@ -146,7 +161,7 @@ public function testResponseWithContentTypeJSON(): void self::assertSame($expected, $actual->getHeaderLine(name: 'Content-Type')); self::assertSame([$expected], $actual->getHeader(name: 'Content-Type')); - self::assertSame(['Content-Type' => $expected], $actual->getHeaders()); + self::assertSame(['Content-Type' => [$expected]], $actual->getHeaders()); } public function testResponseWithContentTypePlainText(): void @@ -165,7 +180,7 @@ public function testResponseWithContentTypePlainText(): void self::assertSame($expected, $actual->getHeaderLine(name: 'Content-Type')); self::assertSame([$expected], $actual->getHeader(name: 'Content-Type')); - self::assertSame(['Content-Type' => $expected], $actual->getHeaders()); + self::assertSame(['Content-Type' => [$expected]], $actual->getHeaders()); } public function testResponseWithContentTypeOctetStream(): void @@ -184,7 +199,7 @@ public function testResponseWithContentTypeOctetStream(): void self::assertSame($expected, $actual->getHeaderLine(name: 'Content-Type')); self::assertSame([$expected], $actual->getHeader(name: 'Content-Type')); - self::assertSame(['Content-Type' => $expected], $actual->getHeaders()); + self::assertSame(['Content-Type' => [$expected]], $actual->getHeaders()); } public function testResponseWithContentTypeFormUrlencoded(): void @@ -203,6 +218,6 @@ public function testResponseWithContentTypeFormUrlencoded(): void self::assertSame($expected, $actual->getHeaderLine(name: 'Content-Type')); self::assertSame([$expected], $actual->getHeader(name: 'Content-Type')); - self::assertSame(['Content-Type' => $expected], $actual->getHeaders()); + self::assertSame(['Content-Type' => [$expected]], $actual->getHeaders()); } } diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index 6f98c0b..dabc7bb 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -49,7 +49,7 @@ public function testResponseOk(): void self::assertSame(Code::OK->message(), $actual->getReasonPhrase()); /** @And the headers should contain Content-Type as application/json with charset=utf-8 */ - self::assertSame(['Content-Type' => 'application/json; charset=utf-8'], $actual->getHeaders()); + self::assertSame(['Content-Type' => ['application/json; charset=utf-8']], $actual->getHeaders()); } public function testResponseCreated(): void @@ -81,7 +81,7 @@ public function testResponseCreated(): void self::assertSame(Code::CREATED->message(), $actual->getReasonPhrase()); /** @And the headers should contain Content-Type as application/json with charset=utf-8 */ - self::assertSame(['Content-Type' => 'application/json; charset=utf-8'], $actual->getHeaders()); + self::assertSame(['Content-Type' => ['application/json; charset=utf-8']], $actual->getHeaders()); } public function testResponseAccepted(): void @@ -111,7 +111,7 @@ public function testResponseAccepted(): void self::assertSame(Code::ACCEPTED->message(), $actual->getReasonPhrase()); /** @And the headers should contain Content-Type as application/json with charset=utf-8 */ - self::assertSame(['Content-Type' => 'application/json; charset=utf-8'], $actual->getHeaders()); + self::assertSame(['Content-Type' => ['application/json; charset=utf-8']], $actual->getHeaders()); } public function testResponseNoContent(): void @@ -136,7 +136,7 @@ public function testResponseNoContent(): void self::assertSame(Code::NO_CONTENT->message(), $actual->getReasonPhrase()); /** @And the headers should contain Content-Type as application/json with charset=utf-8 */ - self::assertSame(['Content-Type' => 'application/json; charset=utf-8'], $actual->getHeaders()); + self::assertSame(['Content-Type' => ['application/json; charset=utf-8']], $actual->getHeaders()); } public function testResponseBadRequest(): void @@ -166,7 +166,7 @@ public function testResponseBadRequest(): void self::assertSame(Code::BAD_REQUEST->message(), $actual->getReasonPhrase()); /** @And the headers should contain Content-Type as application/json with charset=utf-8 */ - self::assertSame(['Content-Type' => 'application/json; charset=utf-8'], $actual->getHeaders()); + self::assertSame(['Content-Type' => ['application/json; charset=utf-8']], $actual->getHeaders()); } public function testResponseNotFound(): void @@ -196,7 +196,7 @@ public function testResponseNotFound(): void self::assertSame(Code::NOT_FOUND->message(), $actual->getReasonPhrase()); /** @And the headers should contain Content-Type as application/json with charset=utf-8 */ - self::assertSame(['Content-Type' => 'application/json; charset=utf-8'], $actual->getHeaders()); + self::assertSame(['Content-Type' => ['application/json; charset=utf-8']], $actual->getHeaders()); } public function testResponseConflict(): void @@ -226,7 +226,7 @@ public function testResponseConflict(): void self::assertSame(Code::CONFLICT->message(), $actual->getReasonPhrase()); /** @And the headers should contain Content-Type as application/json with charset=utf-8 */ - self::assertSame(['Content-Type' => 'application/json; charset=utf-8'], $actual->getHeaders()); + self::assertSame(['Content-Type' => ['application/json; charset=utf-8']], $actual->getHeaders()); } public function testResponseUnprocessableEntity(): void @@ -256,7 +256,7 @@ public function testResponseUnprocessableEntity(): void self::assertSame(Code::UNPROCESSABLE_ENTITY->message(), $actual->getReasonPhrase()); /** @And the headers should contain Content-Type as application/json with charset=utf-8 */ - self::assertSame(['Content-Type' => 'application/json; charset=utf-8'], $actual->getHeaders()); + self::assertSame(['Content-Type' => ['application/json; charset=utf-8']], $actual->getHeaders()); } public function testResponseInternalServerError(): void @@ -286,7 +286,7 @@ public function testResponseInternalServerError(): void self::assertSame(Code::INTERNAL_SERVER_ERROR->message(), $actual->getReasonPhrase()); /** @And the headers should contain Content-Type as application/json with charset=utf-8 */ - self::assertSame(['Content-Type' => 'application/json; charset=utf-8'], $actual->getHeaders()); + self::assertSame(['Content-Type' => ['application/json; charset=utf-8']], $actual->getHeaders()); } #[DataProvider('bodyProviderData')]