createMock(AccommodationBookingService::class); $bookingService->expects(self::never())->method('splitOffPeriod'); $controller = $this->buildController($bookingService, $this->form(submitted: false)); $response = $controller->index(new AccommodationBooking(), AccommodationBookingPeriodPosition::Leading, $this->request()); self::assertSame(Response::HTTP_OK, $response->getStatusCode()); self::assertSame('admin/accommodation_booking/modal_period.html.twig', $controller->renderedView); } public function testASideThatWasAlreadySplitIsNotSplitAgain(): void { // The edit page only offers free sides; this is a page left open while the side was taken. $booking = (new AccommodationBooking()) ->setDateFrom(new \DateTimeImmutable('2026-07-01')) ->setDateTo(new \DateTimeImmutable('2026-07-08')); $booking->splitOffPeriod(AccommodationBookingPeriodPosition::Leading, 1); $bookingService = $this->createMock(AccommodationBookingService::class); $bookingService->expects(self::never())->method('splitOffPeriod'); $controller = $this->buildController($bookingService, $this->form(submitted: true)); $response = $controller->index($booking, AccommodationBookingPeriodPosition::Leading, $this->request('POST')); self::assertTrue($response->headers->has('HX-Redirect')); self::assertNull($controller->renderedView); self::assertSame('error', $controller->flashes[0]['type']); } public function testAValidSplitSendsTheBrowserBackToTheEditForm(): void { $booking = new AccommodationBooking(); $bookingService = $this->createMock(AccommodationBookingService::class); $bookingService->expects(self::once())->method('splitOffPeriod') ->with($booking, AccommodationBookingPeriodPosition::Trailing) ->willReturn(new ConstraintViolationList()); $controller = $this->buildController($bookingService, $this->form(submitted: true)); $response = $controller->index($booking, AccommodationBookingPeriodPosition::Trailing, $this->request('POST')); self::assertSame( '/app_admin_accommodationbooking_edit?r=%2Fadmin%2Faccommodation-booking%3Fpage%3D2', $response->headers->get('HX-Redirect'), ); self::assertSame('success', $controller->flashes[0]['type']); } public function testAViolationOfTheBookingIsReportedInTheModal(): void { $violations = new ConstraintViolationList([ new ConstraintViolation('Nach dem Abteilen der Zeiträume muss mindestens eine Nacht für die Gruppe bleiben.', null, [], null, 'dateTo', null), ]); $bookingService = $this->createStub(AccommodationBookingService::class); $bookingService->method('splitOffPeriod')->willReturn($violations); $controller = $this->buildController($bookingService, $this->form(submitted: true, expectsError: true)); $response = $controller->index(new AccommodationBooking(), AccommodationBookingPeriodPosition::Leading, $this->request('POST')); self::assertFalse($response->headers->has('HX-Redirect'), 'the modal stays open on the error'); self::assertSame('admin/accommodation_booking/modal_period.html.twig', $controller->renderedView); self::assertSame([], $controller->flashes); } public function testAnImplausibleTotalIsReportedInTheModal(): void { $bookingService = $this->createStub(AccommodationBookingService::class); $bookingService->method('splitOffPeriod') ->willThrowException(new ImplausibleBookingPriceException(AccommodationBooking::MAX_TOTAL_PRICE + 1, 40, 2)); $controller = $this->buildController($bookingService, $this->form(submitted: true, expectsError: true)); $response = $controller->index(new AccommodationBooking(), AccommodationBookingPeriodPosition::Leading, $this->request('POST')); self::assertFalse($response->headers->has('HX-Redirect')); self::assertSame('admin/accommodation_booking/modal_period.html.twig', $controller->renderedView); } private function request(string $method = 'GET'): Request { return Request::create( '/admin/accommodation-booking/1/period/split/leading?r=%2Fadmin%2Faccommodation-booking%3Fpage%3D2', $method, server: ['HTTP_HX-Request' => 'true'], ); } private function form(bool $submitted, bool $expectsError = false): FormInterface { $form = $expectsError ? $this->createMock(FormInterface::class) : $this->createStub(FormInterface::class); if ($form instanceof \PHPUnit\Framework\MockObject\MockObject) { $form->expects(self::once())->method('addError'); } $form->method('handleRequest')->willReturn($form); $form->method('isSubmitted')->willReturn($submitted); $form->method('isValid')->willReturn(true); return $form; } private function buildController(AccommodationBookingService $bookingService, FormInterface $form): TestableSplitPeriodController { return new TestableSplitPeriodController($bookingService, $this->createStub(LoggerInterface::class), $form); } } final class TestableSplitPeriodController extends SplitPeriodController { public ?string $renderedView = null; /** @var list */ public array $flashes = []; public function __construct( AccommodationBookingService $bookingService, LoggerInterface $logger, private readonly FormInterface $form, ) { parent::__construct($bookingService, $logger); } protected function createForm(string $type, mixed $data = null, array $options = []): FormInterface { return $this->form; } /** * @param array $parameters */ protected function render(string $view, array $parameters = [], ?Response $response = null): Response { $this->renderedView = $view; return new Response(); } 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); } }