currentStep = 3; $dto->accommodationId = 1; $dto->dateFrom = new \DateTimeImmutable('2026-03-01'); $dto->dateTo = new \DateTimeImmutable('2026-03-05'); $dto->selectedBoardServiceId = 10; $dto->selectedAdditionalServiceIds = [20]; $session = new Session(new MockArraySessionStorage()); $request = Request::create('/groups/booking/step-3', 'GET'); $request->setSession($session); $sessionManager = new AccommodationSessionManager(); $sessionManager->save($request, $dto); $form = $this->createMock(FormInterface::class); $form ->method('handleRequest') ->willReturnSelf(); $form ->method('isSubmitted') ->willReturn(false); $accommodation = (new Accommodation()) ->setName('Hotel') ->setCalendarCode('HOTEL') ->setMaxAdolescentAge(17); $boardService = (new BoardService())->setLabel('Halbpension')->setPrice(1000); $additionalService = (new AdditionalService())->setLabel('Skipass')->setPrice(2000); $bookingService = $this->createMock(AccommodationBookingService::class); $bookingService ->method('loadAccommodation') ->with(1) ->willReturn($accommodation); $bookingService ->expects(self::once()) ->method('loadAvailableServices') ->with($dto, $accommodation) ->willReturn([ 'boardServices' => [$boardService], 'additionalServices' => [$additionalService], 'groupedAdditionalServices' => [], 'ungroupedAdditionalServices' => [$additionalService], ]); $controller = new TestableAccommodationStep3Controller($bookingService, $sessionManager, $form); $response = $controller->index($request); self::assertSame(200, $response->getStatusCode()); self::assertSame('groups/booking/step_3.html.twig', $controller->renderedView); self::assertSame([$boardService], $controller->renderedParameters['ctx']->boardServices); self::assertSame([$additionalService], $controller->renderedParameters['ctx']->additionalServices); } public function testClickingBuchenOpensConfirmationModalWithoutPersisting(): void { $dto = new AccommodationBookingDto(); $dto->currentStep = 3; $dto->accommodationId = 1; $dto->isInquiry = false; $session = new Session(new MockArraySessionStorage()); $request = Request::create('/groups/booking/step-3', 'POST'); $request->setSession($session); $sessionManager = new AccommodationSessionManager(); $sessionManager->save($request, $dto); $submitBookingButton = $this->getMockBuilder(SubmitButton::class) ->disableOriginalConstructor() ->onlyMethods(['isClicked']) ->getMock(); $submitBookingButton->method('isClicked')->willReturn(true); $mainForm = $this->createMock(FormInterface::class); $mainForm->method('handleRequest')->willReturnSelf(); $mainForm->method('isSubmitted')->willReturn(true); $mainForm->method('isValid')->willReturn(true); $mainForm->method('has')->with('submitBooking')->willReturn(true); $mainForm->method('get')->with('submitBooking')->willReturn($submitBookingButton); $confirmationForm = $this->createMock(FormInterface::class); $accommodation = (new Accommodation()) ->setName('Hotel') ->setCalendarCode('HOTEL') ->setMaxAdolescentAge(17); $bookingService = $this->createMock(AccommodationBookingService::class); $bookingService->method('loadAccommodation')->with(1)->willReturn($accommodation); $bookingService->method('loadAvailableServices')->willReturn([ 'boardServices' => [], 'additionalServices' => [], 'groupedAdditionalServices' => [], 'ungroupedAdditionalServices' => [], ]); $bookingService->expects(self::never())->method('persist'); $controller = new TestableAccommodationStep3Controller( $bookingService, $sessionManager, [AccommodationStep3Type::class => $mainForm, AccommodationStep3ConfirmationType::class => $confirmationForm], ); $response = $controller->index($request); self::assertSame(200, $response->getStatusCode()); self::assertSame('groups/booking/step_3.html.twig', $controller->renderedView); self::assertSame($confirmationForm, $controller->renderedParameters['confirmationForm']); self::assertFalse($dto->forceInquiry); self::assertSame($dto, $sessionManager->getDto($request)); } public function testConfirmPersistsBookingOnValidConfirmationForm(): void { $dto = new AccommodationBookingDto(); $dto->currentStep = 3; $dto->accommodationId = 1; $dto->isInquiry = false; $session = new Session(new MockArraySessionStorage()); $request = Request::create('/groups/booking/step-3/confirm', 'POST'); $request->setSession($session); $request->headers->set('HX-Request', 'true'); $sessionManager = new AccommodationSessionManager(); $sessionManager->save($request, $dto); $confirmationForm = $this->createMock(FormInterface::class); $confirmationForm->method('handleRequest')->willReturnSelf(); $confirmationForm->method('isSubmitted')->willReturn(true); $confirmationForm->method('isValid')->willReturn(true); $accommodation = (new Accommodation()) ->setName('Hotel') ->setCalendarCode('HOTEL') ->setMaxAdolescentAge(17); $booking = (new AccommodationBooking())->setIsInquiry(false); $bookingService = $this->createMock(AccommodationBookingService::class); $bookingService->method('loadAccommodation')->with(1)->willReturn($accommodation); $bookingService->method('loadAvailableServices')->willReturn([ 'boardServices' => [], 'additionalServices' => [], 'groupedAdditionalServices' => [], 'ungroupedAdditionalServices' => [], ]); $bookingService->method('loadPrices')->willReturn([]); $bookingService->expects(self::once())->method('persist')->willReturn($booking); $bookingService->expects(self::once())->method('sendNotificationEmail'); $controller = new TestableAccommodationStep3Controller( $bookingService, $sessionManager, [AccommodationStep3ConfirmationType::class => $confirmationForm], ); $response = $controller->confirm($request); self::assertSame(200, $response->getStatusCode()); self::assertTrue($response->headers->has('HX-Redirect')); self::assertNull($sessionManager->getDto($request)); } public function testConfirmRerendersModalWhenTermsNotAccepted(): void { $dto = new AccommodationBookingDto(); $dto->currentStep = 3; $dto->accommodationId = 1; $dto->isInquiry = false; $session = new Session(new MockArraySessionStorage()); $request = Request::create('/groups/booking/step-3/confirm', 'POST'); $request->setSession($session); $sessionManager = new AccommodationSessionManager(); $sessionManager->save($request, $dto); $confirmationForm = $this->createMock(FormInterface::class); $confirmationForm->method('handleRequest')->willReturnSelf(); $confirmationForm->method('isSubmitted')->willReturn(true); $confirmationForm->method('isValid')->willReturn(false); $accommodation = (new Accommodation()) ->setName('Hotel') ->setCalendarCode('HOTEL') ->setMaxAdolescentAge(17); $bookingService = $this->createMock(AccommodationBookingService::class); $bookingService->method('loadAccommodation')->with(1)->willReturn($accommodation); $bookingService->expects(self::never())->method('persist'); $controller = new TestableAccommodationStep3Controller( $bookingService, $sessionManager, [AccommodationStep3ConfirmationType::class => $confirmationForm], ); $response = $controller->confirm($request); self::assertSame(200, $response->getStatusCode()); self::assertFalse($response->headers->has('HX-Redirect')); self::assertSame('groups/booking/_step3_confirmation_modal.html.twig', $controller->renderedView); self::assertNotNull($sessionManager->getDto($request)); } } final class TestableAccommodationStep3Controller extends Step3Controller { /** * @var array */ public array $renderedParameters = []; public string $renderedView = ''; /** * @param FormInterface|array> $form */ public function __construct( AccommodationBookingService $bookingService, AccommodationSessionManager $sessionManager, private readonly FormInterface|array $form, ) { parent::__construct($bookingService, $sessionManager); } /** * @return FormInterface */ protected function createForm(string $type, mixed $data = null, array $options = []): FormInterface { if (is_array($this->form)) { return $this->form[$type]; } return $this->form; } /** * @param array $parameters */ protected function redirectToRoute(string $route, array $parameters = [], int $status = 302): RedirectResponse { return new RedirectResponse('/'.$route, $status); } /** * @param array $parameters */ protected function generateUrl(string $route, array $parameters = [], int $referenceType = 1): string { return '/'.$route; } protected function addFlash(string $type, mixed $message): void { // no-op: avoids requiring a full service container in these unit tests } /** * @param array $parameters */ protected function render(string $view, array $parameters = [], ?Response $response = null): Response { $this->renderedView = $view; $this->renderedParameters = $parameters; return new Response('ok'); } }