createMock(AccommodationBookingService::class); $bookingService->expects(self::never())->method('changeAccommodation'); $controller = $this->buildController($bookingService, $this->unsubmittedForm()); $response = $controller->index($this->draft(), $this->request()); self::assertSame(Response::HTTP_OK, $response->getStatusCode()); self::assertSame('admin/accommodation_booking/modal_change_accommodation.html.twig', $controller->renderedView); } /** * @dataProvider lockedStatuses */ public function testABookingThatHasLeftEntwurfCannotMoveHouse(AccommodationBookingStatus $status): void { // The customer may already be looking at the offer under their access link. $booking = $this->draft(); $booking->setStatus($status); $selected = new Accommodation(); $bookingService = $this->createMock(AccommodationBookingService::class); $bookingService->expects(self::never())->method('changeAccommodation'); $controller = $this->buildController($bookingService, $this->submittedForm($selected)); $response = $controller->index($booking, $this->request('POST')); self::assertTrue($response->headers->has('HX-Redirect')); self::assertNull($controller->renderedView); self::assertSame('error', $controller->flashes[0]['type']); } /** * @return iterable */ public static function lockedStatuses(): iterable { yield 'requested' => [AccommodationBookingStatus::Requested]; yield 'open' => [AccommodationBookingStatus::Open]; yield 'received' => [AccommodationBookingStatus::Received]; yield 'confirmed' => [AccommodationBookingStatus::Confirmed]; yield 'discarded' => [AccommodationBookingStatus::Discarded]; } public function testAValidSubmitChangesTheHouseAndSendsTheBrowserBackToTheEditForm(): void { $booking = $this->draft(); $selected = new Accommodation(); $bookingService = $this->createMock(AccommodationBookingService::class); $bookingService->expects(self::once())->method('changeAccommodation')->with($booking, $selected); $controller = $this->buildController($bookingService, $this->submittedForm($selected)); $response = $controller->index($booking, $this->request('POST')); self::assertSame( '/app_admin_accommodationbooking_edit?r=%2Fadmin%2Faccommodation-booking%3Fpage%3D2', $response->headers->get('HX-Redirect'), 'the still-encoded return url has to survive so the edit page leads back to the list', ); self::assertSame('success', $controller->flashes[0]['type']); } public function testPickingTheSameHouseIsNotReportedAsAChange(): void { $booking = $this->draft(); $accommodation = $booking->getAccommodation(); self::assertNotNull($accommodation); $bookingService = $this->createMock(AccommodationBookingService::class); $bookingService->expects(self::never())->method('changeAccommodation'); $controller = $this->buildController($bookingService, $this->submittedForm($accommodation)); $response = $controller->index($booking, $this->request('POST')); self::assertTrue($response->headers->has('HX-Redirect')); self::assertSame([], $controller->flashes); } private function draft(): AccommodationBooking { $booking = new AccommodationBooking(); $booking->setStatus(AccommodationBookingStatus::Draft); $booking->setAccommodation((new Accommodation())->setName('Seehaus')); return $booking; } private function request(string $method = 'GET'): Request { // Every entry point is htmx — the button opens the modal, the modal posts back — so // the header is what makes htmxRedirect() answer with HX-Redirect rather than a 302. return Request::create( '/admin/accommodation-booking/1/change-accommodation?r=%2Fadmin%2Faccommodation-booking%3Fpage%3D2', $method, server: ['HTTP_HX-Request' => 'true'], ); } private function unsubmittedForm(): FormInterface { $form = $this->createMock(FormInterface::class); $form->method('handleRequest')->willReturn($form); $form->method('isSubmitted')->willReturn(false); return $form; } private function submittedForm(Accommodation $selected): FormInterface { $field = $this->createMock(FormInterface::class); $field->method('getData')->willReturn($selected); $form = $this->createMock(FormInterface::class); $form->method('handleRequest')->willReturn($form); $form->method('isSubmitted')->willReturn(true); $form->method('isValid')->willReturn(true); $form->method('get')->with('accommodation')->willReturn($field); return $form; } private function buildController( AccommodationBookingService $bookingService, FormInterface $form, ): TestableChangeAccommodationController { return new TestableChangeAccommodationController( $bookingService, $this->createMock(LoggerInterface::class), $form, ); } } final class TestableChangeAccommodationController extends ChangeAccommodationController { 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); } }