Skip to content

Commit

Permalink
Merge pull request #588 from systemli/feat/voucher_user_validation
Browse files Browse the repository at this point in the history
feat(voucher): Check whether user is suspicious before creating voucher
  • Loading branch information
doobry-systemli committed Mar 31, 2024
2 parents 6265691 + 0806ccd commit 66d40d8
Show file tree
Hide file tree
Showing 13 changed files with 284 additions and 42 deletions.
2 changes: 1 addition & 1 deletion config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ services:
arguments:
$mailLocation: "%env(DOVECOT_MAIL_LOCATION)%"

App\Command\VoucherCreationCommand:
App\Command\VoucherCreateCommand:
arguments:
$appUrl: "%env(APP_URL)%"

Expand Down
6 changes: 5 additions & 1 deletion config/validator/validation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ App\Entity\User:
constraints:
- App\Validator\Constraints\EmailDomain: ~

App\Entity\Voucher:
constraints:
- App\Validator\Constraints\VoucherUser: ~

App\Form\Model\Registration:
properties:
email:
Expand All @@ -53,7 +57,7 @@ App\Form\Model\Registration:
minLength: 3
maxLength: 32
voucher:
- App\Validator\Constraints\Voucher:
- App\Validator\Constraints\VoucherExists:
exists: true
plainPassword:
- App\Validator\Constraints\PasswordPolicy: ~
Expand Down
3 changes: 3 additions & 0 deletions default_translations/de/validators.de.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@ registration:
email-too-long: Der eingegebene Benutzername darf nicht mehr als %max% Zeichen enthalten.
email-unexpected-characters: 'Der eingegebene Benutzername enthält nicht erlaubte
Zeichen. Erlaubt sind: Buchstaben und Zahlen, sowie -, _ und .'

voucher:
suspicious-user: "Nicht erlaubt, Einladungscodes für verdächtige Benutzer:innen anzulegen."
3 changes: 3 additions & 0 deletions default_translations/en/validators.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,6 @@ registration:
email-too-short: "The username must contain at least %min% characters."
email-too-long: "The username must contain at most %max% characters."
email-unexpected-characters: "The username contains unexpected characters. Only valid: Letters and numbers, as well as -, _ and ."

voucher:
suspicious-user: "Not allowed to create voucher for suspicious users."
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Feature: VoucherCreationCommand
Feature: VoucherCreateCommand

Background:
Given the database is clean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

namespace App\Command;

use App\Creator\VoucherCreator;
use App\Entity\User;
use App\Factory\VoucherFactory;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
Expand All @@ -14,9 +14,13 @@
use Symfony\Component\Security\Core\Exception\UserNotFoundException;

#[AsCommand(name: 'app:voucher:create')]
class VoucherCreationCommand extends Command
class VoucherCreateCommand extends Command
{
public function __construct(private readonly EntityManagerInterface $manager, private readonly RouterInterface $router, private readonly string $appUrl)
public function __construct(
private readonly EntityManagerInterface $manager,
private readonly RouterInterface $router,
private readonly VoucherCreator $creator,
private readonly string $appUrl)
{
parent::__construct();
}
Expand All @@ -25,10 +29,10 @@ protected function configure(): void
{
$this
->setDescription('Create voucher for a specific user')
->addOption('user', 'u', InputOption::VALUE_REQUIRED, 'User who get the voucher(s)')
->addOption('count', 'c', InputOption::VALUE_OPTIONAL, 'Count of the voucher which will created', 3)
->addOption('print', 'p', InputOption::VALUE_NONE, 'Print out vouchers')
->addOption('print-links', 'l', InputOption::VALUE_NONE, 'Print out links to vouchers');
->addOption('user', 'u', InputOption::VALUE_REQUIRED, 'User who gets the voucher(s) assigned')
->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');
}

protected function execute(InputInterface $input, OutputInterface $output): int
Expand All @@ -44,7 +48,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
}

