diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..22aac70
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,14 @@
+/tests export-ignore
+/vendor export-ignore
+
+/LICENSE export-ignore
+/Makefile export-ignore
+/README.md export-ignore
+/phpmd.xml export-ignore
+/phpunit.xml export-ignore
+/phpstan.neon.dist export-ignore
+/infection.json.dist export-ignore
+
+/.github export-ignore
+/.gitignore export-ignore
+/.gitattributes export-ignore
diff --git a/Makefile b/Makefile
index 9bf35fe..1b3026e 100644
--- a/Makefile
+++ b/Makefile
@@ -1,14 +1,17 @@
DOCKER_RUN = docker run --rm -it --net=host -v ${PWD}:/app -w /app gustavofreze/php:8.2
-.PHONY: configure test test-no-coverage review show-reports clean
+.PHONY: configure test test-file test-no-coverage review show-reports clean
configure:
@${DOCKER_RUN} composer update --optimize-autoloader
-test: review
+test:
@${DOCKER_RUN} composer tests
-test-no-coverage: review
+test-file:
+ @${DOCKER_RUN} composer tests-file-no-coverage ${FILE}
+
+test-no-coverage:
@${DOCKER_RUN} composer tests-no-coverage
review:
@@ -19,4 +22,4 @@ show-reports:
clean:
@sudo chown -R ${USER}:${USER} ${PWD}
- @rm -rf report vendor
+ @rm -rf report vendor .phpunit.cache
diff --git a/composer.json b/composer.json
index a8a1df1..95ec214 100644
--- a/composer.json
+++ b/composer.json
@@ -8,9 +8,6 @@
"minimum-stability": "stable",
"keywords": [
"psr",
- "psr-4",
- "psr-7",
- "psr-12",
"http",
"http-code",
"tiny-blocks",
@@ -24,6 +21,10 @@
"homepage": "https://github.com/gustavofreze"
}
],
+ "support": {
+ "issues": "https://github.com/tiny-blocks/http/issues",
+ "source": "https://github.com/tiny-blocks/http"
+ },
"config": {
"sort-packages": true,
"allow-plugins": {
@@ -42,15 +43,16 @@
},
"require": {
"php": "^8.2",
- "tiny-blocks/serializer": "^3.0",
+ "tiny-blocks/serializer": "^3",
"psr/http-message": "^1.1",
"ext-mbstring": "*"
},
"require-dev": {
- "infection/infection": "^0.27",
"phpmd/phpmd": "^2.15",
- "phpunit/phpunit": "^10.5",
- "squizlabs/php_codesniffer": "^3.8"
+ "phpunit/phpunit": "^11",
+ "phpstan/phpstan": "^1",
+ "infection/infection": "^0.29",
+ "squizlabs/php_codesniffer": "^3.10"
},
"suggest": {
"ext-mbstring": "Provides multibyte-specific string functions that help us deal with multibyte encodings in PHP."
@@ -58,13 +60,15 @@
"scripts": {
"phpcs": "phpcs --standard=PSR12 --extensions=php ./src",
"phpmd": "phpmd ./src text phpmd.xml --suffixes php --exclude /src/HttpCode.php --exclude /src/Internal/Response --ignore-violations-on-exit",
+ "phpstan": "phpstan analyse -c phpstan.neon.dist --quiet --no-progress",
"test": "phpunit --log-junit=report/coverage/junit.xml --coverage-xml=report/coverage/coverage-xml --coverage-html=report/coverage/coverage-html tests",
"test-mutation": "infection --only-covered --logger-html=report/coverage/mutation-report.html --coverage=report/coverage --min-msi=100 --min-covered-msi=100 --threads=4",
"test-no-coverage": "phpunit --no-coverage",
"test-mutation-no-coverage": "infection --only-covered --min-msi=100 --threads=4",
"review": [
"@phpcs",
- "@phpmd"
+ "@phpmd",
+ "@phpstan"
],
"tests": [
"@test",
@@ -73,6 +77,9 @@
"tests-no-coverage": [
"@test-no-coverage",
"@test-mutation-no-coverage"
+ ],
+ "tests-file-no-coverage": [
+ "@test-no-coverage"
]
}
}
diff --git a/infection.json.dist b/infection.json.dist
index dcff8e5..739162f 100644
--- a/infection.json.dist
+++ b/infection.json.dist
@@ -1,22 +1,25 @@
{
- "$schema": "vendor/infection/infection/resources/schema.json",
- "tmpDir": "report/",
- "logs": {
- "text": "report/logs/infection-text.log",
- "summary": "report/logs/infection-summary.log"
- },
+ "timeout": 10,
+ "testFramework": "phpunit",
+ "tmpDir": "report/infection/",
"source": {
"directories": [
"src"
]
},
- "timeout": 10,
+ "logs": {
+ "text": "report/infection/logs/infection-text.log",
+ "summary": "report/infection/logs/infection-summary.log"
+ },
"mutators": {
"@default": true,
- "LogicalOr": false,
- "InstanceOf_": false,
- "UnwrapArrayMap": false,
+ "CastInt": false,
+ "CastString": false,
+ "MatchArmRemoval": false,
"MethodCallRemoval": false
},
- "testFramework": "phpunit"
+ "phpUnit": {
+ "configDir": "",
+ "customPath": "./vendor/bin/phpunit"
+ }
}
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
new file mode 100644
index 0000000..ffd0c26
--- /dev/null
+++ b/phpstan.neon.dist
@@ -0,0 +1,11 @@
+parameters:
+ paths:
+ - src
+ level: 9
+ tmpDir: report/phpstan
+ ignoreErrors:
+ - '#function fread expects#'
+ - '#expects object, mixed given#'
+ - '#expects resource, resource#'
+ - '#value type specified in iterable#'
+ reportUnmatchedIgnoredErrors: false
diff --git a/phpunit.xml b/phpunit.xml
index a325068..7f080dd 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -1,35 +1,35 @@
+ beStrictAboutOutputDuringTests="true">
-
-
-
-
-
-
-
+
- tests
+ tests
+
+
+
+
+
+
+
+
-
-
diff --git a/src/HttpCode.php b/src/HttpCode.php
index 3a51b1a..a62397f 100644
--- a/src/HttpCode.php
+++ b/src/HttpCode.php
@@ -1,5 +1,7 @@
detach();
fclose($resource);
}
- public function detach(): mixed
+ public function detach()
{
$resource = $this->resource;
$this->resource = null;
@@ -49,7 +61,9 @@ public function getSize(): ?int
return null;
}
- return intval(fstat($this->resource)['size']);
+ $size = fstat($this->resource);
+
+ return is_array($size) ? (int)$size['size'] : null;
}
public function tell(): int
@@ -58,7 +72,7 @@ public function tell(): int
throw new MissingResourceStream();
}
- return ftell($this->resource);
+ return (int)ftell($this->resource);
}
public function eof(): bool
@@ -86,7 +100,7 @@ public function read(int $length): string
throw new NonReadableStream();
}
- return fread($this->resource, $length);
+ return (string)fread($this->resource, $length);
}
public function write(string $string): int
@@ -95,7 +109,7 @@ public function write(string $string): int
throw new NonWritableStream();
}
- return fwrite($this->resource, $string);
+ return (int)fwrite($this->resource, $string);
}
public function isReadable(): bool
@@ -117,11 +131,11 @@ public function isWritable(): bool
$mode = $this->metaData->getMode();
- return strstr($mode, 'x')
- || strstr($mode, 'w')
- || strstr($mode, 'c')
- || strstr($mode, 'a')
- || strstr($mode, '+');
+ return str_contains($mode, 'x')
+ || str_contains($mode, 'w')
+ || str_contains($mode, 'c')
+ || str_contains($mode, 'a')
+ || str_contains($mode, '+');
}
public function isSeekable(): bool
@@ -136,7 +150,7 @@ public function getContents(): string
}
if (!$this->contentFetched) {
- $this->content = stream_get_contents($this->resource);
+ $this->content = (string)stream_get_contents($this->resource);
$this->contentFetched = true;
}
diff --git a/src/Internal/Stream/StreamFactory.php b/src/Internal/Stream/StreamFactory.php
index e9cfde9..52695cc 100644
--- a/src/Internal/Stream/StreamFactory.php
+++ b/src/Internal/Stream/StreamFactory.php
@@ -1,5 +1,7 @@
write(string: json_encode($data->toArray()));
- $stream->rewind();
-
- return $stream;
- }
-
- if (is_object($data)) {
- $stream->write(string: json_encode(get_object_vars($data)));
- $stream->rewind();
-
- return $stream;
- }
-
- if (is_scalar($data) || is_array($data)) {
- $stream->write(string: json_encode($data));
- $stream->rewind();
-
- return $stream;
- }
+ $dataToWrite = match (true) {
+ is_a($data, Serializer::class) => $data->toJson(),
+ is_object($data) => (string)json_encode(get_object_vars($data)),
+ is_scalar($data) || is_array($data) => (string)json_encode($data, JSON_PRESERVE_ZERO_FRACTION),
+ default => ''
+ };
- $stream->write(string: '');
+ $stream->write(string: $dataToWrite);
$stream->rewind();
return $stream;
diff --git a/src/Internal/Stream/StreamMetaData.php b/src/Internal/Stream/StreamMetaData.php
index 40b5e97..7a85248 100644
--- a/src/Internal/Stream/StreamMetaData.php
+++ b/src/Internal/Stream/StreamMetaData.php
@@ -1,5 +1,7 @@
message();
+ /** @Then the message should match the expected string */
self::assertEquals($expected, $actual);
}
- /**
- * @dataProvider providerForTestIsHttpCode
- */
+ #[DataProvider('httpCodesDataProvider')]
public function testIsHttpCode(int $httpCode, bool $expected): void
{
+ /** @Given an integer representing an HTTP code */
+ /** @When checking if it is a valid HTTP code */
$actual = HttpCode::isHttpCode(httpCode: $httpCode);
+ /** @Then the result should match the expected boolean */
self::assertEquals($expected, $actual);
}
- public static function providerForTestMessage(): array
+ public static function messagesDataProvider(): array
{
return [
- [
- 'httpCode' => HttpCode::CONTINUE,
- 'expected' => '100 Continue'
- ],
- [
+ 'OK message' => [
'httpCode' => HttpCode::OK,
'expected' => '200 OK'
],
- [
+ 'Created message' => [
'httpCode' => HttpCode::CREATED,
'expected' => '201 Created'
],
- [
- 'httpCode' => HttpCode::NON_AUTHORITATIVE_INFORMATION,
- 'expected' => '203 Non Authoritative Information'
+ 'Continue message' => [
+ 'httpCode' => HttpCode::CONTINUE,
+ 'expected' => '100 Continue'
],
- [
+ 'Permanent Redirect message' => [
'httpCode' => HttpCode::PERMANENT_REDIRECT,
'expected' => '308 Permanent Redirect'
],
- [
- 'httpCode' => HttpCode::PERMANENT_REDIRECT,
- 'expected' => '308 Permanent Redirect'
+ 'Internal Server Error message' => [
+ 'httpCode' => HttpCode::INTERNAL_SERVER_ERROR,
+ 'expected' => '500 Internal Server Error'
+ ],
+ 'Non Authoritative Information message' => [
+ 'httpCode' => HttpCode::NON_AUTHORITATIVE_INFORMATION,
+ 'expected' => '203 Non Authoritative Information'
],
- [
+ 'Proxy Authentication Required message' => [
'httpCode' => HttpCode::PROXY_AUTHENTICATION_REQUIRED,
'expected' => '407 Proxy Authentication Required'
],
- [
- 'httpCode' => HttpCode::INTERNAL_SERVER_ERROR,
- 'expected' => '500 Internal Server Error'
- ]
];
}
- public static function providerForTestIsHttpCode(): array
+ public static function httpCodesDataProvider(): array
{
return [
- [
- 'httpCode' => HttpCode::CONTINUE->value,
- 'expected' => true
+ 'Invalid code 0' => [
+ 'httpCode' => 0,
+ 'expected' => false
],
- [
- 'httpCode' => HttpCode::OK->value,
- 'expected' => true
+ 'Invalid code -1' => [
+ 'httpCode' => -1,
+ 'expected' => false
],
- [
+ 'Invalid code 1054' => [
'httpCode' => 1054,
'expected' => false
],
- [
- 'httpCode' => 0,
- 'expected' => false
+ 'Valid code 200 OK' => [
+ 'httpCode' => HttpCode::OK->value,
+ 'expected' => true
],
- [
- 'httpCode' => -1,
- 'expected' => false
+ 'Valid code 100 Continue' => [
+ 'httpCode' => HttpCode::CONTINUE->value,
+ 'expected' => true
],
- [
+ 'Valid code 500 Internal Server Error' => [
'httpCode' => HttpCode::INTERNAL_SERVER_ERROR->value,
'expected' => true
]
diff --git a/tests/HttpResponseTest.php b/tests/HttpResponseTest.php
index c5fbdc4..2c79057 100644
--- a/tests/HttpResponseTest.php
+++ b/tests/HttpResponseTest.php
@@ -1,20 +1,23 @@
getBody()->__toString());
self::assertEquals($expected, $response->getBody()->getContents());
self::assertEquals(HttpCode::OK->value, $response->getStatusCode());
@@ -22,13 +25,13 @@ public function testResponseOk(mixed $data, mixed $expected): void
self::assertEquals($this->defaultHeaderFrom(code: HttpCode::OK), $response->getHeaders());
}
- /**
- * @dataProvider providerData
- */
+ #[DataProvider('providerData')]
public function testResponseCreated(mixed $data, mixed $expected): void
{
+ /** @Given a valid HTTP response with status Created */
$response = HttpResponse::created(data: $data);
+ /** @Then verify that the response body and headers are correct */
self::assertEquals($expected, $response->getBody()->__toString());
self::assertEquals($expected, $response->getBody()->getContents());
self::assertEquals(HttpCode::CREATED->value, $response->getStatusCode());
@@ -36,13 +39,13 @@ public function testResponseCreated(mixed $data, mixed $expected): void
self::assertEquals($this->defaultHeaderFrom(code: HttpCode::CREATED), $response->getHeaders());
}
- /**
- * @dataProvider providerData
- */
+ #[DataProvider('providerData')]
public function testResponseAccepted(mixed $data, mixed $expected): void
{
+ /** @Given a valid HTTP response with status Accepted */
$response = HttpResponse::accepted(data: $data);
+ /** @Then verify that the response body and headers are correct */
self::assertEquals($expected, $response->getBody()->__toString());
self::assertEquals($expected, $response->getBody()->getContents());
self::assertEquals(HttpCode::ACCEPTED->value, $response->getStatusCode());
@@ -52,8 +55,10 @@ public function testResponseAccepted(mixed $data, mixed $expected): void
public function testResponseNoContent(): void
{
+ /** @Given a valid HTTP response with status No Content */
$response = HttpResponse::noContent();
+ /** @Then verify that the response body is empty and headers are correct */
self::assertEquals('', $response->getBody()->__toString());
self::assertEquals('', $response->getBody()->getContents());
self::assertEquals(HttpCode::NO_CONTENT->value, $response->getStatusCode());
@@ -61,13 +66,13 @@ public function testResponseNoContent(): void
self::assertEquals($this->defaultHeaderFrom(code: HttpCode::NO_CONTENT), $response->getHeaders());
}
- /**
- * @dataProvider providerData
- */
+ #[DataProvider('providerData')]
public function testResponseBadRequest(mixed $data, mixed $expected): void
{
+ /** @Given a valid HTTP response with status Bad Request */
$response = HttpResponse::badRequest(data: $data);
+ /** @Then verify that the response body and headers are correct */
self::assertEquals($expected, $response->getBody()->__toString());
self::assertEquals($expected, $response->getBody()->getContents());
self::assertEquals(HttpCode::BAD_REQUEST->value, $response->getStatusCode());
@@ -75,13 +80,13 @@ public function testResponseBadRequest(mixed $data, mixed $expected): void
self::assertEquals($this->defaultHeaderFrom(code: HttpCode::BAD_REQUEST), $response->getHeaders());
}
- /**
- * @dataProvider providerData
- */
+ #[DataProvider('providerData')]
public function testResponseNotFound(mixed $data, mixed $expected): void
{
+ /** @Given a valid HTTP response with status Not Found */
$response = HttpResponse::notFound(data: $data);
+ /** @Then verify that the response body and headers are correct */
self::assertEquals($expected, $response->getBody()->__toString());
self::assertEquals($expected, $response->getBody()->getContents());
self::assertEquals(HttpCode::NOT_FOUND->value, $response->getStatusCode());
@@ -89,13 +94,13 @@ public function testResponseNotFound(mixed $data, mixed $expected): void
self::assertEquals($this->defaultHeaderFrom(code: HttpCode::NOT_FOUND), $response->getHeaders());
}
- /**
- * @dataProvider providerData
- */
+ #[DataProvider('providerData')]
public function testResponseConflict(mixed $data, mixed $expected): void
{
+ /** @Given a valid HTTP response with status Conflict */
$response = HttpResponse::conflict(data: $data);
+ /** @Then verify that the response body and headers are correct */
self::assertEquals($expected, $response->getBody()->__toString());
self::assertEquals($expected, $response->getBody()->getContents());
self::assertEquals(HttpCode::CONFLICT->value, $response->getStatusCode());
@@ -103,13 +108,13 @@ public function testResponseConflict(mixed $data, mixed $expected): void
self::assertEquals($this->defaultHeaderFrom(code: HttpCode::CONFLICT), $response->getHeaders());
}
- /**
- * @dataProvider providerData
- */
+ #[DataProvider('providerData')]
public function testResponseUnprocessableEntity(mixed $data, mixed $expected): void
{
+ /** @Given a valid HTTP response with status Unprocessable Entity */
$response = HttpResponse::unprocessableEntity(data: $data);
+ /** @Then verify that the response body and headers are correct */
self::assertEquals($expected, $response->getBody()->__toString());
self::assertEquals($expected, $response->getBody()->getContents());
self::assertEquals(HttpCode::UNPROCESSABLE_ENTITY->value, $response->getStatusCode());
@@ -117,13 +122,13 @@ public function testResponseUnprocessableEntity(mixed $data, mixed $expected): v
self::assertEquals($this->defaultHeaderFrom(code: HttpCode::UNPROCESSABLE_ENTITY), $response->getHeaders());
}
- /**
- * @dataProvider providerData
- */
+ #[DataProvider('providerData')]
public function testResponseInternalServerError(mixed $data, mixed $expected): void
{
+ /** @Given a valid HTTP response with status Internal Server Error */
$response = HttpResponse::internalServerError(data: $data);
+ /** @Then verify that the response body and headers are correct */
self::assertEquals($expected, $response->getBody()->__toString());
self::assertEquals($expected, $response->getBody()->getContents());
self::assertEquals(HttpCode::INTERNAL_SERVER_ERROR->value, $response->getStatusCode());
@@ -134,29 +139,29 @@ public function testResponseInternalServerError(mixed $data, mixed $expected): v
public static function providerData(): array
{
return [
- [
- 'data' => new Xyz(value: 10),
- 'expected' => '{"value":10}'
- ],
- [
- 'data' => new Xpto(value: 9.99),
- 'expected' => (new Xpto(value: 9.99))->toJson()
- ],
- [
+ 'Null value' => [
'data' => null,
'expected' => null
],
- [
+ 'Empty string' => [
'data' => '',
'expected' => '""'
],
- [
+ 'Boolean true value' => [
'data' => true,
'expected' => 'true'
],
- [
+ 'Large integer value' => [
'data' => 10000000000,
'expected' => '10000000000'
+ ],
+ 'Xyz object serialization' => [
+ 'data' => new Xyz(value: 10),
+ 'expected' => '{"value":10}'
+ ],
+ 'Xpto object serialization with toJson' => [
+ 'data' => new Xpto(value: 9.99),
+ 'expected' => (new Xpto(value: 9.99))->toJson()
]
];
}
diff --git a/tests/Internal/HttpHeadersTest.php b/tests/Internal/HttpHeadersTest.php
index 5988c72..a467aa8 100644
--- a/tests/Internal/HttpHeadersTest.php
+++ b/tests/Internal/HttpHeadersTest.php
@@ -1,5 +1,7 @@
addFrom(key: 'X-Custom-Header', value: 'value1')
->addFrom(key: 'X-Custom-Header', value: 'value2')
->removeFrom(key: 'X-Custom-Header');
+ /** @Then all headers should be removed */
self::assertTrue($actual->hasNoHeaders());
self::assertFalse($actual->hasHeader(key: 'X-Custom-Header'));
}
public function testAddFromCode(): void
{
+ /** @Given HttpHeaders */
$actual = HttpHeaders::build()->addFromCode(code: HttpCode::OK);
- $expected = ['Status' => [HttpCode::OK->message()]];
- self::assertEquals($expected, $actual->toArray());
+ /** @Then the Status header should be added with the correct value */
+ self::assertEquals(['Status' => [HttpCode::OK->message()]], $actual->toArray());
}
public function testAddFromContentType(): void
{
+ /** @Given HttpHeaders */
$headers = HttpHeaders::build()->addFromContentType(header: HttpContentType::APPLICATION_JSON);
+
+ /** @When adding a Content-Type header */
$actual = $headers->toArray();
- $expected = ['Content-Type' => [HttpContentType::APPLICATION_JSON->value]];
- self::assertEquals($expected, $actual);
+ /** @Then the Content-Type header should match the expected value */
+ self::assertEquals(['Content-Type' => [HttpContentType::APPLICATION_JSON->value]], $actual);
}
public function testGetHeader(): void
{
+ /** @Given HttpHeaders with duplicate headers */
$headers = HttpHeaders::build()
->addFrom(key: 'X-Custom-Header', value: 'value1')
->addFrom(key: 'X-Custom-Header', value: 'value2');
+
+ /** @When retrieving the header */
$actual = $headers->getHeader(key: 'X-Custom-Header');
- $expected = ['value1', 'value2'];
- self::assertEquals($expected, $actual);
+ /** @Then the header values should match the expected array */
+ self::assertEquals(['value1', 'value2'], $actual);
}
public function testToArrayWithNonUniqueValues(): void
{
+ /** @Given HttpHeaders with duplicate values for a single header */
$headers = HttpHeaders::build()
->addFrom(key: 'X-Custom-Header', value: 'value1')
->addFrom(key: 'X-Custom-Header', value: 'value1');
+
+ /** @When converting the headers to an array */
$actual = $headers->toArray();
- $expected = ['X-Custom-Header' => ['value1']];
- self::assertEquals($expected, $actual);
+ /** @Then duplicate values should be collapsed into a single entry */
+ self::assertEquals(['X-Custom-Header' => ['value1']], $actual);
}
}
diff --git a/tests/Internal/ResponseTest.php b/tests/Internal/ResponseTest.php
index a7422b8..1d0a6d2 100644
--- a/tests/Internal/ResponseTest.php
+++ b/tests/Internal/ResponseTest.php
@@ -1,5 +1,7 @@
[HttpCode::OK->message()],
'Content-Type' => [HttpContentType::APPLICATION_JSON->value]
- ];
-
- self::assertEquals($expected, $response->getHeaders());
+ ], $response->getHeaders());
}
public function testGetProtocolVersion(): void
{
+ /** @Given a Response */
$response = Response::from(code: HttpCode::OK, data: [], headers: null);
+ /** @Then the protocol version should be 1.1 */
self::assertEquals('1.1', $response->getProtocolVersion());
}
public function testGetHeaders(): void
{
+ /** @Given a Response with specific headers */
$headers = HttpHeaders::build()->addFromContentType(header: HttpContentType::APPLICATION_JSON);
$response = Response::from(code: HttpCode::OK, data: [], headers: $headers);
- $expected = [HttpContentType::APPLICATION_JSON->value];
+ /** @Then the Response should return the correct headers */
self::assertEquals($headers->toArray(), $response->getHeaders());
- self::assertEquals($expected, $response->getHeader(name: 'Content-Type'));
+ self::assertEquals([HttpContentType::APPLICATION_JSON->value], $response->getHeader(name: 'Content-Type'));
}
public function testHasHeader(): void
{
+ /** @Given a Response with a specific Content-Type header */
$headers = HttpHeaders::build()->addFromContentType(header: HttpContentType::TEXT_PLAIN);
$response = Response::from(code: HttpCode::OK, data: [], headers: $headers);
- $expected = [HttpContentType::TEXT_PLAIN->value];
+ /** @Then the Response should correctly indicate that it has the Content-Type header */
self::assertTrue($response->hasHeader(name: 'Content-Type'));
- self::assertEquals($expected, $response->getHeader(name: 'Content-Type'));
+ self::assertEquals([HttpContentType::TEXT_PLAIN->value], $response->getHeader(name: 'Content-Type'));
}
public function testGetHeaderLine(): void
{
+ /** @Given a Response with a specific Content-Type header */
$headers = HttpHeaders::build()->addFromContentType(header: HttpContentType::APPLICATION_JSON);
$response = Response::from(code: HttpCode::OK, data: [], headers: $headers);
+ /** @Then the header line should match the expected value */
self::assertEquals(HttpContentType::APPLICATION_JSON->value, $response->getHeaderLine(name: 'Content-Type'));
}
public function testWithHeader(): void
{
+ /** @Given a Response */
$value = '2850bf62-8383-4e9f-b237-d41247a1df3b';
$response = Response::from(code: HttpCode::OK, data: [], headers: null);
- $response->withHeader(name: 'Token', value: $value);
- $expected = [$value];
+ /** @When adding a new header */
+ $response->withHeader(name: 'Token', value: $value);
- self::assertEquals($expected, $response->getHeader(name: 'Token'));
+ /** @Then the new header should be included in the Response */
+ self::assertEquals([$value], $response->getHeader(name: 'Token'));
}
public function testWithoutHeader(): void
{
+ /** @Given a Response with default headers */
$response = Response::from(code: HttpCode::OK, data: [], headers: null);
- $response->withoutHeader('Status');
- $expected = [HttpContentType::APPLICATION_JSON->value];
+ /** @When removing the Status header */
+ $response->withoutHeader(name: 'Status');
+
+ /** @Then the Status header should be empty and Content-Type should remain intact */
self::assertEmpty($response->getHeader(name: 'Status'));
- self::assertEquals($expected, $response->getHeader(name: 'Content-Type'));
+ self::assertEquals([HttpContentType::APPLICATION_JSON->value], $response->getHeader(name: 'Content-Type'));
}
public function testExceptionWhenBadMethodCallOnWithBody(): void
{
+ /** @Given a Response */
$response = Response::from(code: HttpCode::OK, data: [], headers: null);
+ /** @Then a BadMethodCall exception should be thrown when calling withBody */
self::expectException(BadMethodCall::class);
self::expectExceptionMessage('Method cannot be used.');
+ /** @When attempting to call withBody */
$response->withBody(body: StreamFactory::from(data: []));
}
public function testExceptionWhenBadMethodCallOnWithStatus(): void
{
+ /** @Given a Response */
$response = Response::from(code: HttpCode::OK, data: [], headers: null);
+ /** @Then a BadMethodCall exception should be thrown when calling withStatus */
self::expectException(BadMethodCall::class);
self::expectExceptionMessage('Method cannot be used.');
+ /** @When attempting to call withStatus */
$response->withStatus(code: HttpCode::OK->value);
}
public function testExceptionWhenBadMethodCallOnWithAddedHeader(): void
{
+ /** @Given a Response */
$response = Response::from(code: HttpCode::OK, data: [], headers: null);
+ /** @Then a BadMethodCall exception should be thrown when calling withAddedHeader */
self::expectException(BadMethodCall::class);
self::expectExceptionMessage('Method cannot be used.');
+ /** @When attempting to call withAddedHeader */
$response->withAddedHeader(name: '', value: '');
}
public function testExceptionWhenBadMethodCallOnWithProtocolVersion(): void
{
+ /** @Given a Response */
$response = Response::from(code: HttpCode::OK, data: [], headers: null);
+ /** @Then a BadMethodCall exception should be thrown when calling withProtocolVersion */
self::expectException(BadMethodCall::class);
self::expectExceptionMessage('Method cannot be used.');
+ /** @When attempting to call withProtocolVersion */
$response->withProtocolVersion(version: '');
}
}
diff --git a/tests/Internal/Stream/StreamTest.php b/tests/Internal/Stream/StreamTest.php
index fbdbbda..f298729 100644
--- a/tests/Internal/Stream/StreamTest.php
+++ b/tests/Internal/Stream/StreamTest.php
@@ -1,14 +1,18 @@
resource);
+
+ /** @When retrieving metadata */
+ $actual = $stream->getMetadata();
+ $expected = StreamMetaData::from(data: stream_get_meta_data($this->resource))->toArray();
+
+ /** @Then the metadata should match the expected values */
+ self::assertEquals($expected['uri'], $actual['uri']);
+ self::assertEquals($expected['mode'], $actual['mode']);
+ self::assertEquals($expected['seekable'], $actual['seekable']);
+ self::assertEquals($expected['streamType'], $actual['streamType']);
+ }
+
+ public function testCloseWithoutResource(): void
{
+ /** @Given a stream that has already been closed */
$stream = Stream::from(resource: $this->resource);
$stream->close();
+ /** @When closing the stream again */
+ $stream->close();
+
+ /** @Then the stream should remain closed and detached */
self::assertFalse($stream->isReadable());
self::assertFalse($stream->isWritable());
self::assertFalse($stream->isSeekable());
self::assertFalse(is_resource($this->resource));
}
- public function testCloseWithoutResource(): void
+ public function testCloseDetachesResource(): void
{
+ /** @Given a stream resource */
$stream = Stream::from(resource: $this->resource);
- $stream->close();
+
+ /** @When the stream is closed */
$stream->close();
+ /** @Then the stream should be detached and no longer readable, writable, or seekable */
self::assertFalse($stream->isReadable());
self::assertFalse($stream->isWritable());
self::assertFalse($stream->isSeekable());
self::assertFalse(is_resource($this->resource));
}
- public function testEofReturnsTrueAtEndOfStream(): void
+ public function testSeekMovesCursorPosition(): void
{
+ /** @Given a stream with data */
$stream = Stream::from(resource: $this->resource);
- $stream->write(string: 'Hello');
- $eofBeforeRead = $stream->eof();
- $stream->read(length: 5);
+ $stream->write(string: 'Hello, world!');
- self::assertTrue($stream->eof());
- self::assertTrue($stream->isReadable());
- self::assertFalse($eofBeforeRead);
+ /** @When seeking to a specific position */
+ $stream->seek(offset: 7);
+ $tellAfterFirstSeek = $stream->tell();
+ $stream->seek(offset: 0, whence: SEEK_END);
+
+ /** @Then the cursor position should be updated correctly */
+ self::assertTrue($stream->isWritable());
+ self::assertTrue($stream->isSeekable());
+ self::assertEquals(7, $tellAfterFirstSeek);
+ self::assertEquals(13, $stream->tell());
}
- public function testGetMetadata(): void
+ public function testGetSizeReturnsCorrectSize(): void
{
+ /** @Given a stream */
$stream = Stream::from(resource: $this->resource);
- $actual = $stream->getMetadata();
- $expected = StreamMetaData::from(data: stream_get_meta_data($this->resource))->toArray();
- self::assertEquals($expected['uri'], $actual['uri']);
- self::assertEquals($expected['mode'], $actual['mode']);
- self::assertEquals($expected['seekable'], $actual['seekable']);
- self::assertEquals($expected['streamType'], $actual['streamType']);
+ /** @When writing to the stream */
+ $sizeBeforeWrite = $stream->getSize();
+ $stream->write(string: 'Hello, world!');
+
+ /** @Then the size should be updated correctly */
+ self::assertEquals(0, $sizeBeforeWrite);
+ self::assertEquals(13, $stream->getSize());
}
- public function testGetMetadataWhenKeyIsUnknown(): void
+ public function testIsWritableForCreateMode(): void
{
- $stream = Stream::from(resource: $this->resource);
- $actual = $stream->getMetadata(key: 'UNKNOWN');
+ /** @Given a file that does not exist */
+ unlink($this->temporary);
- self::assertNull($actual);
+ /** @When opening the stream in create mode ('x') */
+ $stream = Stream::from(resource: fopen($this->temporary, 'x'));
+
+ /** @Then the stream should be writable */
+ self::assertTrue($stream->isWritable());
}
- public function testSeekMovesCursorPosition(): void
+ #[DataProvider('modesDataProvider')]
+ public function testIsWritableForVariousModes(string $mode, bool $expected): void
{
- $stream = Stream::from(resource: $this->resource);
- $stream->write(string: 'Hello, world!');
- $stream->seek(offset: 7);
- $tellAfterFirstSeek = $stream->tell();
- $stream->seek(offset: 0, whence: SEEK_END);
+ /** @Given a stream opened in a specific mode */
+ $stream = Stream::from(resource: fopen('php://memory', $mode));
- self::assertTrue($stream->isWritable());
- self::assertTrue($stream->isSeekable());
- self::assertEquals(7, $tellAfterFirstSeek);
- self::assertEquals(13, $stream->tell());
+ /** @Then check if the stream is writable based on the mode */
+ self::assertEquals($expected, $stream->isWritable());
}
public function testRewindResetsCursorPosition(): void
{
+ /** @Given a stream with data */
$stream = Stream::from(resource: $this->resource);
$stream->write(string: 'Hello, world!');
+
+ /** @When rewinding the stream */
$stream->seek(offset: 7);
$stream->rewind();
+ /** @Then the cursor position should be reset to the beginning */
self::assertEquals(0, $stream->tell());
}
- public function testGetSizeReturnsCorrectSize(): void
+ public function testEofReturnsTrueAtEndOfStream(): void
{
+ /** @Given a stream with data */
$stream = Stream::from(resource: $this->resource);
- $sizeBeforeWrite = $stream->getSize();
- $stream->write(string: 'Hello, world!');
+ $stream->write(string: 'Hello');
- self::assertEquals(0, $sizeBeforeWrite);
- self::assertEquals(13, $stream->getSize());
+ /** @When reaching the end of the stream */
+ $eofBeforeRead = $stream->eof();
+ $stream->read(length: 5);
+
+ /** @Then EOF should return true */
+ self::assertTrue($stream->eof());
+ self::assertTrue($stream->isReadable());
+ self::assertFalse($eofBeforeRead);
}
- public function testGetSizeReturnsNullWhenWithoutResource(): void
+ public function testGetMetadataWhenKeyIsUnknown(): void
{
+ /** @Given a stream */
$stream = Stream::from(resource: $this->resource);
- $stream->close();
- self::assertNull($stream->getSize());
+ /** @When retrieving metadata for an unknown key */
+ $actual = $stream->getMetadata(key: 'UNKNOWN');
+
+ /** @Then the result should be null */
+ self::assertNull($actual);
}
- public function testExceptionWhenMissingResourceStreamOnTell(): void
+ public function testToStringRewindsStreamIfNotSeekable(): void
{
+ /** @Given a stream */
$stream = Stream::from(resource: $this->resource);
- self::expectException(MissingResourceStream::class);
- self::expectExceptionMessage('No resource available.');
+ /** @When writing and converting the stream to string */
+ $stream->write(string: 'Hello, world!');
- $stream->close();
- $stream->tell();
+ /** @Then the content should match the written data */
+ self::assertEquals('Hello, world!', (string)$stream);
}
- public function testToStringRewindsStreamIfNotSeekable(): void
+ public function testGetSizeReturnsNullWhenWithoutResource(): void
{
+ /** @Given a stream that has been closed */
$stream = Stream::from(resource: $this->resource);
- $stream->write(string: 'Hello, world!');
- $actual = (string)$stream;
+ $stream->close();
- self::assertEquals('Hello, world!', $actual);
+ /** @Then getSize should return null */
+ self::assertNull($stream->getSize());
}
public function testExceptionWhenNonSeekableStream(): void
{
+ /** @Given a stream */
$stream = Stream::from(resource: $this->resource);
+ /** @When attempting to seek on a closed stream */
self::expectException(NonSeekableStream::class);
self::expectExceptionMessage('Stream is not seekable.');
@@ -156,8 +210,10 @@ public function testExceptionWhenNonSeekableStream(): void
public function testExceptionWhenNonWritableStream(): void
{
+ /** @Given a read-only stream */
$stream = Stream::from(resource: fopen($this->temporary, 'r'));
+ /** @When attempting to write to the stream */
self::expectException(NonWritableStream::class);
self::expectExceptionMessage('Stream is not writable.');
@@ -166,21 +222,61 @@ public function testExceptionWhenNonWritableStream(): void
public function testExceptionWhenNonReadableStreamOnRead(): void
{
+ /** @Given a write-only stream */
$stream = Stream::from(resource: fopen($this->temporary, 'w'));
+ /** @When attempting to read from the stream */
self::expectException(NonReadableStream::class);
self::expectExceptionMessage('Stream is not readable.');
$stream->read(length: 13);
}
+ public function testExceptionWhenInvalidResourceProvided(): void
+ {
+ /** @Given an invalid resource (e.g., a string) */
+ $resource = 'not_a_resource';
+
+ /** @Then an InvalidResource exception should be thrown */
+ $this->expectException(InvalidResource::class);
+ $this->expectExceptionMessage('The provided value is not a valid resource.');
+
+ /** @When calling the from method with an invalid resource */
+ Stream::from(resource: $resource);
+ }
+
+ public function testExceptionWhenMissingResourceStreamOnTell(): void
+ {
+ /** @Given a stream */
+ $stream = Stream::from(resource: $this->resource);
+
+ /** @When attempting to call tell on a closed stream */
+ self::expectException(MissingResourceStream::class);
+ self::expectExceptionMessage('No resource available.');
+
+ $stream->close();
+ $stream->tell();
+ }
+
public function testExceptionWhenNonReadableStreamOnGetContents(): void
{
+ /** @Given a write-only stream */
$stream = Stream::from(resource: fopen($this->temporary, 'w'));
+ /** @When attempting to get contents of the stream */
self::expectException(NonReadableStream::class);
self::expectExceptionMessage('Stream is not readable.');
$stream->getContents();
}
+
+ public static function modesDataProvider(): array
+ {
+ return [
+ 'Read mode (r)' => ['mode' => 'r', 'expected' => false],
+ 'Write mode (w)' => ['mode' => 'w', 'expected' => true],
+ 'Append mode (a)' => ['mode' => 'a', 'expected' => true],
+ 'Mixed read/write mode (r+)' => ['mode' => 'r+', 'expected' => true]
+ ];
+ }
}
diff --git a/tests/Mock/Xpto.php b/tests/Models/Xpto.php
similarity index 87%
rename from tests/Mock/Xpto.php
rename to tests/Models/Xpto.php
index 5c4564f..b82369b 100644
--- a/tests/Mock/Xpto.php
+++ b/tests/Models/Xpto.php
@@ -1,6 +1,6 @@