diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e898dc1..6be2ffc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,6 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ env.PHP_VERSION }} - extensions: bcmath tools: composer:2 - name: Validate composer.json @@ -52,7 +51,6 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ env.PHP_VERSION }} - extensions: bcmath tools: composer:2 - name: Download vendor artifact from build @@ -77,7 +75,6 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ env.PHP_VERSION }} - extensions: bcmath tools: composer:2 - name: Download vendor artifact from build diff --git a/infection.json.dist b/infection.json.dist index 9e795b5..45c49fc 100644 --- a/infection.json.dist +++ b/infection.json.dist @@ -16,8 +16,7 @@ "customPath": "./vendor/bin/phpunit" }, "mutators": { - "@default": true, - "LogicalOr": false + "@default": true }, "minCoveredMsi": 100, "testFramework": "phpunit" diff --git a/src/Internal/Response/Stream/Stream.php b/src/Internal/Response/Stream/Stream.php index 2954aa6..41734fb 100644 --- a/src/Internal/Response/Stream/Stream.php +++ b/src/Internal/Response/Stream/Stream.php @@ -132,13 +132,7 @@ public function isWritable(): bool return false; } - $mode = $this->metaData->getMode(); - - return str_contains($mode, 'x') - || str_contains($mode, 'w') - || str_contains($mode, 'c') - || str_contains($mode, 'a') - || str_contains($mode, '+'); + return strpbrk($this->metaData->getMode(), 'xwca+') !== false; } public function isSeekable(): bool diff --git a/src/Response.php b/src/Response.php index c96da70..411c43b 100644 --- a/src/Response.php +++ b/src/Response.php @@ -34,6 +34,16 @@ public static function badRequest(mixed $body, Headers ...$headers): ResponseInt return InternalResponse::createWithBody($body, Code::BAD_REQUEST, ...$headers); } + public static function unauthorized(mixed $body, Headers ...$headers): ResponseInterface + { + return InternalResponse::createWithBody($body, Code::UNAUTHORIZED, ...$headers); + } + + public static function forbidden(mixed $body, Headers ...$headers): ResponseInterface + { + return InternalResponse::createWithBody($body, Code::FORBIDDEN, ...$headers); + } + public static function notFound(mixed $body, Headers ...$headers): ResponseInterface { return InternalResponse::createWithBody($body, Code::NOT_FOUND, ...$headers); diff --git a/src/Responses.php b/src/Responses.php index 1ac5aac..9ee1392 100644 --- a/src/Responses.php +++ b/src/Responses.php @@ -57,6 +57,24 @@ public static function noContent(Headers ...$headers): ResponseInterface; */ public static function badRequest(mixed $body, Headers ...$headers): ResponseInterface; + /** + * Creates a response with a 401 Unauthorized status. + * + * @param mixed $body The body of the response. + * @param Headers ...$headers Optional additional headers for the response. + * @return ResponseInterface The generated 401 Unauthorized response. + */ + public static function unauthorized(mixed $body, Headers ...$headers): ResponseInterface; + + /** + * Creates a response with a 403 Forbidden status. + * + * @param mixed $body The body of the response. + * @param Headers ...$headers Optional additional headers for the response. + * @return ResponseInterface The generated 403 Forbidden response. + */ + public static function forbidden(mixed $body, Headers ...$headers): ResponseInterface; + /** * Creates a response with a 404 Not Found status. * diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index dabc7bb..353ea83 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -169,6 +169,66 @@ public function testResponseBadRequest(): void self::assertSame(['Content-Type' => ['application/json; charset=utf-8']], $actual->getHeaders()); } + public function testResponseUnauthorized(): void + { + /** @Given a body with error details */ + $body = [ + 'error' => 'Unauthorized', + 'message' => 'Authentication is required to access this resource.' + ]; + + /** @When we create the HTTP response with this body */ + $actual = Response::unauthorized(body: $body); + + /** @Then the protocol version should be "1.1" */ + self::assertSame('1.1', $actual->getProtocolVersion()); + + /** @And the body of the response should match the JSON-encoded body */ + self::assertSame(json_encode($body, JSON_PRESERVE_ZERO_FRACTION), $actual->getBody()->__toString()); + self::assertSame(json_encode($body, JSON_PRESERVE_ZERO_FRACTION), $actual->getBody()->getContents()); + + /** @And the status code should be 401 */ + self::assertSame(Code::UNAUTHORIZED->value, $actual->getStatusCode()); + self::assertTrue(Code::isValidCode(code: $actual->getStatusCode())); + self::assertTrue(Code::isErrorCode(code: $actual->getStatusCode())); + + /** @And the reason phrase should be "Unauthorized" */ + self::assertSame(Code::UNAUTHORIZED->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()); + } + + public function testResponseForbidden(): void + { + /** @Given a body with error details */ + $body = [ + 'error' => 'Forbidden', + 'message' => 'You do not have permission to access this resource.' + ]; + + /** @When we create the HTTP response with this body */ + $actual = Response::forbidden(body: $body); + + /** @Then the protocol version should be "1.1" */ + self::assertSame('1.1', $actual->getProtocolVersion()); + + /** @And the body of the response should match the JSON-encoded body */ + self::assertSame(json_encode($body, JSON_PRESERVE_ZERO_FRACTION), $actual->getBody()->__toString()); + self::assertSame(json_encode($body, JSON_PRESERVE_ZERO_FRACTION), $actual->getBody()->getContents()); + + /** @And the status code should be 403 */ + self::assertSame(Code::FORBIDDEN->value, $actual->getStatusCode()); + self::assertTrue(Code::isValidCode(code: $actual->getStatusCode())); + self::assertTrue(Code::isErrorCode(code: $actual->getStatusCode())); + + /** @And the reason phrase should be "Forbidden" */ + self::assertSame(Code::FORBIDDEN->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()); + } + public function testResponseNotFound(): void { /** @Given a body with error details */