From 8da9b5b0a5f049e2339b94fb91808cffcdc6e9bf Mon Sep 17 00:00:00 2001 From: Ben Peachey Date: Mon, 5 May 2025 12:48:32 +0200 Subject: [PATCH 1/8] Change PHPUnit config to use `build/` directory for temporary files. --- .config/phpunit.xml.dist | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.config/phpunit.xml.dist b/.config/phpunit.xml.dist index 9beadd4..676dd2a 100644 --- a/.config/phpunit.xml.dist +++ b/.config/phpunit.xml.dist @@ -7,7 +7,7 @@ beStrictAboutOutputDuringTests="true" beStrictAboutTodoAnnotatedTests="true" bootstrap="../vendor/autoload.php" - cacheResultFile="../.phpunit.cache/test-results" + cacheResultFile="../build/.phpunit.cache/test-results" convertDeprecationsToExceptions="true" failOnRisky="true" failOnWarning="true" @@ -20,7 +20,7 @@ - + ../src/ From f1ed93314d38d05099f94adcdb40575afd109102 Mon Sep 17 00:00:00 2001 From: Ben Peachey Date: Mon, 5 May 2025 12:49:17 +0200 Subject: [PATCH 2/8] Delete dummy test. --- tests/dummyTest.php | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 tests/dummyTest.php diff --git a/tests/dummyTest.php b/tests/dummyTest.php deleted file mode 100644 index 4f94796..0000000 --- a/tests/dummyTest.php +++ /dev/null @@ -1,20 +0,0 @@ -assertTrue(true); - } -} \ No newline at end of file From 49218be41f8ebf9b26056c8da2a18e3cd46548e8 Mon Sep 17 00:00:00 2001 From: Ben Peachey Date: Mon, 5 May 2025 12:49:34 +0200 Subject: [PATCH 3/8] Add unit-tests for Exception class. --- tests/Unit/ExceptionTest.php | 105 +++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 tests/Unit/ExceptionTest.php diff --git a/tests/Unit/ExceptionTest.php b/tests/Unit/ExceptionTest.php new file mode 100644 index 0000000..d1a525b --- /dev/null +++ b/tests/Unit/ExceptionTest.php @@ -0,0 +1,105 @@ +expectException(TypeError::class); + $this->expectExceptionMessageMatches('/Too few arguments .+ 0 passed/'); + + Exception::create(); + } + + /** + * @testdox Exception should complain when called without a context + */ + public function testCreateWithoutContext(): void + { + $this->expectException(TypeError::class); + $this->expectExceptionMessageMatches('/Too few arguments .+ 1 passed/'); + + Exception::create(self::MOCK_MESSAGE); + } + + /** + * @testdox Exception should complain when called with invalid message + */ + public function testCreateWithInvalidMessage(): void + { + $this->expectException(TypeError::class); + $this->expectExceptionMessageMatches('/Argument #1 .+ must be of type string/'); + + Exception::create(null, self::MOCK_CONTEXT); + } + + /** + * @testdox Exception should complain when called with invalid context + */ + public function testCreateWithInvalidContext(): void + { + $this->expectException(TypeError::class); + $this->expectExceptionMessageMatches('/Argument #2 .+ must be of type array/'); + + Exception::create(self::MOCK_MESSAGE, null); + } + + /** + * @testdox Exception should complain when given context does not match provided message format + */ + public function testCreateWithIncorrectContext(): void + { + $this->expectException(\ValueError::class); + $this->expectExceptionMessageMatches('/The arguments array must contain 1 items?, 0 given/'); + + Exception::create(self::MOCK_MESSAGE, []); + } + + /** + * @testdox Exception should be created when called with valid message and context + */ + public function testCreateWithMessageAndContext(): void + { + $expected = Exception::class; + $actual = Exception::create(self::MOCK_MESSAGE, self::MOCK_CONTEXT); + + $this->assertInstanceOf($expected, $actual); + } + + /** + * @testdox Created Exception should have the correct message when called with a message and context + */ + public function testCreateFormatsErrorMessage(): void + { + $expected = 'Error: Test'; + $actual = Exception::create(self::MOCK_MESSAGE, self::MOCK_CONTEXT)->getMessage(); + + $this->assertSame($expected, $actual); + } + + /** + * @testdox Exception should be created when called with a message, context and previous exception + */ + public function testCreateSetsPreviousException(): void + { + $expected = new \Exception('Previous exception'); + $actual = Exception::create(self::MOCK_MESSAGE, self::MOCK_CONTEXT, $expected)->getPrevious(); + + $this->assertSame($expected, $actual); + } +} From 06b33759d3d61493c44c2a85008af1f4b5dc6eb6 Mon Sep 17 00:00:00 2001 From: Ben Peachey Date: Mon, 5 May 2025 16:10:30 +0200 Subject: [PATCH 4/8] Add first draft unit-tests for Server class. --- tests/Unit/ServerTest.php | 179 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 tests/Unit/ServerTest.php diff --git a/tests/Unit/ServerTest.php b/tests/Unit/ServerTest.php new file mode 100644 index 0000000..1e6c0d8 --- /dev/null +++ b/tests/Unit/ServerTest.php @@ -0,0 +1,179 @@ +expectException(TypeError::class); + $this->expectExceptionMessageMatches('/Too few arguments .+ 0 passed/'); + + new Server(); + } + + /** + * @testdox Server should complain when instantiated without a response + * @covers ::__construct + */ + public function testServerConstructWithoutResponse() + { + $this->expectException(TypeError::class); + $this->expectExceptionMessageMatches('/Too few arguments .+ 1 passed/'); + + $mockFilesystem = $this->createMock(FilesystemInterface::class); + + new Server($mockFilesystem); + } + + /** + * @testdox Server should be instantiated when given a filesystem and a response + * @covers ::__construct + */ + public function testServerConstructWithFilesystemAndResponse() + { + $mockFilesystem = $this->createMock(FilesystemInterface::class); + $mockResponse = $this->createMock(ResponseInterface::class); + + $server = new Server($mockFilesystem, $mockResponse); + + $this->assertInstanceOf(Server::class, $server); + } + + /** + * @testdox Server should complain when asked to RespondToRequest without a request + * @covers ::respondToRequest + */ + public function testServerRespondToRequestWithoutRequest() + { + $this->expectException(TypeError::class); + $this->expectExceptionMessageMatches('/Too few arguments .+ 0 passed/'); + + $mockFilesystem = $this->createMock(FilesystemInterface::class); + $mockResponse = $this->createMock(ResponseInterface::class); + + $server = new Server($mockFilesystem, $mockResponse); + + $server->respondToRequest(); + } + + /** + * @testdox Server should complain when asked to RespondToRequest with a request with an unknown HTTP method + * + * @covers ::respondToRequest + * + * @uses \Pdsinterop\Solid\Resources\Exception + */ + public function testServerRespondToRequestWithUnknownHttpMethod() + { + // Assert + $this->expectException(Exception::class); + $this->expectExceptionMessage(vsprintf(Server::ERROR_UNKNOWN_HTTP_METHOD, [self::MOCK_HTTP_METHOD])); + + // Arrange + $mockRequest = $this->createMockRequest(); + $mockResponse = $this->createMock(ResponseInterface::class); + $mockFilesystem = $this->createMockFilesystem(); + + //Act + $server = new Server($mockFilesystem, $mockResponse); + $server->respondToRequest($mockRequest); + } + + /** + * @testdox Server should return provided response when asked to RespondToRequest with va lid request + * + * @covers ::respondToRequest + */ + public function testServerRespondToRequestWithRequest() + { + // Arrange + $mockFilesystem = $this->createMockFilesystem(); + $mockRequest = $this->createMockRequest('GET'); + $expected = $this->createMockResponse(); + + // Act + $server = new Server($mockFilesystem, $expected); + $actual = $server->respondToRequest($mockRequest); + + // Assert + $this->assertSame($expected, $actual); + } + + ////////////////////////////// MOCKS AND STUBS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\ + + public function createMockFilesystem(): FilesystemInterface|MockObject + { + $mockFilesystem = $this->getMockBuilder(FilesystemInterface::class) + ->onlyMethods([ + 'addPlugin', 'copy', 'createDir', 'delete', 'deleteDir', 'get', 'getMetadata', 'getMimetype', 'getSize', 'getTimestamp', 'getVisibility', 'has', 'listContents', 'put', 'putStream', 'read', 'readAndDelete', 'readStream', 'rename', 'setVisibility', 'update', 'updateStream', 'write', 'writeStream' + ]) + ->addMethods(['asMime']) + ->getMock(); + + $mockAsMime = $this->getMockBuilder(AsMime::class) + // ->onlyMethods(['getMimetype', 'getSize', 'getTimestamp']) + ->addMethods(['has']) + ->disableOriginalConstructor() + ->getMock(); + + $mockFilesystem->method('asMime')->willReturn($mockAsMime); + + return $mockFilesystem; + } + + public function createMockRequest($httpMethod = self::MOCK_HTTP_METHOD): ServerRequestInterface|MockObject + { + $mockRequest = $this->createMock(ServerRequestInterface::class); + + $mockUri = $this->createMock(UriInterface::class); + $mockUri->method('getPath')->willReturn(self::MOCK_PATH); + + $mockBody = $this->createMock(StreamInterface::class); + + $mockRequest->method('getUri')->willReturn($mockUri); + $mockRequest->method('getQueryParams')->willReturn([]); + $mockRequest->method('getMethod')->willReturn($httpMethod); + $mockRequest->method('getBody')->willReturn($mockBody); + // $mockRequest->method('getMethod')->willReturn('GET'); + $mockRequest->method('getHeaderLine')->willReturn(''); + + return $mockRequest; + } + + public function createMockResponse(): ResponseInterface|MockObject + { + $mockResponse = $this->createMock(ResponseInterface::class); + $mockBody = $this->createMock(StreamInterface::class); + + $mockResponse->method('getBody')->willReturn($mockBody); + $mockResponse->method('withStatus')->willReturnSelf(); + + return $mockResponse; + } +} From 839c3738fbc04a3657762ee6238369423d1d293c Mon Sep 17 00:00:00 2001 From: Ben Peachey Date: Mon, 5 May 2025 16:11:15 +0200 Subject: [PATCH 5/8] Add `build/` directory to git ignore file. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 3af7618..e11542a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ # Directories to ignore +/build /vendor # Files to ignore From f004a707e7a9b160e9981d87c6ae6fe787864de7 Mon Sep 17 00:00:00 2001 From: Ben Peachey Date: Mon, 5 May 2025 16:16:23 +0200 Subject: [PATCH 6/8] Add PHP 8.4 to GitHub Action (GHA) test matrix. --- .github/workflows/php.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 33c065c..39c4a51 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -78,6 +78,7 @@ jobs: - '8.1' # from 2021-11 to 2023-11 (2025-12) - '8.2' # from 2022-12 to 2024-12 (2026-12) - '8.3' # from 2023-11 to 2025-12 (2027-12) + - '8.4' # from 2024-11 to 2026-12 (2028-12) steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 @@ -122,6 +123,7 @@ jobs: - '8.1' # from 2021-11 to 2023-11 (2025-12) - '8.2' # from 2022-12 to 2024-12 (2026-12) - '8.3' # from 2023-11 to 2025-12 (2027-12) + - '8.4' # from 2024-11 to 2026-12 (2028-12) steps: - uses: actions/checkout@v4 - uses: docker://pipelinecomponents/php-codesniffer From de099b83a4179e1ded6e6cf4f38388af89e6b897 Mon Sep 17 00:00:00 2001 From: Ben Peachey Date: Mon, 5 May 2025 16:58:18 +0200 Subject: [PATCH 7/8] Update `laminas-diactoros` from v2 to v3. --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index fb38675..48a1bee 100644 --- a/composer.json +++ b/composer.json @@ -20,13 +20,13 @@ "require": { "php": "^8.0", "ext-mbstring": "*", - "laminas/laminas-diactoros": "^2.14", + "laminas/laminas-diactoros": "^3.0", "league/flysystem": "^1.0", "mjrider/flysystem-factory": "^0.7", "pdsinterop/flysystem-rdf": "^0.5", "pietercolpaert/hardf": "^0.3", "psr/http-factory": "^1.0", - "psr/http-message": "^1.0", + "psr/http-message": "^1.1 || ^2.0", "textalk/websocket": "^1.5" }, "scripts": { From 62783191b75f94c8914dc34119e92e0365ae572a Mon Sep 17 00:00:00 2001 From: Ben Peachey Date: Mon, 5 May 2025 17:30:52 +0200 Subject: [PATCH 8/8] Add fix in ServerTest for PHP 8.4 + PHPUnit 9 issue. --- composer.json | 2 +- tests/Unit/ServerTest.php | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 48a1bee..587abd6 100644 --- a/composer.json +++ b/composer.json @@ -39,6 +39,6 @@ }, "type": "library", "require-dev": { - "phpunit/phpunit": "^9" + "phpunit/phpunit": "^9.0 | ^10.0" } } diff --git a/tests/Unit/ServerTest.php b/tests/Unit/ServerTest.php index 1e6c0d8..e5043ab 100644 --- a/tests/Unit/ServerTest.php +++ b/tests/Unit/ServerTest.php @@ -23,6 +23,27 @@ class ServerTest extends TestCase const MOCK_HTTP_METHOD = 'MOCK'; const MOCK_PATH = '/mock/path'; + public static function setUpBeforeClass(): void + { + $phpUnitVersion = \PHPUnit\Runner\Version::id(); + + /* PHP 8.4.0 and PHPUnit 9 triggers a Deprecation Warning, which PHPUnit + * promotes to an Exception, which causes tests to fail.This is fixed in + * PHPUnit v10. As a workaround for v9, instead of loading the real + * interface, a fixed interface is loaded on the fly. + */ + if ( + version_compare(PHP_VERSION, '8.4.0', '>=') + && version_compare($phpUnitVersion, '9.0.0', '>=') + && version_compare($phpUnitVersion, '10.0.0', '<') + ) { + $file = __DIR__ . '/../../vendor/league/flysystem/src/FilesystemInterface.php'; + $contents = file_get_contents($file); + $contents = str_replace(['