diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ea5b271..b161d7c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -3,10 +3,10 @@ name: 'CI' on: push: branches: - - main + - '2.0' pull_request: branches: - - main + - '2.0' jobs: cs-fixer: @@ -22,7 +22,7 @@ jobs: steps: - name: 'Check out' - uses: 'actions/checkout@v2' + uses: 'actions/checkout@v6' - name: 'Set up PHP' @@ -38,7 +38,7 @@ jobs: - name: 'Cache dependencies' - uses: 'actions/cache@v2' + uses: 'actions/cache@v4' with: path: '${{ steps.composer-cache.outputs.cache-dir }}' key: "php-${{ matrix.php-version }}-composer-locked-${{ hashFiles('composer.lock') }}" @@ -52,49 +52,6 @@ jobs: name: 'Check the code style' run: 'make cs-full' - phpstan: - name: 'PhpStan' - - runs-on: 'ubuntu-latest' - - strategy: - matrix: - php-version: - - '8.2' - - steps: - - - name: 'Check out' - uses: 'actions/checkout@v2' - - - - name: 'Set up PHP' - uses: 'shivammathur/setup-php@v2' - with: - php-version: '${{ matrix.php-version }}' - coverage: 'none' - - - - name: 'Get Composer cache directory' - id: 'composer-cache' - run: 'echo "::set-output name=cache-dir::$(composer config cache-files-dir)"' - - - - name: 'Cache dependencies' - uses: 'actions/cache@v2' - with: - path: '${{ steps.composer-cache.outputs.cache-dir }}' - key: "php-${{ matrix.php-version }}-composer-locked-${{ hashFiles('composer.lock') }}" - restore-keys: 'php-${{ matrix.php-version }}-composer-locked-' - - - - name: 'Install dependencies' - run: 'composer update --no-progress --prefer-stable' - - - - name: 'Run PhpStan' - run: 'vendor/bin/phpstan analyze --no-progress' - tests: name: 'PHPUnit' @@ -106,21 +63,22 @@ jobs: - php-version: '8.2' composer-options: '--prefer-stable' - symfony-version: '6.3' + symfony-version: '^6.4' + - php-version: '8.2' composer-options: '--prefer-stable' - symfony-version: '^6.4' + symfony-version: '^7.4' - - php-version: '8.2' + php-version: '8.4' composer-options: '--prefer-stable' - symfony-version: '^7.0' + symfony-version: '^8.0' steps: - name: 'Check out' - uses: 'actions/checkout@v2' + uses: 'actions/checkout@v6' - name: 'Set up PHP' @@ -136,7 +94,7 @@ jobs: - name: 'Cache dependencies' - uses: 'actions/cache@v2' + uses: 'actions/cache@v4' with: path: '${{ steps.composer-cache.outputs.cache-dir }}' key: "php-${{ matrix.php-version }}-composer-locked-${{ hashFiles('composer.lock') }}" diff --git a/.gitignore b/.gitignore index 32cb520..453f870 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ symfony.lock .phpunit.result.cache .php-cs-fixer.cache +.phpunit.cache/ diff --git a/composer.json b/composer.json index 334e623..6ab8c68 100644 --- a/composer.json +++ b/composer.json @@ -19,18 +19,18 @@ } ], "require": { - "php": ">=8.2", - "symfony/config": "^6.0 || ^7.0", + "php": "^8.2", + "symfony/config": "^6.4 || ^7.4 || ^8.0", "symfony/polyfill-mbstring": "^1.5.0", - "symfony/translation": "^6.0 || ^7.0", - "symfony/validator": "^6.0 || ^7.0" + "symfony/translation": "^6.4 || ^7.4 || ^8.0", + "symfony/validator": "^6.4 || ^7.4 || ^8.0" }, "require-dev": { "phpstan/phpstan": "^1.10", "phpstan/phpstan-phpunit": "^1.1", "phpstan/phpstan-symfony": "^1.2", "phpunit/phpunit": "^9.5", - "symfony/phpunit-bridge": "^6.0 || ^7.0" + "symfony/phpunit-bridge": "^7.4 || ^8.0" }, "minimum-stability": "dev", "prefer-stable": true, diff --git a/phpunit.xml.dist b/phpunit.xml.dist index b81e113..811c285 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -17,7 +17,7 @@ - + diff --git a/src/Validator/Constraints/ConstraintCompatTrait.php b/src/Validator/Constraints/ConstraintCompatTrait.php new file mode 100644 index 0000000..bc1ecb9 --- /dev/null +++ b/src/Validator/Constraints/ConstraintCompatTrait.php @@ -0,0 +1,154 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Rollerworks\Component\PasswordStrength\Validator\Constraints; + +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; +use Symfony\Component\Validator\Exception\InvalidOptionsException; +use Symfony\Component\Validator\Exception\MissingOptionsException; + +if (method_exists(Constraint::class, 'normalizeOptions')) { + /** + * @internal + */ + trait ConstraintCompatTrait + { + protected function initOptions(?array $options, ?array $groups, mixed $payload): void + { + // Noop + } + } +} else { + + /** + * @internal + */ + trait ConstraintCompatTrait + { + protected function initOptions(?array $options, ?array $groups, mixed $payload): void + { + if ($options === null) { + return; + } + + trigger_deprecation('symfony/validator', '7.4', 'Support for evaluating options in the %1$s class is deprecated. Initialize properties in the constructor of %1$s instead.', static::class); + + $options = $this->normalizeOptions($options); + + if ($groups !== null) { + $options['groups'] = $groups; + } + $options['payload'] = $payload ?? $options['payload'] ?? null; + + foreach ($options as $name => $value) { + $this->{$name} = $value; + } + } + + /** + * @deprecated since Symfony 7.4 + * + * @return array + */ + protected function normalizeOptions(mixed $options): array + { + $normalizedOptions = []; + $defaultOption = $this->getDefaultOption(false); + $invalidOptions = []; + $missingOptions = array_flip($this->getRequiredOptions(false)); + $knownOptions = get_class_vars(static::class); + + if (\is_array($options) && isset($options['value']) && ! property_exists($this, 'value')) { + if ($defaultOption === null) { + throw new ConstraintDefinitionException(\sprintf('No default option is configured for constraint "%s".', static::class)); + } + + $options[$defaultOption] = $options['value']; + unset($options['value']); + } + + if (\is_array($options)) { + reset($options); + } + + if ($options && \is_array($options) && \is_string(key($options))) { + foreach ($options as $option => $value) { + if (\array_key_exists($option, $knownOptions)) { + $normalizedOptions[$option] = $value; + unset($missingOptions[$option]); + } else { + $invalidOptions[] = $option; + } + } + } elseif ($options !== null && ! (\is_array($options) && \count($options) === 0)) { + if ($defaultOption === null) { + throw new ConstraintDefinitionException(\sprintf('No default option is configured for constraint "%s".', static::class)); + } + + if (\array_key_exists($defaultOption, $knownOptions)) { + $normalizedOptions[$defaultOption] = $options; + unset($missingOptions[$defaultOption]); + } else { + $invalidOptions[] = $defaultOption; + } + } + + if (\count($invalidOptions) > 0) { + throw new InvalidOptionsException(\sprintf('The options "%s" do not exist in constraint "%s".', implode('", "', $invalidOptions), static::class), $invalidOptions); + } + + if (\count($missingOptions) > 0) { + throw new MissingOptionsException(\sprintf('The options "%s" must be set for constraint "%s".', implode('", "', array_keys($missingOptions)), static::class), array_keys($missingOptions)); + } + + return $normalizedOptions; + } + + /** + * Returns the name of the default option. + * + * Override this method to define a default option. + * + * @deprecated since Symfony 7.4 + * @see __construct() + */ + public function getDefaultOption(): ?string + { + if (\func_num_args() === 0 || func_get_arg(0)) { + trigger_deprecation('symfony/validator', '7.4', 'The %s() method is deprecated.', __METHOD__); + } + + return null; + } + + /** + * Returns the name of the required options. + * + * Override this method if you want to define required options. + * + * @return string[] + * + * @deprecated since Symfony 7.4 + * @see __construct() + */ + public function getRequiredOptions(): array + { + if (\func_num_args() === 0 || func_get_arg(0)) { + trigger_deprecation('symfony/validator', '7.4', 'The %s() method is deprecated.', __METHOD__); + } + + return []; + } + } +} diff --git a/src/Validator/Constraints/PasswordRequirements.php b/src/Validator/Constraints/PasswordRequirements.php index 0e89c5f..9ff78f7 100644 --- a/src/Validator/Constraints/PasswordRequirements.php +++ b/src/Validator/Constraints/PasswordRequirements.php @@ -15,12 +15,13 @@ /** * @Annotation - * * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class PasswordRequirements extends Constraint { + use ConstraintCompatTrait; + public string $tooShortMessage = 'Your password must be at least {{length}} characters long.'; public string $missingLettersMessage = 'Your password must include at least one letter.'; public string $requireCaseDiffMessage = 'Your password must include both upper and lower case letters.'; @@ -49,6 +50,7 @@ public function __construct( ?string $missingSpecialCharacterMessage = null ) { parent::__construct($options ?? [], $groups, $payload); + $this->initOptions($options, $groups, $payload); $this->tooShortMessage = $tooShortMessage ?? $this->tooShortMessage; $this->missingLettersMessage = $missingLettersMessage ?? $this->missingLettersMessage; diff --git a/src/Validator/Constraints/PasswordStrength.php b/src/Validator/Constraints/PasswordStrength.php index d4c364b..ccbc853 100644 --- a/src/Validator/Constraints/PasswordStrength.php +++ b/src/Validator/Constraints/PasswordStrength.php @@ -15,12 +15,13 @@ /** * @Annotation - * * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class PasswordStrength extends Constraint { + use ConstraintCompatTrait; + public string $tooShortMessage = 'Your password must be at least {{length}} characters long.'; public string $message = 'password_too_weak'; public int $minLength = 6; @@ -51,6 +52,7 @@ public function __construct( } parent::__construct($finalOptions, $groups, $payload); + $this->initOptions($finalOptions, $groups, $payload); $this->minLength = $minLength ?? $this->minLength; $this->unicodeEquality = $unicodeEquality ?? $this->unicodeEquality; diff --git a/tests/Validator/PasswordRequirementsValidatorTest.php b/tests/Validator/PasswordRequirementsValidatorTest.php index 9e0eb40..5720399 100644 --- a/tests/Validator/PasswordRequirementsValidatorTest.php +++ b/tests/Validator/PasswordRequirementsValidatorTest.php @@ -46,7 +46,7 @@ public function empty_is_valid(): void } /** - * @dataProvider provideValid_value_constraintsCases + * @dataProvider provideValid_value_constraintsCasesLegacy * * @test */ @@ -59,14 +59,39 @@ public function valid_value_constraints(string $value, PasswordRequirements $con $this->assertNoViolation(); } + /** + * @return iterable + */ + public static function provideValid_value_constraintsCasesLegacy(): iterable + { + return [ + ['test', new PasswordRequirements(['minLength' => 3])], + ['1234567', new PasswordRequirements(['requireLetters' => false])], + ['1234567', new PasswordRequirements(['requireLetters' => false])], + ['aBcDez', new PasswordRequirements(['requireCaseDiff' => true])], + ['abcdef', new PasswordRequirements(['requireNumbers' => false])], + ['123456', new PasswordRequirements(['requireLetters' => false, 'requireNumbers' => true])], + ['123456789', new PasswordRequirements(['requireLetters' => false, 'requireNumbers' => true])], + ['abcd12345', new PasswordRequirements(['requireLetters' => true, 'requireNumbers' => true])], + ['1234abc56789', new PasswordRequirements(['requireLetters' => true, 'requireNumbers' => true])], + + ['®', new PasswordRequirements(['minLength' => 1, 'requireLetters' => false, 'requireSpecialCharacter' => true])], + ['»', new PasswordRequirements(['minLength' => 1, 'requireLetters' => false, 'requireSpecialCharacter' => true])], + ['<>', new PasswordRequirements(['minLength' => 1, 'requireLetters' => false, 'requireSpecialCharacter' => true])], + ['{}', new PasswordRequirements(['minLength' => 1, 'requireLetters' => false, 'requireSpecialCharacter' => true])], + ]; + } + /** * @dataProvider provideViolation_value_constraintsCases * * @test * + * @group legacy + * * @param array}> $violations */ - public function violation_value_constraints(string $value, PasswordRequirements $constraint, array $violations = []): void + public function violation_value_constraints_legacy(string $value, PasswordRequirements $constraint, array $violations = []): void { $this->value = $value; /** @var ConstraintViolationAssertion $constraintViolationAssertion */ @@ -97,26 +122,40 @@ public function violation_value_constraints(string $value, PasswordRequirements } /** - * @return iterable + * @dataProvider provideViolation_value_constraintsCases + * + * @test + * + * @param array}> $violations */ - public static function provideValid_value_constraintsCases(): iterable + public function violation_value_constraints(string $value, PasswordRequirements $constraint, array $violations = []): void { - return [ - ['test', new PasswordRequirements(['minLength' => 3])], - ['1234567', new PasswordRequirements(['requireLetters' => false])], - ['1234567', new PasswordRequirements(['requireLetters' => false])], - ['aBcDez', new PasswordRequirements(['requireCaseDiff' => true])], - ['abcdef', new PasswordRequirements(['requireNumbers' => false])], - ['123456', new PasswordRequirements(['requireLetters' => false, 'requireNumbers' => true])], - ['123456789', new PasswordRequirements(['requireLetters' => false, 'requireNumbers' => true])], - ['abcd12345', new PasswordRequirements(['requireLetters' => true, 'requireNumbers' => true])], - ['1234abc56789', new PasswordRequirements(['requireLetters' => true, 'requireNumbers' => true])], + $this->value = $value; + /** @var ConstraintViolationAssertion $constraintViolationAssertion */ + $constraintViolationAssertion = null; // Shut-up PHPStan - ['®', new PasswordRequirements(['minLength' => 1, 'requireLetters' => false, 'requireSpecialCharacter' => true])], - ['»', new PasswordRequirements(['minLength' => 1, 'requireLetters' => false, 'requireSpecialCharacter' => true])], - ['<>', new PasswordRequirements(['minLength' => 1, 'requireLetters' => false, 'requireSpecialCharacter' => true])], - ['{}', new PasswordRequirements(['minLength' => 1, 'requireLetters' => false, 'requireSpecialCharacter' => true])], - ]; + $this->validator->validate($value, $constraint); + + /** + * @var array $violation + */ + foreach ($violations as $i => $violation) { + if ($i === 0) { + $constraintViolationAssertion = $this->buildViolation($violation[0]) + ->setParameters($violation[1] ?? []) + ->setInvalidValue($value) + ; + } else { + $constraintViolationAssertion = $constraintViolationAssertion->buildNextViolation($violation[0]) + ->setParameters($violation[1] ?? []) + ->setInvalidValue($value) + ; + } + + if ($i == \count($violations) - 1) { + $constraintViolationAssertion->assertRaised(); + } + } } /** @@ -127,25 +166,48 @@ public static function provideViolation_value_constraintsCases(): iterable $constraint = new PasswordRequirements(); return [ - ['1', new PasswordRequirements(['minLength' => 2, 'requireLetters' => false]), [ + ['1', new PasswordRequirements(minLength: 2, requireLetters: false), [ [$constraint->tooShortMessage, ['{{length}}' => 2]], ]], - ['test', new PasswordRequirements(['requireLetters' => true]), [ + ['test', new PasswordRequirements(requireLetters: true), [ [$constraint->tooShortMessage, ['{{length}}' => $constraint->minLength]], ]], - ['123456', new PasswordRequirements(['requireLetters' => true]), [ + ['123456', new PasswordRequirements(requireLetters: true), [ [$constraint->missingLettersMessage], ]], - ['abcdez', new PasswordRequirements(['requireCaseDiff' => true]), [ + ['abcdez', new PasswordRequirements(requireCaseDiff: true), [ [$constraint->requireCaseDiffMessage], ]], - ['!@#$%^&*()-', new PasswordRequirements(['requireLetters' => true, 'requireNumbers' => true]), [ + ['!@#$%^&*()-', new PasswordRequirements(requireLetters: true, requireNumbers: true), [ [$constraint->missingLettersMessage], [$constraint->missingNumbersMessage], ]], - ['aerfghy', new PasswordRequirements(['requireLetters' => false, 'requireSpecialCharacter' => true]), [ + ['aerfghy', new PasswordRequirements(requireLetters: false, requireSpecialCharacter: true), [ [$constraint->missingSpecialCharacterMessage], ]], ]; } + + /** + * @return iterable + */ + public static function provideValid_value_constraintsCases(): iterable + { + return [ + ['test', new PasswordRequirements(['minLength' => 3])], + ['1234567', new PasswordRequirements(['requireLetters' => false])], + ['1234567', new PasswordRequirements(['requireLetters' => false])], + ['aBcDez', new PasswordRequirements(['requireCaseDiff' => true])], + ['abcdef', new PasswordRequirements(['requireNumbers' => false])], + ['123456', new PasswordRequirements(['requireLetters' => false, 'requireNumbers' => true])], + ['123456789', new PasswordRequirements(['requireLetters' => false, 'requireNumbers' => true])], + ['abcd12345', new PasswordRequirements(['requireLetters' => true, 'requireNumbers' => true])], + ['1234abc56789', new PasswordRequirements(['requireLetters' => true, 'requireNumbers' => true])], + + ['®', new PasswordRequirements(['minLength' => 1, 'requireLetters' => false, 'requireSpecialCharacter' => true])], + ['»', new PasswordRequirements(['minLength' => 1, 'requireLetters' => false, 'requireSpecialCharacter' => true])], + ['<>', new PasswordRequirements(['minLength' => 1, 'requireLetters' => false, 'requireSpecialCharacter' => true])], + ['{}', new PasswordRequirements(['minLength' => 1, 'requireLetters' => false, 'requireSpecialCharacter' => true])], + ]; + } } diff --git a/tests/Validator/PasswordStrengthLegacyTest.php b/tests/Validator/PasswordStrengthLegacyTest.php new file mode 100644 index 0000000..ac9578a --- /dev/null +++ b/tests/Validator/PasswordStrengthLegacyTest.php @@ -0,0 +1,304 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Rollerworks\Component\PasswordStrength\Tests\Validator; + +use Rollerworks\Component\PasswordStrength\Validator\Constraints\PasswordStrength; +use Rollerworks\Component\PasswordStrength\Validator\Constraints\PasswordStrengthValidator; +use Symfony\Component\Translation\Translator; +use Symfony\Component\Validator\ConstraintValidatorInterface; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; +use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; + +/** + * @internal + * + * @template-extends ConstraintValidatorTestCase + * + * @group legacy + */ +final class PasswordStrengthLegacyTest extends ConstraintValidatorTestCase +{ + /** + * @var array + */ + private static $levelToLabel = [ + 1 => 'very_weak', + 2 => 'weak', + 3 => 'medium', + 4 => 'strong', + 5 => 'very_strong', + ]; + + protected function createValidator(): ConstraintValidatorInterface + { + return new PasswordStrengthValidator(new Translator('en')); + } + + /** @test */ + public function constraints_options_are_properly_resolved(): void + { + // Default option + $constraint = new PasswordStrength(3); + self::assertEquals(3, $constraint->minStrength); + + // By option + $constraint = new PasswordStrength(['minStrength' => 3]); + self::assertEquals(3, $constraint->minStrength); + + // Specific argument + $constraint = new PasswordStrength(null, null, null, 3); + self::assertEquals(3, $constraint->minStrength); + } + + /** @test */ + public function null_is_valid(): void + { + $this->validator->validate(null, new PasswordStrength(6)); + + $this->assertNoViolation(); + } + + /** @test */ + public function empty_is_valid(): void + { + $this->validator->validate('', new PasswordStrength(6)); + + $this->assertNoViolation(); + } + + /** @test */ + public function expects_string_compatible_type(): void + { + $this->expectException(UnexpectedTypeException::class); + + $this->validator->validate(new \stdClass(), new PasswordStrength(5)); + } + + /** + * @return iterable + */ + public static function provideStrongPasswords(): iterable + { + return [ + ['Foobar!55!'], + ['Foobar$55'], + ['Foobar€55'], + ['Foobar€55'], + ]; + } + + /** @test */ + public function short_password_will_not_pass(): void + { + $constraint = new PasswordStrength(['minStrength' => 5, 'minLength' => 6]); + + $this->validator->validate('foo', $constraint); + + $parameters = [ + '{{length}}' => 6, + ]; + + $this->buildViolation('Your password must be at least {{length}} characters long.') + ->setParameters($parameters) + ->assertRaised() + ; + } + + /** @test */ + public function short_password_in_multi_byte_will_not_pass(): void + { + $constraint = new PasswordStrength(['minStrength' => 5, 'minLength' => 7]); + + $this->validator->validate('foöled', $constraint); + + $parameters = [ + '{{length}}' => 7, + ]; + + $this->buildViolation('Your password must be at least {{length}} characters long.') + ->setParameters($parameters) + ->assertRaised() + ; + } + + /** + * @dataProvider provideWeak_passwords_will_not_passCases + * + * @test + */ + public function weak_passwords_will_not_pass(int $minStrength, string $value, int $currentStrength, string $tips = ''): void + { + $constraint = new PasswordStrength(['minStrength' => $minStrength, 'minLength' => 6]); + + $this->validator->validate($value, $constraint); + + $parameters = [ + '{{ length }}' => 6, + '{{ min_strength }}' => 'rollerworks_password.strength_level.' . self::$levelToLabel[$minStrength], + '{{ current_strength }}' => 'rollerworks_password.strength_level.' . self::$levelToLabel[$currentStrength], + '{{ strength_tips }}' => $tips, + ]; + + $this->buildViolation('password_too_weak') + ->setParameters($parameters) + ->assertRaised() + ; + } + + /** + * @return iterable + */ + public static function provideWeak_passwords_will_not_passCases(): iterable + { + $pre = 'rollerworks_password.tip.'; + + return [ + // Very weak + [2, 'weaker', 1, "{$pre}uppercase_letters, {$pre}numbers, {$pre}special_chars, {$pre}length"], + [2, '123456', 1, "{$pre}letters, {$pre}special_chars, {$pre}length"], + [2, 'foobar', 1, "{$pre}uppercase_letters, {$pre}numbers, {$pre}special_chars, {$pre}length"], + [2, '!.!.!.', 1, "{$pre}letters, {$pre}numbers, {$pre}length"], + + // Weak + [3, 'wee6eak', 2, "{$pre}uppercase_letters, {$pre}special_chars, {$pre}length"], + [3, 'foobar!', 2, "{$pre}uppercase_letters, {$pre}numbers, {$pre}length"], + [3, 'Foobar', 2, "{$pre}numbers, {$pre}special_chars, {$pre}length"], + [3, '123456!', 2, "{$pre}letters, {$pre}length"], + [3, '7857375923752947', 2, "{$pre}letters, {$pre}special_chars"], + [3, 'FSDFJSLKFFSDFDSF', 2, "{$pre}lowercase_letters, {$pre}numbers, {$pre}special_chars"], + [3, 'fjsfjdljfsjsjjlsj', 2, "{$pre}uppercase_letters, {$pre}numbers, {$pre}special_chars"], + + // Medium + [4, 'Foobar!', 3, "{$pre}numbers, {$pre}length"], + [4, 'foo-b0r!', 3, "{$pre}uppercase_letters, {$pre}length"], + [4, 'fjsfjdljfsjsjjls1', 3, "{$pre}uppercase_letters, {$pre}special_chars"], + [4, '785737592375294b', 3, "{$pre}uppercase_letters, {$pre}special_chars"], + ]; + } + + /** + * @dataProvider provideWeak_passwords_with_unicode_will_not_passCases + * + * @test + */ + public function weak_passwords_with_unicode_will_not_pass(int $minStrength, string $value, int $currentStrength, string $tips = ''): void + { + $constraint = new PasswordStrength(['minStrength' => $minStrength, 'minLength' => 6, 'unicodeEquality' => true]); + + $this->validator->validate($value, $constraint); + + $parameters = [ + '{{ length }}' => 6, + '{{ min_strength }}' => 'rollerworks_password.strength_level.' . self::$levelToLabel[$minStrength], + '{{ current_strength }}' => 'rollerworks_password.strength_level.' . self::$levelToLabel[$currentStrength], + '{{ strength_tips }}' => $tips, + ]; + + $this->buildViolation('password_too_weak') + ->setParameters($parameters) + ->assertRaised() + ; + } + + /** + * @return iterable + */ + public static function provideWeak_passwords_with_unicode_will_not_passCases(): iterable + { + $pre = 'rollerworks_password.tip.'; + + // \u{FD3E} = ﴾ = Arabic ornate left parenthesis + + return [ + // Very weak + [2, 'weaker', 1, "{$pre}uppercase_letters, {$pre}numbers, {$pre}special_chars, {$pre}length"], + [2, '123456', 1, "{$pre}letters, {$pre}special_chars, {$pre}length"], + [2, '²²²²²²', 1, "{$pre}letters, {$pre}special_chars, {$pre}length"], + [2, 'foobar', 1, "{$pre}uppercase_letters, {$pre}numbers, {$pre}special_chars, {$pre}length"], + [2, 'ömgwat', 1, "{$pre}uppercase_letters, {$pre}numbers, {$pre}special_chars, {$pre}length"], + [2, '!.!.!.', 1, "{$pre}letters, {$pre}numbers, {$pre}length"], + [2, '!.!.!﴾', 1, "{$pre}letters, {$pre}numbers, {$pre}length"], + + // Weak + [3, 'wee6eak', 2, "{$pre}uppercase_letters, {$pre}special_chars, {$pre}length"], + [3, 'foobar!', 2, "{$pre}uppercase_letters, {$pre}numbers, {$pre}length"], + [3, 'Foobar', 2, "{$pre}numbers, {$pre}special_chars, {$pre}length"], + [3, '123456!', 2, "{$pre}letters, {$pre}length"], + [3, '7857375923752947', 2, "{$pre}letters, {$pre}special_chars"], + [3, 'FSDFJSLKFFSDFDSF', 2, "{$pre}lowercase_letters, {$pre}numbers, {$pre}special_chars"], + [3, 'FÜKFJSLKFFSDFDSF', 2, "{$pre}lowercase_letters, {$pre}numbers, {$pre}special_chars"], + [3, 'fjsfjdljfsjsjjlsj', 2, "{$pre}uppercase_letters, {$pre}numbers, {$pre}special_chars"], + + // Medium + [4, 'Foobar﴾', 3, "{$pre}numbers, {$pre}length"], + [4, 'foo-b0r!', 3, "{$pre}uppercase_letters, {$pre}length"], + [4, 'fjsfjdljfsjsjjls1', 3, "{$pre}uppercase_letters, {$pre}special_chars"], + [4, '785737592375294b', 3, "{$pre}uppercase_letters, {$pre}special_chars"], + ]; + } + + /** + * @dataProvider provideStrong_passwords_will_passCases + * + * @test + */ + public function strong_passwords_will_pass(string $value): void + { + $constraint = new PasswordStrength(5); + + $this->validator->validate($value, $constraint); + + $this->assertNoViolation(); + } + + /** + * @return iterable + */ + public static function provideStrong_passwords_will_passCases(): iterable + { + return [ + ['Foobar$55_4&F'], + ['L33RoyJ3Jenkins!'], + ]; + } + + /** @test */ + public function constraint_get_default_option(): void + { + $constraint = new PasswordStrength(5); + + self::assertEquals(5, $constraint->minStrength); + } + + /** @test */ + public function parameters_are_translated_when_translator_is_missing(): void + { + $this->validator = new PasswordStrengthValidator(); + $this->validator->initialize($this->context); + + $constraint = new PasswordStrength(['minStrength' => 5, 'minLength' => 6]); + + $this->validator->validate('FD43f.!', $constraint); + + $parameters = [ + '{{ length }}' => 6, + '{{ current_strength }}' => 'Strong', + '{{ min_strength }}' => 'Very strong', + '{{ strength_tips }}' => 'add more characters', + ]; + + $this->buildViolation('password_too_weak') + ->setParameters($parameters) + ->assertRaised() + ; + } +} diff --git a/tests/Validator/PasswordStrengthTest.php b/tests/Validator/PasswordStrengthTest.php index 1a82025..070a38b 100644 --- a/tests/Validator/PasswordStrengthTest.php +++ b/tests/Validator/PasswordStrengthTest.php @@ -41,22 +41,6 @@ protected function createValidator(): ConstraintValidatorInterface return new PasswordStrengthValidator(new Translator('en')); } - /** @test */ - public function constraints_options_are_properly_resolved(): void - { - // Default option - $constraint = new PasswordStrength(3); - self::assertEquals(3, $constraint->minStrength); - - // By option - $constraint = new PasswordStrength(['minStrength' => 3]); - self::assertEquals(3, $constraint->minStrength); - - // Specific argument - $constraint = new PasswordStrength(null, null, null, 3); - self::assertEquals(3, $constraint->minStrength); - } - /** @test */ public function null_is_valid(): void { @@ -81,74 +65,6 @@ public function expects_string_compatible_type(): void $this->validator->validate(new \stdClass(), new PasswordStrength(5)); } - /** - * @return iterable - */ - public function provideWeak_passwords_will_not_passCases(): iterable - { - $pre = 'rollerworks_password.tip.'; - - return [ - // Very weak - [2, 'weaker', 1, "{$pre}uppercase_letters, {$pre}numbers, {$pre}special_chars, {$pre}length"], - [2, '123456', 1, "{$pre}letters, {$pre}special_chars, {$pre}length"], - [2, 'foobar', 1, "{$pre}uppercase_letters, {$pre}numbers, {$pre}special_chars, {$pre}length"], - [2, '!.!.!.', 1, "{$pre}letters, {$pre}numbers, {$pre}length"], - - // Weak - [3, 'wee6eak', 2, "{$pre}uppercase_letters, {$pre}special_chars, {$pre}length"], - [3, 'foobar!', 2, "{$pre}uppercase_letters, {$pre}numbers, {$pre}length"], - [3, 'Foobar', 2, "{$pre}numbers, {$pre}special_chars, {$pre}length"], - [3, '123456!', 2, "{$pre}letters, {$pre}length"], - [3, '7857375923752947', 2, "{$pre}letters, {$pre}special_chars"], - [3, 'FSDFJSLKFFSDFDSF', 2, "{$pre}lowercase_letters, {$pre}numbers, {$pre}special_chars"], - [3, 'fjsfjdljfsjsjjlsj', 2, "{$pre}uppercase_letters, {$pre}numbers, {$pre}special_chars"], - - // Medium - [4, 'Foobar!', 3, "{$pre}numbers, {$pre}length"], - [4, 'foo-b0r!', 3, "{$pre}uppercase_letters, {$pre}length"], - [4, 'fjsfjdljfsjsjjls1', 3, "{$pre}uppercase_letters, {$pre}special_chars"], - [4, '785737592375294b', 3, "{$pre}uppercase_letters, {$pre}special_chars"], - ]; - } - - /** - * @return iterable - */ - public function provideWeak_passwords_with_unicode_will_not_passCases(): iterable - { - $pre = 'rollerworks_password.tip.'; - - // \u{FD3E} = ﴾ = Arabic ornate left parenthesis - - return [ - // Very weak - [2, 'weaker', 1, "{$pre}uppercase_letters, {$pre}numbers, {$pre}special_chars, {$pre}length"], - [2, '123456', 1, "{$pre}letters, {$pre}special_chars, {$pre}length"], - [2, '²²²²²²', 1, "{$pre}letters, {$pre}special_chars, {$pre}length"], - [2, 'foobar', 1, "{$pre}uppercase_letters, {$pre}numbers, {$pre}special_chars, {$pre}length"], - [2, 'ömgwat', 1, "{$pre}uppercase_letters, {$pre}numbers, {$pre}special_chars, {$pre}length"], - [2, '!.!.!.', 1, "{$pre}letters, {$pre}numbers, {$pre}length"], - [2, '!.!.!﴾', 1, "{$pre}letters, {$pre}numbers, {$pre}length"], - - // Weak - [3, 'wee6eak', 2, "{$pre}uppercase_letters, {$pre}special_chars, {$pre}length"], - [3, 'foobar!', 2, "{$pre}uppercase_letters, {$pre}numbers, {$pre}length"], - [3, 'Foobar', 2, "{$pre}numbers, {$pre}special_chars, {$pre}length"], - [3, '123456!', 2, "{$pre}letters, {$pre}length"], - [3, '7857375923752947', 2, "{$pre}letters, {$pre}special_chars"], - [3, 'FSDFJSLKFFSDFDSF', 2, "{$pre}lowercase_letters, {$pre}numbers, {$pre}special_chars"], - [3, 'FÜKFJSLKFFSDFDSF', 2, "{$pre}lowercase_letters, {$pre}numbers, {$pre}special_chars"], - [3, 'fjsfjdljfsjsjjlsj', 2, "{$pre}uppercase_letters, {$pre}numbers, {$pre}special_chars"], - - // Medium - [4, 'Foobar﴾', 3, "{$pre}numbers, {$pre}length"], - [4, 'foo-b0r!', 3, "{$pre}uppercase_letters, {$pre}length"], - [4, 'fjsfjdljfsjsjjls1', 3, "{$pre}uppercase_letters, {$pre}special_chars"], - [4, '785737592375294b', 3, "{$pre}uppercase_letters, {$pre}special_chars"], - ]; - } - /** * @return iterable */ @@ -162,21 +78,10 @@ public static function provideStrongPasswords(): iterable ]; } - /** - * @return iterable - */ - public static function provideStrong_passwords_will_passCases(): iterable - { - return [ - ['Foobar$55_4&F'], - ['L33RoyJ3Jenkins!'], - ]; - } - /** @test */ public function short_password_will_not_pass(): void { - $constraint = new PasswordStrength(['minStrength' => 5, 'minLength' => 6]); + $constraint = new PasswordStrength(minStrength: 5, minLength: 6); $this->validator->validate('foo', $constraint); @@ -193,7 +98,7 @@ public function short_password_will_not_pass(): void /** @test */ public function short_password_in_multi_byte_will_not_pass(): void { - $constraint = new PasswordStrength(['minStrength' => 5, 'minLength' => 7]); + $constraint = new PasswordStrength(minStrength: 5, minLength: 7); $this->validator->validate('foöled', $constraint); @@ -208,13 +113,13 @@ public function short_password_in_multi_byte_will_not_pass(): void } /** - * @dataProvider provideWeak_passwords_will_not_passCases - * * @test + * + * @dataProvider provideWeak_passwords_will_not_passCases */ public function weak_passwords_will_not_pass(int $minStrength, string $value, int $currentStrength, string $tips = ''): void { - $constraint = new PasswordStrength(['minStrength' => $minStrength, 'minLength' => 6]); + $constraint = new PasswordStrength(minStrength: $minStrength, minLength: 6); $this->validator->validate($value, $constraint); @@ -231,6 +136,37 @@ public function weak_passwords_will_not_pass(int $minStrength, string $value, in ; } + /** + * @return iterable + */ + public static function provideWeak_passwords_will_not_passCases(): iterable + { + $pre = 'rollerworks_password.tip.'; + + return [ + // Very weak + [2, 'weaker', 1, "{$pre}uppercase_letters, {$pre}numbers, {$pre}special_chars, {$pre}length"], + [2, '123456', 1, "{$pre}letters, {$pre}special_chars, {$pre}length"], + [2, 'foobar', 1, "{$pre}uppercase_letters, {$pre}numbers, {$pre}special_chars, {$pre}length"], + [2, '!.!.!.', 1, "{$pre}letters, {$pre}numbers, {$pre}length"], + + // Weak + [3, 'wee6eak', 2, "{$pre}uppercase_letters, {$pre}special_chars, {$pre}length"], + [3, 'foobar!', 2, "{$pre}uppercase_letters, {$pre}numbers, {$pre}length"], + [3, 'Foobar', 2, "{$pre}numbers, {$pre}special_chars, {$pre}length"], + [3, '123456!', 2, "{$pre}letters, {$pre}length"], + [3, '7857375923752947', 2, "{$pre}letters, {$pre}special_chars"], + [3, 'FSDFJSLKFFSDFDSF', 2, "{$pre}lowercase_letters, {$pre}numbers, {$pre}special_chars"], + [3, 'fjsfjdljfsjsjjlsj', 2, "{$pre}uppercase_letters, {$pre}numbers, {$pre}special_chars"], + + // Medium + [4, 'Foobar!', 3, "{$pre}numbers, {$pre}length"], + [4, 'foo-b0r!', 3, "{$pre}uppercase_letters, {$pre}length"], + [4, 'fjsfjdljfsjsjjls1', 3, "{$pre}uppercase_letters, {$pre}special_chars"], + [4, '785737592375294b', 3, "{$pre}uppercase_letters, {$pre}special_chars"], + ]; + } + /** * @dataProvider provideWeak_passwords_with_unicode_will_not_passCases * @@ -238,7 +174,7 @@ public function weak_passwords_will_not_pass(int $minStrength, string $value, in */ public function weak_passwords_with_unicode_will_not_pass(int $minStrength, string $value, int $currentStrength, string $tips = ''): void { - $constraint = new PasswordStrength(['minStrength' => $minStrength, 'minLength' => 6, 'unicodeEquality' => true]); + $constraint = new PasswordStrength(minStrength: $minStrength, minLength: 6, unicodeEquality: true); $this->validator->validate($value, $constraint); @@ -255,6 +191,43 @@ public function weak_passwords_with_unicode_will_not_pass(int $minStrength, stri ; } + /** + * @return iterable + */ + public static function provideWeak_passwords_with_unicode_will_not_passCases(): iterable + { + $pre = 'rollerworks_password.tip.'; + + // \u{FD3E} = ﴾ = Arabic ornate left parenthesis + + return [ + // Very weak + [2, 'weaker', 1, "{$pre}uppercase_letters, {$pre}numbers, {$pre}special_chars, {$pre}length"], + [2, '123456', 1, "{$pre}letters, {$pre}special_chars, {$pre}length"], + [2, '²²²²²²', 1, "{$pre}letters, {$pre}special_chars, {$pre}length"], + [2, 'foobar', 1, "{$pre}uppercase_letters, {$pre}numbers, {$pre}special_chars, {$pre}length"], + [2, 'ömgwat', 1, "{$pre}uppercase_letters, {$pre}numbers, {$pre}special_chars, {$pre}length"], + [2, '!.!.!.', 1, "{$pre}letters, {$pre}numbers, {$pre}length"], + [2, '!.!.!﴾', 1, "{$pre}letters, {$pre}numbers, {$pre}length"], + + // Weak + [3, 'wee6eak', 2, "{$pre}uppercase_letters, {$pre}special_chars, {$pre}length"], + [3, 'foobar!', 2, "{$pre}uppercase_letters, {$pre}numbers, {$pre}length"], + [3, 'Foobar', 2, "{$pre}numbers, {$pre}special_chars, {$pre}length"], + [3, '123456!', 2, "{$pre}letters, {$pre}length"], + [3, '7857375923752947', 2, "{$pre}letters, {$pre}special_chars"], + [3, 'FSDFJSLKFFSDFDSF', 2, "{$pre}lowercase_letters, {$pre}numbers, {$pre}special_chars"], + [3, 'FÜKFJSLKFFSDFDSF', 2, "{$pre}lowercase_letters, {$pre}numbers, {$pre}special_chars"], + [3, 'fjsfjdljfsjsjjlsj', 2, "{$pre}uppercase_letters, {$pre}numbers, {$pre}special_chars"], + + // Medium + [4, 'Foobar﴾', 3, "{$pre}numbers, {$pre}length"], + [4, 'foo-b0r!', 3, "{$pre}uppercase_letters, {$pre}length"], + [4, 'fjsfjdljfsjsjjls1', 3, "{$pre}uppercase_letters, {$pre}special_chars"], + [4, '785737592375294b', 3, "{$pre}uppercase_letters, {$pre}special_chars"], + ]; + } + /** * @dataProvider provideStrong_passwords_will_passCases * @@ -269,12 +242,15 @@ public function strong_passwords_will_pass(string $value): void $this->assertNoViolation(); } - /** @test */ - public function constraint_get_default_option(): void + /** + * @return iterable + */ + public static function provideStrong_passwords_will_passCases(): iterable { - $constraint = new PasswordStrength(5); - - self::assertEquals(5, $constraint->minStrength); + return [ + ['Foobar$55_4&F'], + ['L33RoyJ3Jenkins!'], + ]; } /** @test */ @@ -283,7 +259,7 @@ public function parameters_are_translated_when_translator_is_missing(): void $this->validator = new PasswordStrengthValidator(); $this->validator->initialize($this->context); - $constraint = new PasswordStrength(['minStrength' => 5, 'minLength' => 6]); + $constraint = new PasswordStrength(minStrength: 5, minLength: 6); $this->validator->validate('FD43f.!', $constraint);