|null */ private ?array $sentOptions = null; /** @var array|null */ private ?array $sentContext = null; private ?string $generatedRoute = null; private ?int $generatedReferenceType = null; public function testMailsEveryAdministrator(): void { $handler = $this->handler( $this->user(7, 'nominee@example.org'), [ $this->user(1, 'first@ep-reisen.de', [Role::ADMIN]), $this->user(2, 'second@ep-reisen.de', [Role::ADMIN]), ], ); $handler(new RoleNominationMessage(7, [Role::ADMIN])); self::assertSame(['first@ep-reisen.de', 'second@ep-reisen.de'], $this->sentOptions['to']); self::assertSame('email/role_nomination.html.twig', $this->sentOptions['template']); // Labelled for a human, not the raw role string. self::assertSame(['Administration'], $this->sentContext['roles']); // The filtered user list, not app_admin_user_permissions: that route renders a bare htmx // fragment, which a mail client following the link would show unstyled. self::assertSame('app_admin_user', $this->generatedRoute); // Absolute, because a worker has no request to borrow a host from. self::assertSame(UrlGeneratorInterface::ABSOLUTE_URL, $this->generatedReferenceType); self::assertSame('https://my.ep-reisen.de/generated', $this->sentContext['approvalUrl']); } public function testAMissingAccountIsANoOp(): void { $handler = $this->handler(null, [$this->user(1, 'admin@ep-reisen.de', [Role::ADMIN])]); $handler(new RoleNominationMessage(7, [Role::ADMIN])); self::assertNull($this->sentOptions); } public function testWithoutAnAdministratorNothingIsSent(): void { $handler = $this->handler($this->user(7, 'nominee@example.org'), []); $handler(new RoleNominationMessage(7, [Role::ADMIN])); self::assertNull($this->sentOptions); } /** * @param string[] $roles */ private function user(int $id, string $email, array $roles = []): User { $user = (new User($email))->setRoles($roles); // The id is generated by Doctrine and has no setter, but the notification links to it. $property = new \ReflectionProperty(User::class, 'id'); $property->setValue($user, $id); return $user; } /** * @param User[] $administrators */ private function handler(?User $nominee, array $administrators): RoleNominationHandler { $userRepository = $this->createStub(UserRepository::class); $userRepository->method('find')->willReturn($nominee); $userRepository->method('findAdministrators')->willReturn($administrators); $mailer = $this->createStub(Mailer::class); $mailer ->method('createAndSendEmail') ->willReturnCallback(function (array $context, array $options): void { $this->sentContext = $context; $this->sentOptions = $options; }) ; // Records what was asked of the router rather than imitating its output: what matters is // which route the mail points at and that it is absolute, not how a query string is escaped. $urlGenerator = $this->createStub(UrlGeneratorInterface::class); $urlGenerator ->method('generate') ->willReturnCallback(function (string $route, array $parameters, int $referenceType): string { $this->generatedRoute = $route; $this->generatedReferenceType = $referenceType; return 'https://my.ep-reisen.de/generated'; }) ; return new RoleNominationHandler( $userRepository, $mailer, new RoleApprovalUrlGenerator($urlGenerator), $this->createStub(LoggerInterface::class), ); } }