135 lines
4.8 KiB
PHP
135 lines
4.8 KiB
PHP
<?php
|
|||
|
|
declare(strict_types=1);
|
||
|
|
|
||
|
|
require_once __DIR__ . '/../app/Domain/User/PasswordPolicy.php';
|
||
|
|
require_once __DIR__ . '/../app/Domain/User/UserRecord.php';
|
||
|
|
|
||
|
|
use App\Domain\User\PasswordPolicy;
|
||
|
|
use App\Domain\User\UserRecord;
|
||
|
|
|
||
|
|
function user_record_assert_same(mixed $expected, mixed $actual, string $message): void
|
||
|
|
{
|
||
|
|
if ($expected !== $actual) {
|
||
|
|
throw new RuntimeException($message . "\nExpected: " . var_export($expected, true) . "\nActual: " . var_export($actual, true));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$service = new UserRecord();
|
||
|
|
|
||
|
|
$normalized = $service->normalize([
|
||
|
|
'name' => ' Alice Example ',
|
||
|
|
'email' => ' ALICE@EXAMPLE.TEST ',
|
||
|
|
'role_id' => '2',
|
||
|
|
'active' => 'yes',
|
||
|
|
]);
|
||
|
|
user_record_assert_same([
|
||
|
|
'name' => 'Alice Example',
|
||
|
|
'email' => 'alice@example.test',
|
||
|
|
'role_id' => 2,
|
||
|
|
'is_active' => true,
|
||
|
|
], $normalized, 'User records should normalize accepted fields deterministically.');
|
||
|
|
|
||
|
|
$invalid = $service->validate([
|
||
|
|
'name' => ' ',
|
||
|
|
'email' => 'not-an-email',
|
||
|
|
'role_id' => 0,
|
||
|
|
'is_active' => 'sometimes',
|
||
|
|
]);
|
||
|
|
user_record_assert_same(false, $invalid['valid'], 'Invalid user records should report valid=false.');
|
||
|
|
foreach (['name', 'email', 'role_id', 'is_active'] as $field) {
|
||
|
|
if (!isset($invalid['errors'][$field])) {
|
||
|
|
throw new RuntimeException("Expected validation error for {$field}.");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$valid = $service->validate([
|
||
|
|
'name' => ' Alice Example ',
|
||
|
|
'email' => ' ALICE@EXAMPLE.TEST ',
|
||
|
|
'role_id' => '3',
|
||
|
|
'is_active' => 'off',
|
||
|
|
]);
|
||
|
|
user_record_assert_same(true, $valid['valid'], 'Valid user records should report valid=true.');
|
||
|
|
user_record_assert_same([], $valid['errors'], 'Valid user records should contain no field errors.');
|
||
|
|
user_record_assert_same(false, $valid['is_active'], 'False form values should normalize to false.');
|
||
|
|
|
||
|
|
$tooLong = $service->validate([
|
||
|
|
'name' => str_repeat('N', 121),
|
||
|
|
'email' => str_repeat('e', 179) . '@example.test',
|
||
|
|
'role_id' => 1,
|
||
|
|
]);
|
||
|
|
if (!isset($tooLong['errors']['name'], $tooLong['errors']['email'])) {
|
||
|
|
throw new RuntimeException('Expected schema-sized name and email limits to be enforced.');
|
||
|
|
}
|
||
|
|
|
||
|
|
$passwordPolicy = new PasswordPolicy();
|
||
|
|
user_record_assert_same(true, $passwordPolicy->validate('Long&Strong123')['valid'], 'A password meeting every strength rule should pass.');
|
||
|
|
|
||
|
|
$weakPasswords = [
|
||
|
|
'Short1!' => 'minimum length',
|
||
|
|
'alllowercase1!' => 'uppercase letter',
|
||
|
|
'ALLUPPERCASE1!' => 'lowercase letter',
|
||
|
|
'NoDigitsHere!' => 'number',
|
||
|
|
'NoSymbolsHere1' => 'symbol',
|
||
|
|
'Password123!' => 'common placeholder',
|
||
|
|
'Ch@ngeMe123!' => 'common placeholder',
|
||
|
|
];
|
||
|
|
foreach ($weakPasswords as $password => $expectedRule) {
|
||
|
|
$result = $passwordPolicy->validate($password);
|
||
|
|
if ($result['valid'] || !in_array($expectedRule, $result['errors'], true)) {
|
||
|
|
throw new RuntimeException("Expected password '{$password}' to fail the {$expectedRule} rule.");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$initial = $service->validateForCreate([
|
||
|
|
'name' => 'New User',
|
||
|
|
'email' => 'new.user@example.test',
|
||
|
|
'role_id' => 1,
|
||
|
|
'password' => 'Password123!',
|
||
|
|
]);
|
||
|
|
if ($initial['valid'] || !isset($initial['errors']['password'])) {
|
||
|
|
throw new RuntimeException('Initial passwords must satisfy the password policy.');
|
||
|
|
}
|
||
|
|
if (array_key_exists('password', $initial)) {
|
||
|
|
throw new RuntimeException('Validation results must not return a plaintext password.');
|
||
|
|
}
|
||
|
|
|
||
|
|
$strongInitial = $service->validateForCreate([
|
||
|
|
'name' => 'New User',
|
||
|
|
'email' => 'new.user@example.test',
|
||
|
|
'role_id' => 1,
|
||
|
|
'password' => 'Unique&Secure123',
|
||
|
|
]);
|
||
|
|
user_record_assert_same(true, $strongInitial['valid'], 'A strong initial password should pass user creation validation.');
|
||
|
|
user_record_assert_same(false, $passwordPolicy->validateReset('Welcome123!')['valid'], 'Reset passwords must reject common placeholders.');
|
||
|
|
user_record_assert_same(true, $passwordPolicy->validateReset('Another$Safe456')['valid'], 'Strong reset passwords should pass.');
|
||
|
|
|
||
|
|
$display = $service->display([
|
||
|
|
'id' => 42,
|
||
|
|
'name' => 'Alice Example',
|
||
|
|
'email' => 'alice@example.test',
|
||
|
|
'role_id' => 2,
|
||
|
|
'role_name' => 'Accounts',
|
||
|
|
'is_active' => true,
|
||
|
|
'last_login_at' => '2026-09-01 08:00:00',
|
||
|
|
'password' => 'plaintext',
|
||
|
|
'password_hash' => '$2y$secret',
|
||
|
|
'reset_password' => 'reset-secret',
|
||
|
|
'reset_token' => 'token-secret',
|
||
|
|
'unknown' => 'omit',
|
||
|
|
]);
|
||
|
|
user_record_assert_same([
|
||
|
|
'id' => 42,
|
||
|
|
'name' => 'Alice Example',
|
||
|
|
'email' => 'alice@example.test',
|
||
|
|
'role_id' => 2,
|
||
|
|
'role_name' => 'Accounts',
|
||
|
|
'is_active' => true,
|
||
|
|
'last_login_at' => '2026-09-01 08:00:00',
|
||
|
|
], $display, 'Display data must be explicitly allow-listed and exclude all password material.');
|
||
|
|
user_record_assert_same($display, $service->toDisplay([
|
||
|
|
...$display,
|
||
|
|
'password_hash' => 'must-not-leak',
|
||
|
|
]), 'toDisplay should provide the same safe projection.');
|
||
|
|
|
||
|
|
printf("User record tests: 5 passed\n");
|