travelLoader = $this->createStub(TravelLoader::class); $this->apiClient = $this->createStub(ApiClient::class); $this->cache = $this->createStub(CacheInterface::class); $this->logger = $this->createStub(LoggerInterface::class); $this->travelSnapshotService = $this->createStub(TravelSnapshotManager::class); $this->travelLookupService = $this->createStub(TravelIndex::class); $this->travelEnrichmentService = $this->createStub(TravelEnricher::class); } private function service(): TravelDataProvider { return $this->service ??= new TravelDataProvider( $this->travelLoader, $this->apiClient, $this->cache, $this->logger, $this->travelSnapshotService, $this->travelLookupService, $this->travelEnrichmentService, false, // preferRemote true, // enableFallback ); } public function testGetTravelDataFromXmlSuccess(): void { $this->travelLoader = $this->createMock(TravelLoader::class); $this->travelSnapshotService = $this->createMock(TravelSnapshotManager::class); $this->travelEnrichmentService = $this->createMock(TravelEnricher::class); $dateId = 12345; $hotelId = 67890; $travel = new Travel(); $travel->id = $dateId; $travel->hotelId = $hotelId; $this->travelLoader ->expects($this->once()) ->method('loadById') ->with($dateId, $hotelId) ->willReturn($travel); $this->travelEnrichmentService ->expects($this->once()) ->method('enrichFromXml') ->with($travel) ->willReturn(true); $this->travelSnapshotService ->expects($this->once()) ->method('upsertFromTravel') ->with($travel); $result = $this->service()->getTravelDataFromXml($dateId, $hotelId); $this->assertSame($travel, $result); } public function testGetTravelDataFromXmlSkipsSnapshotWhenEnrichmentFails(): void { $this->travelLoader = $this->createMock(TravelLoader::class); $this->travelSnapshotService = $this->createMock(TravelSnapshotManager::class); $this->travelEnrichmentService = $this->createMock(TravelEnricher::class); $dateId = 12345; $hotelId = 67890; $travel = new Travel(); $travel->id = $dateId; $travel->hotelId = $hotelId; $this->travelLoader ->expects($this->once()) ->method('loadById') ->with($dateId, $hotelId) ->willReturn($travel); $this->travelEnrichmentService ->expects($this->once()) ->method('enrichFromXml') ->with($travel) ->willReturn(false); $this->travelSnapshotService ->expects($this->never()) ->method('upsertFromTravel'); $result = $this->service()->getTravelDataFromXml($dateId, $hotelId); $this->assertSame($travel, $result); } public function testGetTravelDataFromXmlNotFound(): void { $this->travelLoader = $this->createMock(TravelLoader::class); $this->travelEnrichmentService = $this->createMock(TravelEnricher::class); $dateId = 12345; $hotelId = 67890; $this->travelLoader ->expects($this->once()) ->method('loadById') ->with($dateId, $hotelId) ->willThrowException(new TravelNotFoundException($dateId)); $this->travelEnrichmentService ->expects($this->never()) ->method('enrichFromXml'); $this->expectException(TravelNotFoundException::class); $this->service()->getTravelDataFromXml($dateId, $hotelId); } public function testGetTravelDataFromApiSuccess(): void { $this->apiClient = $this->createMock(ApiClient::class); $this->travelSnapshotService = $this->createMock(TravelSnapshotManager::class); $this->travelLookupService = $this->createMock(TravelIndex::class); $this->travelEnrichmentService = $this->createMock(TravelEnricher::class); $dateId = 12345; $hotelId = 67890; $productId = 555; $travel = new Travel(); $travel->id = $dateId; $travel->hotelId = $hotelId; $this->travelLookupService ->expects($this->once()) ->method('mapDateIdToProductId') ->with($dateId) ->willReturn($productId); $this->apiClient ->expects($this->once()) ->method('getTravelData') ->with($productId, $hotelId) ->willReturn($travel); $this->travelEnrichmentService ->expects($this->once()) ->method('patchInsurances') ->with($travel); $this->travelSnapshotService ->expects($this->once()) ->method('upsertFromTravel') ->with($travel); $result = $this->service()->getTravelDataFromApi($dateId, $hotelId); $this->assertSame($travel, $result); } public function testGetTravelDataFromApiSnapshotFailureDoesNotDiscardApiTravel(): void { $this->logger = $this->createMock(LoggerInterface::class); $this->travelSnapshotService = $this->createMock(TravelSnapshotManager::class); $dateId = 12345; $hotelId = 67890; $productId = 555; $travel = new Travel(); $travel->id = $dateId; $travel->hotelId = $hotelId; $this->travelLookupService ->method('mapDateIdToProductId') ->willReturn($productId); $this->apiClient ->method('getTravelData') ->willReturn($travel); $this->travelSnapshotService ->expects($this->once()) ->method('upsertFromTravel') ->willThrowException(new \RuntimeException('DB write failed')); $this->logger ->expects($this->once()) ->method('warning') ->with('Failed to persist travel snapshot after API load', $this->arrayHasKey('dateId')); $result = $this->service()->getTravelDataFromApi($dateId, $hotelId); $this->assertNotNull($result); $this->assertSame($travel, $result); } public function testGetTravelDataFromApiCannotMapDateId(): void { $this->apiClient = $this->createMock(ApiClient::class); $this->travelLookupService = $this->createMock(TravelIndex::class); $dateId = 12345; $hotelId = 67890; $this->travelLookupService ->expects($this->once()) ->method('mapDateIdToProductId') ->with($dateId) ->willReturn(null); $this->apiClient ->expects($this->never()) ->method('getTravelData'); $result = $this->service()->getTravelDataFromApi($dateId, $hotelId); $this->assertNull($result); } public function testExistsLocallyDelegatesToLookupService(): void { $this->travelLookupService = $this->createMock(TravelIndex::class); $dateId = 12345; $hotelId = 67890; $this->travelLookupService ->expects($this->once()) ->method('existsLocally') ->with($dateId, $hotelId) ->willReturn(true); $this->assertTrue($this->service()->existsLocally($dateId, $hotelId)); } public function testGetAvailableSourcesDelegatesToLookupService(): void { $this->travelLookupService = $this->createMock(TravelIndex::class); $dateId = 12345; $hotelId = 67890; $expected = ['local' => true, 'remote' => true]; $this->travelLookupService ->expects($this->once()) ->method('getAvailableSources') ->with($dateId, $hotelId) ->willReturn($expected); $this->assertSame($expected, $this->service()->getAvailableSources($dateId, $hotelId)); } public function testGenerateFilesMapDelegatesToLookupService(): void { $this->travelLookupService = $this->createMock(TravelIndex::class); $mapping = [12345 => ['id' => 12345, 'hotels' => []]]; $this->travelLookupService ->expects($this->once()) ->method('generateFilesMap') ->willReturn($mapping); $this->assertSame($mapping, $this->service()->generateFilesMap()); } public function testGetTravelDataFromLocalPrefersSnapshot(): void { $this->travelLoader = $this->createMock(TravelLoader::class); $this->travelSnapshotService = $this->createMock(TravelSnapshotManager::class); $this->travelEnrichmentService = $this->createMock(TravelEnricher::class); $dateId = 12345; $hotelId = 67890; $travel = new Travel(); $travel->id = $dateId; $travel->hotelId = $hotelId; $this->travelSnapshotService ->expects($this->once()) ->method('loadTravel') ->with($dateId, $hotelId) ->willReturn($travel); $this->travelLoader ->expects($this->never()) ->method('loadById'); $this->travelEnrichmentService ->expects($this->once()) ->method('patchInsurances') ->with($travel); $result = $this->service()->getTravelDataFromLocal($dateId, $hotelId); $this->assertSame($travel, $result); } public function testGetTravelDataFromLocalFallsBackToXmlWhenSnapshotMissing(): void { $this->travelLoader = $this->createMock(TravelLoader::class); $this->travelSnapshotService = $this->createMock(TravelSnapshotManager::class); $this->travelEnrichmentService = $this->createMock(TravelEnricher::class); $dateId = 12345; $hotelId = 67890; $travel = new Travel(); $travel->id = $dateId; $travel->hotelId = $hotelId; $this->travelSnapshotService ->expects($this->once()) ->method('loadTravel') ->with($dateId, $hotelId) ->willReturn(null); $this->travelLoader ->expects($this->once()) ->method('loadById') ->with($dateId, $hotelId) ->willReturn($travel); $this->travelEnrichmentService ->expects($this->once()) ->method('enrichFromXml') ->with($travel) ->willReturn(true); $this->travelSnapshotService ->expects($this->once()) ->method('upsertFromTravel') ->with($travel); $this->travelEnrichmentService ->expects($this->once()) ->method('patchInsurances') ->with($travel); $result = $this->service()->getTravelDataFromLocal($dateId, $hotelId); $this->assertSame($travel, $result); } public function testGetTravelDataFromLocalPropagatesTravelNotFoundFromXmlFallback(): void { $this->travelLoader = $this->createMock(TravelLoader::class); $this->travelSnapshotService = $this->createMock(TravelSnapshotManager::class); $dateId = 12345; $hotelId = 67890; $this->travelSnapshotService ->expects($this->once()) ->method('loadTravel') ->with($dateId, $hotelId) ->willReturn(null); $this->travelLoader ->expects($this->once()) ->method('loadById') ->with($dateId, $hotelId) ->willThrowException(new TravelNotFoundException($dateId)); $this->expectException(TravelNotFoundException::class); $this->service()->getTravelDataFromLocal($dateId, $hotelId); } public function testSnapshotLoadFailureFallsBackToXml(): void { $this->travelLoader = $this->createMock(TravelLoader::class); $this->logger = $this->createMock(LoggerInterface::class); $this->travelSnapshotService = $this->createMock(TravelSnapshotManager::class); $dateId = 12345; $hotelId = 67890; $travel = new Travel(); $travel->id = $dateId; $travel->hotelId = $hotelId; $this->travelSnapshotService ->expects($this->once()) ->method('loadTravel') ->with($dateId, $hotelId) ->willThrowException(new \RuntimeException('DB connection failed')); $this->logger ->expects($this->once()) ->method('warning') ->with('Snapshot lookup failed, falling back to XML', $this->arrayHasKey('dateId')); $this->travelLoader ->expects($this->once()) ->method('loadById') ->with($dateId, $hotelId) ->willReturn($travel); $result = $this->service()->getTravelDataFromLocal($dateId, $hotelId); $this->assertSame($travel, $result); } public function testSnapshotPersistenceFailureDoesNotDiscardXmlTravel(): void { $this->travelLoader = $this->createMock(TravelLoader::class); $this->logger = $this->createMock(LoggerInterface::class); $this->travelSnapshotService = $this->createMock(TravelSnapshotManager::class); $dateId = 12345; $hotelId = 67890; $travel = new Travel(); $travel->id = $dateId; $travel->hotelId = $hotelId; $this->travelLoader ->expects($this->once()) ->method('loadById') ->with($dateId, $hotelId) ->willReturn($travel); $this->travelEnrichmentService ->method('enrichFromXml') ->willReturn(true); $this->travelSnapshotService ->expects($this->once()) ->method('upsertFromTravel') ->willThrowException(new \RuntimeException('DB write failed')); $this->logger ->expects($this->once()) ->method('warning') ->with('Failed to persist travel snapshot after XML load', $this->arrayHasKey('dateId')); $result = $this->service()->getTravelDataFromXml($dateId, $hotelId); $this->assertNotNull($result); $this->assertSame($travel, $result); } public function testGetTravelDataDelegatesToLoadUncached(): void { $this->travelLoader = $this->createMock(TravelLoader::class); $this->cache = $this->createMock(CacheInterface::class); $this->travelSnapshotService = $this->createMock(TravelSnapshotManager::class); $dateId = 12345; $hotelId = 67890; $travel = new Travel(); $travel->id = $dateId; $travel->hotelId = $hotelId; $this->cache ->expects($this->never()) ->method('get'); $this->travelSnapshotService ->expects($this->once()) ->method('loadTravel') ->with($dateId, $hotelId) ->willReturn(null); $this->travelLoader ->expects($this->once()) ->method('loadById') ->with($dateId, $hotelId) ->willReturn($travel); $result = $this->service()->getTravelData($dateId, $hotelId, false); $this->assertSame($travel, $result); } }