mockClient([new MockResponse('[{"id":1}]')]); $this->assertEquals([['id' => 1]], $this->getClientInstance($httpClient)->get('api/hotels')); } public function testTheTokenIsSentAsABearer(): void { $authorization = null; $httpClient = $this->mockClient(function (string $method, string $url, array $options) use (&$authorization) { $this->assertEquals('GET', $method); $authorization = $this->headerOf($options, 'authorization'); return new MockResponse('[]'); }); $this->getClientInstance($httpClient)->get('api/hotels'); $this->assertEquals('Authorization: Bearer token-1', $authorization); } public function testQueryParametersArePassedOn(): void { $requestedUrl = null; $httpClient = $this->mockClient(function (string $method, string $url) use (&$requestedUrl) { $requestedUrl = $url; return new MockResponse('[]'); }); $this->getClientInstance($httpClient)->get('api/travels/export', ['page' => 2, 'perPage' => 500]); $this->assertStringContainsString('page=2', $requestedUrl); $this->assertStringContainsString('perPage=500', $requestedUrl); } public function testARejectedTokenIsRenewedOnce(): void { $httpClient = $this->mockClient([ new MockResponse('{"message":"Unauthorized"}', ['http_code' => 401]), new MockResponse('[{"id":1}]'), ]); $result = $this->getClientInstance($httpClient)->get('api/hotels'); $this->assertEquals([['id' => 1]], $result); $this->assertEquals(2, $httpClient->getRequestsCount()); } public function testARepeatedlyRejectedTokenFailsWithoutLooping(): void { $httpClient = $this->mockClient([ new MockResponse('{"message":"Unauthorized"}', ['http_code' => 401]), new MockResponse('{"message":"Unauthorized"}', ['http_code' => 401]), ]); try { $this->getClientInstance($httpClient)->get('api/hotels'); $this->fail('Expected an ApiException'); } catch (ApiException) { } $this->assertEquals(2, $httpClient->getRequestsCount()); } public function testAMissingScopeFails(): void { $httpClient = $this->mockClient([new MockResponse('{"message":"Forbidden"}', ['http_code' => 403])]); $this->expectException(ApiException::class); $this->expectExceptionMessageMatches('/scope/'); $this->getClientInstance($httpClient)->get('api/hotels'); } public function testAServerErrorFails(): void { $httpClient = $this->mockClient([new MockResponse('', ['http_code' => 500])]); $this->expectException(ApiException::class); $this->getClientInstance($httpClient)->get('api/hotels'); } public function testAnErrorReportedWithStatusOkFails(): void { // some MyE&P endpoints answer 200 with an error body, so the payload has to decide $httpClient = $this->mockClient([ new MockResponse('{"error":"upstream unavailable","type":"api_error"}'), ]); $this->expectException(ApiException::class); $this->getClientInstance($httpClient)->get('api/products'); } public function testAnUnreadableBodyFails(): void { $httpClient = $this->mockClient([new MockResponse('not json')]); $this->expectException(ApiException::class); $this->getClientInstance($httpClient)->get('api/hotels'); } public function testAnUnreachableApiFails(): void { $httpClient = $this->mockClient([new MockResponse('', ['error' => 'connection refused'])]); $this->expectException(ApiException::class); $this->getClientInstance($httpClient)->get('api/hotels'); } private function mockClient(array|callable $responses): MockHttpClient { return new MockHttpClient($responses, 'https://myep.example/'); } private function headerOf(array $options, string $name): ?string { foreach ($options['headers'] ?? [] as $header) { if (str_starts_with(strtolower($header), $name.':')) { return $header; } } return null; } private function getClientInstance(MockHttpClient $httpClient): ApiClient { $tokenClient = new MockHttpClient(function () { static $count = 0; return new MockResponse(json_encode([ 'access_token' => 'token-'.++$count, 'expires_in' => 600, ])); }); $tokens = new AccessTokenProvider($tokenClient, new ArrayAdapter(), new NullLogger(), [ 'client_id' => 'team-app', 'client_secret' => 'secret', 'token_path' => 'https://myep.example/token', ]); return new ApiClient($httpClient, $tokens, new NullLogger()); } }