Files
myep/tests/Service/TravelSnapshotServiceTest.php
T

511 lines
16 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\Service;
use App\BusProNet\ApiClient;
use App\BusProNet\Exception\ApiClientException;
use App\BusProNet\Model\ExtendedServiceAvailabilityResponse;
use App\BusProNet\Model\Hotel;
use App\BusProNet\Model\Travel;
use App\Entity\TravelSnapshot;
use App\Repository\TravelSnapshotRepository;
use App\Service\TravelSnapshotService;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Symfony\Component\Serializer\SerializerInterface;
class TravelSnapshotServiceTest extends TestCase
{
private TravelSnapshotRepository $snapshotRepository;
private EntityManagerInterface $entityManager;
private ApiClient $apiClient;
private LoggerInterface $logger;
private SerializerInterface $serializer;
private TravelSnapshotService $service;
protected function setUp(): void
{
$this->snapshotRepository = $this->createMock(TravelSnapshotRepository::class);
$this->entityManager = $this->createMock(EntityManagerInterface::class);
$this->apiClient = $this->createMock(ApiClient::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->serializer = $this->createMock(SerializerInterface::class);
$this->service = new TravelSnapshotService(
$this->snapshotRepository,
$this->entityManager,
$this->apiClient,
$this->logger,
$this->serializer,
14,
);
}
public function testUpsertCreatesNewSnapshot(): void
{
$travel = new Travel();
$travel->id = 100;
$travel->hotelId = 200;
$this->snapshotRepository
->expects($this->once())
->method('findByDateAndHotel')
->with(100, 200)
->willReturn(null);
$this->serializer
->expects($this->once())
->method('serialize')
->with($travel, 'json')
->willReturn('{"id":100}');
$this->entityManager
->expects($this->once())
->method('persist')
->with($this->isInstanceOf(TravelSnapshot::class));
$this->entityManager
->expects($this->once())
->method('flush');
$this->service->upsertFromTravel($travel);
}
public function testUpsertSkipsWhenHashUnchanged(): void
{
$travel = new Travel();
$travel->id = 100;
$travel->hotelId = 200;
$payload = '{"id":100}';
$hash = hash('sha256', $payload);
$snapshot = new TravelSnapshot(100, 200, $payload, $hash);
$this->snapshotRepository
->expects($this->once())
->method('findByDateAndHotel')
->with(100, 200)
->willReturn($snapshot);
$this->serializer
->expects($this->once())
->method('serialize')
->with($travel, 'json')
->willReturn($payload);
$this->entityManager
->expects($this->never())
->method('flush');
$this->service->upsertFromTravel($travel);
}
public function testUpsertUpdatesWhenHashChanged(): void
{
$travel = new Travel();
$travel->id = 100;
$travel->hotelId = 200;
$oldPayload = '{"id":100,"old":true}';
$oldHash = hash('sha256', $oldPayload);
$snapshot = new TravelSnapshot(100, 200, $oldPayload, $oldHash);
$newPayload = '{"id":100,"old":false}';
$this->snapshotRepository
->expects($this->once())
->method('findByDateAndHotel')
->with(100, 200)
->willReturn($snapshot);
$this->serializer
->expects($this->once())
->method('serialize')
->with($travel, 'json')
->willReturn($newPayload);
$this->entityManager
->expects($this->once())
->method('flush');
$this->service->upsertFromTravel($travel);
}
public function testLoadTravelReturnsNullWhenNoSnapshot(): void
{
$this->snapshotRepository
->expects($this->once())
->method('findByDateAndHotel')
->with(100, 200)
->willReturn(null);
$result = $this->service->loadTravel(100, 200);
$this->assertNull($result);
}
public function testLoadTravelReturnsNullOnDeserializationError(): void
{
$snapshot = new TravelSnapshot(100, 200, 'invalid-json', hash('sha256', 'invalid-json'));
$this->snapshotRepository
->expects($this->once())
->method('findByDateAndHotel')
->with(100, 200)
->willReturn($snapshot);
$this->serializer
->expects($this->once())
->method('deserialize')
->willThrowException(new \RuntimeException('Deserialization failed'));
$this->logger
->expects($this->once())
->method('warning')
->with('Snapshot payload cannot be deserialized to Travel', $this->arrayHasKey('snapshotId'));
$result = $this->service->loadTravel(100, 200);
$this->assertNull($result);
}
public function testExistsReturnsTrueWhenSnapshotFound(): void
{
$snapshot = new TravelSnapshot(100, 200, '{}', hash('sha256', '{}'));
$this->snapshotRepository
->expects($this->once())
->method('findByDateAndHotel')
->with(100, 200)
->willReturn($snapshot);
$this->assertTrue($this->service->exists(100, 200));
}
public function testExistsReturnsFalseWhenNoSnapshot(): void
{
$this->snapshotRepository
->expects($this->once())
->method('findByDateAndHotel')
->with(100, 200)
->willReturn(null);
$this->assertFalse($this->service->exists(100, 200));
}
public function testLoadTravelReturnsDeserializedTravel(): void
{
$travel = new Travel();
$travel->id = 100;
$travel->hotelId = 200;
$payload = '{"id":100}';
$snapshot = new TravelSnapshot(100, 200, $payload, hash('sha256', $payload));
$this->snapshotRepository
->expects($this->once())
->method('findByDateAndHotel')
->with(100, 200)
->willReturn($snapshot);
$this->serializer
->expects($this->once())
->method('deserialize')
->with($payload, Travel::class, 'json')
->willReturn($travel);
$result = $this->service->loadTravel(100, 200);
$this->assertInstanceOf(Travel::class, $result);
$this->assertSame($travel, $result);
}
public function testUpsertFromTravelSkipsWhenTravelIsIncomplete(): void
{
$travelNoId = new Travel();
$travelNoId->id = null;
$travelNoId->hotelId = 200;
$travelNoHotel = new Travel();
$travelNoHotel->id = 100;
$travelNoHotel->hotelId = null;
$this->serializer->expects($this->never())->method('serialize');
$this->entityManager->expects($this->never())->method('flush');
$this->service->upsertFromTravel($travelNoId);
$this->service->upsertFromTravel($travelNoHotel);
}
public function testFindProductIdByDateIdDelegatesToRepository(): void
{
$this->snapshotRepository
->expects($this->once())
->method('findProductIdByDateId')
->with(42)
->willReturn(365);
$result = $this->service->findProductIdByDateId(42);
$this->assertSame(365, $result);
}
public function testPurgeExpiredSnapshotsDelegatesWithCorrectDate(): void
{
$this->snapshotRepository
->expects($this->once())
->method('deleteExpiredSnapshots')
->with($this->callback(function (\DateTimeImmutable $date) {
$expected = new \DateTimeImmutable('-14 days');
return abs($date->getTimestamp() - $expected->getTimestamp()) < 5;
}))
->willReturn(7);
$result = $this->service->purgeExpiredSnapshots();
$this->assertSame(7, $result);
}
public function testRefreshExtendedSnapshotsSuccessPath(): void
{
$travel = new Travel();
$travel->id = 100;
$travel->hotelId = 200;
$oldPayload = '{"id":100}';
$oldHash = hash('sha256', $oldPayload);
$snapshot = new TravelSnapshot(100, 200, $oldPayload, $oldHash);
$newPayload = '{"id":100,"extended":true}';
$response = new ExtendedServiceAvailabilityResponse([], [], null);
$this->snapshotRepository
->expects($this->once())
->method('findRefreshCandidates')
->willReturn([$snapshot]);
$this->serializer
->expects($this->once())
->method('deserialize')
->with($oldPayload, Travel::class, 'json')
->willReturn($travel);
$this->apiClient
->expects($this->once())
->method('getAvailabilitiesExtended')
->with(100)
->willReturn($response);
$this->serializer
->expects($this->once())
->method('serialize')
->with($travel, 'json')
->willReturn($newPayload);
$this->entityManager
->expects($this->once())
->method('flush');
$result = $this->service->refreshExtendedSnapshots(500, false, 360, []);
$this->assertSame(['processed' => 1, 'updated' => 1, 'failed' => 0], $result);
}
public function testRefreshExtendedSnapshotsApiFailureIncrementsFailedCount(): void
{
$travel = new Travel();
$travel->id = 100;
$travel->hotelId = 200;
$payload = '{"id":100}';
$snapshot = new TravelSnapshot(100, 200, $payload, hash('sha256', $payload));
$this->snapshotRepository
->expects($this->once())
->method('findRefreshCandidates')
->willReturn([$snapshot]);
$this->serializer
->expects($this->once())
->method('deserialize')
->willReturn($travel);
$this->apiClient
->expects($this->once())
->method('getAvailabilitiesExtended')
->willThrowException(new ApiClientException('API error'));
$this->logger
->expects($this->once())
->method('warning')
->with('Failed to fetch extended availability', $this->arrayHasKey('dateId'));
$this->entityManager
->expects($this->never())
->method('flush');
$result = $this->service->refreshExtendedSnapshots(500, false, 360, []);
$this->assertSame(['processed' => 1, 'updated' => 0, 'failed' => 1], $result);
}
public function testRefreshExtendedSnapshotsDeserializationFailureIncrementsFailedCount(): void
{
$payload = '{"id":100}';
$snapshot = new TravelSnapshot(100, 200, $payload, hash('sha256', $payload));
$this->snapshotRepository
->expects($this->once())
->method('findRefreshCandidates')
->willReturn([$snapshot]);
$this->serializer
->expects($this->once())
->method('deserialize')
->willThrowException(new \RuntimeException('Deserialization failed'));
$this->logger
->expects($this->once())
->method('warning')
->with('Snapshot payload cannot be deserialized to Travel', $this->arrayHasKey('snapshotId'));
$this->entityManager
->expects($this->never())
->method('flush');
$result = $this->service->refreshExtendedSnapshots(500, false, 360, []);
$this->assertSame(['processed' => 1, 'updated' => 0, 'failed' => 1], $result);
}
public function testRefreshSkipsAllCandidatesWhenXmlDateIdsIsNull(): void
{
$payload = '{"id":100}';
$snapshot = new TravelSnapshot(100, 200, $payload, hash('sha256', $payload));
$this->snapshotRepository
->expects($this->once())
->method('findRefreshCandidates')
->willReturn([$snapshot]);
$this->apiClient
->expects($this->never())
->method('getAvailabilitiesExtended');
$this->entityManager
->expects($this->never())
->method('flush');
$result = $this->service->refreshExtendedSnapshots(500, false, 360, null);
$this->assertSame(['processed' => 0, 'updated' => 0, 'failed' => 0], $result);
}
public function testRefreshSkipsSnapshotsWithXmlAvailable(): void
{
$payload = '{"id":100}';
$snapshot = new TravelSnapshot(100, 200, $payload, hash('sha256', $payload));
$this->snapshotRepository
->expects($this->once())
->method('findRefreshCandidates')
->willReturn([$snapshot]);
$this->apiClient
->expects($this->never())
->method('getAvailabilitiesExtended');
$result = $this->service->refreshExtendedSnapshots(500, false, 360, [100]);
$this->assertSame(['processed' => 0, 'updated' => 0, 'failed' => 0], $result);
}
public function testPurgeOrphanedFutureSnapshotsReturnsZeroForEmptyList(): void
{
$this->snapshotRepository
->expects($this->once())
->method('deleteOrphanedFutureSnapshots')
->with([], $this->isInstanceOf(\DateTimeImmutable::class))
->willReturn(0);
$result = $this->service->purgeOrphanedFutureSnapshots([]);
$this->assertSame(0, $result);
}
public function testPurgeOrphanedFutureSnapshotsDelegatesWithCorrectArguments(): void
{
$activeIds = [100, 200, 300];
$this->snapshotRepository
->expects($this->once())
->method('deleteOrphanedFutureSnapshots')
->with(
$activeIds,
$this->callback(function (\DateTimeImmutable $date) {
$expected = new \DateTimeImmutable('today');
return abs($date->getTimestamp() - $expected->getTimestamp()) < 5;
})
)
->willReturn(3);
$result = $this->service->purgeOrphanedFutureSnapshots($activeIds);
$this->assertSame(3, $result);
}
public function testGenerateMappingBuildsCorrectStructure(): void
{
$hotel = new Hotel();
$hotel->code = 'HTL1';
$hotel->name = 'Hotel One';
$travel1 = new Travel();
$travel1->id = 10;
$travel1->hotelId = 20;
$travel1->code = 'TRIP1';
$travel1->label = 'Trip One';
$travel1->productId = 365;
$travel1->hotel = $hotel;
$travel1->dateFrom = new \DateTimeImmutable('2026-06-01');
$travel1->dateTo = new \DateTimeImmutable('2026-06-14');
$payload1 = '{"id":10}';
$snapshot1 = new TravelSnapshot(10, 20, $payload1, hash('sha256', $payload1));
$snapshot1->setDateCode('TRIP1')
->setLabel('Trip One')
->setDateFrom(new \DateTimeImmutable('2026-06-01'))
->setDateTo(new \DateTimeImmutable('2026-06-14'))
->setHotelCode('HTL1')
->setHotelLabel('Hotel One');
$payload2 = '{"id":10,"h":21}';
$snapshot2 = new TravelSnapshot(10, 21, $payload2, hash('sha256', $payload2));
$snapshot2->setDateCode('TRIP1')
->setLabel('Trip One')
->setDateFrom(new \DateTimeImmutable('2026-06-01'))
->setDateTo(new \DateTimeImmutable('2026-06-14'))
->setHotelCode('HTL2')
->setHotelLabel('Hotel Two');
$this->snapshotRepository
->expects($this->once())
->method('findAllForMapping')
->willReturn([$snapshot1, $snapshot2]);
$mapping = $this->service->generateMapping();
$this->assertArrayHasKey(10, $mapping);
$this->assertSame('TRIP1', $mapping[10]['code']);
$this->assertArrayHasKey(20, $mapping[10]['hotels']);
$this->assertArrayHasKey(21, $mapping[10]['hotels']);
$this->assertSame('HTL1', $mapping[10]['hotels'][20]['code']);
$this->assertSame('HTL2', $mapping[10]['hotels'][21]['code']);
}
}