feat: unfiltered product export for the website's catalogue import

This commit is contained in:
fromme committed 2026-09-30 12:19:10 +02:00
1 parent 272c811f4f
commit 1d42565a84
11 files changed
+970 -33

No files matched your search

+62 -17
View File
@@ -5,8 +5,9 @@ declare(strict_types=1);
namespace App\Tests\Controller\Api;
use App\Controller\Api\ExportController;
use App\Model\TravelExport;
use App\Model\TravelExportQuery;
use App\Model\Export;
use App\Model\ExportQuery;
use App\Service\ProductExportBuilder;
use App\Service\TravelExportBuilder;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Container;
@@ -16,7 +17,7 @@ class ExportControllerTest extends TestCase
{
public function testPaginatesAndReportsWholeExportFigures(): void
{
$meta = $this->call(new TravelExportQuery(1, 5))['meta'];
$meta = $this->call(new ExportQuery(1, 5))['meta'];
self::assertSame(1, $meta['page']);
self::assertSame(5, $meta['perPage']);
@@ -27,8 +28,8 @@ class ExportControllerTest extends TestCase
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'];
$first = $this->call(new ExportQuery(1, 5))['meta'];
$last = $this->call(new ExportQuery(3, 5))['meta'];
foreach (['total', 'totalPages', 'skipped', 'pickupsWithoutMasterData', 'sourceGeneratedAt', 'generatedAt'] as $key) {
self::assertSame($first[$key], $last[$key], $key.' must not vary by page');
@@ -37,8 +38,8 @@ class ExportControllerTest extends TestCase
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');
$first = array_column($this->call(new ExportQuery(1, 5))['data'], 'busProId');
$second = array_column($this->call(new ExportQuery(2, 5))['data'], 'busProId');
self::assertSame([1, 2, 3, 4, 5], $first);
self::assertSame([6, 7, 8, 9, 10], $second);
@@ -47,7 +48,7 @@ class ExportControllerTest extends TestCase
public function testTheLastPageIsShort(): void
{
$data = $this->call(new TravelExportQuery(3, 5))['data'];
$data = $this->call(new ExportQuery(3, 5))['data'];
self::assertCount(2, $data);
self::assertSame([11, 12], array_column($data, 'busProId'));
@@ -55,7 +56,7 @@ class ExportControllerTest extends TestCase
public function testAPageBeyondTheEndIsEmptyRatherThanAnError(): void
{
$body = $this->call(new TravelExportQuery(99, 5));
$body = $this->call(new ExportQuery(99, 5));
self::assertSame([], $body['data']);
self::assertSame(12, $body['meta']['total']);
@@ -84,9 +85,9 @@ class ExportControllerTest extends TestCase
public function testAnUnknownSourceExportIsNullRatherThanOmitted(): void
{
$builder = $this->createStub(TravelExportBuilder::class);
$builder->method('build')->willReturn(new TravelExport([], 0, 0, new \DateTimeImmutable()));
$builder->method('build')->willReturn(new Export([], 0, 0, new \DateTimeImmutable()));
$meta = json_decode((string) $this->controller($builder)->travels(new TravelExportQuery())->getContent(), true)['meta'];
$meta = json_decode((string) $this->controller($builder)->travels(new ExportQuery())->getContent(), true)['meta'];
self::assertArrayHasKey('sourceGeneratedAt', $meta);
self::assertNull($meta['sourceGeneratedAt']);
@@ -103,13 +104,57 @@ class ExportControllerTest extends TestCase
$this->expectException(\RuntimeException::class);
$this->controller($builder)->travels(new TravelExportQuery());
$this->controller($builder)->travels(new ExportQuery());
}
public function testProductsUseTheSameEnvelopeAndPaging(): void
{
$rows = [];
for ($i = 1; $i <= 3; ++$i) {
$rows[] = ['date' => ['busProId' => $i], 'hotel' => ['busProId' => 1]];
}
$productBuilder = $this->createStub(ProductExportBuilder::class);
$productBuilder->method('build')->willReturn(new Export(
$rows,
2,
5,
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($this->createStub(TravelExportBuilder::class), $productBuilder)
->products(new ExportQuery(2, 2));
$body = json_decode((string) $response->getContent(), true);
self::assertSame(Response::HTTP_OK, $response->getStatusCode());
self::assertSame([3], array_column(array_column($body['data'], 'date'), 'busProId'));
self::assertSame([
'page' => 2,
'perPage' => 2,
'total' => 3,
'totalPages' => 2,
'skipped' => 2,
'pickupsWithoutMasterData' => 5,
'sourceGeneratedAt' => '2026-09-28T23:10:15+02:00',
'generatedAt' => '2026-09-29T03:20:00+02:00',
], $body['meta']);
}
public function testAProductBuilderFailureIsNotDowngradedToATwoHundred(): void
{
$productBuilder = $this->createStub(ProductExportBuilder::class);
$productBuilder->method('build')->willThrowException(new \RuntimeException('cache exploded'));
$this->expectException(\RuntimeException::class);
$this->controller($this->createStub(TravelExportBuilder::class), $productBuilder)->products(new ExportQuery());
}
/**
* @return array<string, mixed>
*/
private function call(?TravelExportQuery $query = null, int $skipped = 0, int $withoutMasterData = 0): array
private function call(?ExportQuery $query = null, int $skipped = 0, int $withoutMasterData = 0): array
{
$rows = [];
for ($i = 1; $i <= 12; ++$i) {
@@ -117,7 +162,7 @@ class ExportControllerTest extends TestCase
}
$builder = $this->createStub(TravelExportBuilder::class);
$builder->method('build')->willReturn(new TravelExport(
$builder->method('build')->willReturn(new Export(
$rows,
$skipped,
$withoutMasterData,
@@ -125,16 +170,16 @@ class ExportControllerTest extends TestCase
new \DateTimeImmutable('2026-09-28 23:10:15', new \DateTimeZone('Europe/Berlin')),
));
$response = $this->controller($builder)->travels($query ?? new TravelExportQuery());
$response = $this->controller($builder)->travels($query ?? new ExportQuery());
self::assertSame(Response::HTTP_OK, $response->getStatusCode());
return json_decode((string) $response->getContent(), true);
}
private function controller(TravelExportBuilder $builder): ExportController
private function controller(TravelExportBuilder $builder, ?ProductExportBuilder $productBuilder = null): ExportController
{
$controller = new ExportController($builder);
$controller = new ExportController($builder, $productBuilder ?? $this->createStub(ProductExportBuilder::class));
// No 'serializer' service registered, so AbstractController::json() falls back to
// JsonResponse -- which is what this endpoint produces in production anyway.