Files
myep-team/tests/MyEp/Import/TravelExportClientTest.php

202 lines
7.0 KiB
PHP

<?php
namespace App\Tests\MyEp\Import;
use App\MyEp\Api\AccessTokenProvider;
use App\MyEp\Api\ApiClient;
use App\MyEp\Api\ApiException;
use App\MyEp\Import\TravelExportClient;
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 TravelExportClientTest extends TestCase
{
public function testEveryPageIsRead(): void
{
$httpClient = $this->mockClient([
$this->page([$this->row(1, 100), $this->row(2, 200)], 1, 2),
$this->page([$this->row(3, 300)], 2, 2),
]);
$rows = $this->getClientInstance($httpClient)->fetchAll()->rows;
$this->assertCount(3, $rows);
$this->assertEquals([1, 2, 3], array_map(fn ($row) => $row->busProId, $rows));
$this->assertEquals(2, $httpClient->getRequestsCount());
}
public function testASinglePageIsNotFollowedByAnother(): void
{
$httpClient = $this->mockClient([$this->page([$this->row(1, 100)], 1, 1)]);
$this->getClientInstance($httpClient)->fetchAll();
$this->assertEquals(1, $httpClient->getRequestsCount());
}
public function testTheRequestedPageIsPassedOn(): void
{
$urls = [];
$httpClient = new MockHttpClient(function (string $method, string $url) use (&$urls) {
$urls[] = $url;
return $this->page([$this->row(count($urls), 100)], count($urls), 2);
}, 'https://myep.example/');
$this->getClientInstance($httpClient)->fetchAll();
// the path is asserted because it has already moved once - it was api/travels/export
// until MyE&P found that ambiguous against api/travels/{dateCode}
$this->assertStringStartsWith('https://myep.example/api/exports/travels?', $urls[0]);
$this->assertStringContainsString('page=1', $urls[0]);
$this->assertStringContainsString('page=2', $urls[1]);
$this->assertStringContainsString('perPage=500', $urls[0]);
}
public function testAnUnusableRowIsSkippedRatherThanFatal(): void
{
$httpClient = $this->mockClient([
$this->page([
['busProId' => 1],
$this->row(2, 200),
], 1, 1),
]);
$rows = $this->getClientInstance($httpClient)->fetchAll()->rows;
$this->assertCount(1, $rows);
$this->assertEquals(2, $rows[0]->busProId);
}
public function testAnEmptyExportYieldsNoRows(): void
{
$httpClient = $this->mockClient([$this->page([], 1, 1)]);
$this->assertEquals([], $this->getClientInstance($httpClient)->fetchAll()->rows);
}
public function testAPayloadWithoutADataListFails(): void
{
$httpClient = $this->mockClient([new MockResponse('{"meta":{"totalPages":1}}')]);
$this->expectException(ApiException::class);
$this->getClientInstance($httpClient)->fetchAll();
}
private function row(int $busProId, int $hotelBusProId): array
{
return [
'busProId' => $busProId,
'code' => 'CODE'.$busProId,
'product' => 'Produkt '.$busProId,
'dateFrom' => '2026-04-04',
'dateTo' => '2026-04-11',
'hotel' => [
'busProId' => $hotelBusProId,
'code' => 'AAA',
'name' => 'Haus',
'costUnit' => null,
'country' => 'CH',
'countryCode' => 'ch',
],
'pickups' => [],
];
}
/**
* Both counters are losses MyE&P made on its side - a dropped travel, a boarding point with no
* town - and the import is the only place they surface, so the client has to carry them out.
*/
public function testTheCountersMyEpReportsAreCarriedOut(): void
{
$httpClient = $this->mockClient([$this->page([$this->row(1, 100)], 1, 1, skipped: 3, pickupsWithoutMasterData: 21)]);
$export = $this->getClientInstance($httpClient)->fetchAll();
$this->assertSame(3, $export->skipped);
$this->assertSame(21, $export->pickupsWithoutMasterData);
$this->assertSame('2026-09-29T03:20:00+02:00', $export->generatedAt);
}
/**
* They describe the whole export rather than the page, so they repeat unchanged and the first
* page answers for the run. Reading the last one instead would be just as correct today and
* wrong the moment a page is served from a newer build.
*/
public function testTheCountersAreReadFromTheFirstPage(): void
{
$httpClient = $this->mockClient([
$this->page([$this->row(1, 100)], 1, 2, skipped: 3, pickupsWithoutMasterData: 21),
$this->page([$this->row(2, 200)], 2, 2, skipped: 99, pickupsWithoutMasterData: 99),
]);
$export = $this->getClientInstance($httpClient)->fetchAll();
$this->assertSame(3, $export->skipped);
$this->assertSame(21, $export->pickupsWithoutMasterData);
}
/**
* An export whose meta block says nothing must not read as one that dropped nothing by
* accident of a missing key, but zero is the only honest default available here.
*/
public function testAMetaBlockWithoutCountersReadsAsZero(): void
{
$httpClient = new MockHttpClient(
[new MockResponse(json_encode(['data' => [$this->row(1, 100)], 'meta' => ['totalPages' => 1]]))],
'https://myep.example/',
);
$export = $this->getClientInstance($httpClient)->fetchAll();
$this->assertSame(0, $export->skipped);
$this->assertSame(0, $export->pickupsWithoutMasterData);
$this->assertNull($export->generatedAt);
}
private function page(
array $rows,
int $page,
int $totalPages,
int $skipped = 0,
int $pickupsWithoutMasterData = 0,
): MockResponse {
return new MockResponse(json_encode([
'data' => $rows,
'meta' => [
'page' => $page,
'perPage' => 500,
'total' => count($rows),
'totalPages' => $totalPages,
'skipped' => $skipped,
'pickupsWithoutMasterData' => $pickupsWithoutMasterData,
'generatedAt' => '2026-09-29T03:20:00+02:00',
],
]));
}
private function mockClient(array $responses): MockHttpClient
{
return new MockHttpClient($responses, 'https://myep.example/');
}
private function getClientInstance(MockHttpClient $httpClient): TravelExportClient
{
$tokenClient = new MockHttpClient(
fn () => new MockResponse('{"access_token":"token","expires_in":600}'),
'https://myep.example/',
);
$tokens = new AccessTokenProvider($tokenClient, new ArrayAdapter(), new NullLogger(), [
'client_id' => 'team-app',
'client_secret' => 'secret',
]);
return new TravelExportClient(new ApiClient($httpClient, $tokens, new NullLogger()), new NullLogger());
}
}