146 lines
5.3 KiB
PHP
146 lines
5.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Controller\Api;
|
|
|
|
use App\Controller\Api\ExportController;
|
|
use App\Model\TravelExport;
|
|
use App\Model\TravelExportQuery;
|
|
use App\Service\TravelExportBuilder;
|
|
use PHPUnit\Framework\TestCase;
|
|
use Symfony\Component\DependencyInjection\Container;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
|
|
class ExportControllerTest extends TestCase
|
|
{
|
|
public function testPaginatesAndReportsWholeExportFigures(): void
|
|
{
|
|
$meta = $this->call(new TravelExportQuery(1, 5))['meta'];
|
|
|
|
self::assertSame(1, $meta['page']);
|
|
self::assertSame(5, $meta['perPage']);
|
|
self::assertSame(12, $meta['total'], 'total is the whole export, not this page');
|
|
self::assertSame(3, $meta['totalPages']);
|
|
}
|
|
|
|
public function testCountersAreIdenticalOnEveryPage(): void
|
|
{
|
|
// Whole-export figures repeated per page, so a consumer can abort after page 1.
|
|
$first = $this->call(new TravelExportQuery(1, 5))['meta'];
|
|
$last = $this->call(new TravelExportQuery(3, 5))['meta'];
|
|
|
|
foreach (['total', 'totalPages', 'skipped', 'pickupsWithoutMasterData', 'sourceGeneratedAt', 'generatedAt'] as $key) {
|
|
self::assertSame($first[$key], $last[$key], $key.' must not vary by page');
|
|
}
|
|
}
|
|
|
|
public function testConsecutivePagesDoNotOverlap(): void
|
|
{
|
|
$first = array_column($this->call(new TravelExportQuery(1, 5))['data'], 'busProId');
|
|
$second = array_column($this->call(new TravelExportQuery(2, 5))['data'], 'busProId');
|
|
|
|
self::assertSame([1, 2, 3, 4, 5], $first);
|
|
self::assertSame([6, 7, 8, 9, 10], $second);
|
|
self::assertSame([], array_intersect($first, $second));
|
|
}
|
|
|
|
public function testTheLastPageIsShort(): void
|
|
{
|
|
$data = $this->call(new TravelExportQuery(3, 5))['data'];
|
|
|
|
self::assertCount(2, $data);
|
|
self::assertSame([11, 12], array_column($data, 'busProId'));
|
|
}
|
|
|
|
public function testAPageBeyondTheEndIsEmptyRatherThanAnError(): void
|
|
{
|
|
$body = $this->call(new TravelExportQuery(99, 5));
|
|
|
|
self::assertSame([], $body['data']);
|
|
self::assertSame(12, $body['meta']['total']);
|
|
}
|
|
|
|
public function testDefaultsToTheFirstPageOfFiveHundred(): void
|
|
{
|
|
$body = $this->call();
|
|
|
|
self::assertSame(1, $body['meta']['page']);
|
|
self::assertSame(500, $body['meta']['perPage']);
|
|
self::assertSame(1, $body['meta']['totalPages']);
|
|
self::assertCount(12, $body['data']);
|
|
}
|
|
|
|
public function testCarriesTheBuilderCounters(): void
|
|
{
|
|
$meta = $this->call(null, 7, 21)['meta'];
|
|
|
|
self::assertSame(7, $meta['skipped']);
|
|
self::assertSame(21, $meta['pickupsWithoutMasterData']);
|
|
self::assertSame('2026-09-29T03:20:00+02:00', $meta['generatedAt']);
|
|
self::assertSame('2026-09-28T23:10:15+02:00', $meta['sourceGeneratedAt']);
|
|
}
|
|
|
|
public function testAnUnknownSourceExportIsNullRatherThanOmitted(): void
|
|
{
|
|
$builder = $this->createStub(TravelExportBuilder::class);
|
|
$builder->method('build')->willReturn(new TravelExport([], 0, 0, new \DateTimeImmutable()));
|
|
|
|
$meta = json_decode((string) $this->controller($builder)->travels(new TravelExportQuery())->getContent(), true)['meta'];
|
|
|
|
self::assertArrayHasKey('sourceGeneratedAt', $meta);
|
|
self::assertNull($meta['sourceGeneratedAt']);
|
|
}
|
|
|
|
/**
|
|
* A builder failure must reach the consumer as a 5xx, never as the 200-with-an-error-body
|
|
* shape TravelController::hotelAvailability() uses: the retry logic keys on the status code.
|
|
*/
|
|
public function testABuilderFailureIsNotDowngradedToATwoHundred(): void
|
|
{
|
|
$builder = $this->createStub(TravelExportBuilder::class);
|
|
$builder->method('build')->willThrowException(new \RuntimeException('cache exploded'));
|
|
|
|
$this->expectException(\RuntimeException::class);
|
|
|
|
$this->controller($builder)->travels(new TravelExportQuery());
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function call(?TravelExportQuery $query = null, int $skipped = 0, int $withoutMasterData = 0): array
|
|
{
|
|
$rows = [];
|
|
for ($i = 1; $i <= 12; ++$i) {
|
|
$rows[] = ['busProId' => $i, 'hotel' => ['busProId' => 1], 'pickups' => []];
|
|
}
|
|
|
|
$builder = $this->createStub(TravelExportBuilder::class);
|
|
$builder->method('build')->willReturn(new TravelExport(
|
|
$rows,
|
|
$skipped,
|
|
$withoutMasterData,
|
|
new \DateTimeImmutable('2026-09-29 03:20:00', new \DateTimeZone('Europe/Berlin')),
|
|
new \DateTimeImmutable('2026-09-28 23:10:15', new \DateTimeZone('Europe/Berlin')),
|
|
));
|
|
|
|
$response = $this->controller($builder)->travels($query ?? new TravelExportQuery());
|
|
|
|
self::assertSame(Response::HTTP_OK, $response->getStatusCode());
|
|
|
|
return json_decode((string) $response->getContent(), true);
|
|
}
|
|
|
|
private function controller(TravelExportBuilder $builder): ExportController
|
|
{
|
|
$controller = new ExportController($builder);
|
|
|
|
// No 'serializer' service registered, so AbstractController::json() falls back to
|
|
// JsonResponse -- which is what this endpoint produces in production anyway.
|
|
$controller->setContainer(new Container());
|
|
|
|
return $controller;
|
|
}
|
|
}
|