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(); $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()); } }