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");