feat: read BusProNet base data from the MyE&P API
This commit is contained in:
1 parent
1505f9b50b
commit
2e9328fc51
37 files changed
+2302
-148
No files matched your search
@@ -0,0 +1,166 @@
|
||||
<?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());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user