665 lines
20 KiB
PHP
665 lines
20 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Service;
|
|
|
|
use App\BusProNet\ApiClient;
|
|
use App\BusProNet\Model\BaseData;
|
|
use App\BusProNet\Model\Room;
|
|
use App\BusProNet\Model\ServiceAvailabilityResponse;
|
|
use App\BusProNet\Model\Travel;
|
|
use App\BusProNet\XmlLoader\TravelLoader;
|
|
use App\Exception\TravelNotFoundException;
|
|
use App\Service\TravelDataProvider;
|
|
use App\Service\TravelEnricher;
|
|
use App\Service\TravelIndex;
|
|
use App\Service\TravelSnapshotManager;
|
|
use PHPUnit\Framework\TestCase;
|
|
use Psr\Log\LoggerInterface;
|
|
use Symfony\Contracts\Cache\CacheInterface;
|
|
|
|
class TravelDataProviderTest extends TestCase
|
|
{
|
|
private TravelDataProvider $service;
|
|
private TravelLoader $travelLoader;
|
|
private ApiClient $apiClient;
|
|
private CacheInterface $cache;
|
|
private LoggerInterface $logger;
|
|
private TravelSnapshotManager $travelSnapshotService;
|
|
private TravelIndex $travelLookupService;
|
|
private TravelEnricher $travelEnrichmentService;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->travelLoader = $this->createMock(TravelLoader::class);
|
|
$this->apiClient = $this->createMock(ApiClient::class);
|
|
$this->cache = $this->createMock(CacheInterface::class);
|
|
$this->logger = $this->createMock(LoggerInterface::class);
|
|
$this->travelSnapshotService = $this->createMock(TravelSnapshotManager::class);
|
|
$this->travelLookupService = $this->createMock(TravelIndex::class);
|
|
$this->travelEnrichmentService = $this->createMock(TravelEnricher::class);
|
|
|
|
$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
|
|
{
|
|
$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
|
|
{
|
|
$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
|
|
{
|
|
$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
|
|
{
|
|
$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
|
|
{
|
|
$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
|
|
{
|
|
$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
|
|
{
|
|
$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
|
|
{
|
|
$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
|
|
{
|
|
$mapping = [12345 => ['id' => 12345, 'hotels' => []]];
|
|
|
|
$this->travelLookupService
|
|
->expects($this->once())
|
|
->method('generateFilesMap')
|
|
->willReturn($mapping);
|
|
|
|
$this->assertSame($mapping, $this->service->generateFilesMap());
|
|
}
|
|
|
|
public function testGetTravelDataFromLocalPrefersSnapshot(): void
|
|
{
|
|
$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
|
|
{
|
|
$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
|
|
{
|
|
$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
|
|
{
|
|
$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
|
|
{
|
|
$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
|
|
{
|
|
$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);
|
|
}
|
|
|
|
public function testEnrichWithFreshAvailabilitiesRefreshesRoomInventory(): void
|
|
{
|
|
$dateId = 12345;
|
|
$hotelId = 67890;
|
|
|
|
$travel = new Travel();
|
|
$travel->id = $dateId;
|
|
$travel->hotelId = $hotelId;
|
|
$travel->dateTo = new \DateTimeImmutable('2030-01-06');
|
|
|
|
$room1 = new Room();
|
|
$room1->id = 1;
|
|
$room1->label = 'Doppelzimmer';
|
|
$room1->available = 2;
|
|
$room1->status = 'Frei';
|
|
|
|
$room2 = new Room();
|
|
$room2->id = 2;
|
|
$room2->label = 'Familienzimmer';
|
|
$room2->available = 1;
|
|
$room2->status = 'Frei';
|
|
|
|
$travel->rooms = [$room1, $room2];
|
|
|
|
$liveRoom = new Room();
|
|
$liveRoom->id = 1;
|
|
$liveRoom->available = 0;
|
|
$liveRoom->status = 'Frei';
|
|
$liveRoom->price = 199.5;
|
|
|
|
$liveRoom2 = new Room();
|
|
$liveRoom2->id = 2;
|
|
$liveRoom2->available = 1;
|
|
$liveRoom2->status = 'Frei';
|
|
$liveRoom2->price = 249.0;
|
|
|
|
$this->apiClient
|
|
->expects($this->once())
|
|
->method('getAvailabilities')
|
|
->with($dateId)
|
|
->willReturn(new ServiceAvailabilityResponse([]));
|
|
|
|
$this->apiClient
|
|
->expects($this->once())
|
|
->method('getHotelAvailability')
|
|
->with($dateId, $hotelId, $travel->dateTo)
|
|
->willReturn(new BaseData([
|
|
1 => $liveRoom,
|
|
2 => $liveRoom2,
|
|
]));
|
|
|
|
$this->travelLoader
|
|
->expects($this->once())
|
|
->method('patchRoomAvailabilities')
|
|
->with($travel, $this->isInstanceOf(BaseData::class))
|
|
->willReturnCallback(static function (Travel $travel, BaseData $roomAvailabilities): void {
|
|
$liveRooms = $roomAvailabilities->getItems();
|
|
|
|
foreach ($travel->rooms as $room) {
|
|
$liveRoom = $liveRooms[$room->id] ?? null;
|
|
if (!($liveRoom instanceof Room)) {
|
|
continue;
|
|
}
|
|
|
|
$room->available = $liveRoom->available;
|
|
$room->price = $liveRoom->price;
|
|
}
|
|
});
|
|
|
|
$this->service->enrichWithFreshAvailabilities($travel, false, refreshRooms: true);
|
|
|
|
$this->assertSame(0, $travel->rooms[0]->available);
|
|
$this->assertSame(199.5, $travel->rooms[0]->price);
|
|
$this->assertSame(1, $travel->rooms[1]->available);
|
|
$this->assertSame(249.0, $travel->rooms[1]->price);
|
|
$this->assertSame([2 => $travel->rooms[1]], $travel->getAvailableRooms());
|
|
}
|
|
|
|
public function testPatchRoomAvailabilitiesLogsMissingRoomIdsAsContractIssue(): void
|
|
{
|
|
$travel = new Travel();
|
|
$travel->id = 12345;
|
|
$travel->hotelId = 67890;
|
|
|
|
$room1 = new Room();
|
|
$room1->id = 1;
|
|
$room1->available = 2;
|
|
$room1->status = 'Frei';
|
|
|
|
$room2 = new Room();
|
|
$room2->id = 2;
|
|
$room2->available = 1;
|
|
$room2->status = 'Frei';
|
|
|
|
$travel->rooms = [$room1, $room2];
|
|
|
|
$liveRoom = new Room();
|
|
$liveRoom->id = 1;
|
|
$liveRoom->available = 0;
|
|
$liveRoom->status = 'Frei';
|
|
|
|
$this->logger
|
|
->expects($this->once())
|
|
->method('warning')
|
|
->with(
|
|
'Hotel availability response is missing room IDs from the travel snapshot',
|
|
$this->callback(static function (array $context): bool {
|
|
return [2] === $context['missingRoomIds']
|
|
&& 12345 === $context['travelId']
|
|
&& 67890 === $context['hotelId'];
|
|
})
|
|
);
|
|
|
|
$this->travelLoader
|
|
->expects($this->once())
|
|
->method('patchRoomAvailabilities')
|
|
->with($travel, $this->isInstanceOf(BaseData::class));
|
|
|
|
$this->service->patchRoomAvailabilities($travel, new BaseData([
|
|
1 => $liveRoom,
|
|
]));
|
|
}
|
|
|
|
public function testEnrichWithFreshAvailabilitiesDoesNotForceRefreshHotelAvailabilityByDefault(): void
|
|
{
|
|
$travel = new Travel();
|
|
$travel->id = 12345;
|
|
$travel->hotelId = 67890;
|
|
$travel->dateTo = new \DateTimeImmutable('2030-01-06');
|
|
|
|
$this->cache
|
|
->expects($this->never())
|
|
->method('delete');
|
|
|
|
$this->cache
|
|
->expects($this->exactly(2))
|
|
->method('get')
|
|
->willReturnCallback(static fn (string $key, callable $callback) => $callback(new \Symfony\Component\Cache\CacheItem()));
|
|
|
|
$this->apiClient
|
|
->expects($this->once())
|
|
->method('getAvailabilities')
|
|
->with($travel->id)
|
|
->willReturn(new ServiceAvailabilityResponse([]));
|
|
|
|
$this->apiClient
|
|
->expects($this->once())
|
|
->method('getHotelAvailability')
|
|
->with($travel->id, $travel->hotelId, $travel->dateTo)
|
|
->willReturn(new BaseData([]));
|
|
|
|
$this->service->enrichWithFreshAvailabilities($travel, refreshRooms: true);
|
|
}
|
|
|
|
public function testEnrichWithFreshAvailabilitiesDoesNotRefreshRoomsByDefault(): void
|
|
{
|
|
$travel = new Travel();
|
|
$travel->id = 12345;
|
|
$travel->hotelId = 67890;
|
|
$travel->dateTo = new \DateTimeImmutable('2030-01-06');
|
|
|
|
$this->cache
|
|
->expects($this->once())
|
|
->method('get')
|
|
->willReturnCallback(static fn (string $key, callable $callback) => $callback(new \Symfony\Component\Cache\CacheItem()));
|
|
|
|
$this->apiClient
|
|
->expects($this->once())
|
|
->method('getAvailabilities')
|
|
->with($travel->id)
|
|
->willReturn(new ServiceAvailabilityResponse([]));
|
|
|
|
$this->apiClient
|
|
->expects($this->never())
|
|
->method('getHotelAvailability');
|
|
|
|
$this->travelLoader
|
|
->expects($this->never())
|
|
->method('patchRoomAvailabilities');
|
|
|
|
$this->service->enrichWithFreshAvailabilities($travel);
|
|
}
|
|
|
|
public function testEnrichWithFreshAvailabilitiesForceRefreshInvalidatesHotelAvailabilityCache(): void
|
|
{
|
|
$travel = new Travel();
|
|
$travel->id = 12345;
|
|
$travel->hotelId = 67890;
|
|
$travel->dateTo = new \DateTimeImmutable('2030-01-06');
|
|
|
|
$this->cache
|
|
->expects($this->exactly(2))
|
|
->method('delete')
|
|
->with($this->logicalOr(
|
|
$this->equalTo('availability_12345'),
|
|
$this->equalTo('hotel_availability_12345_67890_20300106')
|
|
));
|
|
|
|
$this->cache
|
|
->expects($this->exactly(2))
|
|
->method('get')
|
|
->willReturnCallback(static fn (string $key, callable $callback) => $callback(new \Symfony\Component\Cache\CacheItem()));
|
|
|
|
$this->apiClient
|
|
->expects($this->once())
|
|
->method('getAvailabilities')
|
|
->with($travel->id)
|
|
->willReturn(new ServiceAvailabilityResponse([]));
|
|
|
|
$this->apiClient
|
|
->expects($this->once())
|
|
->method('getHotelAvailability')
|
|
->with($travel->id, $travel->hotelId, $travel->dateTo)
|
|
->willReturn(new BaseData([]));
|
|
|
|
$this->service->enrichWithFreshAvailabilities($travel, true, true, refreshRooms: true);
|
|
}
|
|
}
|