Anreise
Mit dem Bus
what the fake database holds */ private array $stored = []; private ContentSyncState $state; private ContentFeedClient&Stub $client; protected function setUp(): void { $this->state = new ContentSyncState(); $this->client = $this->createStub(ContentFeedClient::class); } public function testAFailedManifestChangesNothing(): void { $this->stored = ['page:1:de' => $this->document('page', 1)]; $this->client->method('manifest')->willThrowException(new ContentFeedStoppedException('Manifest has no items')); $report = $this->syncer()->run(); self::assertSame('Manifest has no items', $report->stoppedBy); self::assertArrayHasKey('page:1:de', $this->stored); self::assertNull($this->state->getManifestGeneratedAt()); self::assertSame('Manifest has no items', $this->state->getLastError()); } public function testItemsMissingFromTheManifestAreDeleted(): void { $this->stored = ['page:1:de' => $this->document('page', 1), 'page:2:de' => $this->document('page', 2)]; $this->client->method('manifest')->willReturn($this->manifest([$this->pageItem(1)])); $this->client->method('page')->willReturn(PageFetchResult::ok('
Eins
')); $report = $this->syncer()->run(); self::assertSame(1, $report->deleted); self::assertSame(['page:1:de'], array_keys($this->stored)); self::assertEquals(new \DateTimeImmutable('2026-10-08 08:15:00'), $this->state->getManifestGeneratedAt()); } public function testRecordsAbsentFromTheResponseAreDeleted(): void { $this->client->method('manifest')->willReturn($this->manifest([$this->recordItem('product', 412), $this->recordItem('product', 413)])); $this->client->method('records')->willReturn([412 => ['uid' => 412, 'name' => 'Val Thorens', 'code' => 'VTH', 'text' => ['teaser' => 'Ski
']]]); $report = $this->syncer()->run(); self::assertSame(1, $report->records); self::assertSame(['product:412:de'], array_keys($this->stored)); self::assertSame("# Val Thorens\n\nSki", $this->stored['product:412:de']->getBodyMd()); self::assertTrue($this->stored['product:412:de']->isIndexed()); } public function testADetailPageIsMergedIntoItsRecordAndNotIndexedItself(): void { $this->client->method('manifest')->willReturn($this->manifest([ $this->recordItem('product', 412, url: null), $this->pageItem(7, detailOf: [['type' => 'product', 'uid' => 412]]), ])); $this->client->method('records')->willReturn([412 => ['uid' => 412, 'name' => 'Val Thorens', 'text' => []]]); $this->client->method('page')->willReturn(PageFetchResult::ok('Mit dem Bus

Text
')); $report = $this->syncer('2026-10-08 12:00:00')->run(); self::assertSame(50, $report->pages); self::assertSame(10, $report->deferred); } public function testInsideTheNightWindowEveryDuePageIsFetched(): void { $items = array_map(fn (int $uid): ManifestItem => $this->pageItem($uid), range(1, 60)); $this->client->method('manifest')->willReturn($this->manifest($items)); $this->client->method('page')->willReturn(PageFetchResult::ok('Text
')); // 02:00 in Berlin. $report = $this->syncer('2026-10-08 00:00:00')->run(); self::assertSame(60, $report->pages); self::assertSame(0, $report->deferred); } public function testAPausedFeedIsNotCalled(): void { $this->state->setPausedUntil(new \DateTimeImmutable('2026-10-08 13:00:00', new \DateTimeZone('UTC'))); $client = $this->createMock(ContentFeedClient::class); $client->expects(self::never())->method('manifest'); $this->client = $client; $report = $this->syncer()->run(); self::assertNotNull($report->stoppedBy); } private function syncer(string $now = '2026-10-08 12:00:00'): ContentSyncer { $repository = $this->createStub(ContentDocumentRepository::class); $repository->method('findAllByKey')->willReturnCallback(fn (): array => $this->stored); $stateRepository = $this->createStub(ContentSyncStateRepository::class); $stateRepository->method('get')->willReturn($this->state); $entityManager = $this->createStub(EntityManagerInterface::class); $entityManager->method('persist')->willReturnCallback(function (object $entity): void { if ($entity instanceof ContentDocument) { $this->stored[$entity->getKey()] = $entity; } }); $entityManager->method('remove')->willReturnCallback(function (object $entity): void { if ($entity instanceof ContentDocument) { unset($this->stored[$entity->getKey()]); } }); $entityManager->method('contains')->willReturnCallback(fn (object $entity): bool => $entity instanceof ContentDocument && ($this->stored[$entity->getKey()] ?? null) === $entity); $converter = new HtmlToMarkdownConverter(); return new ContentSyncer( $this->client, $repository, $stateRepository, new DocumentBodyBuilder($converter), $converter, $this->createStub(TripCatalog::class), $entityManager, new MockClock(new \DateTimeImmutable($now, new \DateTimeZone('UTC'))), new NullLogger(), ); } /** * @param list