travelLoader = $this->createMock(TravelLoader::class); $this->hotelLoader = $this->createMock(HotelLoader::class); $this->travelSnapshotService = $this->createMock(TravelSnapshotManager::class); $this->logger = $this->createMock(LoggerInterface::class); $this->service = new TravelIndex( $this->travelLoader, $this->hotelLoader, $this->travelSnapshotService, $this->logger, ); } // ------------------------------------------------------------------------- // generateFilesMap // ------------------------------------------------------------------------- public function testGenerateFilesMapMergesXmlAndSnapshotMappings(): void { $dateId = 100; $hotelId = 200; $snapshotOnlyDateId = 300; $xmlMapping = [ $dateId => ['id' => $dateId, 'code' => 'ABC', 'hotels' => [ $hotelId => ['id' => $hotelId], ]], ]; $snapshotMapping = [ $dateId => ['id' => $dateId, 'code' => 'ABC', 'hotels' => [ 999 => ['id' => 999], // extra hotel only in snapshot ]], $snapshotOnlyDateId => ['id' => $snapshotOnlyDateId, 'code' => 'XYZ', 'hotels' => []], ]; $this->travelLoader->method('generateFilesMap')->willReturn($xmlMapping); $this->travelSnapshotService->method('generateMapping')->willReturn($snapshotMapping); $result = $this->service->generateFilesMap(); // XML entry is preserved $this->assertArrayHasKey($dateId, $result); // Snapshot-only date is merged in $this->assertArrayHasKey($snapshotOnlyDateId, $result); // Extra hotel from snapshot is added to the XML date entry $this->assertArrayHasKey(999, $result[$dateId]['hotels']); // Original XML hotel is still present $this->assertArrayHasKey($hotelId, $result[$dateId]['hotels']); } public function testGenerateFilesMapIsMemoizedWithinRequest(): void { $mapping = [100 => ['id' => 100, 'code' => 'A', 'hotels' => []]]; $this->travelLoader ->expects($this->once()) ->method('generateFilesMap') ->willReturn($mapping); $this->travelSnapshotService ->expects($this->once()) ->method('generateMapping') ->willReturn([]); // Two calls — loaders invoked only once $this->service->generateFilesMap(); $result = $this->service->generateFilesMap(); $this->assertSame($mapping, $result); } public function testGenerateFilesMapFallsBackToSnapshotsOnXmlFailure(): void { $snapshotMapping = [100 => ['id' => 100, 'code' => 'A', 'hotels' => []]]; $this->travelLoader ->method('generateFilesMap') ->willThrowException(new \RuntimeException('Filesystem error')); $this->travelSnapshotService ->method('generateMapping') ->willReturn($snapshotMapping); $this->logger ->expects($this->once()) ->method('warning') ->with('Failed to generate XML files mapping, fallback to snapshots only', $this->arrayHasKey('error')); $result = $this->service->generateFilesMap(); $this->assertSame($snapshotMapping, $result); } // ------------------------------------------------------------------------- // mapDateCodeToId // ------------------------------------------------------------------------- public function testMapDateCodeToIdReturnsMatchingId(): void { $this->travelLoader->method('generateFilesMap')->willReturn([ 42 => ['id' => 42, 'code' => 'WI25', 'hotels' => []], ]); $this->travelSnapshotService->method('generateMapping')->willReturn([]); $result = $this->service->mapDateCodeToId('WI25'); $this->assertSame(42, $result); } public function testMapDateCodeToIdReturnsNullWhenNotFound(): void { $this->travelLoader->method('generateFilesMap')->willReturn([]); $this->travelSnapshotService->method('generateMapping')->willReturn([]); $result = $this->service->mapDateCodeToId('UNKNOWN'); $this->assertNull($result); } // ------------------------------------------------------------------------- // mapHotelCodeToId // ------------------------------------------------------------------------- public function testMapHotelCodeToIdDelegatesToHotelLoader(): void { $this->hotelLoader ->expects($this->once()) ->method('mapCodeToId') ->with('HTL001') ->willReturn(77); $result = $this->service->mapHotelCodeToId('HTL001'); $this->assertSame(77, $result); } public function testMapHotelCodeToIdReturnsNullOnException(): void { $this->hotelLoader ->method('mapCodeToId') ->willThrowException(new \RuntimeException('Not found')); $result = $this->service->mapHotelCodeToId('MISSING'); $this->assertNull($result); } // ------------------------------------------------------------------------- // mapDateIdToProductId // ------------------------------------------------------------------------- public function testMapDateIdToProductIdPrefersSnapshot(): void { $this->travelSnapshotService ->expects($this->once()) ->method('findProductIdByDateId') ->with(100) ->willReturn(999); $this->travelLoader ->expects($this->never()) ->method('mapDateIdToProductId'); $result = $this->service->mapDateIdToProductId(100); $this->assertSame(999, $result); } public function testMapDateIdToProductIdFallsBackToLoaderWhenSnapshotReturnsNull(): void { $this->travelSnapshotService ->expects($this->once()) ->method('findProductIdByDateId') ->with(100) ->willReturn(null); $this->travelLoader ->expects($this->once()) ->method('mapDateIdToProductId') ->with(100) ->willReturn(42); $result = $this->service->mapDateIdToProductId(100); $this->assertSame(42, $result); } // ------------------------------------------------------------------------- // existsLocally // ------------------------------------------------------------------------- public function testExistsLocallyTrueFromSnapshot(): void { $dateId = 12345; $hotelId = 67890; $this->travelSnapshotService ->expects($this->once()) ->method('exists') ->with($dateId, $hotelId) ->willReturn(true); $this->travelLoader ->expects($this->never()) ->method('generateFilesMap'); $this->assertTrue($this->service->existsLocally($dateId, $hotelId)); } public function testExistsLocallyTrueFromXmlMap(): void { $dateId = 12345; $hotelId = 67890; $mapping = [ $dateId => [ 'id' => $dateId, 'hotels' => [$hotelId => ['id' => $hotelId]], ], ]; $this->travelSnapshotService->method('exists')->willReturn(false); $this->travelLoader->method('generateFilesMap')->willReturn($mapping); $this->travelSnapshotService->method('generateMapping')->willReturn([]); $this->assertTrue($this->service->existsLocally($dateId, $hotelId)); } public function testExistsLocallyFalseNoTravel(): void { $this->travelSnapshotService->method('exists')->willReturn(false); $this->travelLoader->method('generateFilesMap')->willReturn([]); $this->travelSnapshotService->method('generateMapping')->willReturn([]); $this->assertFalse($this->service->existsLocally(12345, 67890)); } public function testExistsLocallyFalseNoHotel(): void { $dateId = 12345; $hotelId = 67890; $mapping = [ $dateId => ['id' => $dateId, 'hotels' => []], ]; $this->travelSnapshotService->method('exists')->willReturn(false); $this->travelLoader->method('generateFilesMap')->willReturn($mapping); $this->travelSnapshotService->method('generateMapping')->willReturn([]); $this->assertFalse($this->service->existsLocally($dateId, $hotelId)); } // ------------------------------------------------------------------------- // getAvailableSources // ------------------------------------------------------------------------- public function testGetAvailableSourcesReturnsBothFlags(): void { $dateId = 12345; $hotelId = 67890; $mapping = [ $dateId => ['id' => $dateId, 'hotels' => [$hotelId => []]], ]; $this->travelSnapshotService->method('exists')->willReturn(false); $this->travelSnapshotService->method('generateMapping')->willReturn([]); $this->travelSnapshotService->method('findProductIdByDateId')->willReturn(null); $this->travelLoader->method('generateFilesMap')->willReturn($mapping); $this->travelLoader->method('mapDateIdToProductId')->with($dateId)->willReturn(555); $result = $this->service->getAvailableSources($dateId, $hotelId); $this->assertEquals(['local' => true, 'remote' => true], $result); } }