travelDataService = $this->createMock(TravelDataProvider::class); $this->service = new BookingSessionManager($this->travelDataService); } public function testGetOrCreateBookingCreateDtoThrowsWhenSessionMissing(): void { $request = $this->createRequestWithSession(); $this->expectException(BookingSessionNotFoundException::class); $this->service->getOrCreateBookingCreateDto($request); } public function testSaveAndLoadBookingDtoHydratesTravel(): void { $request = $this->createRequestWithSession(); $storedTravel = new Travel(); $storedTravel->id = 12; $storedTravel->hotelId = 34; $bookingDto = new BookingDto($storedTravel, 34); $bookingDto->booking = new Booking(); $bookingDto->booking->travelData = $storedTravel; $hydratedTravel = new Travel(); $hydratedTravel->id = 12; $hydratedTravel->hotelId = 34; $hydratedTravel->label = 'Hydrated travel'; $hydratedRoom = new Room(); $hydratedRoom->id = 7; $hydratedRoom->available = 0; $hydratedRoom->status = 'Frei'; $hydratedTravel->dateTo = new \DateTimeImmutable('2030-01-06'); $hydratedTravel->rooms = [$hydratedRoom]; $this->travelDataService ->expects($this->once()) ->method('getTravelData') ->with(12, 34) ->willReturn($hydratedTravel); $this->travelDataService ->expects($this->once()) ->method('enrichWithFreshAvailabilities') ->with($hydratedTravel, true, false, true) ->willReturnCallback(static function (Travel $travel): bool { $travel->rooms[0]->available = 0; return true; }); $this->service->markCreateAvailabilityRefreshed($request); $this->service->saveBookingDto($request, $bookingDto, BookingDto::MODE_CREATE); $loadedDto = $this->service->getBookingDto($request, BookingDto::MODE_CREATE); $this->assertNotNull($loadedDto); $this->assertSame($hydratedTravel, $loadedDto?->travel); $this->assertSame($hydratedTravel, $loadedDto?->booking?->travelData); $this->assertSame(0, $loadedDto?->travel->rooms[0]->available); } public function testSaveAndLoadBookingDtoForcesRefreshWhenCreateAvailabilityIsStale(): void { $request = $this->createRequestWithSession(); $storedTravel = new Travel(); $storedTravel->id = 12; $storedTravel->hotelId = 34; $bookingDto = new BookingDto($storedTravel, 34); $hydratedTravel = new Travel(); $hydratedTravel->id = 12; $hydratedTravel->hotelId = 34; $hydratedTravel->dateTo = new \DateTimeImmutable('2030-01-06'); $request->getSession()->set( BookingSessionManager::BOOKING_CREATE_AVAILABILITY_REFRESHED_AT_KEY, time() - 601 ); $this->travelDataService ->expects($this->once()) ->method('getTravelData') ->with(12, 34) ->willReturn($hydratedTravel); $this->travelDataService ->expects($this->once()) ->method('enrichWithFreshAvailabilities') ->with($hydratedTravel, true, true, true) ->willReturn(true); $this->service->saveBookingDto($request, $bookingDto, BookingDto::MODE_CREATE); $loadedDto = $this->service->getBookingDto($request, BookingDto::MODE_CREATE); $this->assertNotNull($loadedDto); $this->assertSame($hydratedTravel, $loadedDto?->travel); } public function testRefreshCreateAvailabilityForcesLiveRefreshAndResetsUnavailableSelections(): void { $request = $this->createRequestWithSession(); $travel = new Travel(); $travel->id = 12; $travel->hotelId = 34; $room = new Room(); $room->id = 7; $room->available = 1; $room->status = 'Frei'; $travel->rooms = [$room]; $bookingDto = new BookingDto($travel, 34); $selection = new RoomSelectionDto(); $selection->id = 7; $selection->quantity = 2; $bookingDto->roomSelections = [7 => $selection]; $this->travelDataService ->expects($this->once()) ->method('enrichWithFreshAvailabilities') ->with($travel, true, true, true) ->willReturn(true); $result = $this->service->refreshCreateAvailability($request, $bookingDto, true); $this->assertTrue($result->availabilityRefreshed); $this->assertTrue($result->roomSelectionQuantityReset); $this->assertTrue($result->roomSelectionSnapshotChanged); $this->assertTrue($result->hasAvailableRooms); $this->assertSame(0, $bookingDto->roomSelections[7]->quantity); $this->assertSame($bookingDto, $request->getSession()->get(BookingSessionManager::BOOKING_CREATE_KEY)); } public function testBaselineSnapshotIsCreatedAndReused(): void { $request = $this->createRequestWithSession(); $bookingDto = new BookingDto(new Travel(), 99); $selection = new RoomSelectionDto(); $selection->id = 1; $selection->quantity = 2; $bookingDto->roomSelections = [$selection]; $first = $this->service->getOrCreateBaselineSnapshot($request, $bookingDto); $this->assertSame([[1, 2, 0.0]], $first); $selection->quantity = 5; $second = $this->service->getOrCreateBaselineSnapshot($request, $bookingDto); $this->assertSame($first, $second); $this->assertSame([[1, 2, 0.0]], $second); } public function testSaveBaselineSnapshotReplacesExistingSnapshot(): void { $request = $this->createRequestWithSession(); $bookingDto = new BookingDto(new Travel(), 99); $selection = new RoomSelectionDto(); $selection->id = 1; $selection->quantity = 2; $bookingDto->roomSelections = [$selection]; $this->service->saveBaselineSnapshot($request, $bookingDto); $selection->quantity = 4; $updated = $this->service->saveBaselineSnapshot($request, $bookingDto); $this->assertSame([[1, 4, 0.0]], $updated); $this->assertSame([[1, 4, 0.0]], $request->getSession()->get(BookingSessionManager::BOOKING_CREATE_BASELINE_KEY)); } public function testStoreReturnUrlFallsBackToDefaultForInvalidInput(): void { $request = $this->createRequestWithSession(); $this->service->storeReturnUrl($request, 'javascript:alert(1)'); $this->assertSame(BookingSessionManager::DEFAULT_RETURN_URL, $this->service->getReturnUrl($request)); } public function testClearBookingSessionPreservesReturnUrl(): void { $request = $this->createRequestWithSession(); $this->service->storeReturnUrl($request, 'https://example.test/after-booking'); $this->service->clearBookingSession($request); $this->assertSame('https://example.test/after-booking', $this->service->getReturnUrl($request)); } private function createRequestWithSession(): Request { $session = new Session(new MockArraySessionStorage()); $request = new Request(); $request->setSession($session); return $request; } }