createMock(AccommodationBookingService::class); $bookingService->expects(self::never())->method('sendAccessLink'); $controller = new TestableSendAccessLinkController($bookingService, $this->createStub(LoggerInterface::class)); $response = $controller->index($this->openBooking(), Request::create('/admin/accommodation-booking/1/send-access-link')); self::assertSame(Response::HTTP_OK, $response->getStatusCode()); self::assertSame('admin/accommodation_booking/modal_send_access_link.html.twig', $controller->renderedView); } public function testPostSendsTheLinkAndRedirectsTheBrowser(): void { $booking = $this->openBooking(); $bookingService = $this->createMock(AccommodationBookingService::class); $bookingService->expects(self::once())->method('sendAccessLink')->with($booking); $controller = new TestableSendAccessLinkController($bookingService, $this->createStub(LoggerInterface::class)); $response = $controller->index($booking, Request::create('/admin/accommodation-booking/1/send-access-link', 'POST')); self::assertTrue($response->headers->has('HX-Redirect')); } public function testABookingWithoutALinkHasNothingToSend(): void { $booking = $this->openBooking(); $booking->setAccessLinkIssuedAt(null); $bookingService = $this->createMock(AccommodationBookingService::class); $bookingService->expects(self::never())->method('sendAccessLink'); $controller = new TestableSendAccessLinkController($bookingService, $this->createStub(LoggerInterface::class)); $response = $controller->index($booking, Request::create('/admin/accommodation-booking/1/send-access-link', 'POST')); self::assertSame(Response::HTTP_FOUND, $response->getStatusCode()); } /** * Drafts are held back even when a link is on the record — a booking pushed back into * Entwurf keeps its accessLinkIssuedAt, and it must not be handed out again. */ #[DataProvider('requestMethods')] public function testADraftLinkIsNeverSentEvenIfOneWasIssuedEarlier(string $method): void { $booking = $this->openBooking(); $booking->setStatus(AccommodationBookingStatus::Draft); $bookingService = $this->createMock(AccommodationBookingService::class); $bookingService->expects(self::never())->method('sendAccessLink'); $controller = new TestableSendAccessLinkController($bookingService, $this->createStub(LoggerInterface::class)); $response = $controller->index($booking, Request::create('/admin/accommodation-booking/1/send-access-link', $method)); self::assertSame(Response::HTTP_FOUND, $response->getStatusCode()); self::assertNull($controller->renderedView, 'not even the modal offering the action'); } /** * @return iterable */ public static function requestMethods(): iterable { yield 'GET' => [Request::METHOD_GET]; yield 'POST' => [Request::METHOD_POST]; } public function testPostWithAnInvalidTokenIsDenied(): void { $bookingService = $this->createMock(AccommodationBookingService::class); $bookingService->expects(self::never())->method('sendAccessLink'); $controller = new TestableSendAccessLinkController($bookingService, $this->createStub(LoggerInterface::class), tokenValid: false); $this->expectException(AccessDeniedException::class); $controller->index($this->openBooking(), Request::create('/admin/accommodation-booking/1/send-access-link', 'POST')); } public function testABookingWithoutAnEmailAddressGetsNoLink(): void { $booking = $this->openBooking(); $booking->setEmail(null); $bookingService = $this->createMock(AccommodationBookingService::class); $bookingService->expects(self::never())->method('sendAccessLink'); $controller = new TestableSendAccessLinkController($bookingService, $this->createStub(LoggerInterface::class)); $response = $controller->index($booking, Request::create('/admin/accommodation-booking/1/send-access-link', 'POST')); self::assertSame(['error'], array_column($controller->flashes, 'type')); self::assertStringContainsString('app_admin_accommodationbooking_edit', (string) $response->headers->get('HX-Redirect')); } private function openBooking(): AccommodationBooking { $booking = new AccommodationBooking(); $booking->setStatus(AccommodationBookingStatus::Open); $booking->setGroupName('Schulklasse 7b'); $booking->setEmail('customer@example.com'); $booking->setAccessLinkIssuedAt(new \DateTimeImmutable()); return $booking; } } final class TestableSendAccessLinkController extends SendAccessLinkController { public ?string $renderedView = null; /** @var list */ public array $flashes = []; public function __construct( AccommodationBookingService $bookingService, LoggerInterface $logger, private readonly bool $tokenValid = true, ) { parent::__construct($bookingService, $logger); } protected function isCsrfTokenValid(string $id, #[\SensitiveParameter] ?string $token): bool { return $this->tokenValid; } /** * @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); } }