setIsInquiry(true); $bookingRepository = $this->createMock(AccommodationBookingRepository::class); $bookingRepository->method('findOneBy')->with(['uuid' => $booking->getUuid()])->willReturn($booking); $linkSigner = $this->createMock(AccommodationBookingLinkSigner::class); $linkSigner->method('isValidLinkRequest')->willReturn(true); $breakdownCalculator = $this->createMock(AccommodationBookingBreakdownCalculator::class); $breakdownCalculator->method('compute')->with($booking)->willReturn(['total' => 1000]); $bookingService = $this->createMock(AccommodationBookingService::class); $bookingService->expects(self::never())->method('acceptBooking'); $controller = new TestableOfferController($bookingRepository, $linkSigner, $breakdownCalculator, $bookingService); $response = $controller->index($booking->getUuid(), Request::create('/groups/booking/offer/'.$booking->getUuid())); self::assertSame(Response::HTTP_OK, $response->getStatusCode()); self::assertSame('offer', $response->getContent()); self::assertSame('groups/booking/offer.html.twig', $controller->renderedView); self::assertSame(['booking' => $booking, 'priceBreakdown' => ['total' => 1000]], $controller->renderedParameters); } public function testPostAcceptsInquiryAndRedirectsToSameUrl(): void { $booking = new AccommodationBooking(); $booking->setIsInquiry(true); $bookingRepository = $this->createMock(AccommodationBookingRepository::class); $bookingRepository->method('findOneBy')->willReturn($booking); $linkSigner = $this->createMock(AccommodationBookingLinkSigner::class); $linkSigner->method('isValidLinkRequest')->willReturn(true); $bookingService = $this->createMock(AccommodationBookingService::class); $bookingService->expects(self::once())->method('acceptBooking')->with($booking); $controller = new TestableOfferController( $bookingRepository, $linkSigner, $this->createMock(AccommodationBookingBreakdownCalculator::class), $bookingService, ); $request = Request::create( '/groups/booking/offer/'.$booking->getUuid().'?t=123&_hash=abc', 'POST', ['_token' => 'valid-token'], ); $response = $controller->index($booking->getUuid(), $request); self::assertSame(Response::HTTP_FOUND, $response->getStatusCode()); self::assertSame($request->getUri(), $response->headers->get('Location')); self::assertSame('accept_accommodation_offer_'.$booking->getUuid(), $controller->csrfTokenId); self::assertSame('valid-token', $controller->csrfToken); } public function testPostRejectsInvalidCsrfToken(): void { $booking = new AccommodationBooking(); $booking->setIsInquiry(true); $bookingRepository = $this->createMock(AccommodationBookingRepository::class); $bookingRepository->method('findOneBy')->willReturn($booking); $linkSigner = $this->createMock(AccommodationBookingLinkSigner::class); $linkSigner->method('isValidLinkRequest')->willReturn(true); $bookingService = $this->createMock(AccommodationBookingService::class); $bookingService->expects(self::never())->method('acceptBooking'); $controller = new TestableOfferController( $bookingRepository, $linkSigner, $this->createMock(AccommodationBookingBreakdownCalculator::class), $bookingService, ); $controller->csrfValid = false; $this->expectException(\Symfony\Component\Security\Core\Exception\AccessDeniedException::class); $controller->index( $booking->getUuid(), Request::create('/groups/booking/offer/'.$booking->getUuid(), 'POST', ['_token' => 'invalid']), ); } public function testGetRendersConfirmationWithoutAcceptFormForBooking(): void { $booking = new AccommodationBooking(); $booking->setIsInquiry(false); $bookingRepository = $this->createMock(AccommodationBookingRepository::class); $bookingRepository->method('findOneBy')->willReturn($booking); $linkSigner = $this->createMock(AccommodationBookingLinkSigner::class); $linkSigner->method('isValidLinkRequest')->willReturn(true); $bookingService = $this->createMock(AccommodationBookingService::class); $bookingService->expects(self::never())->method('acceptBooking'); $controller = new TestableOfferController( $bookingRepository, $linkSigner, $this->createMock(AccommodationBookingBreakdownCalculator::class), $bookingService, ); // A POST on an already-accepted booking should not trigger acceptBooking again — falls through to render. $response = $controller->index($booking->getUuid(), Request::create('/groups/booking/offer/'.$booking->getUuid(), 'POST')); self::assertSame(Response::HTTP_OK, $response->getStatusCode()); self::assertSame('groups/booking/offer.html.twig', $controller->renderedView); } public function testInvalidLinkReturnsUnavailableResponse(): void { $booking = new AccommodationBooking(); $bookingRepository = $this->createMock(AccommodationBookingRepository::class); $bookingRepository->method('findOneBy')->willReturn($booking); $linkSigner = $this->createMock(AccommodationBookingLinkSigner::class); $linkSigner->method('isValidLinkRequest')->willReturn(false); $controller = new TestableOfferController( $bookingRepository, $linkSigner, $this->createMock(AccommodationBookingBreakdownCalculator::class), $this->createMock(AccommodationBookingService::class), ); $response = $controller->index($booking->getUuid(), Request::create('/groups/booking/offer/'.$booking->getUuid())); self::assertSame(Response::HTTP_OK, $response->getStatusCode()); self::assertSame('groups/booking/offer_unavailable.html.twig', $controller->renderedView); } public function testUnknownUuidReturnsUnavailableResponse(): void { $bookingRepository = $this->createMock(AccommodationBookingRepository::class); $bookingRepository->method('findOneBy')->willReturn(null); $linkSigner = $this->createMock(AccommodationBookingLinkSigner::class); $linkSigner->expects(self::never())->method('isValidLinkRequest'); $controller = new TestableOfferController( $bookingRepository, $linkSigner, $this->createMock(AccommodationBookingBreakdownCalculator::class), $this->createMock(AccommodationBookingService::class), ); $response = $controller->index('unknown-uuid', Request::create('/groups/booking/offer/unknown-uuid')); self::assertSame(Response::HTTP_OK, $response->getStatusCode()); self::assertSame('groups/booking/offer_unavailable.html.twig', $controller->renderedView); } } final class TestableOfferController extends OfferController { public bool $csrfValid = true; public ?string $csrfTokenId = null; public ?string $csrfToken = null; public ?string $renderedView = null; /** @var array */ public array $renderedParameters = []; protected function isCsrfTokenValid(string $id, ?string $token): bool { $this->csrfTokenId = $id; $this->csrfToken = $token; return $this->csrfValid; } protected function render(string $view, array $parameters = [], ?Response $response = null): Response { $this->renderedView = $view; $this->renderedParameters = $parameters; if ('groups/booking/offer.html.twig' === $view) { $content = $parameters['booking']->isInquiry() ? 'offer' : 'confirmed'; } else { $content = 'unavailable'; } $response ??= new Response(); $response->setContent($content); return $response; } }