currentStep = 4; $dto->accommodationId = 1; $dto->isInquiry = false; $session = new Session(new MockArraySessionStorage()); $request = Request::create('/groups/booking/step-4', 'GET'); $request->setSession($session); $sessionManager = new AccommodationSessionManager(); $sessionManager->save($request, $dto); $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' => [], ]); $controller = new TestableStep4Controller($bookingService, $sessionManager, $this->createStub(FormInterface::class)); $response = $controller->index($request); self::assertSame(200, $response->getStatusCode()); self::assertSame('groups/booking/step_4.html.twig', $controller->renderedView); self::assertSame($dto, $controller->renderedParameters['dto']); } public function testIndexRedirectsToStep3WhenNotYetReached(): void { $dto = new AccommodationBookingDto(); $dto->currentStep = 3; $dto->accommodationId = 1; $session = new Session(new MockArraySessionStorage()); $request = Request::create('/groups/booking/step-4', 'GET'); $request->setSession($session); $sessionManager = new AccommodationSessionManager(); $sessionManager->save($request, $dto); $controller = new TestableStep4Controller( $this->createStub(AccommodationBookingService::class), $sessionManager, $this->createStub(FormInterface::class), ); $response = $controller->index($request); self::assertInstanceOf(RedirectResponse::class, $response); self::assertSame('/app_groups_booking_step_3', $response->getTargetUrl()); } public function testIndexPostWithValidCsrfPersistsAsInquiryAndRedirects(): void { $dto = new AccommodationBookingDto(); $dto->currentStep = 4; $dto->accommodationId = 1; $session = new Session(new MockArraySessionStorage()); $request = Request::create('/groups/booking/step-4', 'POST'); $request->setSession($session); $sessionManager = new AccommodationSessionManager(); $sessionManager->save($request, $dto); $accommodation = (new Accommodation()) ->setName('Hotel') ->setCalendarCode('HOTEL') ->setMaxAdolescentAge(17); $booking = (new AccommodationBooking())->setOrigin(AccommodationBookingOrigin::Offer); $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('finalizeBooking')->willReturn($booking); $inquiryForm = $this->createStub(FormInterface::class); $inquiryForm->method('handleRequest')->willReturnSelf(); $inquiryForm->method('isSubmitted')->willReturn(true); $inquiryForm->method('isValid')->willReturn(true); $controller = new TestableStep4Controller($bookingService, $sessionManager, $inquiryForm); $response = $controller->index($request); self::assertInstanceOf(RedirectResponse::class, $response); self::assertSame('/app_groups_booking_success', $response->getTargetUrl()); self::assertTrue($dto->forceInquiry); self::assertNull($sessionManager->getDto($request)); } public function testIndexPostWithInvalidFormThrowsAccessDenied(): void { $dto = new AccommodationBookingDto(); $dto->currentStep = 4; $dto->accommodationId = 1; $session = new Session(new MockArraySessionStorage()); $request = Request::create('/groups/booking/step-4', 'POST'); $request->setSession($session); $sessionManager = new AccommodationSessionManager(); $sessionManager->save($request, $dto); $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('finalizeBooking'); $inquiryForm = $this->createStub(FormInterface::class); $inquiryForm->method('handleRequest')->willReturnSelf(); $inquiryForm->method('isSubmitted')->willReturn(true); $inquiryForm->method('isValid')->willReturn(false); $controller = new TestableStep4Controller($bookingService, $sessionManager, $inquiryForm); $this->expectException(\Symfony\Component\Security\Core\Exception\AccessDeniedException::class); $controller->index($request); } public function testConfirmInquiryGetRendersModal(): void { $dto = new AccommodationBookingDto(); $dto->currentStep = 4; $dto->accommodationId = 1; $session = new Session(new MockArraySessionStorage()); $request = Request::create('/groups/booking/step-4/confirm-inquiry', 'GET'); $request->setSession($session); $sessionManager = new AccommodationSessionManager(); $sessionManager->save($request, $dto); $controller = new TestableStep4Controller( $this->createStub(AccommodationBookingService::class), $sessionManager, $this->createStub(FormInterface::class), ); $response = $controller->confirmInquiry($request); self::assertSame(200, $response->getStatusCode()); self::assertSame('groups/booking/modal_inquiry_confirmation.html.twig', $controller->renderedView); } public function testConfirmInquiryRedirectsToStep3WhenNotYetReached(): void { $dto = new AccommodationBookingDto(); $dto->currentStep = 3; $dto->accommodationId = 1; $session = new Session(new MockArraySessionStorage()); $request = Request::create('/groups/booking/step-4/confirm-inquiry', 'GET'); $request->setSession($session); $sessionManager = new AccommodationSessionManager(); $sessionManager->save($request, $dto); $controller = new TestableStep4Controller( $this->createStub(AccommodationBookingService::class), $sessionManager, $this->createStub(FormInterface::class), ); $response = $controller->confirmInquiry($request); self::assertInstanceOf(RedirectResponse::class, $response); self::assertSame('/app_groups_booking_step_3', $response->getTargetUrl()); } public function testConfirmGetRendersModalWithFreshForm(): void { $dto = new AccommodationBookingDto(); $dto->currentStep = 4; $dto->accommodationId = 1; $session = new Session(new MockArraySessionStorage()); $request = Request::create('/groups/booking/step-4/confirm', 'GET'); $request->setSession($session); $sessionManager = new AccommodationSessionManager(); $sessionManager->save($request, $dto); $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('finalizeBooking'); $confirmationForm = $this->createStub(FormInterface::class); $confirmationForm->method('handleRequest')->willReturnSelf(); $confirmationForm->method('isSubmitted')->willReturn(false); $controller = new TestableStep4Controller($bookingService, $sessionManager, $confirmationForm); $response = $controller->confirm($request); self::assertSame(200, $response->getStatusCode()); self::assertSame('groups/booking/modal_booking_confirmation.html.twig', $controller->renderedView); self::assertSame($confirmationForm, $controller->renderedParameters['confirmationForm']); self::assertSame($dto, $sessionManager->getDto($request)); } public function testConfirmPostValidPersistsAndRedirects(): void { $dto = new AccommodationBookingDto(); $dto->currentStep = 4; $dto->accommodationId = 1; $session = new Session(new MockArraySessionStorage()); $request = Request::create('/groups/booking/step-4/confirm', 'POST'); $request->setSession($session); $request->headers->set('HX-Request', 'true'); $sessionManager = new AccommodationSessionManager(); $sessionManager->save($request, $dto); $accommodation = (new Accommodation()) ->setName('Hotel') ->setCalendarCode('HOTEL') ->setMaxAdolescentAge(17); $booking = (new AccommodationBooking())->setOrigin(AccommodationBookingOrigin::Direct); $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('finalizeBooking')->willReturn($booking); $confirmationForm = $this->createStub(FormInterface::class); $confirmationForm->method('handleRequest')->willReturnSelf(); $confirmationForm->method('isSubmitted')->willReturn(true); $confirmationForm->method('isValid')->willReturn(true); $controller = new TestableStep4Controller($bookingService, $sessionManager, $confirmationForm); $response = $controller->confirm($request); self::assertSame(200, $response->getStatusCode()); self::assertTrue($response->headers->has('HX-Redirect')); self::assertNull($sessionManager->getDto($request)); } public function testConfirmPostInvalidReRendersModalWithoutPersisting(): void { $dto = new AccommodationBookingDto(); $dto->currentStep = 4; $dto->accommodationId = 1; $session = new Session(new MockArraySessionStorage()); $request = Request::create('/groups/booking/step-4/confirm', 'POST'); $request->setSession($session); $sessionManager = new AccommodationSessionManager(); $sessionManager->save($request, $dto); $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('finalizeBooking'); $confirmationForm = $this->createStub(FormInterface::class); $confirmationForm->method('handleRequest')->willReturnSelf(); $confirmationForm->method('isSubmitted')->willReturn(true); $confirmationForm->method('isValid')->willReturn(false); $controller = new TestableStep4Controller($bookingService, $sessionManager, $confirmationForm); $response = $controller->confirm($request); self::assertSame(200, $response->getStatusCode()); self::assertFalse($response->headers->has('HX-Redirect')); self::assertSame('groups/booking/modal_booking_confirmation.html.twig', $controller->renderedView); self::assertNotNull($sessionManager->getDto($request)); } } final class TestableStep4Controller extends Step4Controller { /** * @var array */ public array $renderedParameters = []; public string $renderedView = ''; public function __construct( AccommodationBookingService $bookingService, AccommodationSessionManager $sessionManager, private readonly FormInterface $form, ) { parent::__construct( $bookingService, $sessionManager, new AccommodationTermsUrlProvider(['AT' => '', 'CH' => '', 'IT' => ''], 'https://example.test/agb/'), ); } /** * @return FormInterface */ protected function createForm(string $type, mixed $data = null, array $options = []): FormInterface { return $this->form; } protected function getParameter(string $name): array|bool|string|int|float|\UnitEnum|null { return 'https://example.test/agb/'; } /** * @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'); } }