assertNotified(AccommodationBookingStatus::Open); $this->submitStatusChange(AccommodationBookingStatus::Draft, AccommodationBookingStatus::Open, $bookingService); } public function testTransitionToAcceptedIssuesAccessLinkAndSendsCustomerEmail(): void { $bookingService = $this->assertNotified(AccommodationBookingStatus::Accepted); $this->submitStatusChange(AccommodationBookingStatus::Draft, AccommodationBookingStatus::Accepted, $bookingService); } public function testTransitionToDiscardedSendsNothing(): void { $bookingService = $this->assertNotNotified(); $this->submitStatusChange(AccommodationBookingStatus::Open, AccommodationBookingStatus::Discarded, $bookingService); } public function testSavingWithoutStatusChangeSendsNothing(): void { $bookingService = $this->assertNotNotified(); $this->submitStatusChange(AccommodationBookingStatus::Accepted, AccommodationBookingStatus::Accepted, $bookingService); } private function assertNotified(AccommodationBookingStatus $expected): AccommodationBookingService { $bookingService = $this->createMock(AccommodationBookingService::class); $bookingService->expects(self::once())->method('refreshPriceSnapshot'); $bookingService ->expects(self::once()) ->method('issueAccessLink') ->with(self::callback(static fn (AccommodationBooking $booking) => $expected === $booking->getStatus())); $bookingService->expects(self::once())->method('sendCustomerConfirmationEmail'); return $bookingService; } private function assertNotNotified(): AccommodationBookingService { $bookingService = $this->createMock(AccommodationBookingService::class); $bookingService->expects(self::once())->method('refreshPriceSnapshot'); $bookingService->expects(self::never())->method('issueAccessLink'); $bookingService->expects(self::never())->method('sendCustomerConfirmationEmail'); return $bookingService; } private function submitStatusChange( AccommodationBookingStatus $from, AccommodationBookingStatus $to, AccommodationBookingService $bookingService, ): void { $booking = new AccommodationBooking(); $booking->setStatus($from); // The form is what moves the entity to its new status during handleRequest(). $form = $this->createMock(FormInterface::class); $form->method('handleRequest')->willReturnCallback(static function () use ($booking, $to, $form) { $booking->setStatus($to); return $form; }); $form->method('isSubmitted')->willReturn(true); $form->method('isValid')->willReturn(true); $form->method('has')->willReturn(false); $entityManager = $this->createMock(EntityManagerInterface::class); $entityManager->expects(self::once())->method('flush'); $controller = new TestableEditController( $entityManager, $this->createMock(LoggerInterface::class), $this->createMock(BoardServiceRepository::class), $this->createMock(AdditionalServiceRepository::class), $this->createMock(UserRepository::class), $bookingService, $form, ); $response = $controller->index($booking, Request::create('/admin/accommodation-booking/1/edit', 'POST')); self::assertSame(Response::HTTP_FOUND, $response->getStatusCode()); } public function testManagerWithoutAdminRoleGetsNoBetreuerChoices(): void { $userRepo = $this->createMock(UserRepository::class); $userRepo->expects(self::never())->method('findGroupsStaff'); $controller = $this->buildController($userRepo, granted: false); $controller->index(new AccommodationBooking(), Request::create('/admin/accommodation-booking/1/edit')); self::assertSame([], $controller->capturedOptions['assignable_managers']); } public function testAdminGetsTheGroupsStaffAsBetreuerChoices(): void { $staff = [new User('manager@example.test')]; $userRepo = $this->createMock(UserRepository::class); $userRepo->method('findGroupsStaff')->willReturn($staff); $controller = $this->buildController($userRepo, granted: true); $controller->index(new AccommodationBooking(), Request::create('/admin/accommodation-booking/1/edit')); self::assertSame($staff, $controller->capturedOptions['assignable_managers']); } public function testAnAssignedUserWhoLostTheRoleStaysSelectable(): void { $formerManager = new User('former@example.test'); $staff = [new User('manager@example.test')]; $userRepo = $this->createMock(UserRepository::class); $userRepo->method('findGroupsStaff')->willReturn($staff); $booking = new AccommodationBooking(); $booking->setManagedBy($formerManager); $controller = $this->buildController($userRepo, granted: true); $controller->index($booking, Request::create('/admin/accommodation-booking/1/edit')); self::assertSame([$staff[0], $formerManager], $controller->capturedOptions['assignable_managers']); } private function buildController(UserRepository $userRepo, bool $granted): TestableEditController { // An unsubmitted form: index() falls through to render() and only the options // handed to createForm() are of interest here. $form = $this->createMock(FormInterface::class); $form->method('handleRequest')->willReturn($form); $form->method('isSubmitted')->willReturn(false); return new TestableEditController( $this->createMock(EntityManagerInterface::class), $this->createMock(LoggerInterface::class), $this->createMock(BoardServiceRepository::class), $this->createMock(AdditionalServiceRepository::class), $userRepo, $this->createMock(AccommodationBookingService::class), $form, $granted, ); } } final class TestableEditController extends EditController { /** @var array */ public array $capturedOptions = []; public function __construct( EntityManagerInterface $entityManager, LoggerInterface $logger, BoardServiceRepository $boardServiceRepo, AdditionalServiceRepository $additionalServiceRepo, UserRepository $userRepo, AccommodationBookingService $bookingService, private readonly FormInterface $form, private readonly bool $granted = false, ) { parent::__construct($entityManager, $logger, $boardServiceRepo, $additionalServiceRepo, $userRepo, $bookingService); } protected function createForm(string $type, mixed $data = null, array $options = []): FormInterface { $this->capturedOptions = $options; return $this->form; } protected function isGranted(mixed $attribute, mixed $subject = null): bool { return $this->granted; } /** * @param array $parameters */ protected function render(string $view, array $parameters = [], ?Response $response = null): Response { return new Response(); } protected function addFlash(string $type, mixed $message): void { } /** * @param array $parameters */ protected function generateUrl(string $route, array $parameters = [], int $referenceType = 1): string { return '/'.$route.'?'.http_build_query($parameters); } }