diff --git a/docs/installation/commands.md b/docs/installation/commands.md index c729975b..da13357f 100644 --- a/docs/installation/commands.md +++ b/docs/installation/commands.md @@ -5,23 +5,19 @@ This app brings custom commands: ```text app:admin:password Set password of admin user app:alias:delete Delete an alias +app:api-token:create Create a new API token with specified name and scopes +app:api-token:delete Delete an API token by its plain token app:domain:delete Delete a domain and all associated data (users, aliases, vouchers) app:metrics Global Metrics for Userli -app:openpgp:delete-key Delete OpenPGP key for email -app:openpgp:import-key Import OpenPGP key for email app:openpgp:show-key Show OpenPGP key of email -app:report:weekly Send weekly report to all admins app:users:delete Delete a user -app:users:list List users app:users:mailcrypt Get MailCrypt values for user app:users:quota Get quota of user if set app:users:registration:mail Send a registration mail to a user -app:users:remove Removes all mailboxes from deleted users app:users:reset Reset a user -app:users:restore Reset a user +app:users:restore Restore a user app:voucher:count Get count of vouchers for a specific user app:voucher:create Create voucher for a specific user -app:voucher:unlink Remove connection between vouchers and accounts after 3 months ``` Get more information about each command by running: diff --git a/src/Command/AbstractUsersCommand.php b/src/Command/AbstractUsersCommand.php deleted file mode 100644 index 7bf717c6..00000000 --- a/src/Command/AbstractUsersCommand.php +++ /dev/null @@ -1,84 +0,0 @@ -addOption('user', 'u', InputOption::VALUE_REQUIRED, 'User to act upon') - ->addOption('dry-run', null, InputOption::VALUE_NONE); - } - - protected function getUser(InputInterface $input, OutputInterface $output): ?User - { - $email = $input->getOption('user'); - if (empty($email) || null === $user = $this->manager->getRepository(User::class)->findByEmail($email)) { - $output->writeln(sprintf('User with email %s not found!', $email)); - - return null; - } - - return $user; - } - - /** - * @throws PasswordPolicyException - * @throws PasswordMismatchException - */ - protected function askForPassword(InputInterface $input, OutputInterface $output): string - { - $questionHelper = $this->getHelper('question'); - assert($questionHelper instanceof QuestionHelper); - - $passwordQuest = new Question('New password: '); - $passwordQuest->setValidator(function ($value) { - if ($this->passwordStrengthHandler->validate($value)) { - throw new PasswordPolicyException(); - } - - return $value; - }); - $passwordQuest->setHidden(true); - $passwordQuest->setHiddenFallback(false); - $passwordQuest->setMaxAttempts(5); - - $password = $questionHelper->ask($input, $output, $passwordQuest); - - $passwordConfirmQuest = new Question('Repeat password: '); - $passwordConfirmQuest->setHidden(true); - $passwordConfirmQuest->setHiddenFallback(false); - - $passwordConfirm = $questionHelper->ask($input, $output, $passwordConfirmQuest); - - if ($password !== $passwordConfirm) { - throw new PasswordMismatchException(); - } - - return $password; - } -} diff --git a/src/Command/AdminPasswordCommand.php b/src/Command/AdminPasswordCommand.php index 593aee0b..74a2cf17 100644 --- a/src/Command/AdminPasswordCommand.php +++ b/src/Command/AdminPasswordCommand.php @@ -4,49 +4,49 @@ namespace App\Command; +use App\Exception\PasswordMismatchException; +use App\Exception\PasswordPolicyException; use App\Helper\AdminPasswordUpdater; -use Override; +use App\Service\ConsolePasswordHelper; +use Symfony\Component\Console\Attribute\Argument; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Helper\QuestionHelper; -use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Question\Question; -#[AsCommand(name: 'app:admin:password', description: 'Set password of admin user', help: <<<'TXT' -Set password of admin user. Create primary user and domain if not created before. -TXT)] -final class AdminPasswordCommand extends Command +#[AsCommand( + name: 'app:admin:password', + description: 'Set password of admin user', + help: 'Set password of admin user. Create primary user and domain if do not exist.' +)] +final readonly class AdminPasswordCommand { - /** - * AdminPasswordCommand constructor. - */ - public function __construct(private readonly AdminPasswordUpdater $updater) - { - parent::__construct(); + public function __construct( + private AdminPasswordUpdater $updater, + private ConsolePasswordHelper $consolePasswordHelper, + ) { } - #[Override] - protected function configure(): void - { - $this - ->addArgument('password', InputArgument::OPTIONAL, 'Admin password'); - } - - #[Override] - protected function execute(InputInterface $input, OutputInterface $output): int - { - $password = $input->getArgument('password'); - if (null === $password) { - $helper = $this->getHelper('question'); - assert($helper instanceof QuestionHelper); - $question = new Question('Please enter new admin password:'); - $password = $helper->ask($input, $output, $question); + public function __invoke( + #[Argument(description: 'Admin password (omit for interactive prompt)')] + ?string $password = null, + ?InputInterface $input = null, + ?OutputInterface $output = null, + ): int { + try { + if (null !== $password) { + $this->consolePasswordHelper->validatePassword($password); + } else { + $password = $this->consolePasswordHelper->askForPassword($input, $output); + } + } catch (PasswordPolicyException|PasswordMismatchException $e) { + $output->writeln(sprintf('%s', $e->getMessage())); + + return Command::FAILURE; } $this->updater->updateAdminPassword($password); - return 0; + return Command::SUCCESS; } } diff --git a/src/Command/AliasDeleteCommand.php b/src/Command/AliasDeleteCommand.php index 9c9e9a69..1fa4238a 100644 --- a/src/Command/AliasDeleteCommand.php +++ b/src/Command/AliasDeleteCommand.php @@ -4,61 +4,54 @@ namespace App\Command; -use App\Entity\Alias; -use App\Entity\User; use App\Handler\DeleteHandler; -use Doctrine\ORM\EntityManagerInterface; -use Override; +use App\Repository\AliasRepository; +use App\Repository\UserRepository; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Attribute\Option; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; #[AsCommand(name: 'app:alias:delete', description: 'Delete an alias')] -final class AliasDeleteCommand extends Command +final readonly class AliasDeleteCommand { - public function __construct(private readonly EntityManagerInterface $manager, private readonly DeleteHandler $deleteHandler) - { - parent::__construct(); + public function __construct( + private UserRepository $userRepository, + private AliasRepository $aliasRepository, + private DeleteHandler $deleteHandler, + ) { } - #[Override] - protected function configure(): void - { - $this - ->addOption('user', 'u', InputOption::VALUE_REQUIRED, 'User who owns the alias (optional)') - ->addOption('alias', 'a', InputOption::VALUE_REQUIRED, 'Alias address to delete') - ->addOption('dry-run', null, InputOption::VALUE_NONE); - } - - #[Override] - protected function execute(InputInterface $input, OutputInterface $output): int - { - $email = $input->getOption('user'); - $source = $input->getOption('alias'); - + public function __invoke( + #[Option(name: 'alias', description: 'Alias address to delete', shortcut: 'a')] + ?string $source = null, + #[Option(name: 'user', description: 'User who owns the alias (optional)', shortcut: 'u')] + ?string $email = null, + #[Option(name: 'dry-run', description: 'Show what would be deleted without actually deleting')] + bool $dryRun = false, + ?OutputInterface $output = null, + ): int { $user = null; - if ($email && null === $user = $this->manager->getRepository(User::class)->findByEmail($email)) { + if ($email && null === $user = $this->userRepository->findByEmail($email)) { $output->writeln(sprintf("User with email '%s' not found!", $email)); return Command::FAILURE; } - if (empty($source) || null === $alias = $this->manager->getRepository(Alias::class)->findOneBySource($source)) { + if (empty($source) || null === $alias = $this->aliasRepository->findOneBySource($source)) { $output->writeln(sprintf("Alias with address '%s' not found!", $source)); return Command::FAILURE; } - if ($input->getOption('dry-run')) { - if ($user) { + if ($dryRun) { + if ($user !== null) { $output->write(sprintf("Would delete alias %s of user %s\n", $source, $email)); } else { $output->write(sprintf("Would delete alias %s\n", $source)); } } else { - if ($user) { + if ($user !== null) { $output->write(sprintf("Deleting alias %s of user %s\n", $source, $email)); $this->deleteHandler->deleteAlias($alias, $user); } else { diff --git a/src/Command/ApiTokenCreateCommand.php b/src/Command/ApiTokenCreateCommand.php index 6821d063..8019e9cf 100644 --- a/src/Command/ApiTokenCreateCommand.php +++ b/src/Command/ApiTokenCreateCommand.php @@ -8,12 +8,9 @@ use App\Form\Model\ApiToken as ApiTokenModel; use App\Service\ApiTokenManager; use Exception; -use Override; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Attribute\Option; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Validator\Validator\ValidatorInterface; @@ -21,37 +18,21 @@ name: 'app:api-token:create', description: 'Create a new API token with specified name and scopes' )] -final class ApiTokenCreateCommand extends Command +final readonly class ApiTokenCreateCommand { public function __construct( - private readonly ApiTokenManager $apiTokenManager, - private readonly ValidatorInterface $validator, + private ApiTokenManager $apiTokenManager, + private ValidatorInterface $validator, ) { - parent::__construct(); } - #[Override] - protected function configure(): void - { - $this - ->addOption('name', 't', InputOption::VALUE_REQUIRED, 'Name for the API token') - ->addOption( - 'scopes', - 's', - InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, - 'Scopes for the API token (available: '.implode(', ', ApiScope::all()).')', - [] - ); - } - - #[Override] - protected function execute(InputInterface $input, OutputInterface $output): int - { - $io = new SymfonyStyle($input, $output); - - $name = (string) $input->getOption('name'); - $scopes = (array) $input->getOption('scopes'); - + public function __invoke( + #[Option(description: 'Name for the API token', shortcut: 't')] + string $name = '', + #[Option(description: 'Scopes for the API token (available: '.ApiScope::ALL_SCOPES_DESCRIPTION.')', shortcut: 's')] + array $scopes = [], + ?SymfonyStyle $io = null, + ): int { $model = new ApiTokenModel(); $model->setName($name); $model->setScopes($scopes); diff --git a/src/Command/ApiTokenDeleteCommand.php b/src/Command/ApiTokenDeleteCommand.php index 67d8763a..3b134315 100644 --- a/src/Command/ApiTokenDeleteCommand.php +++ b/src/Command/ApiTokenDeleteCommand.php @@ -6,40 +6,29 @@ use App\Service\ApiTokenManager; use Exception; -use Override; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Attribute\Option; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; #[AsCommand( name: 'app:api-token:delete', description: 'Delete an API token by its plain token' )] -final class ApiTokenDeleteCommand extends Command +final readonly class ApiTokenDeleteCommand { public function __construct( - private readonly ApiTokenManager $apiTokenManager, + private ApiTokenManager $apiTokenManager, ) { - parent::__construct(); } - #[Override] - protected function configure(): void - { - $this - ->addOption('token', 't', InputOption::VALUE_REQUIRED, 'The plain API token to delete'); - } - - #[Override] - protected function execute(InputInterface $input, OutputInterface $output): int - { - $io = new SymfonyStyle($input, $output); - + public function __invoke( + #[Option(description: 'The plain API token to delete', shortcut: 't')] + ?string $token = null, + ?SymfonyStyle $io = null, + ): int { try { - $plainToken = (string) $input->getOption('token'); + $plainToken = (string) $token; $apiToken = $this->apiTokenManager->findOne($plainToken); if ($apiToken === null) { diff --git a/src/Command/DomainDeleteCommand.php b/src/Command/DomainDeleteCommand.php index aad1b790..1e9c70f2 100644 --- a/src/Command/DomainDeleteCommand.php +++ b/src/Command/DomainDeleteCommand.php @@ -6,41 +6,30 @@ use App\Repository\DomainRepository; use App\Service\DomainManager; -use Override; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Attribute\Option; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; #[AsCommand( name: 'app:domain:delete', description: 'Delete a domain and all associated data (users, aliases, vouchers)' )] -final class DomainDeleteCommand extends Command +final readonly class DomainDeleteCommand { public function __construct( - private readonly DomainRepository $repository, - private readonly DomainManager $manager, + private DomainRepository $repository, + private DomainManager $manager, ) { - parent::__construct(); } - #[Override] - protected function configure(): void - { - $this - ->addOption('domain', 'd', InputOption::VALUE_REQUIRED, 'The domain name to delete') - ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Show what would be deleted without actually deleting'); - } - - #[Override] - protected function execute(InputInterface $input, OutputInterface $output): int - { - $io = new SymfonyStyle($input, $output); - - $domainName = $input->getOption('domain'); + public function __invoke( + #[Option(name: 'domain', description: 'The domain name to delete', shortcut: 'd')] + ?string $domainName = null, + #[Option(name: 'dry-run', description: 'Show what would be deleted without actually deleting')] + bool $dryRun = false, + ?SymfonyStyle $io = null, + ): int { if (empty($domainName)) { $io->error('Please provide a domain name with --domain.'); @@ -56,7 +45,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $stats = $this->manager->getDomainStats($domain); - if ($input->getOption('dry-run')) { + if ($dryRun) { $io->note(sprintf("Would delete domain '%s' with:", $domainName)); $io->listing([ sprintf('%d users', $stats['users']), diff --git a/src/Command/MetricsCommand.php b/src/Command/MetricsCommand.php index b7b5d019..b8019186 100644 --- a/src/Command/MetricsCommand.php +++ b/src/Command/MetricsCommand.php @@ -9,10 +9,8 @@ use App\Repository\OpenPgpKeyRepository; use App\Repository\UserRepository; use App\Repository\VoucherRepository; -use Override; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** @@ -26,20 +24,18 @@ * * * * * * php /path/to/bin/console app:metrics | sponge /path/to/metrics/userli.prom */ #[AsCommand(name: 'app:metrics', description: 'Global Metrics for Userli')] -final class MetricsCommand extends Command +final readonly class MetricsCommand { public function __construct( - private readonly UserRepository $userRepository, - private readonly VoucherRepository $voucherRepository, - private readonly DomainRepository $domainRepository, - private readonly AliasRepository $aliasRepository, - private readonly OpenPgpKeyRepository $openPgpKeyRepository, + private UserRepository $userRepository, + private VoucherRepository $voucherRepository, + private DomainRepository $domainRepository, + private AliasRepository $aliasRepository, + private OpenPgpKeyRepository $openPgpKeyRepository, ) { - parent::__construct(); } - #[Override] - protected function execute(InputInterface $input, OutputInterface $output): int + public function __invoke(OutputInterface $output): int { $activeUsersTotal = $this->userRepository->countUsers(); $deletedUsersTotal = $this->userRepository->countDeletedUsers(); @@ -94,6 +90,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $output->writeln('# TYPE userli_openpgpkeys_total gauge'); $output->writeln('userli_openpgpkeys_total '.$openPgpKeysTotal); - return 0; + return Command::SUCCESS; } } diff --git a/src/Command/OpenPgpDeleteKeyCommand.php b/src/Command/OpenPgpDeleteKeyCommand.php deleted file mode 100644 index fcb23b1a..00000000 --- a/src/Command/OpenPgpDeleteKeyCommand.php +++ /dev/null @@ -1,51 +0,0 @@ -addArgument( - 'email', - InputOption::VALUE_REQUIRED, - 'email address of the OpenPGP key'); - } - - #[Override] - protected function execute(InputInterface $input, OutputInterface $output): int - { - // parse arguments - $email = $input->getArgument('email'); - - // Check if OpenPGP key exists - $openPgpKey = $this->manager->getKey($email); - if (null === $openPgpKey) { - $output->writeln(sprintf('No OpenPGP key found for email %s', $email)); - } else { - // Delete the key - $this->manager->deleteKey($openPgpKey->getEmail()); - $output->writeln(sprintf('Deleted OpenPGP key for email %s: %s', $openPgpKey->getEmail(), $openPgpKey->getKeyFingerprint())); - } - - return Command::SUCCESS; - } -} diff --git a/src/Command/OpenPgpImportKeyCommand.php b/src/Command/OpenPgpImportKeyCommand.php deleted file mode 100644 index 327e6733..00000000 --- a/src/Command/OpenPgpImportKeyCommand.php +++ /dev/null @@ -1,67 +0,0 @@ -addArgument( - 'email', - InputOption::VALUE_REQUIRED, - 'email address of the OpenPGP key') - ->addArgument( - 'file', - InputOption::VALUE_REQUIRED, - 'file to read the key from'); - } - - #[Override] - protected function execute(InputInterface $input, OutputInterface $output): int - { - // parse arguments - $email = $input->getArgument('email'); - $file = $input->getArgument('file'); - - // Read contents from file - if (!is_file($file)) { - throw new RuntimeException('File not found: '.$file); - } - - $content = file_get_contents($file); - - // Import the key - try { - $openPgpKey = $this->manager->importKey($content, $email); - } catch (NoGpgKeyForUserException|MultipleGpgKeysForUserException $e) { - $output->writeln(sprintf('Error: %s in %s', $e->getMessage(), $file)); - - return Command::FAILURE; - } - - $output->writeln(sprintf('Imported OpenPGP key for email %s: %s', $email, $openPgpKey->getKeyFingerprint())); - - return Command::SUCCESS; - } -} diff --git a/src/Command/OpenPgpShowKeyCommand.php b/src/Command/OpenPgpShowKeyCommand.php index 1409be9e..2f088c12 100644 --- a/src/Command/OpenPgpShowKeyCommand.php +++ b/src/Command/OpenPgpShowKeyCommand.php @@ -5,38 +5,23 @@ namespace App\Command; use App\Service\OpenPgpKeyManager; -use Override; +use Symfony\Component\Console\Attribute\Argument; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; #[AsCommand(name: 'app:openpgp:show-key', description: 'Show OpenPGP key of email')] -final class OpenPgpShowKeyCommand extends Command +final readonly class OpenPgpShowKeyCommand { - public function __construct(private readonly OpenPgpKeyManager $manager) + public function __construct(private OpenPgpKeyManager $manager) { - parent::__construct(); } - #[Override] - protected function configure(): void - { - $this - ->addArgument( - 'email', - InputOption::VALUE_REQUIRED, - 'email address of the OpenPGP key'); - } - - #[Override] - protected function execute(InputInterface $input, OutputInterface $output): int - { - // parse arguments - $email = $input->getArgument('email'); - - // Check if OpenPGP key exists + public function __invoke( + #[Argument(description: 'email address of the OpenPGP key')] + string $email, + OutputInterface $output, + ): int { $openPgpKey = $this->manager->getKey($email); if (null === $openPgpKey) { $output->writeln(sprintf('No OpenPGP key found for email %s', $email)); diff --git a/src/Command/UsersDeleteCommand.php b/src/Command/UsersDeleteCommand.php index a9fc5bd8..d708c052 100644 --- a/src/Command/UsersDeleteCommand.php +++ b/src/Command/UsersDeleteCommand.php @@ -5,34 +5,35 @@ namespace App\Command; use App\Handler\DeleteHandler; -use App\Handler\PasswordStrengthHandler; -use Doctrine\ORM\EntityManagerInterface; -use Override; +use App\Repository\UserRepository; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Attribute\Option; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; #[AsCommand(name: 'app:users:delete', description: 'Delete a user')] -final class UsersDeleteCommand extends AbstractUsersCommand +final readonly class UsersDeleteCommand { public function __construct( - EntityManagerInterface $manager, - PasswordStrengthHandler $passwordStrengthHandler, - private readonly DeleteHandler $deleteHandler, + private UserRepository $userRepository, + private DeleteHandler $deleteHandler, ) { - parent::__construct($manager, $passwordStrengthHandler); } - #[Override] - protected function execute(InputInterface $input, OutputInterface $output): int - { - $user = $this->getUser($input, $output); - if (null === $user) { + public function __invoke( + #[Option(name: 'user', description: 'User to act upon', shortcut: 'u')] + ?string $email = null, + #[Option(name: 'dry-run', description: 'Simulate without making changes')] + bool $dryRun = false, + ?OutputInterface $output = null, + ): int { + if (empty($email) || null === $user = $this->userRepository->findByEmail($email)) { + $output->writeln(sprintf('User with email %s not found!', $email)); + return Command::FAILURE; } - if ($input->getOption('dry-run')) { + if ($dryRun) { $output->write(sprintf("Would delete user %s\n", $user->getEmail())); } else { $output->write(sprintf("Deleting user %s\n", $user->getEmail())); diff --git a/src/Command/UsersMailCryptCommand.php b/src/Command/UsersMailCryptCommand.php index 7f7e6aed..23154c70 100644 --- a/src/Command/UsersMailCryptCommand.php +++ b/src/Command/UsersMailCryptCommand.php @@ -6,60 +6,45 @@ use App\Enum\MailCrypt; use App\Handler\MailCryptKeyHandler; -use App\Handler\PasswordStrengthHandler; use App\Handler\UserAuthenticationHandler; +use App\Repository\UserRepository; use App\Service\SettingsService; -use Doctrine\ORM\EntityManagerInterface; use Exception; -use Override; +use Symfony\Component\Console\Attribute\Argument; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Attribute\Option; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; #[AsCommand(name: 'app:users:mailcrypt', description: 'Get MailCrypt values for user')] -final class UsersMailCryptCommand extends AbstractUsersCommand +final readonly class UsersMailCryptCommand { public function __construct( - EntityManagerInterface $manager, - PasswordStrengthHandler $passwordStrengthHandler, - private readonly UserAuthenticationHandler $handler, - private readonly MailCryptKeyHandler $mailCryptKeyHandler, - private readonly SettingsService $settingsService, + private UserRepository $userRepository, + private UserAuthenticationHandler $handler, + private MailCryptKeyHandler $mailCryptKeyHandler, + private SettingsService $settingsService, ) { - parent::__construct($manager, $passwordStrengthHandler); - } - - #[Override] - protected function configure(): void - { - parent::configure(); - $this - ->addArgument( - 'password', - InputOption::VALUE_OPTIONAL, - 'password of supplied email address' - ); } /** * @throws Exception */ - #[Override] - protected function execute(InputInterface $input, OutputInterface $output): int - { + public function __invoke( + #[Option(name: 'user', description: 'User to act upon', shortcut: 'u')] + ?string $email = null, + #[Argument(description: 'password of supplied email address')] + ?string $password = null, + ?OutputInterface $output = null, + ): int { $mailCrypt = MailCrypt::from($this->settingsService->get('mail_crypt')); if ($mailCrypt === MailCrypt::DISABLED) { return Command::FAILURE; } - // parse arguments - $password = $input->getArgument('password'); + if (empty($email) || null === $user = $this->userRepository->findByEmail($email)) { + $output->writeln(sprintf('User with email %s not found!', $email)); - // Check if user exists - $user = $this->getUser($input, $output); - if (null === $user) { return Command::FAILURE; } @@ -68,7 +53,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int } if ($password) { - $password = $password[0]; // verify user credentials if (null === $user = $this->handler->authenticate($user, $password)) { return Command::FAILURE; diff --git a/src/Command/UsersQuotaCommand.php b/src/Command/UsersQuotaCommand.php index ceca4333..06c2c615 100644 --- a/src/Command/UsersQuotaCommand.php +++ b/src/Command/UsersQuotaCommand.php @@ -4,24 +4,31 @@ namespace App\Command; -use Override; +use App\Repository\UserRepository; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Attribute\Option; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; #[AsCommand(name: 'app:users:quota', description: 'Get quota of user if set')] -final class UsersQuotaCommand extends AbstractUsersCommand +final readonly class UsersQuotaCommand { - #[Override] - protected function execute(InputInterface $input, OutputInterface $output): int - { - $user = $this->getUser($input, $output); - if (null === $user) { + public function __construct( + private UserRepository $userRepository, + ) { + } + + public function __invoke( + #[Option(name: 'user', description: 'User to act upon', shortcut: 'u')] + ?string $email = null, + ?OutputInterface $output = null, + ): int { + if (empty($email) || null === $user = $this->userRepository->findByEmail($email)) { + $output->writeln(sprintf('User with email %s not found!', $email)); + return Command::FAILURE; } - // get quota $quota = $user->getQuota(); if (null === $quota) { return Command::SUCCESS; diff --git a/src/Command/UsersRegistrationMailCommand.php b/src/Command/UsersRegistrationMailCommand.php index 64c6e9ca..7eb5f947 100644 --- a/src/Command/UsersRegistrationMailCommand.php +++ b/src/Command/UsersRegistrationMailCommand.php @@ -4,49 +4,38 @@ namespace App\Command; -use App\Entity\User; use App\Mail\WelcomeMailer; -use Doctrine\ORM\EntityManagerInterface; +use App\Repository\UserRepository; use Exception; -use Override; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Attribute\Option; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\Security\Core\Exception\UserNotFoundException; #[AsCommand(name: 'app:users:registration:mail', description: 'Send a registration mail to a user')] -final class UsersRegistrationMailCommand extends Command +final readonly class UsersRegistrationMailCommand { public function __construct( - private readonly EntityManagerInterface $manager, - private readonly WelcomeMailer $welcomeMailer, + private UserRepository $userRepository, + private WelcomeMailer $welcomeMailer, #[Autowire('kernel.default_locale')] - private readonly string $defaultLocale, + private string $defaultLocale, ) { - parent::__construct(); - } - - #[Override] - protected function configure(): void - { - $this - ->addOption('user', 'u', InputOption::VALUE_REQUIRED, 'User who get the voucher(s)') - ->addOption('locale', 'l', InputOption::VALUE_OPTIONAL, 'the locale', $this->defaultLocale); } /** * @throws Exception */ - #[Override] - protected function execute(InputInterface $input, OutputInterface $output): int - { - $email = $input->getOption('user'); - $locale = $input->getOption('locale'); - - if (empty($email) || null === $user = $this->manager->getRepository(User::class)->findByEmail($email)) { + public function __invoke( + #[Option(name: 'user', description: 'User who get the voucher(s)', shortcut: 'u')] + ?string $email = null, + #[Option(description: 'the locale', shortcut: 'l')] + ?string $locale = null, + ): int { + $locale ??= $this->defaultLocale; + + if (empty($email) || null === $user = $this->userRepository->findByEmail($email)) { throw new UserNotFoundException(sprintf('User with email %s not found!', $email)); } diff --git a/src/Command/UsersResetCommand.php b/src/Command/UsersResetCommand.php index 0b44681b..efdb2f3c 100644 --- a/src/Command/UsersResetCommand.php +++ b/src/Command/UsersResetCommand.php @@ -6,33 +6,39 @@ use App\Exception\PasswordMismatchException; use App\Exception\PasswordPolicyException; -use App\Handler\PasswordStrengthHandler; +use App\Repository\UserRepository; +use App\Service\ConsolePasswordHelper; use App\Service\UserResetService; -use Doctrine\ORM\EntityManagerInterface; -use Override; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Attribute\Option; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Style\SymfonyStyle; #[AsCommand(name: 'app:users:reset', description: 'Reset a user')] -final class UsersResetCommand extends AbstractUsersCommand +final readonly class UsersResetCommand { public function __construct( - EntityManagerInterface $manager, - PasswordStrengthHandler $passwordStrengthHandler, - private readonly UserResetService $userResetService, + private UserRepository $userRepository, + private UserResetService $userResetService, + private ConsolePasswordHelper $consolePasswordHelper, ) { - parent::__construct($manager, $passwordStrengthHandler); } - #[Override] - protected function execute(InputInterface $input, OutputInterface $output): int - { - $user = $this->getUser($input, $output); - if (null === $user) { + public function __invoke( + #[Option(name: 'user', description: 'User to act upon', shortcut: 'u')] + ?string $email = null, + #[Option(name: 'dry-run', description: 'Simulate without making changes')] + bool $dryRun = false, + ?InputInterface $input = null, + ?OutputInterface $output = null, + ): int { + $io = new SymfonyStyle($input, $output); + + if (empty($email) || null === $user = $this->userRepository->findByEmail($email)) { + $output->writeln(sprintf('User with email %s not found!', $email)); + return Command::FAILURE; } @@ -42,22 +48,19 @@ protected function execute(InputInterface $input, OutputInterface $output): int return Command::FAILURE; } - $questionHelper = $this->getHelper('question'); - assert($questionHelper instanceof QuestionHelper); - $confirmQuest = new ConfirmationQuestion('Really reset user? This will clear their mailbox: (yes|no) ', false); - if (!$questionHelper->ask($input, $output, $confirmQuest)) { + if (!$io->confirm('Really reset user? This will clear their mailbox', false)) { return Command::SUCCESS; } try { - $password = $this->askForPassword($input, $output); + $password = $this->consolePasswordHelper->askForPassword($input, $output); } catch (PasswordPolicyException|PasswordMismatchException $e) { $output->writeln(sprintf('%s', $e->getMessage())); return Command::FAILURE; } - if ($input->getOption('dry-run')) { + if ($dryRun) { $output->write(sprintf("\nWould reset user %s\n\n", $user->getEmail())); return Command::SUCCESS; diff --git a/src/Command/UsersRestoreCommand.php b/src/Command/UsersRestoreCommand.php index 4f448e9a..2eb9d581 100644 --- a/src/Command/UsersRestoreCommand.php +++ b/src/Command/UsersRestoreCommand.php @@ -6,31 +6,36 @@ use App\Exception\PasswordMismatchException; use App\Exception\PasswordPolicyException; -use App\Handler\PasswordStrengthHandler; +use App\Repository\UserRepository; +use App\Service\ConsolePasswordHelper; use App\Service\UserRestoreService; -use Doctrine\ORM\EntityManagerInterface; -use Override; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Attribute\Option; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; #[AsCommand(name: 'app:users:restore', description: 'Restore a user')] -final class UsersRestoreCommand extends AbstractUsersCommand +final readonly class UsersRestoreCommand { public function __construct( - EntityManagerInterface $manager, - PasswordStrengthHandler $passwordStrengthHandler, - private readonly UserRestoreService $userRestoreService, + private UserRepository $userRepository, + private UserRestoreService $userRestoreService, + private ConsolePasswordHelper $consolePasswordHelper, ) { - parent::__construct($manager, $passwordStrengthHandler); } - #[Override] - protected function execute(InputInterface $input, OutputInterface $output): int - { - $user = $this->getUser($input, $output); - if (null === $user) { + public function __invoke( + #[Option(name: 'user', description: 'User to act upon', shortcut: 'u')] + ?string $email = null, + #[Option(name: 'dry-run', description: 'Simulate without making changes')] + bool $dryRun = false, + ?InputInterface $input = null, + ?OutputInterface $output = null, + ): int { + if (empty($email) || null === $user = $this->userRepository->findByEmail($email)) { + $output->writeln(sprintf('User with email %s not found!', $email)); + return Command::FAILURE; } @@ -41,14 +46,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int } try { - $password = $this->askForPassword($input, $output); + $password = $this->consolePasswordHelper->askForPassword($input, $output); } catch (PasswordPolicyException|PasswordMismatchException $e) { $output->writeln(sprintf('%s', $e->getMessage())); return Command::FAILURE; } - if ($input->getOption('dry-run')) { + if ($dryRun) { $output->write(sprintf("\nWould restore user %s\n\n", $user->getEmail())); return Command::SUCCESS; diff --git a/src/Command/VoucherCountCommand.php b/src/Command/VoucherCountCommand.php index c533c381..58a77b5f 100644 --- a/src/Command/VoucherCountCommand.php +++ b/src/Command/VoucherCountCommand.php @@ -4,26 +4,35 @@ namespace App\Command; -use App\Entity\Voucher; -use Override; +use App\Repository\UserRepository; +use App\Repository\VoucherRepository; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Attribute\Option; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; #[AsCommand(name: 'app:voucher:count', description: 'Get count of vouchers for a specific user')] -final class VoucherCountCommand extends AbstractUsersCommand +final readonly class VoucherCountCommand { - #[Override] - protected function execute(InputInterface $input, OutputInterface $output): int - { - $user = $this->getUser($input, $output); - if (null === $user) { + public function __construct( + private UserRepository $userRepository, + private VoucherRepository $voucherRepository, + ) { + } + + public function __invoke( + #[Option(name: 'user', description: 'User to act upon', shortcut: 'u')] + ?string $email = null, + ?OutputInterface $output = null, + ): int { + if (empty($email) || null === $user = $this->userRepository->findByEmail($email)) { + $output->writeln(sprintf('User with email %s not found!', $email)); + return Command::FAILURE; } - $usedCount = $this->manager->getRepository(Voucher::class)->countVouchersByUser($user, true); - $unusedCount = $this->manager->getRepository(Voucher::class)->countVouchersByUser($user, false); + $usedCount = $this->voucherRepository->countVouchersByUser($user, true); + $unusedCount = $this->voucherRepository->countVouchersByUser($user, false); $output->writeln(sprintf('Voucher count for user %s', $user->getEmail())); $output->writeln(sprintf('Used: %d', $usedCount)); $output->writeln(sprintf('Unused: %d', $unusedCount)); diff --git a/src/Command/VoucherCreateCommand.php b/src/Command/VoucherCreateCommand.php index ed2eea78..5598e587 100644 --- a/src/Command/VoucherCreateCommand.php +++ b/src/Command/VoucherCreateCommand.php @@ -4,54 +4,49 @@ namespace App\Command; -use App\Entity\Domain; -use App\Handler\PasswordStrengthHandler; +use App\Repository\DomainRepository; +use App\Repository\UserRepository; use App\Service\SettingsService; use App\Service\VoucherManager; -use Doctrine\ORM\EntityManagerInterface; -use Override; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Attribute\Option; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Routing\RouterInterface; #[AsCommand(name: 'app:voucher:create', description: 'Create voucher for a specific user')] -final class VoucherCreateCommand extends AbstractUsersCommand +final readonly class VoucherCreateCommand { public function __construct( - EntityManagerInterface $manager, - PasswordStrengthHandler $passwordStrengthHandler, - private readonly RouterInterface $router, - private readonly VoucherManager $voucherManager, - private readonly SettingsService $settingsService, + private UserRepository $userRepository, + private DomainRepository $domainRepository, + private RouterInterface $router, + private VoucherManager $voucherManager, + private SettingsService $settingsService, ) { - parent::__construct($manager, $passwordStrengthHandler); } - #[Override] - protected function configure(): void - { - parent::configure(); - $this - ->addOption('count', 'c', InputOption::VALUE_OPTIONAL, 'How many voucher to create', 3) - ->addOption('print', 'p', InputOption::VALUE_NONE, 'Show vouchers') - ->addOption('print-links', 'l', InputOption::VALUE_NONE, 'Show links to vouchers') - ->addOption('domain', 'd', InputOption::VALUE_OPTIONAL, 'Domain for the voucher (default: user domain)'); - } + public function __invoke( + #[Option(name: 'user', description: 'User to act upon', shortcut: 'u')] + ?string $email = null, + #[Option(description: 'How many voucher to create', shortcut: 'c')] + int $count = 3, + #[Option(description: 'Show vouchers', shortcut: 'p')] + bool $print = false, + #[Option(name: 'print-links', description: 'Show links to vouchers', shortcut: 'l')] + bool $printLinks = false, + #[Option(name: 'domain', description: 'Domain for the voucher (default: user domain)', shortcut: 'd')] + ?string $domainName = null, + ?OutputInterface $output = null, + ): int { + if (empty($email) || null === $user = $this->userRepository->findByEmail($email)) { + $output->writeln(sprintf('User with email %s not found!', $email)); - #[Override] - protected function execute(InputInterface $input, OutputInterface $output): int - { - $user = $this->getUser($input, $output); - if (null === $user) { return Command::FAILURE; } - $domainName = $input->getOption('domain'); if (null !== $domainName) { - $domain = $this->manager->getRepository(Domain::class)->findByName($domainName); + $domain = $this->domainRepository->findByName($domainName); if (null === $domain) { $output->writeln(sprintf('Domain %s not found!', $domainName)); @@ -65,14 +60,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int $context = $this->router->getContext(); $context->setBaseUrl($this->settingsService->get('app_url')); - for ($i = 1; $i <= $input->getOption('count'); ++$i) { + for ($i = 1; $i <= $count; ++$i) { $voucher = $this->voucherManager->create($user, $domain); - if (true === $input->getOption('print-links')) { + if ($printLinks) { $output->write(sprintf("%s\n", $this->router->generate( 'register_voucher', ['voucher' => $voucher->getCode()] ))); - } elseif (true === $input->getOption('print')) { + } elseif ($print) { $output->write(sprintf("%s\n", $voucher->getCode())); } } diff --git a/src/Enum/ApiScope.php b/src/Enum/ApiScope.php index 25204770..1c68fe50 100644 --- a/src/Enum/ApiScope.php +++ b/src/Enum/ApiScope.php @@ -12,6 +12,8 @@ enum ApiScope: string case RETENTION = 'retention'; case ROUNDCUBE = 'roundcube'; + public const ALL_SCOPES_DESCRIPTION = 'keycloak, dovecot, postfix, retention, roundcube'; + public static function all(): array { $scopes = self::cases(); diff --git a/src/Service/ConsolePasswordHelper.php b/src/Service/ConsolePasswordHelper.php new file mode 100644 index 00000000..f05131f0 --- /dev/null +++ b/src/Service/ConsolePasswordHelper.php @@ -0,0 +1,87 @@ +setValidator(function ($value) { + if ($this->passwordStrengthHandler->validate($value)) { + throw new PasswordPolicyException(); + } + + return $value; + }); + $passwordQuest->setHidden(true); + $passwordQuest->setHiddenFallback(false); + $passwordQuest->setMaxAttempts(5); + + $password = $io->askQuestion($passwordQuest); + + $passwordConfirmQuest = new Question('Repeat password: '); + $passwordConfirmQuest->setHidden(true); + $passwordConfirmQuest->setHiddenFallback(false); + + $passwordConfirm = $io->askQuestion($passwordConfirmQuest); + + if ($password !== $passwordConfirm) { + throw new PasswordMismatchException(); + } + + $violations = $this->validator->validate($password, [ + new NotCompromisedPassword(skipOnError: true), + ]); + + if ($violations->count() > 0) { + throw new PasswordPolicyException($violations->get(0)->getMessage()); + } + + return $password; + } + + /** + * Validate a password against the password policy and compromised password check. + * + * @throws PasswordPolicyException + */ + public function validatePassword(string $password): void + { + $violations = $this->validator->validate($password, [ + new Assert\NotBlank(), + new PasswordPolicy(), + new NotCompromisedPassword(skipOnError: true), + ]); + + if ($violations->count() > 0) { + throw new PasswordPolicyException($violations->get(0)->getMessage()); + } + } +} diff --git a/tests/Command/AdminPasswordCommandTest.php b/tests/Command/AdminPasswordCommandTest.php index 9f9719c7..7cd5a2e3 100644 --- a/tests/Command/AdminPasswordCommandTest.php +++ b/tests/Command/AdminPasswordCommandTest.php @@ -5,31 +5,90 @@ namespace App\Tests\Command; use App\Command\AdminPasswordCommand; +use App\Handler\PasswordStrengthHandler; use App\Helper\AdminPasswordUpdater; +use App\Service\ConsolePasswordHelper; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Application; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Tester\CommandTester; +use Symfony\Component\Validator\ConstraintViolationList; +use Symfony\Component\Validator\Validator\ValidatorInterface; class AdminPasswordCommandTest extends TestCase { - public function testExecute(): void + private CommandTester $commandTester; + + protected function setUp(): void { $updater = $this->createStub(AdminPasswordUpdater::class); + $validator = $this->createStub(ValidatorInterface::class); + $validator->method('validate')->willReturn(new ConstraintViolationList()); + $consolePasswordHelper = new ConsolePasswordHelper(new PasswordStrengthHandler(), $validator); - $command = new AdminPasswordCommand($updater); + $command = new AdminPasswordCommand($updater, $consolePasswordHelper); $app = new Application(); $app->addCommand($command); - $commandTester = new CommandTester($command); - $commandTester->execute(['password' => 'test']); + $this->commandTester = new CommandTester($app->find('app:admin:password')); + } + + public function testExecute(): void + { + $exitCode = $this->commandTester->execute(['password' => 'longtestpassword1234']); + + self::assertSame(Command::SUCCESS, $exitCode); + self::assertEquals('', $this->commandTester->getDisplay()); + } + + public function testExecuteInteractive(): void + { + $this->commandTester->setInputs(['longtestpassword1234', 'longtestpassword1234']); + + $exitCode = $this->commandTester->execute([]); + + self::assertSame(Command::SUCCESS, $exitCode); + self::assertStringContainsString('New password:', $this->commandTester->getDisplay()); + } + + public function testExecuteShortPasswordInteractive(): void + { + $this->commandTester->setInputs(['short', 'short', 'short', 'short', 'short']); - $output = $commandTester->getDisplay(); - self::assertEquals('', $output); + $exitCode = $this->commandTester->execute([]); + + self::assertSame(Command::FAILURE, $exitCode); + self::assertStringContainsString("The password doesn't comply with our security policy.", $this->commandTester->getDisplay()); + } + + public function testExecutePasswordsDontMatch(): void + { + $this->commandTester->setInputs(['longtestpassword1234', 'different']); + + $exitCode = $this->commandTester->execute([]); + + self::assertSame(Command::FAILURE, $exitCode); + self::assertStringContainsString("The passwords don't match.", $this->commandTester->getDisplay()); + } + + public function testCommandConfiguration(): void + { + $updater = $this->createStub(AdminPasswordUpdater::class); + $validator = $this->createStub(ValidatorInterface::class); + $validator->method('validate')->willReturn(new ConstraintViolationList()); + $consolePasswordHelper = new ConsolePasswordHelper(new PasswordStrengthHandler(), $validator); + + $command = new AdminPasswordCommand($updater, $consolePasswordHelper); + + $app = new Application(); + $app->addCommand($command); + $wrappedCommand = $app->find('app:admin:password'); - $commandTester->setInputs(['password via interactive command\n']); - $commandTester->execute([]); + self::assertEquals('app:admin:password', $wrappedCommand->getName()); + self::assertEquals('Set password of admin user', $wrappedCommand->getDescription()); - $output = $commandTester->getDisplay(); - self::assertStringContainsString('Please enter new admin password', $output); + $definition = $wrappedCommand->getDefinition(); + self::assertTrue($definition->hasArgument('password')); + self::assertFalse($definition->getArgument('password')->isRequired()); } } diff --git a/tests/Command/AliasDeleteCommandTest.php b/tests/Command/AliasDeleteCommandTest.php index 1a1bf584..be10ee79 100644 --- a/tests/Command/AliasDeleteCommandTest.php +++ b/tests/Command/AliasDeleteCommandTest.php @@ -10,7 +10,6 @@ use App\Handler\DeleteHandler; use App\Repository\AliasRepository; use App\Repository\UserRepository; -use Doctrine\ORM\EntityManagerInterface; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Application; use Symfony\Component\Console\Tester\CommandTester; @@ -35,15 +34,9 @@ protected function setUp(): void $aliasRepository->method('findOneBySource') ->willReturn($alias); - $manager = $this->createStub(EntityManagerInterface::class); - $manager->method('getRepository')->willReturnMap([ - [User::class, $userRepository], - [Alias::class, $aliasRepository], - ]); - $deleteHandler = $this->createStub(DeleteHandler::class); - $this->command = new AliasDeleteCommand($manager, $deleteHandler); + $this->command = new AliasDeleteCommand($userRepository, $aliasRepository, $deleteHandler); } public function testExecute(): void @@ -87,4 +80,21 @@ public function testExecuteWithoutAlias(): void self::assertStringContainsString('Alias with address \'\' not found!', $output); self::assertEquals(1, $commandTester->getStatusCode()); } + + public function testCommandConfiguration(): void + { + $application = new Application(); + $application->addCommand($this->command); + $command = $application->find('app:alias:delete'); + + self::assertEquals('app:alias:delete', $command->getName()); + self::assertEquals('Delete an alias', $command->getDescription()); + + $definition = $command->getDefinition(); + self::assertTrue($definition->hasOption('alias')); + self::assertEquals('a', $definition->getOption('alias')->getShortcut()); + self::assertTrue($definition->hasOption('user')); + self::assertEquals('u', $definition->getOption('user')->getShortcut()); + self::assertTrue($definition->hasOption('dry-run')); + } } diff --git a/tests/Command/ApiTokenCreateCommandTest.php b/tests/Command/ApiTokenCreateCommandTest.php index 359fd9e6..ae916512 100644 --- a/tests/Command/ApiTokenCreateCommandTest.php +++ b/tests/Command/ApiTokenCreateCommandTest.php @@ -209,10 +209,14 @@ public function testExecuteFailureWithApiTokenManagerException(): void public function testCommandConfiguration(): void { - self::assertEquals('app:api-token:create', $this->command->getName()); - self::assertEquals('Create a new API token with specified name and scopes', $this->command->getDescription()); + $application = new Application(); + $application->addCommand($this->command); + $command = $application->find('app:api-token:create'); + + self::assertEquals('app:api-token:create', $command->getName()); + self::assertEquals('Create a new API token with specified name and scopes', $command->getDescription()); - $definition = $this->command->getDefinition(); + $definition = $command->getDefinition(); // Arguments: none required anymore for name self::assertFalse($definition->hasArgument('name')); diff --git a/tests/Command/ApiTokenDeleteCommandTest.php b/tests/Command/ApiTokenDeleteCommandTest.php index 11d409ed..03491e5d 100644 --- a/tests/Command/ApiTokenDeleteCommandTest.php +++ b/tests/Command/ApiTokenDeleteCommandTest.php @@ -104,9 +104,13 @@ public function testDeleteTokenNotFound(): void public function testCommandConfiguration(): void { - self::assertEquals('app:api-token:delete', $this->command->getName()); + $application = new Application(); + $application->addCommand($this->command); + $command = $application->find('app:api-token:delete'); + + self::assertEquals('app:api-token:delete', $command->getName()); - $definition = $this->command->getDefinition(); + $definition = $command->getDefinition(); self::assertFalse($definition->hasArgument('token')); self::assertTrue($definition->hasOption('token')); } diff --git a/tests/Command/DomainDeleteCommandTest.php b/tests/Command/DomainDeleteCommandTest.php index e49ce877..bdd8e1fc 100644 --- a/tests/Command/DomainDeleteCommandTest.php +++ b/tests/Command/DomainDeleteCommandTest.php @@ -9,6 +9,7 @@ use App\Repository\DomainRepository; use App\Service\DomainManager; use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Tester\CommandTester; @@ -109,9 +110,13 @@ public function testCommandConfiguration(): void $command = new DomainDeleteCommand($repository, $manager); - self::assertEquals('app:domain:delete', $command->getName()); + $application = new Application(); + $application->addCommand($command); + $wrappedCommand = $application->find('app:domain:delete'); - $definition = $command->getDefinition(); + self::assertEquals('app:domain:delete', $wrappedCommand->getName()); + + $definition = $wrappedCommand->getDefinition(); self::assertTrue($definition->hasOption('domain')); self::assertTrue($definition->hasOption('dry-run')); } diff --git a/tests/Command/MetricsCommandTest.php b/tests/Command/MetricsCommandTest.php index eec4d87f..b3099a54 100644 --- a/tests/Command/MetricsCommandTest.php +++ b/tests/Command/MetricsCommandTest.php @@ -11,6 +11,7 @@ use App\Repository\UserRepository; use App\Repository\VoucherRepository; use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Application; use Symfony\Component\Console\Tester\CommandTester; class MetricsCommandTest extends TestCase @@ -56,4 +57,26 @@ public function testExecute(): void self::assertStringContainsString('userli_aliases_total 4', $output); self::assertStringContainsString('userli_openpgpkeys_total 2', $output); } + + public function testCommandConfiguration(): void + { + $command = new MetricsCommand( + $this->createStub(UserRepository::class), + $this->createStub(VoucherRepository::class), + $this->createStub(DomainRepository::class), + $this->createStub(AliasRepository::class), + $this->createStub(OpenPgpKeyRepository::class), + ); + + $application = new Application(); + $application->addCommand($command); + $wrappedCommand = $application->find('app:metrics'); + + self::assertEquals('app:metrics', $wrappedCommand->getName()); + self::assertEquals('Global Metrics for Userli', $wrappedCommand->getDescription()); + + $definition = $wrappedCommand->getDefinition(); + self::assertEmpty($definition->getArguments()); + self::assertEmpty($definition->getOptions()); + } } diff --git a/tests/Command/OpenPgpDeleteKeyCommandTest.php b/tests/Command/OpenPgpDeleteKeyCommandTest.php deleted file mode 100644 index 47b079b2..00000000 --- a/tests/Command/OpenPgpDeleteKeyCommandTest.php +++ /dev/null @@ -1,50 +0,0 @@ -setEmail('alice@example.org'); - - $manager = $this->createStub(OpenPgpKeyManager::class); - $manager->method('getKey')->willReturnMap( - [ - ['alice@example.org', $openPgpKey], - ['nonexistent@example.org', null], - ] - ); - - $this->command = new OpenPgpDeleteKeyCommand($manager); - } - - public function testExecute(): void - { - $commandTester = new CommandTester($this->command); - $commandTester->execute(['email' => 'alice@example.org']); - - $output = $commandTester->getDisplay(); - self::assertStringContainsString('Deleted OpenPGP key for email alice@example.org', $output); - } - - public function testExecuteWithNonexistentEmail(): void - { - $commandTester = new CommandTester($this->command); - $commandTester->execute(['email' => 'nonexistent@example.org']); - - $output = $commandTester->getDisplay(); - self::assertStringContainsString('No OpenPGP key found for email nonexistent@example.org', $output); - } -} diff --git a/tests/Command/OpenPgpImportKeyCommandTest.php b/tests/Command/OpenPgpImportKeyCommandTest.php deleted file mode 100644 index 1a188c0b..00000000 --- a/tests/Command/OpenPgpImportKeyCommandTest.php +++ /dev/null @@ -1,53 +0,0 @@ -setEmail('alice@example.org'); - file_put_contents('/tmp/pubkey.asc', 'mQGNBF+B09wBDACe08x3/cZYBdYfKm062Bj9DtSkq9K7uZSif0alSm1x10hcNh3d31EjIBLPt7PNowYiADj2aLFscC3UjO/nNKqE6wXXPB5yfeW0ES9NxgElDgyHUvimq1H+L2ji+QHrsZwgSVD1NGi/2yVfTuWWjKkcUYjxLFKdLpjfy0I92IagSsPOzGdLHxzwuXvWP/D6FLWDw3n6bddWvysZzRX8PIuICJJ/VZ4lUbfXpzKyMD9hc5Uqpi+ab++1I4wYhy5H5Kll+iBa7vfRAPjKhml9A+SFPfg4tgv+C5izLwGi/1SYBfVMTmwTly42pMyjjGbnWZ4GW7sGbCHlgIpL1zFfoUdXeBZJrG9W4ReoD42LZUZkn+lzSHiv62tjH1Zh+oVlf2sWmCGuFa3WL95mOmUSyY+ne1w8ZlEB2nVq6LU09XxaztYTC65HGS7lZ5MGXsfcWyugBi0uuS01DGHPBZA5Gj/pqAHzoLYo0pEaEWvkKHYOI2bhHd4VikIW6KbJ1cEgc6kAEQEAAbQZQWxpY2UgPGFsaWNlQGV4YW1wbGUub3JnPokB1AQTAQoAPhYhBHMBJUfCXeKg0JeMRq2NUs0igf7CBQJfgdPcAhsDBQkDwmcABQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEK2NUs0igf7CLJoL/2jBag9rkhNAC3omHvt4W8qO6Yx5pmLtes6ABksmXNZ3v9/oGYG6t2nBasfiMOBO806jA7F8HRDTn0Acp2x0qPamsTGWRfFjL9zK4l67ZsPJO1nWN5v2iqF9015TqLosZP02rrT+nbtwZTSNmqrcgEKgl1K3vC1bhwi3a8uAqBr+LbxzpM2/op+Iccus5fAv1L2xlcpQYGjfeQ4Wcl2DBIagLFFJEZeZosMRBD4ljibAIt2xzlPkth4abW0eHcHXfg6cuwZqqRwGC52OnEEHw04T38Uy8Jqgz+4aZYzMUub1hkLAI3CYC9XwKvNM9I0b2M4fwhKjlZxoJXInbu/aNDXKD/fU2tULxObhWfbGN588vGy9VzHL/9Ph7bGPJ4+W0pkyU41pLS8ZA3LtQB40z9lEwd2Bop63abxgObRytIcClbTg/YtVngaaEtuv6tkxVuN7eHX+l6d2buTO3+0jc2XINitqDSHzUlHF8mtpyARH70X3tKGkZxnnml1yhBvBGrkBjQRfgdPcAQwA6TBolO+tbbfGKTH6IikJwA9wYK0W4cK7dXKfwnQznYd2YZ6xnZTQOdMbMnmhjWjsfZ0ddPUttSuavUUCpM7ZF2UpmJQJMNBVJXfgzz+YqlnOcWTp72ZRvOJLOo0cQYFT7g54Ff/R98W0jsz28mi9fZDG6i11SkHJw9H7VZzJ5WwJXsmMdAhcxVb342hUstwL3vseMT+Ni7G+aF/r3gkkmSW2Uo0cG37DCbDuGQGE/F1OCzjxRvCI2hFhAjbxDz1PDLBAflHJFHAcTvyBNURayjKTQvx04Rwk4/JEJzX3ll5+uYgD7WdyoL939U+LyTTzv8gS5TDkaUroMy14VAP+hptvdAtYB8X+FCQPTNQqaHc8mGsH04GIju7hXibJ92lPhb/z8xVDgw15Sqb7cdCPDf+9nPtnZ+mGSJzsaNYcPV1J9WJCfz6jnVOsuxxUh88R4c+r2W/aWKlqqt5DIdcE5BmJTywCX8Ae5IgjgAckh7/6h66XovwpG/ruKruWZqixABEBAAGJAbwEGAEKACYWIQRzASVHwl3ioNCXjEatjVLNIoH+wgUCX4HT3AIbDAUJA8JnAAAKCRCtjVLNIoH+wq9SC/4t41rMGUWet8XrO53bqgxZVyvEznfwfIDs1F/I8OdOUaLN4h8s7xbmgR0TBLFcgavkx6xdQrFHQzNJwW7N99J3GK/Ue03doBhT0l6NgG7zzNrSVeLo/X/uvjHxXYFli6vC13UfOtFSAcfA5v5+zmQ22FlwFAdtLvoQhKdVlTWN5bGqJ2m1MQH+qAtAnxbpeSjlN3jUUVQbaY2nl0HAvJ/ex+KbjCkQ39sIEQ32GVM5ndDhaV2vyjGFpi7mdUUFmvmeLhdca23hHAwjUyQTq2eSZ1QvJQpy+jkMwXNqbUcCONL3+LiGN6rxLD/9xoHdzevYf4LoNu5OtFnEbmGwRS8aN910SwE895epTzFQ0LUlqk1v60mCjI2igAetGiK2Z764FSZZe1L+adLH5R+Z2nGKTvTjuCB4tveNDkf1f4zsPQL+FP9xT4mjoy003maO5Ccoo8ggGlUsqCV6TcqeW7tYU9BTegzasSrNiI5y/bUphMNhWBRccEo8lQr8xtvkrfY='); - - $manager = $this->createStub(OpenPgpKeyManager::class); - $manager->method('importKey')->willReturn($openPgpKey); - - $this->command = new OpenPgpImportKeyCommand($manager); - } - - public function testExecute(): void - { - $commandTester = new CommandTester($this->command); - $commandTester->execute([ - 'email' => 'alice@example.org', - 'file' => '/tmp/pubkey.asc', - ]); - - $output = $commandTester->getDisplay(); - self::assertStringContainsString('Imported OpenPGP key for email alice@example.org', $output); - } - - public function testExecuteWithNonexistentFile(): void - { - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('File not found: /nonexistent'); - - $commandTester = new CommandTester($this->command); - $commandTester->execute([ - 'email' => 'alice@example.org', - 'file' => '/nonexistent', - ]); - } -} diff --git a/tests/Command/OpenPgpShowKeyCommandTest.php b/tests/Command/OpenPgpShowKeyCommandTest.php index b63cbf4f..996fd2cd 100644 --- a/tests/Command/OpenPgpShowKeyCommandTest.php +++ b/tests/Command/OpenPgpShowKeyCommandTest.php @@ -8,6 +8,7 @@ use App\Entity\OpenPgpKey; use App\Service\OpenPgpKeyManager; use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Application; use Symfony\Component\Console\Tester\CommandTester; class OpenPgpShowKeyCommandTest extends TestCase @@ -47,4 +48,18 @@ public function testExecuteWithNonexistentEmail(): void $output = $commandTester->getDisplay(); self::assertStringContainsString('No OpenPGP key found for email nonexistent@example.org', $output); } + + public function testCommandConfiguration(): void + { + $application = new Application(); + $application->addCommand($this->command); + $command = $application->find('app:openpgp:show-key'); + + self::assertEquals('app:openpgp:show-key', $command->getName()); + self::assertEquals('Show OpenPGP key of email', $command->getDescription()); + + $definition = $command->getDefinition(); + self::assertTrue($definition->hasArgument('email')); + self::assertTrue($definition->getArgument('email')->isRequired()); + } } diff --git a/tests/Command/UsersDeleteCommandTest.php b/tests/Command/UsersDeleteCommandTest.php index 9e7770fd..4c42394f 100644 --- a/tests/Command/UsersDeleteCommandTest.php +++ b/tests/Command/UsersDeleteCommandTest.php @@ -7,9 +7,7 @@ use App\Command\UsersDeleteCommand; use App\Entity\User; use App\Handler\DeleteHandler; -use App\Handler\PasswordStrengthHandler; use App\Repository\UserRepository; -use Doctrine\ORM\EntityManagerInterface; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; @@ -27,12 +25,9 @@ protected function setUp(): void $repository->method('findByEmail') ->willReturn($user); - $manager = $this->createStub(EntityManagerInterface::class); - $manager->method('getRepository')->willReturn($repository); - $deleteHandler = $this->createStub(DeleteHandler::class); - $this->command = new UsersDeleteCommand($manager, new PasswordStrengthHandler(), $deleteHandler); + $this->command = new UsersDeleteCommand($repository, $deleteHandler); } public function testExecute(): void @@ -69,4 +64,19 @@ public function testExecuteWithoutUser(): void self::assertSame(Command::FAILURE, $exitCode); self::assertStringContainsString('User with email', $commandTester->getDisplay()); } + + public function testCommandConfiguration(): void + { + $application = new Application(); + $application->addCommand($this->command); + $command = $application->find('app:users:delete'); + + self::assertEquals('app:users:delete', $command->getName()); + self::assertEquals('Delete a user', $command->getDescription()); + + $definition = $command->getDefinition(); + self::assertTrue($definition->hasOption('user')); + self::assertEquals('u', $definition->getOption('user')->getShortcut()); + self::assertTrue($definition->hasOption('dry-run')); + } } diff --git a/tests/Command/UsersMailCryptCommandTest.php b/tests/Command/UsersMailCryptCommandTest.php index b505a018..3238cc17 100644 --- a/tests/Command/UsersMailCryptCommandTest.php +++ b/tests/Command/UsersMailCryptCommandTest.php @@ -7,11 +7,9 @@ use App\Command\UsersMailCryptCommand; use App\Entity\User; use App\Handler\MailCryptKeyHandler; -use App\Handler\PasswordStrengthHandler; use App\Handler\UserAuthenticationHandler; use App\Repository\UserRepository; use App\Service\SettingsService; -use Doctrine\ORM\EntityManagerInterface; use PHPUnit\Framework\MockObject\Stub; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Application; @@ -21,7 +19,6 @@ class UsersMailCryptCommandTest extends TestCase { private UsersMailCryptCommand $command; - private Stub&EntityManagerInterface $entityManager; private Stub&UserRepository $userRepository; private Stub&UserAuthenticationHandler $authenticationHandler; private Stub&MailCryptKeyHandler $mailCryptKeyHandler; @@ -29,19 +26,14 @@ class UsersMailCryptCommandTest extends TestCase protected function setUp(): void { - $this->entityManager = $this->createStub(EntityManagerInterface::class); $this->userRepository = $this->createStub(UserRepository::class); $this->authenticationHandler = $this->createStub(UserAuthenticationHandler::class); $this->mailCryptKeyHandler = $this->createStub(MailCryptKeyHandler::class); $this->settingsService = $this->createStub(SettingsService::class); $this->settingsService->method('get')->willReturn(1); - $this->entityManager->method('getRepository') - ->willReturn($this->userRepository); - $this->command = new UsersMailCryptCommand( - $this->entityManager, - new PasswordStrengthHandler(), + $this->userRepository, $this->authenticationHandler, $this->mailCryptKeyHandler, $this->settingsService, @@ -65,26 +57,23 @@ public function testExecuteWithMailCryptArgumentsWhenSet(): void $authenticationHandler = $this->createMock(UserAuthenticationHandler::class); $mailCryptKeyHandler = $this->createMock(MailCryptKeyHandler::class); - $entityManager = $this->createStub(EntityManagerInterface::class); - $entityManager->method('getRepository')->willReturn($userRepository); - $userRepository->expects(self::once()) ->method('findByEmail') ->with($email) ->willReturn($user); - // The command uses $password[0], so it takes the first character + // The command passes the full password string $authenticationHandler->expects(self::once()) ->method('authenticate') - ->with($user, 'p') + ->with($user, $password) ->willReturn($user); $mailCryptKeyHandler->expects(self::once()) ->method('decrypt') - ->with($user, 'p') + ->with($user, $password) ->willReturn($privateKey); - $command = new UsersMailCryptCommand($entityManager, new PasswordStrengthHandler(), $authenticationHandler, $mailCryptKeyHandler, $this->settingsService); + $command = new UsersMailCryptCommand($userRepository, $authenticationHandler, $mailCryptKeyHandler, $this->settingsService); $application = new Application(); $application->addCommand($command); @@ -196,19 +185,14 @@ public function testExecuteWithMailCryptUnset(): void public function testExecuteWhenMailCryptGloballyDisabled(): void { - $entityManager = $this->createStub(EntityManagerInterface::class); $userRepository = $this->createMock(UserRepository::class); - $entityManager->method('getRepository') - ->willReturn($userRepository); - // Create command with mailCrypt disabled $settingsService = $this->createStub(SettingsService::class); $settingsService->method('get')->willReturn(0); $command = new UsersMailCryptCommand( - $entityManager, - new PasswordStrengthHandler(), + $userRepository, $this->authenticationHandler, $this->mailCryptKeyHandler, $settingsService, @@ -265,21 +249,18 @@ public function testExecuteWithAuthenticationFailure(): void $userRepository = $this->createMock(UserRepository::class); $authenticationHandler = $this->createMock(UserAuthenticationHandler::class); - $entityManager = $this->createStub(EntityManagerInterface::class); - $entityManager->method('getRepository')->willReturn($userRepository); - $userRepository->expects(self::once()) ->method('findByEmail') ->with($email) ->willReturn($user); - // The command uses $password[0], so it takes the first character + // The command passes the full password string $authenticationHandler->expects(self::once()) ->method('authenticate') - ->with($user, 'w') + ->with($user, $password) ->willReturn(null); - $command = new UsersMailCryptCommand($entityManager, new PasswordStrengthHandler(), $authenticationHandler, $this->mailCryptKeyHandler, $this->settingsService); + $command = new UsersMailCryptCommand($userRepository, $authenticationHandler, $this->mailCryptKeyHandler, $this->settingsService); $application = new Application(); $application->addCommand($command); @@ -294,4 +275,20 @@ public function testExecuteWithAuthenticationFailure(): void self::assertSame(1, $commandTester->getStatusCode()); } + + public function testCommandConfiguration(): void + { + $application = new Application(); + $application->addCommand($this->command); + $command = $application->find('app:users:mailcrypt'); + + self::assertEquals('app:users:mailcrypt', $command->getName()); + self::assertEquals('Get MailCrypt values for user', $command->getDescription()); + + $definition = $command->getDefinition(); + self::assertTrue($definition->hasOption('user')); + self::assertEquals('u', $definition->getOption('user')->getShortcut()); + self::assertTrue($definition->hasArgument('password')); + self::assertFalse($definition->getArgument('password')->isRequired()); + } } diff --git a/tests/Command/UsersQuotaCommandTest.php b/tests/Command/UsersQuotaCommandTest.php index fffa8e45..5737da74 100644 --- a/tests/Command/UsersQuotaCommandTest.php +++ b/tests/Command/UsersQuotaCommandTest.php @@ -6,11 +6,8 @@ use App\Command\UsersQuotaCommand; use App\Entity\User; -use App\Handler\PasswordStrengthHandler; use App\Repository\UserRepository; -use Doctrine\ORM\EntityManagerInterface; use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\MockObject\Stub; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; @@ -19,18 +16,13 @@ class UsersQuotaCommandTest extends TestCase { private UsersQuotaCommand $command; - private Stub $entityManager; private MockObject $userRepository; protected function setUp(): void { - $this->entityManager = $this->createStub(EntityManagerInterface::class); $this->userRepository = $this->createMock(UserRepository::class); - $this->entityManager->method('getRepository') - ->willReturn($this->userRepository); - - $this->command = new UsersQuotaCommand($this->entityManager, new PasswordStrengthHandler()); + $this->command = new UsersQuotaCommand($this->userRepository); } public function testExecuteWithQuotaSet(): void @@ -113,4 +105,19 @@ public function testExecuteWithUnknownUser(): void self::assertSame(Command::FAILURE, $exitCode); self::assertStringContainsString('User with email', $commandTester->getDisplay()); } + + public function testCommandConfiguration(): void + { + $application = new Application(); + $application->addCommand($this->command); + $command = $application->find('app:users:quota'); + + self::assertEquals('app:users:quota', $command->getName()); + self::assertEquals('Get quota of user if set', $command->getDescription()); + + $definition = $command->getDefinition(); + self::assertTrue($definition->hasOption('user')); + self::assertEquals('u', $definition->getOption('user')->getShortcut()); + self::assertFalse($definition->hasOption('dry-run')); + } } diff --git a/tests/Command/UsersRegistrationMailCommandTest.php b/tests/Command/UsersRegistrationMailCommandTest.php index eecec020..ff8992ac 100644 --- a/tests/Command/UsersRegistrationMailCommandTest.php +++ b/tests/Command/UsersRegistrationMailCommandTest.php @@ -8,9 +8,7 @@ use App\Entity\User; use App\Mail\WelcomeMailer; use App\Repository\UserRepository; -use Doctrine\ORM\EntityManagerInterface; use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\MockObject\Stub; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Application; use Symfony\Component\Console\Tester\CommandTester; @@ -19,21 +17,16 @@ class UsersRegistrationMailCommandTest extends TestCase { private UsersRegistrationMailCommand $command; - private Stub $entityManager; private MockObject $welcomeMailer; private MockObject $userRepository; protected function setUp(): void { - $this->entityManager = $this->createStub(EntityManagerInterface::class); $this->welcomeMailer = $this->createMock(WelcomeMailer::class); $this->userRepository = $this->createMock(UserRepository::class); - $this->entityManager->method('getRepository') - ->willReturn($this->userRepository); - $this->command = new UsersRegistrationMailCommand( - $this->entityManager, + $this->userRepository, $this->welcomeMailer, 'en' ); @@ -144,4 +137,20 @@ public function testExecuteWithEmptyEmail(): void '--user' => '', ]); } + + public function testCommandConfiguration(): void + { + $application = new Application(); + $application->addCommand($this->command); + $command = $application->find('app:users:registration:mail'); + + self::assertEquals('app:users:registration:mail', $command->getName()); + self::assertEquals('Send a registration mail to a user', $command->getDescription()); + + $definition = $command->getDefinition(); + self::assertTrue($definition->hasOption('user')); + self::assertEquals('u', $definition->getOption('user')->getShortcut()); + self::assertTrue($definition->hasOption('locale')); + self::assertEquals('l', $definition->getOption('locale')->getShortcut()); + } } diff --git a/tests/Command/UsersResetCommandTest.php b/tests/Command/UsersResetCommandTest.php index 8b8fcb4b..ede23808 100644 --- a/tests/Command/UsersResetCommandTest.php +++ b/tests/Command/UsersResetCommandTest.php @@ -8,12 +8,14 @@ use App\Entity\User; use App\Handler\PasswordStrengthHandler; use App\Repository\UserRepository; +use App\Service\ConsolePasswordHelper; use App\Service\UserResetService; -use Doctrine\ORM\EntityManagerInterface; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Tester\CommandTester; +use Symfony\Component\Validator\ConstraintViolationList; +use Symfony\Component\Validator\Validator\ValidatorInterface; class UsersResetCommandTest extends TestCase { @@ -31,12 +33,12 @@ protected function setUp(): void $repository->method('findByEmail') ->willReturn($this->user); - $manager = $this->createStub(EntityManagerInterface::class); - $manager->method('getRepository')->willReturn($repository); - $userResetService = $this->createStub(UserResetService::class); + $validator = $this->createStub(ValidatorInterface::class); + $validator->method('validate')->willReturn(new ConstraintViolationList()); + $consolePasswordHelper = new ConsolePasswordHelper(new PasswordStrengthHandler(), $validator); - $this->command = new UsersResetCommand($manager, new PasswordStrengthHandler(), $userResetService); + $this->command = new UsersResetCommand($repository, $userResetService, $consolePasswordHelper); } public function testExecuteDryRun(): void @@ -118,4 +120,19 @@ public function testExecuteWithoutUser(): void self::assertSame(Command::FAILURE, $exitCode); self::assertStringContainsString('User with email', $commandTester->getDisplay()); } + + public function testCommandConfiguration(): void + { + $application = new Application(); + $application->addCommand($this->command); + $command = $application->find('app:users:reset'); + + self::assertEquals('app:users:reset', $command->getName()); + self::assertEquals('Reset a user', $command->getDescription()); + + $definition = $command->getDefinition(); + self::assertTrue($definition->hasOption('user')); + self::assertEquals('u', $definition->getOption('user')->getShortcut()); + self::assertTrue($definition->hasOption('dry-run')); + } } diff --git a/tests/Command/UsersRestoreCommandTest.php b/tests/Command/UsersRestoreCommandTest.php index 236f74c9..f94e9b34 100644 --- a/tests/Command/UsersRestoreCommandTest.php +++ b/tests/Command/UsersRestoreCommandTest.php @@ -8,17 +8,19 @@ use App\Entity\User; use App\Handler\PasswordStrengthHandler; use App\Repository\UserRepository; +use App\Service\ConsolePasswordHelper; use App\Service\UserRestoreService; -use Doctrine\ORM\EntityManagerInterface; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Tester\CommandTester; +use Symfony\Component\Validator\ConstraintViolationList; +use Symfony\Component\Validator\Validator\ValidatorInterface; class UsersRestoreCommandTest extends TestCase { private UsersRestoreCommand $command; - private UserRestoreService $userRestoreService; + private UserRestoreService&\PHPUnit\Framework\MockObject\Stub $userRestoreService; private User $user; protected function setUp(): void @@ -30,12 +32,12 @@ protected function setUp(): void $repository->method('findByEmail') ->willReturn($this->user); - $manager = $this->createStub(EntityManagerInterface::class); - $manager->method('getRepository')->willReturn($repository); - $this->userRestoreService = $this->createStub(UserRestoreService::class); + $validator = $this->createStub(ValidatorInterface::class); + $validator->method('validate')->willReturn(new ConstraintViolationList()); + $consolePasswordHelper = new ConsolePasswordHelper(new PasswordStrengthHandler(), $validator); - $this->command = new UsersRestoreCommand($manager, new PasswordStrengthHandler(), $this->userRestoreService); + $this->command = new UsersRestoreCommand($repository, $this->userRestoreService, $consolePasswordHelper); } public function testExecuteWithoutMailCrypt(): void @@ -148,4 +150,19 @@ public function testExecuteWithoutUser(): void self::assertSame(Command::FAILURE, $exitCode); self::assertStringContainsString('User with email', $commandTester->getDisplay()); } + + public function testCommandConfiguration(): void + { + $application = new Application(); + $application->addCommand($this->command); + $command = $application->find('app:users:restore'); + + self::assertEquals('app:users:restore', $command->getName()); + self::assertEquals('Restore a user', $command->getDescription()); + + $definition = $command->getDefinition(); + self::assertTrue($definition->hasOption('user')); + self::assertEquals('u', $definition->getOption('user')->getShortcut()); + self::assertTrue($definition->hasOption('dry-run')); + } } diff --git a/tests/Command/VoucherCountCommandTest.php b/tests/Command/VoucherCountCommandTest.php index 90c4a322..ceb6ab03 100644 --- a/tests/Command/VoucherCountCommandTest.php +++ b/tests/Command/VoucherCountCommandTest.php @@ -6,11 +6,8 @@ use App\Command\VoucherCountCommand; use App\Entity\User; -use App\Entity\Voucher; -use App\Handler\PasswordStrengthHandler; use App\Repository\UserRepository; use App\Repository\VoucherRepository; -use Doctrine\ORM\EntityManagerInterface; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; @@ -38,13 +35,7 @@ protected function setUp(): void return [2, 5][$voucherCallCount++]; }); - $manager = $this->createStub(EntityManagerInterface::class); - $manager->method('getRepository')->willReturnMap([ - [User::class, $userRepository], - [Voucher::class, $voucherRepository], - ]); - - $this->command = new VoucherCountCommand($manager, new PasswordStrengthHandler()); + $this->command = new VoucherCountCommand($userRepository, $voucherRepository); } public function testExecuteWithUnknownUser(): void @@ -83,4 +74,19 @@ public function testExecuteWithUser(): void self::assertStringContainsString('Used: 2', $output); self::assertStringContainsString('Unused: 5', $output); } + + public function testCommandConfiguration(): void + { + $application = new Application(); + $application->addCommand($this->command); + $command = $application->find('app:voucher:count'); + + self::assertEquals('app:voucher:count', $command->getName()); + self::assertEquals('Get count of vouchers for a specific user', $command->getDescription()); + + $definition = $command->getDefinition(); + self::assertTrue($definition->hasOption('user')); + self::assertEquals('u', $definition->getOption('user')->getShortcut()); + self::assertFalse($definition->hasOption('dry-run')); + } } diff --git a/tests/Command/VoucherCreateCommandTest.php b/tests/Command/VoucherCreateCommandTest.php index f1e744c8..9464a7e6 100644 --- a/tests/Command/VoucherCreateCommandTest.php +++ b/tests/Command/VoucherCreateCommandTest.php @@ -9,12 +9,10 @@ use App\Entity\User; use App\Entity\Voucher; use App\Exception\ValidationException; -use App\Handler\PasswordStrengthHandler; use App\Repository\DomainRepository; use App\Repository\UserRepository; use App\Service\SettingsService; use App\Service\VoucherManager; -use Doctrine\ORM\EntityManagerInterface; use PHPUnit\Framework\MockObject\Stub; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Application; @@ -39,13 +37,8 @@ protected function setUp(): void $this->domain = new Domain(); $this->domain->setName('example.org'); - $manager = $this->createStub(EntityManagerInterface::class); $this->repository = $this->createStub(UserRepository::class); $this->domainRepository = $this->createStub(DomainRepository::class); - $manager->method('getRepository')->willReturnMap([ - [User::class, $this->repository], - [Domain::class, $this->domainRepository], - ]); $this->router = $this->createStub(RouterInterface::class); $this->voucherManager = $this->createStub(VoucherManager::class); @@ -56,8 +49,8 @@ protected function setUp(): void $this->router->method('getContext')->willReturn($this->requestContext); $this->command = new VoucherCreateCommand( - $manager, - new PasswordStrengthHandler(), + $this->repository, + $this->domainRepository, $this->router, $this->voucherManager, $this->settingsService @@ -178,13 +171,8 @@ public function testExecuteWithPrintOption(): void $user = new User($email); $user->setDomain($this->domain); - $manager = $this->createStub(EntityManagerInterface::class); $repository = $this->createMock(UserRepository::class); $domainRepository = $this->createStub(DomainRepository::class); - $manager->method('getRepository')->willReturnMap([ - [User::class, $repository], - [Domain::class, $domainRepository], - ]); $settingsService = $this->createMock(SettingsService::class); $voucherManager = $this->createMock(VoucherManager::class); $requestContext = $this->createMock(RequestContext::class); @@ -212,7 +200,7 @@ public function testExecuteWithPrintOption(): void ->with($user, $this->domain) ->willReturn($voucher); - $command = new VoucherCreateCommand($manager, new PasswordStrengthHandler(), $router, $voucherManager, $settingsService); + $command = new VoucherCreateCommand($repository, $domainRepository, $router, $voucherManager, $settingsService); $application = new Application(); $application->addCommand($command); @@ -242,13 +230,8 @@ public function testExecuteWithPrintLinksOption(): void $user = new User($email); $user->setDomain($this->domain); - $manager = $this->createStub(EntityManagerInterface::class); $repository = $this->createMock(UserRepository::class); $domainRepository = $this->createStub(DomainRepository::class); - $manager->method('getRepository')->willReturnMap([ - [User::class, $repository], - [Domain::class, $domainRepository], - ]); $settingsService = $this->createMock(SettingsService::class); $voucherManager = $this->createMock(VoucherManager::class); $requestContext = $this->createMock(RequestContext::class); @@ -281,7 +264,7 @@ public function testExecuteWithPrintLinksOption(): void ->with('register_voucher', ['voucher' => $voucherCode]) ->willReturn($baseUrl.'/register/'.$voucherCode); - $command = new VoucherCreateCommand($manager, new PasswordStrengthHandler(), $router, $voucherManager, $settingsService); + $command = new VoucherCreateCommand($repository, $domainRepository, $router, $voucherManager, $settingsService); $application = new Application(); $application->addCommand($command); @@ -373,4 +356,26 @@ public function testExecuteWithUnknownDomain(): void self::assertSame(Command::FAILURE, $exitCode); self::assertStringContainsString('Domain unknown.org not found', $commandTester->getDisplay()); } + + public function testCommandConfiguration(): void + { + $application = new Application(); + $application->addCommand($this->command); + $command = $application->find('app:voucher:create'); + + self::assertEquals('app:voucher:create', $command->getName()); + self::assertEquals('Create voucher for a specific user', $command->getDescription()); + + $definition = $command->getDefinition(); + self::assertTrue($definition->hasOption('user')); + self::assertEquals('u', $definition->getOption('user')->getShortcut()); + self::assertTrue($definition->hasOption('count')); + self::assertEquals('c', $definition->getOption('count')->getShortcut()); + self::assertTrue($definition->hasOption('print')); + self::assertEquals('p', $definition->getOption('print')->getShortcut()); + self::assertTrue($definition->hasOption('print-links')); + self::assertEquals('l', $definition->getOption('print-links')->getShortcut()); + self::assertTrue($definition->hasOption('domain')); + self::assertEquals('d', $definition->getOption('domain')->getShortcut()); + } }