setStatus(AccommodationBookingStatus::Open); $booking->setRemarks("Bitte Zimmer im EG\nDanke!"); $bookingRepository = $this->createMock(AccommodationBookingRepository::class); $bookingRepository->method('findOneBy')->with(['uuid' => $booking->getUuid()])->willReturn($booking); $confirmationForm = $this->createStub(FormInterface::class); $confirmationForm->method('handleRequest')->willReturnSelf(); $confirmationForm->method('isSubmitted')->willReturn(false); $bookingService = $this->serviceWithCompleteContactData(); $bookingService->expects(self::never())->method('acceptBooking'); $controller = new TestableOfferConfirmController( $bookingRepository, $this->authorizedLinkSigner(), $bookingService, $confirmationForm, ); $response = $controller->index($booking->getUuid(), Request::create('/groups/booking/offer/'.$booking->getUuid().'/confirm')); self::assertSame(Response::HTTP_OK, $response->getStatusCode()); self::assertSame('groups/offer/modal_accept_confirmation.html.twig', $controller->renderedView); self::assertSame($booking, $controller->renderedParameters['booking']); self::assertSame($confirmationForm, $controller->renderedParameters['confirmationForm']); self::assertInstanceOf(OfferAcceptDto::class, $controller->formData); self::assertSame("Bitte Zimmer im EG\nDanke!", $controller->formData->remarks, 'the stored remark is prefilled into the modal'); self::assertFalse($controller->formData->termsAccepted); } public function testPostValidAcceptsBookingAndRedirects(): void { $booking = new AccommodationBooking(); $booking->setStatus(AccommodationBookingStatus::Open); $booking->setRemarks('Bitte Zimmer im EG'); $bookingService = $this->serviceWithCompleteContactData(); $bookingService->expects(self::once())->method('acceptBooking')->with($booking, 'Bitte Zimmer im EG'); $controller = new TestableOfferConfirmController( $this->repositoryReturning($booking), $this->authorizedLinkSigner(), $bookingService, $this->submittedForm(valid: true), ); $response = $controller->index($booking->getUuid(), Request::create('/groups/booking/offer/'.$booking->getUuid().'/confirm', 'POST')); self::assertInstanceOf(RedirectResponse::class, $response); self::assertSame('/app_groups_offer_view?uuid='.$booking->getUuid(), $response->getTargetUrl()); self::assertSame([['type' => 'success', 'message' => 'Deine Buchung ist bei uns eingegangen. Sobald sie geprüft ist, erhältst du eine Bestätigung per E-Mail.']], $controller->flashes); } public function testPostValidViaHtmxReturnsHxRedirect(): void { $booking = new AccommodationBooking(); $booking->setStatus(AccommodationBookingStatus::Open); $bookingService = $this->serviceWithCompleteContactData(); // No remark on the booking and none submitted: the empty string clears rather than keeps. $bookingService->expects(self::once())->method('acceptBooking')->with($booking, ''); $controller = new TestableOfferConfirmController( $this->repositoryReturning($booking), $this->authorizedLinkSigner(), $bookingService, $this->submittedForm(valid: true), ); $request = Request::create('/groups/booking/offer/'.$booking->getUuid().'/confirm', 'POST'); $request->headers->set('HX-Request', 'true'); $response = $controller->index($booking->getUuid(), $request); self::assertSame(Response::HTTP_OK, $response->getStatusCode()); self::assertTrue($response->headers->has('HX-Redirect')); } public function testPostInvalidReRendersModalWithoutAccepting(): void { $booking = new AccommodationBooking(); $booking->setStatus(AccommodationBookingStatus::Open); $bookingService = $this->serviceWithCompleteContactData(); $bookingService->expects(self::never())->method('acceptBooking'); $controller = new TestableOfferConfirmController( $this->repositoryReturning($booking), $this->authorizedLinkSigner(), $bookingService, $this->submittedForm(valid: false), ); $response = $controller->index($booking->getUuid(), Request::create('/groups/booking/offer/'.$booking->getUuid().'/confirm', 'POST')); self::assertSame(Response::HTTP_OK, $response->getStatusCode()); self::assertFalse($response->headers->has('HX-Redirect')); self::assertSame('groups/offer/modal_accept_confirmation.html.twig', $controller->renderedView); } #[DataProvider('methods')] public function testSendsToContactPageWhenContactDataIsIncomplete(string $method): void { $booking = new AccommodationBooking(); $booking->setStatus(AccommodationBookingStatus::Open); $bookingService = $this->createMock(AccommodationBookingService::class); $bookingService->method('hasCompleteContactData')->with($booking)->willReturn(false); $bookingService->expects(self::never())->method('acceptBooking'); $controller = new TestableOfferConfirmController( $this->repositoryReturning($booking), $this->authorizedLinkSigner(), $bookingService, ); $request = Request::create('/groups/booking/offer/'.$booking->getUuid().'/confirm', $method); $request->headers->set('HX-Request', 'true'); $response = $controller->index($booking->getUuid(), $request); self::assertSame('/app_groups_offer_contact?uuid='.$booking->getUuid(), $response->headers->get('HX-Redirect')); self::assertNull($controller->renderedView); } /** * @return iterable */ public static function methods(): iterable { yield 'opening the modal' => ['GET']; yield 'submitting it anyway' => ['POST']; } public function testRedirectsWhenAlreadyAccepted(): void { $booking = new AccommodationBooking(); $booking->setStatus(AccommodationBookingStatus::Received); $controller = new TestableOfferConfirmController( $this->repositoryReturning($booking), $this->authorizedLinkSigner(), $this->createStub(AccommodationBookingService::class), ); $response = $controller->index($booking->getUuid(), Request::create('/groups/booking/offer/'.$booking->getUuid().'/confirm')); self::assertInstanceOf(RedirectResponse::class, $response); } public function testRedirectsWhenSessionNotAuthorized(): void { $booking = new AccommodationBooking(); $linkSigner = $this->createStub(AccommodationBookingLinkSigner::class); $linkSigner->method('isSessionAuthorized')->willReturn(false); $controller = new TestableOfferConfirmController( $this->repositoryReturning($booking), $linkSigner, $this->createStub(AccommodationBookingService::class), ); $response = $controller->index($booking->getUuid(), Request::create('/groups/booking/offer/'.$booking->getUuid().'/confirm')); self::assertInstanceOf(RedirectResponse::class, $response); } public function testRedirectsForUnknownUuid(): void { $bookingRepository = $this->createStub(AccommodationBookingRepository::class); $bookingRepository->method('findOneBy')->willReturn(null); $linkSigner = $this->createMock(AccommodationBookingLinkSigner::class); $linkSigner->expects(self::never())->method('isSessionAuthorized'); $controller = new TestableOfferConfirmController( $bookingRepository, $linkSigner, $this->createStub(AccommodationBookingService::class), ); $response = $controller->index('unknown-uuid', Request::create('/groups/booking/offer/unknown-uuid/confirm')); self::assertInstanceOf(RedirectResponse::class, $response); } private function repositoryReturning(AccommodationBooking $booking): AccommodationBookingRepository { $bookingRepository = $this->createStub(AccommodationBookingRepository::class); $bookingRepository->method('findOneBy')->willReturn($booking); return $bookingRepository; } private function authorizedLinkSigner(): AccommodationBookingLinkSigner { $linkSigner = $this->createStub(AccommodationBookingLinkSigner::class); $linkSigner->method('isSessionAuthorized')->willReturn(true); return $linkSigner; } private function serviceWithCompleteContactData(): AccommodationBookingService&MockObject { $bookingService = $this->createMock(AccommodationBookingService::class); $bookingService->method('hasCompleteContactData')->willReturn(true); return $bookingService; } private function submittedForm(bool $valid): FormInterface { $form = $this->createStub(FormInterface::class); $form->method('handleRequest')->willReturnSelf(); $form->method('isSubmitted')->willReturn(true); $form->method('isValid')->willReturn($valid); return $form; } } final class TestableOfferConfirmController extends ConfirmController { public ?string $renderedView = null; /** @var array */ public array $renderedParameters = []; public mixed $formData = null; /** @var array */ public array $flashes = []; public function __construct( AccommodationBookingRepository $bookingRepository, AccommodationBookingLinkSigner $linkSigner, AccommodationBookingService $bookingService, private readonly ?FormInterface $form = null, ) { parent::__construct( $bookingRepository, $linkSigner, $bookingService, new AccommodationTermsUrlProvider(['AT' => '', 'CH' => '', 'IT' => ''], 'https://example.test/agb/'), ); } protected function createForm(string $type, mixed $data = null, array $options = []): FormInterface { $this->formData = $data; return $this->form ?? throw new \LogicException('No form mock configured for this test.'); } protected function addFlash(string $type, mixed $message): void { $this->flashes[] = ['type' => $type, 'message' => $message]; } /** * @param array $parameters */ protected function generateUrl(string $route, array $parameters = [], int $referenceType = 1): string { return '/'.$route.'?'.http_build_query($parameters); } protected function render(string $view, array $parameters = [], ?Response $response = null): Response { $this->renderedView = $view; $this->renderedParameters = $parameters; return $response ?? new Response(''); } }