openBooking(); $dto = new AccommodationBookingDto(); $ctx = new AccommodationBookingContext(accommodation: $booking->getAccommodation(), priceBreakdown: ['total' => 1000]); $bookingService = $this->createMock(AccommodationBookingService::class); $bookingService->method('contactDataFromBooking')->with($booking)->willReturn($dto); $bookingService->method('createOfferContext')->with($booking, $booking->getAccommodation())->willReturn($ctx); $bookingService->expects(self::never())->method('applyContactData'); $form = $this->createStub(FormInterface::class); $form->method('handleRequest')->willReturnSelf(); $form->method('isSubmitted')->willReturn(false); $controller = new TestableOfferContactController( $this->repositoryReturning($booking), $this->linkSigner(authorized: true), $bookingService, $form, ); $response = $controller->index($booking->getUuid(), Request::create('/groups/booking/offer/'.$booking->getUuid().'/contact')); self::assertSame(Response::HTTP_OK, $response->getStatusCode()); self::assertSame('groups/offer/contact.html.twig', $controller->renderedView); self::assertSame($form, $controller->renderedParameters['form']); self::assertSame($ctx, $controller->renderedParameters['ctx']); self::assertSame(['total' => 1000], $controller->renderedParameters['priceBreakdown']); self::assertSame($dto, $controller->formData); self::assertSame(['email_readonly' => true], $controller->formOptions); } public function testPostValidSavesAndReturnsToOffer(): void { $booking = $this->openBooking(); $dto = new AccommodationBookingDto(); $bookingService = $this->createMock(AccommodationBookingService::class); $bookingService->method('contactDataFromBooking')->willReturn($dto); $bookingService->expects(self::once())->method('applyContactData')->with($booking, $dto); $bookingService->expects(self::never())->method('acceptBooking'); $controller = new TestableOfferContactController( $this->repositoryReturning($booking), $this->linkSigner(authorized: true), $bookingService, $this->submittedForm(valid: true), ); $response = $controller->index($booking->getUuid(), Request::create('/groups/booking/offer/'.$booking->getUuid().'/contact', 'POST')); self::assertInstanceOf(RedirectResponse::class, $response); self::assertSame('/app_groups_offer_view?uuid='.$booking->getUuid(), $response->getTargetUrl()); self::assertSame([['type' => 'success', 'message' => 'Deine Kontaktdaten wurden gespeichert. Du kannst die Buchung jetzt abschließen.']], $controller->flashes); } public function testPostInvalidReRendersWithoutSaving(): void { $booking = $this->openBooking(); $bookingService = $this->createMock(AccommodationBookingService::class); $bookingService->method('contactDataFromBooking')->willReturn(new AccommodationBookingDto()); $bookingService->method('createOfferContext')->willReturn(new AccommodationBookingContext(accommodation: $booking->getAccommodation())); $bookingService->expects(self::never())->method('applyContactData'); $controller = new TestableOfferContactController( $this->repositoryReturning($booking), $this->linkSigner(authorized: true), $bookingService, $this->submittedForm(valid: false), ); $response = $controller->index($booking->getUuid(), Request::create('/groups/booking/offer/'.$booking->getUuid().'/contact', 'POST')); self::assertSame(Response::HTTP_OK, $response->getStatusCode()); self::assertSame('groups/offer/contact.html.twig', $controller->renderedView); } #[DataProvider('guards')] public function testRedirectsToOfferWhenNotEditable(AccommodationBookingStatus $status, bool $authorized): void { $booking = new AccommodationBooking(); $booking->setStatus($status); $bookingService = $this->createMock(AccommodationBookingService::class); $bookingService->expects(self::never())->method('applyContactData'); $controller = new TestableOfferContactController( $this->repositoryReturning($booking), $this->linkSigner($authorized), $bookingService, ); $response = $controller->index($booking->getUuid(), Request::create('/groups/booking/offer/'.$booking->getUuid().'/contact', 'POST')); self::assertInstanceOf(RedirectResponse::class, $response); self::assertSame('/app_groups_offer_view?uuid='.$booking->getUuid(), $response->getTargetUrl()); } /** * @return iterable */ public static function guards(): iterable { yield 'unauthorized session' => [AccommodationBookingStatus::Open, false]; yield 'already accepted' => [AccommodationBookingStatus::Received, true]; yield 'discarded' => [AccommodationBookingStatus::Discarded, true]; } private function openBooking(): AccommodationBooking { $booking = new AccommodationBooking(); $booking->setStatus(AccommodationBookingStatus::Open); $booking->setAccommodation(new Accommodation()); return $booking; } private function repositoryReturning(AccommodationBooking $booking): AccommodationBookingRepository { $bookingRepository = $this->createStub(AccommodationBookingRepository::class); $bookingRepository->method('findOneBy')->willReturn($booking); return $bookingRepository; } private function linkSigner(bool $authorized): AccommodationBookingLinkSigner { $linkSigner = $this->createStub(AccommodationBookingLinkSigner::class); $linkSigner->method('isSessionAuthorized')->willReturn($authorized); return $linkSigner; } 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 TestableOfferContactController extends ContactController { public ?string $renderedView = null; /** @var array */ public array $renderedParameters = []; public mixed $formData = null; /** @var array */ public array $formOptions = []; /** @var array */ public array $flashes = []; public function __construct( AccommodationBookingRepository $bookingRepository, AccommodationBookingLinkSigner $linkSigner, AccommodationBookingService $bookingService, private readonly ?FormInterface $form = null, ) { parent::__construct($bookingRepository, $linkSigner, $bookingService); } protected function createForm(string $type, mixed $data = null, array $options = []): FormInterface { $this->formData = $data; $this->formOptions = $options; 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(''); } }