diff --git a/.config/phpunit.xml.dist b/.config/phpunit.xml.dist index 9beadd4..e9bfed1 100644 --- a/.config/phpunit.xml.dist +++ b/.config/phpunit.xml.dist @@ -1,13 +1,14 @@ - - ../tests/ + + ../tests/unit/ - + ../src/ 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 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 diff --git a/composer.json b/composer.json index 63bfff1..0bb7064 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,8 @@ "pdsinterop/flysystem-rdf": "^0.6", "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" }, "require-dev": { "ext-xdebug": "*", diff --git a/tests/unit/ExceptionTest.php b/tests/unit/ExceptionTest.php new file mode 100644 index 0000000..fb7406b --- /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); + } +} diff --git a/tests/unit/ServerTest.php b/tests/unit/ServerTest.php index 6c99246..7df8351 100644 --- a/tests/unit/ServerTest.php +++ b/tests/unit/ServerTest.php @@ -10,6 +10,8 @@ use Laminas\Diactoros\Response; use Laminas\Diactoros\ServerRequest; use League\Flysystem\FilesystemInterface; +use Pdsinterop\Rdf\Flysystem\Plugin\AsMime; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -28,15 +30,37 @@ class ServerTest extends TestCase ////////////////////////////////// FIXTURES \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ const MOCK_BODY = 'php://temp'; - const MOCK_PATH = '/path/to/resource/'; + const MOCK_HTTP_METHOD = 'MOCK'; + const MOCK_PATH = '/mock/path/'; const MOCK_SERVER_PARAMS = []; const MOCK_UPLOADED_FILES = []; const MOCK_URL = 'https://example.com' . self::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(['expectException(ArgumentCountError::class); $this->expectExceptionMessageMatches('/Too few arguments .+ 0 passed/'); @@ -45,7 +69,7 @@ public function testInstatiationWithoutFileSystem() } /** @testdox Server should complain when instantiated without Response */ - public function testInstatiationWithoutResponse() + public function testServerInstatiationWithoutResponse() { $this->expectException(ArgumentCountError::class); $this->expectExceptionMessageMatches('/Too few arguments .+ 1 passed/'); @@ -56,7 +80,7 @@ public function testInstatiationWithoutResponse() } /** @testdox Server should be instantiated when constructed without Graph */ - public function testInstatiationWithoutGraph() + public function testServerInstatiationWithoutGraph() { $mockFileSystem = $this->getMockBuilder(FilesystemInterface::class)->getMock(); $mockResponse = $this->getMockBuilder(ResponseInterface::class)->getMock(); @@ -68,7 +92,7 @@ public function testInstatiationWithoutGraph() } /** @testdox Server should be instantiated when constructed with Graph */ - public function testInstatiationWithGraph() + public function testServerInstatiationWithGraph() { $mockFileSystem = $this->getMockBuilder(FilesystemInterface::class)->getMock(); $mockResponse = $this->getMockBuilder(ResponseInterface::class)->getMock(); @@ -85,20 +109,18 @@ public function testInstatiationWithGraph() * * @covers ::respondToRequest */ - public function testRespondToRequestWithoutRequest() + public function testServerRespondToRequestWithoutRequest() { // Arrange $mockFileSystem = $this->getMockBuilder(FilesystemInterface::class)->getMock(); $mockResponse = $this->getMockBuilder(ResponseInterface::class)->getMock(); - $mockGraph = $this->getMockBuilder(Graph::class)->getMock(); - - $server = new Server($mockFileSystem, $mockResponse, $mockGraph); // Assert $this->expectException(ArgumentCountError::class); $this->expectExceptionMessageMatches('/Too few arguments .+ 0 passed/'); // Act + $server = new Server($mockFileSystem, $mockResponse); $server->respondToRequest(); } @@ -113,21 +135,37 @@ public function testRespondToRequestWithUnsupportedHttpMethod($httpMethod) { // Arrange $mockFileSystem = $this->getMockBuilder(FilesystemInterface::class)->getMock(); - $mockGraph = $this->getMockBuilder(Graph::class)->getMock(); - $request = $this->createRequest($httpMethod); - $mockResponse = new Response(); - - $server = new Server($mockFileSystem, $mockResponse, $mockGraph); + $request = $this->createRequest($httpMethod); // Assert $this->expectException(Exception::class); - $this->expectExceptionMessage('Unknown or unsupported HTTP METHOD'); + $this->expectExceptionMessage(vsprintf(Server::ERROR_UNKNOWN_HTTP_METHOD, [$httpMethod])); // Act + $server = new Server($mockFileSystem, $mockResponse); $server->respondToRequest($request); } + /** + * @testdox Server should return response when asked to RespondToRequest with valid request + * + * @covers ::respondToRequest + */ + public function testServerRespondToRequestWithRequest() + { + // Arrange + $mockFileSystem = $this->createMockFileSystem(); + $request = $this->createRequest('GET'); + + // Act + $server = new Server($mockFileSystem, new Response()); + $response = $server->respondToRequest($request); + + // Assert + $this->assertEquals(200, $response->getStatusCode()); + } + /** * @testdox Server should create a resource when asked to create a resource with Slug header present * @@ -195,11 +233,36 @@ public static function provideUnsupportedHttpMethods() return [ 'string:CONNECT' => ['CONNECT'], 'string:TRACE' => ['TRACE'], - 'string:UNKNOWN' => ['UNKNOWN'], + 'string:UNKNOWN' => [self::MOCK_HTTP_METHOD], ]; } ////////////////////////////// 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', 'getMimetype', 'read']) + ->disableOriginalConstructor() + ->getMock(); + + $mockAsMime->method('getMimetype')->willReturn('text/turtle'); + $mockAsMime->method('has')->willReturn(true); + $mockAsMime->method('read')->willReturn(''); + + $mockFileSystem->method('asMime')->willReturn($mockAsMime); + + return $mockFileSystem; + } + private function createRequest(string $httpMethod, array $headers = []): ServerRequestInterface { return new ServerRequest(