Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
2 / 2
CRAP
100.00% covered (success)
100.00%
1 / 1
SymfonyValidatorPasswordPolicy
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
2 / 2
4
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 assert
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4
5namespace App\Identity\Infrastructure\Security;
6
7use App\Identity\Application\Service\PasswordPolicy;
8use App\Identity\Domain\Exceptions\WeakPassword;
9use Symfony\Component\Validator\Constraints\Length;
10use Symfony\Component\Validator\Constraints\NotCompromisedPassword;
11use Symfony\Component\Validator\Constraints\Regex;
12use Symfony\Component\Validator\Validator\ValidatorInterface;
13
14final readonly class SymfonyValidatorPasswordPolicy implements PasswordPolicy
15{
16    public function __construct(
17        private ValidatorInterface $validator,
18    ) {
19    }
20
21    public function assert(string $plainPassword): void
22    {
23        // skipOnError: fail-open if HaveIBeenPwned is unreachable — an upstream
24        // outage must never block signup/change. The framework's
25        // `not_compromised_password: false` (test env) makes this a no-op offline,
26        // so functional tests never hit the network while Length stays enforced.
27        $violations = $this->validator->validate($plainPassword, [
28            new Length(min: 12),
29            new Regex(pattern: '/[a-z]/', message: 'The password must contain at least one lowercase letter.'),
30            new Regex(pattern: '/[A-Z]/', message: 'The password must contain at least one uppercase letter.'),
31            new Regex(pattern: '/\d/', message: 'The password must contain at least one digit.'),
32            new Regex(pattern: '/[^a-zA-Z\d]/', message: 'The password must contain at least one special character.'),
33            new NotCompromisedPassword(skipOnError: true),
34        ]);
35
36        if (count($violations) > 0) {
37            $messages = [];
38            foreach ($violations as $violation) {
39                $messages[] = (string) $violation->getMessage();
40            }
41
42            throw WeakPassword::of(implode(' ', $messages));
43        }
44    }
45}