167 lines
5.3 KiB
PHP
167 lines
5.3 KiB
PHP
<?php
|
|
|
|
namespace App\Tests\MyEp\Api;
|
|
|
|
use App\MyEp\Api\AccessTokenProvider;
|
|
use App\MyEp\Api\ApiClient;
|
|
use App\MyEp\Api\ApiException;
|
|
use PHPUnit\Framework\TestCase;
|
|
use Psr\Log\NullLogger;
|
|
use Symfony\Component\Cache\Adapter\ArrayAdapter;
|
|
use Symfony\Component\HttpClient\MockHttpClient;
|
|
use Symfony\Component\HttpClient\Response\MockResponse;
|
|
|
|
class ApiClientTest extends TestCase
|
|
{
|
|
public function testPayloadIsReturned(): void
|
|
{
|
|
$httpClient = $this->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('<html>not json</html>')]);
|
|
|
|
$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());
|
|
}
|
|
}
|