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());
+ }
}