diff --git a/CHANGELOG.md b/CHANGELOG.md index 5efbe54..953212f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - New method `Art4\Wegliphant\Client::authenticate()` to set your API key for authorized API requests. +- New method `Art4\Wegliphant\Client::listOwnNotices()` to list all notices for the authorized user. - New class `Art4\Wegliphant\Exception\UnexpectedResponseException` that will be thrown if an error happens while processing the response. ### Changed diff --git a/README.md b/README.md index 909bb1c..7f94be2 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,69 @@ You can find your API key [here](https://www.weg.li/user/edit). $client->authenticate($apiKey); ``` +### List all own notices + +```php +$notices = $client->listOwnNotices(); + +// $notices contains: +[ + [...], + [ + 'token' => '8843d7f92416211de9ebb963ff4ce281', + 'status' => 'shared', + 'street' => 'Musterstraße 123', + 'city' => 'Berlin', + 'zip' => '12305', + 'latitude' => 52.5170365, + 'longitude' => 13.3888599, + 'registration' => 'EX AM 713', + 'color' => 'white', + 'brand' => 'Car brand', + 'charge' => [ + 'tbnr' => '141312', + 'description' => 'Sie parkten im absoluten Haltverbot (Zeichen 283).', + 'fine' => '25.0', + 'bkat' => '§ 41 Abs. 1 iVm Anlage 2, § 49 StVO; § 24 Abs. 1, 3 Nr. 5 StVG; 52 BKat', + 'penalty' => null, + 'fap' => null, + 'points' => 0, + 'valid_from' => '2021-11-09T00:00:00.000+01:00', + 'valid_to' => null, + 'implementation' => null, + 'classification' => 5, + 'variant_table_id' => 741017, + 'rule_id' => 39, + 'table_id' => null, + 'required_refinements' => '00000000000000000000000000000000', + 'number_required_refinements' => 0, + 'max_fine' => '0.0', + 'created_at' => '2023-09-18T15:30:43.312+02:00', + 'updated_at' => '2023-09-18T15:30:43.312+02:00', + ], + 'tbnr' => '141312', + 'start_date' => '2023-11-12T11:31:00.000+01:00', + 'end_date' => '2023-11-12T11:36:00.000+01:00', + 'note' => 'Some user notes', + 'photos' => [ + [ + 'filename' => 'IMG_20231124_113156.jpg', + 'url' => 'https://example.com/storage/IMG_20231124_113156.jpg', + ], + ], + 'created_at' => '2023-11-12T11:33:29.423+01:00', + 'updated_at' => '2023-11-12T11:49:24.383+01:00', + 'sent_at' => '2023-11-12T11:49:24.378+01:00', + 'vehicle_empty' => true, + 'hazard_lights' => false, + 'expired_tuv' => false, + 'expired_eco' => false, + 'over_2_8_tons' => false, + ], + [...], +], +``` + ### List all districts ```php diff --git a/src/Client.php b/src/Client.php index 47e8ff5..3980390 100644 --- a/src/Client.php +++ b/src/Client.php @@ -47,9 +47,7 @@ public function listDistricts(): array { $response = $this->sendJsonRequest('GET', '/districts.json'); - $this->ensureJsonResponse($response, 200); - - return $this->parseJsonResponseToArray($response); + return $this->parseJsonResponseToArray($response, 200); } /** @@ -66,9 +64,7 @@ public function getDistrictByZip(string $zip): array { $response = $this->sendJsonRequest('GET', '/districts/' . $zip . '.json'); - $this->ensureJsonResponse($response, 200); - - return $this->parseJsonResponseToArray($response); + return $this->parseJsonResponseToArray($response, 200); } /** @@ -85,9 +81,24 @@ public function listCharges(): array { $response = $this->sendJsonRequest('GET', '/charges.json'); - $this->ensureJsonResponse($response, 200); + return $this->parseJsonResponseToArray($response, 200); + } + + /** + * List all notices for the authorized user using the endpoint `GET /api/notices` + * + * @link https://www.weg.li/api-docs/index.html#operations-notice-get_notices + * + * @throws \Psr\Http\Client\ClientExceptionInterface If an error happens while processing the request. + * @throws UnexpectedResponseException If an error happens while processing the response. + * + * @return mixed[] + */ + public function listOwnNotices(): array + { + $response = $this->sendJsonRequest('GET', '/api/notices'); - return $this->parseJsonResponseToArray($response); + return $this->parseJsonResponseToArray($response, 200); } /** @@ -108,28 +119,29 @@ private function sendJsonRequest( } /** - * @throws UnexpectedResponseException If the response has the wrong status code or content type header. + * @throws UnexpectedResponseException If the response has the wrong status code. + * @throws UnexpectedResponseException If the response has the wrong content type header. + * @throws UnexpectedResponseException If an error happens while parsing the JSON response. + * + * @return mixed[] */ - private function ensureJsonResponse( - ResponseInterface $response, - int $expectedStatusCode, - ): void { + private function parseJsonResponseToArray(ResponseInterface $response, int $expectedStatusCode): array + { if ($response->getStatusCode() !== $expectedStatusCode) { - throw UnexpectedResponseException::create('Server replied with status code ' . $response->getStatusCode(), $response); + throw UnexpectedResponseException::create( + sprintf( + 'Server replied with the status code %d, but %d was expected.', + $response->getStatusCode(), + $expectedStatusCode, + ), + $response, + ); } if (! str_starts_with($response->getHeaderLine('content-type'), 'application/json')) { throw UnexpectedResponseException::create('Server replied not with JSON content.', $response); } - } - /** - * @throws UnexpectedResponseException If an error happens while parsing the JSON response. - * - * @return mixed[] - */ - private function parseJsonResponseToArray(ResponseInterface $response): array - { $responseBody = $response->getBody()->__toString(); try { diff --git a/tests/Unit/Client/GetDistrictByZipTest.php b/tests/Unit/Client/GetDistrictByZipTest.php index a24e314..150767d 100644 --- a/tests/Unit/Client/GetDistrictByZipTest.php +++ b/tests/Unit/Client/GetDistrictByZipTest.php @@ -121,7 +121,7 @@ public function testGetDistrictByZipThrowsUnexpectedResponseExceptionOnWrongStat ); $this->expectException(UnexpectedResponseException::class); - $this->expectExceptionMessage('Server replied with status code 500'); + $this->expectExceptionMessage('Server replied with the status code 500, but 200 was expected.'); $client->getDistrictByZip('00000'); } diff --git a/tests/Unit/Client/ListChargesTest.php b/tests/Unit/Client/ListChargesTest.php index 84325c7..8247f53 100644 --- a/tests/Unit/Client/ListChargesTest.php +++ b/tests/Unit/Client/ListChargesTest.php @@ -193,7 +193,7 @@ public function testListChargesThrowsUnexpectedResponseExceptionOnWrongStatusCod ); $this->expectException(UnexpectedResponseException::class); - $this->expectExceptionMessage('Server replied with status code 500'); + $this->expectExceptionMessage('Server replied with the status code 500, but 200 was expected.'); $client->listCharges(); } diff --git a/tests/Unit/Client/ListDistrictsTest.php b/tests/Unit/Client/ListDistrictsTest.php index dbdfac3..66a774f 100644 --- a/tests/Unit/Client/ListDistrictsTest.php +++ b/tests/Unit/Client/ListDistrictsTest.php @@ -151,7 +151,7 @@ public function testListDistrictsThrowsUnexpectedResponseExceptionOnWrongStatusC ); $this->expectException(UnexpectedResponseException::class); - $this->expectExceptionMessage('Server replied with status code 500'); + $this->expectExceptionMessage('Server replied with the status code 500, but 200 was expected.'); $client->listDistricts(); } diff --git a/tests/Unit/Client/ListOwnNoticesTest.php b/tests/Unit/Client/ListOwnNoticesTest.php new file mode 100644 index 0000000..03580ed --- /dev/null +++ b/tests/Unit/Client/ListOwnNoticesTest.php @@ -0,0 +1,273 @@ + '8843d7f92416211de9ebb963ff4ce281', + 'status' => 'shared', + 'street' => 'Musterstraße 123', + 'city' => 'Berlin', + 'zip' => '12305', + 'latitude' => 52.5170365, + 'longitude' => 13.3888599, + 'registration' => 'EX AM 713', + 'color' => 'white', + 'brand' => 'Car brand', + 'charge' => [ + 'tbnr' => '141312', + 'description' => 'Sie parkten im absoluten Haltverbot (Zeichen 283).', + 'fine' => '25.0', + 'bkat' => '§ 41 Abs. 1 iVm Anlage 2, § 49 StVO; § 24 Abs. 1, 3 Nr. 5 StVG; 52 BKat', + 'penalty' => null, + 'fap' => null, + 'points' => 0, + 'valid_from' => '2021-11-09T00:00:00.000+01:00', + 'valid_to' => null, + 'implementation' => null, + 'classification' => 5, + 'variant_table_id' => 741017, + 'rule_id' => 39, + 'table_id' => null, + 'required_refinements' => '00000000000000000000000000000000', + 'number_required_refinements' => 0, + 'max_fine' => '0.0', + 'created_at' => '2023-09-18T15:30:43.312+02:00', + 'updated_at' => '2023-09-18T15:30:43.312+02:00', + ], + 'tbnr' => '141312', + 'start_date' => '2023-11-12T11:31:00.000+01:00', + 'end_date' => '2023-11-12T11:36:00.000+01:00', + 'note' => 'Some user notes', + 'photos' => [ + [ + 'filename' => 'IMG_20231112_113156.jpg', + 'url' => 'https://example.com/storage/IMG_20231112_113156.jpg', + ], + ], + 'created_at' => '2023-11-12T11:33:29.423+01:00', + 'updated_at' => '2023-11-12T11:49:24.383+01:00', + 'sent_at' => '2023-11-12T11:49:24.378+01:00', + 'vehicle_empty' => true, + 'hazard_lights' => false, + 'expired_tuv' => false, + 'expired_eco' => false, + 'over_2_8_tons' => false, + ], + ]; + + $request = $this->createMock(RequestInterface::class); + $request->expects($this->exactly(1))->method('withHeader')->willReturn($request); + + $requestFactory = $this->createMock(RequestFactoryInterface::class); + $requestFactory->expects($this->exactly(1))->method('createRequest')->with('GET', 'https://www.weg.li/api/notices')->willReturn($request); + + $stream = $this->createConfiguredMock( + StreamInterface::class, + [ + '__toString' => json_encode($expected), + ], + ); + + $response = $this->createConfiguredMock( + ResponseInterface::class, + [ + 'getStatusCode' => 200, + 'getHeaderLine' => 'application/json', + 'getBody' => $stream, + ] + ); + + $httpClient = $this->createMock(ClientInterface::class); + $httpClient->expects($this->exactly(1))->method('sendRequest')->willReturn($response); + + $client = Client::create( + $httpClient, + $requestFactory, + ); + + $response = $client->listOwnNotices(); + + $this->assertSame( + $expected, + $response, + ); + } + + public function testListOwnNoticesThrowsClientException(): void + { + $request = $this->createMock(RequestInterface::class); + $request->expects($this->exactly(1))->method('withHeader')->willReturn($request); + + $requestFactory = $this->createMock(RequestFactoryInterface::class); + $requestFactory->expects($this->exactly(1))->method('createRequest')->with('GET', 'https://www.weg.li/api/notices')->willReturn($request); + + $httpClient = $this->createMock(ClientInterface::class); + $httpClient->expects($this->exactly(1))->method('sendRequest')->willThrowException( + $this->createMock(ClientExceptionInterface::class), + ); + + $client = Client::create( + $httpClient, + $requestFactory, + ); + + $this->expectException(ClientExceptionInterface::class); + $this->expectExceptionMessage(''); + + $client->listOwnNotices(); + } + + public function testListOwnNoticesThrowsUnexpectedResponseExceptionOnWrongStatusCode(): void + { + $request = $this->createMock(RequestInterface::class); + $request->expects($this->exactly(1))->method('withHeader')->willReturn($request); + + $requestFactory = $this->createMock(RequestFactoryInterface::class); + $requestFactory->expects($this->exactly(1))->method('createRequest')->with('GET', 'https://www.weg.li/api/notices')->willReturn($request); + + $response = $this->createConfiguredMock( + ResponseInterface::class, + [ + 'getStatusCode' => 500, + ] + ); + + $httpClient = $this->createMock(ClientInterface::class); + $httpClient->expects($this->exactly(1))->method('sendRequest')->willReturn($response); + + $client = Client::create( + $httpClient, + $requestFactory, + ); + + $this->expectException(UnexpectedResponseException::class); + $this->expectExceptionMessage('Server replied with the status code 500, but 200 was expected.'); + + $client->listOwnNotices(); + } + + public function testListOwnNoticesThrowsUnexpectedResponseExceptionOnWrongContentTypeHeader(): void + { + $request = $this->createMock(RequestInterface::class); + $request->expects($this->exactly(1))->method('withHeader')->willReturn($request); + + $requestFactory = $this->createMock(RequestFactoryInterface::class); + $requestFactory->expects($this->exactly(1))->method('createRequest')->with('GET', 'https://www.weg.li/api/notices')->willReturn($request); + + $response = $this->createConfiguredMock( + ResponseInterface::class, + [ + 'getStatusCode' => 200, + 'getHeaderLine' => 'text/html', + ] + ); + + $httpClient = $this->createMock(ClientInterface::class); + $httpClient->expects($this->exactly(1))->method('sendRequest')->willReturn($response); + + $client = Client::create( + $httpClient, + $requestFactory, + ); + + $this->expectException(UnexpectedResponseException::class); + $this->expectExceptionMessage('Server replied not with JSON content.'); + + $client->listOwnNotices(); + } + + public function testListOwnNoticesThrowsUnexpectedResponseExceptionOnInvalidJsonBody(): void + { + $request = $this->createMock(RequestInterface::class); + $request->expects($this->exactly(1))->method('withHeader')->willReturn($request); + + $requestFactory = $this->createMock(RequestFactoryInterface::class); + $requestFactory->expects($this->exactly(1))->method('createRequest')->with('GET', 'https://www.weg.li/api/notices')->willReturn($request); + + $stream = $this->createConfiguredMock( + StreamInterface::class, + [ + '__toString' => 'invalid json', + ], + ); + + $response = $this->createConfiguredMock( + ResponseInterface::class, + [ + 'getStatusCode' => 200, + 'getHeaderLine' => 'application/json', + 'getBody' => $stream, + ] + ); + + $httpClient = $this->createMock(ClientInterface::class); + $httpClient->expects($this->exactly(1))->method('sendRequest')->willReturn($response); + + $client = Client::create( + $httpClient, + $requestFactory, + ); + + $this->expectException(UnexpectedResponseException::class); + $this->expectExceptionMessage('Response body contains no valid JSON: invalid json'); + + $client->listOwnNotices(); + } + + public function testListOwnNoticesThrowsUnexpectedResponseExceptionOnJsonBodyWithoutArray(): void + { + $request = $this->createMock(RequestInterface::class); + $request->expects($this->exactly(1))->method('withHeader')->willReturn($request); + + $requestFactory = $this->createMock(RequestFactoryInterface::class); + $requestFactory->expects($this->exactly(1))->method('createRequest')->with('GET', 'https://www.weg.li/api/notices')->willReturn($request); + + $stream = $this->createConfiguredMock( + StreamInterface::class, + [ + '__toString' => '"this is not an array"', + ], + ); + + $response = $this->createConfiguredMock( + ResponseInterface::class, + [ + 'getStatusCode' => 200, + 'getHeaderLine' => 'application/json', + 'getBody' => $stream, + ] + ); + + $httpClient = $this->createMock(ClientInterface::class); + $httpClient->expects($this->exactly(1))->method('sendRequest')->willReturn($response); + + $client = Client::create( + $httpClient, + $requestFactory, + ); + + $this->expectException(UnexpectedResponseException::class); + $this->expectExceptionMessage('Response JSON does not contain an array: "this is not an array"'); + + $client->listOwnNotices(); + } +}