diff --git a/composer.json b/composer.json index abe228a..75671d3 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,7 @@ "minimum-stability": "stable", "prefer-stable": true, "require": { - "php": ">=8.2", + "php": ">=8.3", "ext-ctype": "*", "ext-iconv": "*", "doctrine/dbal": "^3", diff --git a/config/services_test.yaml b/config/services_test.yaml new file mode 100644 index 0000000..0f39803 --- /dev/null +++ b/config/services_test.yaml @@ -0,0 +1,10 @@ +services: + App\Service\Remote\BriloApiUsers: + public: true + autowire: true + App\Service\Remote\BriloApiComments: + public: true + autowire: true + App\Service\Remote\BriloApiPosts: + public: true + autowire: true diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 6c4bfed..2ce2bdc 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -18,8 +18,11 @@ - - tests + + tests/Unit + + + tests/Integration diff --git a/src/Entity/Remote/Brilo/Users/AddressGeo.php b/src/Entity/Remote/Brilo/Users/AddressGeo.php index e5d06f6..ef00d97 100644 --- a/src/Entity/Remote/Brilo/Users/AddressGeo.php +++ b/src/Entity/Remote/Brilo/Users/AddressGeo.php @@ -7,8 +7,8 @@ namespace App\Entity\Remote\Brilo\Users; readonly class AddressGeo { public function __construct( - public float $lat, - public float $lng, + public string $lat, + public string $lng, ) { } } diff --git a/src/Service/Remote/BriloApiComments.php b/src/Service/Remote/BriloApiComments.php new file mode 100644 index 0000000..e6934f3 --- /dev/null +++ b/src/Service/Remote/BriloApiComments.php @@ -0,0 +1,35 @@ +fetchApiResponse('https://jsonplaceholder.typicode.com/comments', Comment::class); + } +} diff --git a/src/Service/Remote/BriloApiFetchTrait.php b/src/Service/Remote/BriloApiFetchTrait.php new file mode 100644 index 0000000..ad27f8c --- /dev/null +++ b/src/Service/Remote/BriloApiFetchTrait.php @@ -0,0 +1,54 @@ + $class Class name of the entity to deserialize data into. + * + * @return T[] + * @throws \Exception + */ + protected function fetchApiResponse(string $uri, string $class): array + { + return $this->retryingFailRequest($this->retryCount, $this->sleepTimeS, [TransportException::class, BriloRemoteApiException::class], $this->logger, function () use ($uri, $class) { + $this->logger->debug('Trying to download brilo users'); + + $response = $this->httpClient->request('GET', $uri); + + if ($response->getStatusCode() != 200) { + $this->logger->error("Can't download brilo users. HTTP status code: " . $response->getStatusCode()); + throw new BriloRemoteApiException("Can't download brilo users. HTTP status code: " . $response->getStatusCode()); + } + + $this->logger->debug('Brilo users downloaded'); + + $this->logger->debug('Deserialize brilo users'); + + // extra attributes muzeme safe ignorovat + return $this->serializer->deserialize($response->getContent(), $class . '[]', 'json'); + }); + } +} diff --git a/src/Service/Remote/BriloApiPosts.php b/src/Service/Remote/BriloApiPosts.php new file mode 100644 index 0000000..e08c3af --- /dev/null +++ b/src/Service/Remote/BriloApiPosts.php @@ -0,0 +1,10 @@ +fetchApiResponse('https://jsonplaceholder.typicode.com/users', User::class); + } +} diff --git a/src/Service/Remote/BriloRemoteApiException.php b/src/Service/Remote/BriloRemoteApiException.php new file mode 100644 index 0000000..b629b5b --- /dev/null +++ b/src/Service/Remote/BriloRemoteApiException.php @@ -0,0 +1,12 @@ + $catchableExceptions + * @param LoggerInterface $logger + * @param callable $callback + * @return mixed + * @throws Exception + */ + protected function retryingFailRequest( + int $count, + float $sleep, + array $catchableExceptions, + LoggerInterface $logger, + callable $callback + ): mixed { + for ($i = 0;; $i++) { + try { + return $callback(); + } catch (Exception $e) { + foreach ($catchableExceptions as $exceptionClass) { + if ($e instanceof $exceptionClass) { + $logger->error("transport: fail request retrying... got catchable exception", [ + 'exception' => $e, + 'try' => $i + ]); + + usleep((int) ($sleep * 1_000_000)); + + if ($i == $count) { + throw $e; + } + + continue 2; + } + } + + throw $e; + } + } + + // phpstan fail + return null; + } +} diff --git a/tests/Common/LoggerTrait.php b/tests/Common/LoggerTrait.php new file mode 100644 index 0000000..f27fa74 --- /dev/null +++ b/tests/Common/LoggerTrait.php @@ -0,0 +1,21 @@ +pushHandler(new TestHandler()); + return $logger; + } +} diff --git a/tests/Integration/Service/Remote/BlogApiUsersTest.php b/tests/Integration/Service/Remote/BlogApiUsersTest.php new file mode 100644 index 0000000..2a60daf --- /dev/null +++ b/tests/Integration/Service/Remote/BlogApiUsersTest.php @@ -0,0 +1,28 @@ +get(BriloApiUsers::class); + $users = $apiUsers->getUsers(); + $this->assertIsArray($users); + $this->assertGreaterThan(0, count($users)); + // Tady bych dal jeste check na zaznam ktery vim ze vzdy existuje + } +} diff --git a/tests/Integration/Service/Remote/BriloApiCommentsTest.php b/tests/Integration/Service/Remote/BriloApiCommentsTest.php new file mode 100644 index 0000000..b33f538 --- /dev/null +++ b/tests/Integration/Service/Remote/BriloApiCommentsTest.php @@ -0,0 +1,28 @@ +get(BriloApiComments::class); + $users = $api->getComments(); + $this->assertIsArray($users); + $this->assertGreaterThan(0, count($users)); + // Tady bych dal jeste check na zaznam ktery vim ze vzdy existuje + } +} diff --git a/tests/Integration/Service/Remote/BriloApiPostsTest.php b/tests/Integration/Service/Remote/BriloApiPostsTest.php new file mode 100644 index 0000000..cf3020a --- /dev/null +++ b/tests/Integration/Service/Remote/BriloApiPostsTest.php @@ -0,0 +1,28 @@ +get(BriloApiPosts::class); + $users = $api->getPosts(); + $this->assertIsArray($users); + $this->assertGreaterThan(0, count($users)); + // Tady bych dal jeste check na zaznam ktery vim ze vzdy existuje + } +} diff --git a/tests/Unit/Service/Remote/BlogApiPostsTest.php b/tests/Unit/Service/Remote/BlogApiPostsTest.php new file mode 100644 index 0000000..b485a7c --- /dev/null +++ b/tests/Unit/Service/Remote/BlogApiPostsTest.php @@ -0,0 +1,10 @@ + 1, + "id" => 1, + "name" => "id labore ex et quam laborum", + "email" => "Eliseo@gardner.biz", + "body" => "laudantium enim quasi est quidem magnam voluptate ipsam eos +tempora quo necessitatibus +dolor quam autem quasi +reiciendis et nam sapiente accusantium" + ], + [ + "postId" => 1, + "id" => 2, + "name" => "quo vero reiciendis velit similique earum", + "email" => "Jayne_Kuhic@sydney.com", + "body" => "est natus enim nihil est dolore omnis voluptatem numquam +et omnis occaecati quod ullam at +voluptatem error expedita pariatur +nihil sint nostrum voluptatem reiciendis et" + ] + ]; + + public function testCommentsListSuccess(): void + { + $testCommentsList = $this->buildTestCommentsList(); + $serializer = $this->createMock(SerializerInterface::class); + $serializer + ->expects($this->once()) + ->method('deserialize') + ->with( + json_encode(self::API_COMMENTS_LIST), + Comment::class . '[]', + 'json' + )->willReturn($testCommentsList); + + $response = new JsonMockResponse(self::API_COMMENTS_LIST); + $mockedClient = new MockHttpClient($response); + $briloApiPosts = new BriloApiComments($mockedClient, $this->getLogger(), $serializer); + $this->assertEquals($testCommentsList, $briloApiPosts->getComments()); + } + + public function testInvalidHttpCodeReturned(): void + { + $serializer = $this->createMock(SerializerInterface::class); + $serializer + ->expects($this->never()) + ->method('deserialize'); + + $response = new JsonMockResponse(self::API_COMMENTS_LIST, [ + 'http_code' => 401, + ]); + $mockedClient = new MockHttpClient(function () use ($response) { + return $response; + }); + $this->expectException(BriloRemoteApiException::class); + $briloApiPosts = new BriloApiComments($mockedClient, $this->getLogger(), $serializer); + $briloApiPosts->getComments(); + } + + /** + * @return Comment[] + */ + public function buildTestCommentsList(): array + { + // Convert self::API_POSTS_LIST to Post[] and return it + return array_map(function ($postData) { + return new Comment( + $postData['id'], + $postData['postId'], + $postData['name'], + $postData['email'], + $postData['body'] + ); + }, self::API_COMMENTS_LIST); + } +} diff --git a/tests/Unit/Service/Remote/BriloApiUsersTest.php b/tests/Unit/Service/Remote/BriloApiUsersTest.php new file mode 100644 index 0000000..57578a5 --- /dev/null +++ b/tests/Unit/Service/Remote/BriloApiUsersTest.php @@ -0,0 +1,142 @@ + 0, + "name" => "Leanne Graham", + "username" => "Bret", + "email" => "Sincere@april.biz", + "address" => [ + "street" => "Kulas Light", + "suite" => "Apt. 555", + "city" => "Gwenborough", + "zipcode" => "92997-3874", + "geo" => [ + "lat" => "-38.3159", + "lng" => "80.1496" + ] + ], + "phone" => "0-770-736-8031 x56442", + "website" => "hildegard.org", + "company" => [ + "name" => "Romaguera-Crona", + "catchPhrase" => "Multi-layered client-server neural-net", + "bs" => "harness real-time e-markets" + ], + ], + [ + "id" => 1, + "name" => "Ervin Howell", + "username" => "Antonette", + "email" => "Shanna@melissa.tv", + "address" => [ + "street" => "Victor Plains", + "suite" => "Suite 878", + "city" => "Wisokyburgh", + "zipcode" => "90565-7771", + "geo" => [ + "lat" => "-44.9509", + "lng" => "-35.4618" + ] + ], + "phone" => "009-692-6593 x09125", + "website" => "anastasia.net", + "company" => [ + "name" => "Deckow-Crist", + "catchPhrase" => "Proactive didactic contingency", + "bs" => "synergize scalable supply-chains" + ], + "nonExistentKey" => "value" + ] + ]; + + public function testUserListSuccess(): void + { + $testUserList = $this->buildTestUserList(); + $serializer = $this->createMock(SerializerInterface::class); + $serializer + ->expects($this->once()) + ->method('deserialize') + ->with( + json_encode(self::API_USER_LIST), + User::class . '[]', + 'json' + )->willReturn($testUserList); + + $response = new JsonMockResponse(self::API_USER_LIST); + $mockedClient = new MockHttpClient($response); + $briloApiUsers = new BriloApiUsers($mockedClient, $this->getLogger(), $serializer); + $this->assertEquals($testUserList, $briloApiUsers->getUsers()); + } + + public function testInvalidHttpCodeReturned(): void + { + $serializer = $this->createMock(SerializerInterface::class); + $serializer + ->expects($this->never()) + ->method('deserialize'); + + $response = new JsonMockResponse(self::API_USER_LIST, [ + 'http_code' => 401, + ]); + $mockedClient = new MockHttpClient(function () use ($response) { + return $response; + }); + $this->expectException(BriloRemoteApiException::class); + $briloApiUsers = new BriloApiUsers($mockedClient, $this->getLogger(), $serializer); + $briloApiUsers->getUsers(); + } + + /** + * @return User[] + */ + private function buildTestUserList(): array + { + // convert self::API_USER_LIST to User[] + return array_map(function ($userData) { + return new User( + $userData['id'], + $userData['name'], + $userData['username'], + $userData['email'], + $userData['phone'], + $userData['website'], + new Address( + $userData['address']['street'], + $userData['address']['suite'], + $userData['address']['city'], + $userData['address']['zipcode'], + new AddressGeo( + $userData['address']['geo']['lat'], + $userData['address']['geo']['lng'] + ) + ), + new Company( + $userData['company']['name'], + $userData['company']['catchPhrase'], + $userData['company']['bs'] + ) + ); + }, self::API_USER_LIST); + } +} diff --git a/tests/Unit/Service/Remote/RetryingClientTraitTest.php b/tests/Unit/Service/Remote/RetryingClientTraitTest.php new file mode 100644 index 0000000..cb9d312 --- /dev/null +++ b/tests/Unit/Service/Remote/RetryingClientTraitTest.php @@ -0,0 +1,62 @@ +callCount = 0; + } + + public function testSuccess(): void + { + $trait = new class { + use RetryingClientTrait { + retryingFailRequest as public; // make the method public + } + }; + + + $result = $trait->retryingFailRequest(2, 0, [\RuntimeException::class], $this->getLogger(), function () { + $this->callCount = $this->callCount + 1; + return 'foo'; + }); + + $this->assertEquals(1, $this->callCount); + $this->assertEquals('foo', $result); + } + public function testRetyingFail(): void + { + $trait = new class { + use RetryingClientTrait { + retryingFailRequest as public; // make the method public + } + }; + + + try { + $trait->retryingFailRequest(2, 0, [\RuntimeException::class], $this->getLogger(), function () { + $this->callCount = $this->callCount + 1; + throw new \RuntimeException("test"); + }); + } catch (\RuntimeException) { + // do nothing + } + + $this->assertEquals(3, $this->callCount); + } + // extra attributes muzeme safe ignorovat +}