From 7d0f06f0bfdd525404b77f65a4c1fef0c4a5269e Mon Sep 17 00:00:00 2001 From: Eldar Shahmaliyev Date: Sun, 10 Nov 2024 15:20:43 +0400 Subject: [PATCH 1/4] style: improve discord notification format --- .github/workflows/discord.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/discord.yml b/.github/workflows/discord.yml index bb0a85c..28430f2 100644 --- a/.github/workflows/discord.yml +++ b/.github/workflows/discord.yml @@ -14,11 +14,10 @@ jobs: uses: Ilshidur/action-discord@0.3.2 with: args: | - 🚀 **New Release**: `${{ github.event.release.tag_name }}` - ${{ github.event.release.prerelease && '`pre-release`' || '`stable`' }} + 🚀 **New Release**: `${{ github.event.release.tag_name }}` ${{ github.event.release.prerelease && '`pre-release`' || '`stable`' }} ${{ github.event.release.body }} 🔗 [View Full Changelog](${{ github.event.release.html_url }}) - 👤 Released by @${{ github.event.release.author.login }} \ No newline at end of file + 👤 Released by @[${{ github.event.release.author.login }}](${{ github.event.release.author.html_url }}) \ No newline at end of file From 51f557dee42a63bcf9c2e3e189ed535a407c4ad1 Mon Sep 17 00:00:00 2001 From: Eldar Shahmaliyev Date: Sun, 10 Nov 2024 15:44:42 +0400 Subject: [PATCH 2/4] Install PSR-7 --- composer.json | 5 ++- composer.lock | 105 +++++++++++++++++++++++++++++++++++++------------- 2 files changed, 82 insertions(+), 28 deletions(-) diff --git a/composer.json b/composer.json index 8abe24a..7f3a761 100644 --- a/composer.json +++ b/composer.json @@ -34,12 +34,13 @@ "ext-posix": "*" }, "require": { + "php": ">=7.4", "ext-json": "*", "ext-curl": "*", "ext-fileinfo": "*", - "php": ">=7.4", "nesbot/carbon": "2.x", - "shahmal1yev/gcollection": "^1.0" + "shahmal1yev/gcollection": "^1.0", + "psr/http-message": "^2.0" }, "scripts": { "test": "vendor/bin/phpunit tests", diff --git a/composer.lock b/composer.lock index c69364b..b013f4a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1702f11dcf6e5e2c11a6758cd92fce0a", + "content-hash": "774e468005f141ff8358712be5680b2a", "packages": [ { "name": "carbonphp/carbon-doctrine-types", @@ -230,6 +230,59 @@ }, "time": "2022-11-25T14:36:26+00:00" }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, { "name": "shahmal1yev/gcollection", "version": "1.0.7", @@ -494,16 +547,16 @@ }, { "name": "symfony/translation", - "version": "v5.4.44", + "version": "v5.4.45", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "6fed3a20b5b87ee9cdd9dacf545922b8fd475921" + "reference": "98f26acc99341ca4bab345fb14d7b1d7cb825bed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/6fed3a20b5b87ee9cdd9dacf545922b8fd475921", - "reference": "6fed3a20b5b87ee9cdd9dacf545922b8fd475921", + "url": "https://api.github.com/repos/symfony/translation/zipball/98f26acc99341ca4bab345fb14d7b1d7cb825bed", + "reference": "98f26acc99341ca4bab345fb14d7b1d7cb825bed", "shasum": "" }, "require": { @@ -571,7 +624,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v5.4.44" + "source": "https://github.com/symfony/translation/tree/v5.4.45" }, "funding": [ { @@ -587,7 +640,7 @@ "type": "tidelift" } ], - "time": "2024-09-15T08:12:35+00:00" + "time": "2024-09-25T14:11:13+00:00" }, { "name": "symfony/translation-contracts", @@ -741,16 +794,16 @@ }, { "name": "fakerphp/faker", - "version": "v1.23.1", + "version": "v1.24.0", "source": { "type": "git", "url": "https://github.com/FakerPHP/Faker.git", - "reference": "bfb4fe148adbf78eff521199619b93a52ae3554b" + "reference": "a136842a532bac9ecd8a1c723852b09915d7db50" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/bfb4fe148adbf78eff521199619b93a52ae3554b", - "reference": "bfb4fe148adbf78eff521199619b93a52ae3554b", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/a136842a532bac9ecd8a1c723852b09915d7db50", + "reference": "a136842a532bac9ecd8a1c723852b09915d7db50", "shasum": "" }, "require": { @@ -798,22 +851,22 @@ ], "support": { "issues": "https://github.com/FakerPHP/Faker/issues", - "source": "https://github.com/FakerPHP/Faker/tree/v1.23.1" + "source": "https://github.com/FakerPHP/Faker/tree/v1.24.0" }, - "time": "2024-01-02T13:46:09+00:00" + "time": "2024-11-07T15:11:20+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.12.0", + "version": "1.12.1", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c" + "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", - "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845", + "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845", "shasum": "" }, "require": { @@ -852,7 +905,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0" + "source": "https://github.com/myclabs/DeepCopy/tree/1.12.1" }, "funding": [ { @@ -860,7 +913,7 @@ "type": "tidelift" } ], - "time": "2024-06-12T14:39:25+00:00" + "time": "2024-11-08T17:47:46+00:00" }, { "name": "nikic/php-parser", @@ -1040,16 +1093,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.7", + "version": "1.12.8", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "dc2b9976bd8b0f84ec9b0e50cc35378551de7af0" + "reference": "f6a60a4d66142b8156c9da923f1972657bc4748c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dc2b9976bd8b0f84ec9b0e50cc35378551de7af0", - "reference": "dc2b9976bd8b0f84ec9b0e50cc35378551de7af0", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/f6a60a4d66142b8156c9da923f1972657bc4748c", + "reference": "f6a60a4d66142b8156c9da923f1972657bc4748c", "shasum": "" }, "require": { @@ -1094,7 +1147,7 @@ "type": "github" } ], - "time": "2024-10-18T11:12:07+00:00" + "time": "2024-11-06T19:06:49+00:00" }, { "name": "phpunit/php-code-coverage", @@ -2591,10 +2644,10 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { + "php": ">=7.4", "ext-json": "*", "ext-curl": "*", - "ext-fileinfo": "*", - "php": ">=7.4" + "ext-fileinfo": "*" }, "platform-dev": { "ext-posix": "*" From 43ce089f40ec011d19daa9b4dab542d2b511ea9b Mon Sep 17 00:00:00 2001 From: Eldar Shahmaliyev Date: Sun, 10 Nov 2024 18:35:03 +0400 Subject: [PATCH 3/4] Implement methods of MessageInterface and add new unit tests --- src/Contracts/Lexicons/RequestContract.php | 4 +- src/Lexicons/Request.php | 98 +++++++++++++++++ tests/Unit/Lexicons/RequestTest.php | 119 +++++++++++++++++++++ 3 files changed, 220 insertions(+), 1 deletion(-) diff --git a/src/Contracts/Lexicons/RequestContract.php b/src/Contracts/Lexicons/RequestContract.php index 954bbf1..c368fb0 100644 --- a/src/Contracts/Lexicons/RequestContract.php +++ b/src/Contracts/Lexicons/RequestContract.php @@ -2,12 +2,14 @@ namespace Atproto\Contracts\Lexicons; +use Psr\Http\Message\MessageInterface; + /** * Interface RequestContract * * This interface defines the contract for an HTTP request. */ -interface RequestContract +interface RequestContract extends MessageInterface { /** * Get the URL of the request. diff --git a/src/Lexicons/Request.php b/src/Lexicons/Request.php index 77527e6..93d7f26 100644 --- a/src/Lexicons/Request.php +++ b/src/Lexicons/Request.php @@ -5,9 +5,107 @@ use Atproto\Contracts\Lexicons\RequestContract; use Atproto\Lexicons\Traits\RequestBuilder; use Atproto\Lexicons\Traits\RequestHandler; +use Psr\Http\Message\MessageInterface; +use Psr\Http\Message\StreamInterface; class Request implements RequestContract { use RequestHandler; use RequestBuilder; + + private string $protocol = '1.1'; + private StreamInterface $body; + + public function getProtocolVersion(): string + { + return $this->protocol; + } + + public function withProtocolVersion(string $version): MessageInterface + { + if ($version === $this->protocol) { + return $this; + } + + $instance = clone $this; + $instance->protocol = $version; + + return $instance; + } + + public function getHeaders(): array + { + return $this->headers; + } + + public function hasHeader(string $name): bool + { + return isset($this->headers[strtolower($name)]); + } + + public function getHeader(string $name): array + { + return $this->hasHeader($name) ? $this->headers[strtolower($name)] : []; + } + + public function getHeaderLine(string $name): string + { + return implode(', ', $this->getHeader($name)); + } + + public function withHeader(string $name, $value): MessageInterface + { + if (! is_array($value) && ! is_string($value)) { + throw new \InvalidArgumentException('$value must be an array or string'); + } + + $instance = clone $this; + $instance->headers[strtolower($name)] = $value; + + return $instance; + } + + public function withAddedHeader(string $name, $value): MessageInterface + { + if (! is_array($value) && ! is_string($value)) { + throw new \InvalidArgumentException('$value must be an array or string'); + } + + if (is_string($value)) { + $value = [$value]; + } + + return $this->withHeader( + $name, + array_reduce( + [$this->getHeader($name), $value], + fn ($carry, array $next) => [...($carry ?: []), ...$next] + ) + ); + } + + public function withoutHeader(string $name): MessageInterface + { + $headers = $this->getHeaders(); + + unset($headers[strtolower($name)]); + + $instance = clone $this; + $instance->headers = $headers; + + return $instance; + } + + public function getBody(): StreamInterface + { + return $this->body; + } + + public function withBody(StreamInterface $body): MessageInterface + { + $instance = clone $this; + $instance->body = $body; + + return $instance; + } } diff --git a/tests/Unit/Lexicons/RequestTest.php b/tests/Unit/Lexicons/RequestTest.php index 4c24efe..a4793e4 100644 --- a/tests/Unit/Lexicons/RequestTest.php +++ b/tests/Unit/Lexicons/RequestTest.php @@ -8,6 +8,8 @@ use Faker\Factory; use Faker\Generator; use PHPUnit\Framework\TestCase; +use Psr\Http\Message\StreamInterface; +use stdClass; use Tests\Supports\Reflection; use TypeError; @@ -192,4 +194,121 @@ protected function randomArray(int $count = null): array $values ); } + + public function testGetProtocolVersionReturnsDefaultVersion(): void + { + $this->assertSame('1.1', $this->request->getProtocolVersion()); + } + + public function testWithProtocolVersionChangesProtocolVersion(): void + { + $message = $this->request->withProtocolVersion('2.0'); + + $this->assertSame('2.0', $message->getProtocolVersion()); + $this->assertSame('1.1', $this->request->getProtocolVersion()); + } + + public function testWithProtocolVersionReturnsSameInstanceIfVersionNotChanged(): void + { + $this->assertSame('1.1', $this->request->getProtocolVersion()); + $this->assertSame($this->request, $this->request->withProtocolVersion('1.1')); + } + + public function testGetHeadersCanGetHeaders(): void + { + $headers = ['content-type' => ['application/json']]; + $request = $this->request->withHeader('Content-Type', ['application/json']); + + $this->assertEquals($headers, $request->getHeaders()); + } + + public function testHasHeaderReturnsTrueIfHeaderExists(): void + { + $request = $this->request->withHeader('Content-Type', ['application/json']); + + $this->assertTrue($request->hasHeader('Content-Type')); + $this->assertTrue($request->hasHeader('content-type')); + $this->assertFalse($request->hasHeader('X-Custom')); + } + + public function testGetHeaderCanGetHeaderValues(): void + { + $request = $this->request->withHeader('Accept', ['application/json', 'text/html']); + + $this->assertEquals(['application/json', 'text/html'], $request->getHeader('Accept')); + $this->assertEquals([], $request->getHeader('X-Custom')); + } + + public function testGetHeaderLineReturnsHeaderValuesSeparatedByComma(): void + { + $request = $this->request->withHeader('Accept', ['application/json', 'text/html']); + + $this->assertSame('application/json, text/html', $request->getHeaderLine('Accept')); + $this->assertSame('', $request->getHeaderLine('X-Custom')); + } + + public function testWithHeaderThrowsExceptionWhenPassedInvalidValue(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('$value must be an array or string'); + $this->request->withHeader('Test', new stdClass()); + } + + public function testWithAddedHeaderThrowsExceptionWhenPassedInvalidValue(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('$value must be an array or string'); + $this->request->withAddedHeader('Test', new stdClass()); + } + + public function testItCanAddHeaders(): void + { + $request = $this->request + ->withHeader('Content-Type', ['application/json']) + ->withAddedHeader('Content-Type', ['text/html']); + + $this->assertEquals(['application/json', 'text/html'], $request->getHeader('Content-Type')); + } + + public function testItCanRemoveHeaders(): void + { + $request = $this->request + ->withHeader('Content-Type', ['application/json']) + ->withoutHeader('Content-Type'); + + $this->assertFalse($request->hasHeader('Content-Type')); + $this->assertEquals([], $request->getHeader('Content-Type')); + } + + public function testItCanSetBody(): void + { + $streamMock = $this->createMock(StreamInterface::class); + + $request = $this->request->withBody($streamMock); + + $this->assertSame($streamMock, $request->getBody()); + } + + public function testMaintainsImmutabilityWhenSettingBody(): void + { + $stream1 = $this->createMock(StreamInterface::class); + $stream2 = $this->createMock(StreamInterface::class); + + $request1 = $this->request->withBody($stream1); + $request2 = $request1->withBody($stream2); + + $this->assertNotSame($request1, $request2); + $this->assertSame($stream1, $request1->getBody()); + $this->assertSame($stream2, $request2->getBody()); + } + + public function testMaintainsImmutabilityWhenSettingHeaders(): void + { + $request1 = $this->request->withHeader('Content-Type', ['application/json']); + $request2 = $request1->withHeader('Content-Type', ['text/html']); + + $this->assertNotSame($request1, $request2); + $this->assertEquals(['application/json'], $request1->getHeader('Content-Type')); + $this->assertEquals(['text/html'], $request2->getHeader('Content-Type')); + } } From e3273ed9396ca51c450ed6fc56fc5ff16f792ff6 Mon Sep 17 00:00:00 2001 From: Eldar Shahmaliyev Date: Sun, 10 Nov 2024 18:46:12 +0400 Subject: [PATCH 4/4] Update PHP workflow --- .github/workflows/php.yml | 46 ++++++++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index a108bbd..b7c1c4d 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -2,30 +2,27 @@ name: PHP Workflow on: push: - branches: [ "main", "development" ] + branches: [ "main" ] pull_request: - branches: [ "main", "development" ] + branches: [ "main" ] permissions: contents: read jobs: - build: + unit-tests: runs-on: ubuntu-latest strategy: matrix: php-version: ['7.4', '8.0', '8.1'] - name: PHP ${{ matrix.php-version }} Test - - env: - BLUESKY_IDENTIFIER: ${{ secrets.BLUESKY_IDENTIFIER }} - BLUESKY_PASSWORD: ${{ secrets.BLUESKY_PASSWORD }} + name: PHP ${{ matrix.php-version }} Unit Tests steps: - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} - extensions: mbstring, intl + extensions: mbstring, intl, json, curl, fileinfo + coverage: none - name: Cache dependencies uses: actions/cache@v3 @@ -46,8 +43,31 @@ jobs: - name: Run Unit Tests run: composer run-script test-unit - - name: Run Feature Tests - run: composer run-script test-feature - - name: Static Analyse - run: composer run-script analyse \ No newline at end of file + run: composer run-script analyse + + feature-tests: + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' + strategy: + matrix: + php-version: ['7.4', '8.0', '8.1'] + name: PHP ${{ matrix.php-version }} Feature Tests + env: + BLUESKY_IDENTIFIER: ${{ secrets.BLUESKY_IDENTIFIER }} + BLUESKY_PASSWORD: ${{ secrets.BLUESKY_PASSWORD }} + + steps: + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + extensions: mbstring, intl, json, curl, fileinfo + coverage: none + + - uses: actions/checkout@v4 + + - name: Install Dependencies + run: composer install --prefer-dist --no-progress + + - name: Run Feature Tests + run: composer run-script test-feature \ No newline at end of file