for ($i = 1; $i <= $input->getOption('count'); ++$i) {
$voucher = VoucherFactory::create($user);
$voucher = $this->creator->create($user);
if (true === $input->getOption('print-links')) {
$output->write(sprintf("%s\n", $this->router->generate(
'register_voucher',
Expand All @@ -53,12 +57,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
} elseif (true === $input->getOption('print')) {
$output->write(sprintf("%s\n", $voucher->getCode()));
}

$this->manager->persist($voucher);
}

$this->manager->flush();

return 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Exception\MissingOptionsException;

class Voucher extends Constraint
class VoucherExists extends Constraint
{
/**
* @var bool|null
Expand All @@ -26,7 +26,9 @@ public function __construct($options = null)
parent::__construct($options);

if (null === $this->exists) {
throw new MissingOptionsException(sprintf('Option "exists" must be given for constraint %s', __CLASS__), ['min', 'max']);
throw new MissingOptionsException(
sprintf('Option "exists" must be given for constraint %s', __CLASS__), ['min', 'max']
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,15 @@

use App\Entity\Voucher;
use App\Repository\VoucherRepository;
use App\Validator\Constraints\Voucher as VoucherConstraint;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;

/**
* Class VoucherValidator.
*/
class VoucherValidator extends ConstraintValidator
class VoucherExistsValidator extends ConstraintValidator
{
private readonly VoucherRepository $voucherRepository;

/**
* VoucherValidator constructor.
*/
public function __construct(EntityManagerInterface $manager)
{
$this->voucherRepository = $manager->getRepository(Voucher::class);
Expand All @@ -33,8 +26,8 @@ public function __construct(EntityManagerInterface $manager)
*/
public function validate($value, Constraint $constraint): void
{
if (!$constraint instanceof VoucherConstraint) {
throw new UnexpectedTypeException($constraint, Voucher::class);
if (!$constraint instanceof VoucherExists) {
throw new UnexpectedTypeException($constraint, VoucherExists::class);
}

if (!is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) {
Expand Down
13 changes: 13 additions & 0 deletions src/Validator/Constraints/VoucherUser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace App\Validator\Constraints;

use Symfony\Component\Validator\Constraint;

class VoucherUser extends Constraint
{
public function getTargets(): string
{
return self::CLASS_CONSTRAINT;
}
}
36 changes: 36 additions & 0 deletions src/Validator/Constraints/VoucherUserValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace App\Validator\Constraints;

use App\Entity\User;
use App\Entity\Voucher;
use App\Enum\Roles;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;

class VoucherUserValidator extends ConstraintValidator
{
/**
* Checks if the passed value is valid.
*
* @param mixed $value The value that should be validated
* @param Constraint $constraint The constraint for the validation
*/
public function validate(mixed $value, Constraint $constraint): void
{
if (!$constraint instanceof VoucherUser) {
throw new UnexpectedTypeException($constraint, VoucherUser::class);
}

if (!$value instanceof Voucher) {
return;
}

/** @var User $user */
$user = $value->getUser();
if ($user->hasRole(Roles::SUSPICIOUS)) {
$this->context->addViolation('voucher.suspicious-user');
}
}
}
135 changes: 135 additions & 0 deletions tests/Command/VoucherCreateCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<?php

namespace App\Tests\Command;

use App\Command\VoucherCreateCommand;
use App\Creator\VoucherCreator;
use App\Entity\User;
use App\Entity\Voucher;
use App\Exception\ValidationException;
use App\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;

class VoucherCreateCommandTest extends TestCase
{
private VoucherCreateCommand $command;
private UserRepository $repository;
private RouterInterface $router;
private VoucherCreator $creator;
private string $baseUrl = 'https://users.example.org/register';
private string $voucherCode = 'code';

public function setUp(): void
{
$manager = $this->createMock(EntityManagerInterface::class);
$this->repository = $this->createMock(UserRepository::class);
$manager->method('getRepository')->willReturn($this->repository);

$this->router = $this->createMock(RouterInterface::class);

$this->creator = $this->createMock(VoucherCreator::class);

$this->command = new VoucherCreateCommand($manager, $this->router, $this->creator, $this->baseUrl);
}

public function testExecuteWithUnknownUser(): void
{
$this->repository->method('findByEmail')
->willReturn(null);

$application = new Application();
$application->add($this->command);

$command = $application->find('app:voucher:create');
$commandTester = new CommandTester($command);

$this->expectException(UserNotFoundException::class);
$commandTester->execute([
'--user' => 'user@example.org',
'--count' => 1,
'--print'
]);

$output = $commandTester->getDisplay();
self::assertStringContainsString('', $output);
}

public function testExecuteWithUser(): void
{
$user = new User();
$user->setEmail('user@example.org');
$this->repository->method('findByEmail')
->willReturn($user);

$this->router->method('generate')
->willReturn($this->baseUrl . '/' . $this->voucherCode);

$voucher = new Voucher();
$voucher->setCode($this->voucherCode);
$this->creator->method('create')
->willReturn($voucher);

$application = new Application();
$application->add($this->command);

$command = $application->find('app:voucher:create');
$commandTester = new CommandTester($command);

// Test show vouchers

$commandTester->execute([
'--user' => $user->getEmail(),
'--count' => 1,
'--print' => true,
]);

$commandTester->assertCommandIsSuccessful();

$output = $commandTester->getDisplay();
$this->assertStringContainsString($this->voucherCode, $output);

// Test show links to vouchers

$commandTester->execute([
'--user' => $user->getEmail(),
'--count' => 1,
'--print-links' => true,
]);

$commandTester->assertCommandIsSuccessful();

$output = $commandTester->getDisplay();
$this->assertStringContainsString($this->baseUrl . '/' . $this->voucherCode, $output);
}

public function testExecuteWithSuspiciousUser(): void
{
$user = new User();
$user->setEmail('suspicious@example.org');
$this->repository->method('findByEmail')
->willReturn($user);

$voucher = new Voucher();
$voucher->setCode($this->voucherCode);
$exception = $this->createMock(ValidationException::class);
$this->creator->method('create')
->willThrowException($exception);

$application = new Application();
$application->add($this->command);

$command = $application->find('app:voucher:create');
$commandTester = new CommandTester($command);

$this->expectException(ValidationException::class);
$commandTester->execute([
'--user' => $user->getEmail(),
'--count' => 1,
]);
}
}
Loading

0 comments on commit 66d40d8

Please sign in to comment.