All checks were successful
CI / secret-scan (pull_request) Successful in 9s
CI / php-lint (pull_request) Successful in 24s
CI / static-tests (pull_request) Successful in 47s
CI / js-tests (pull_request) Successful in 20s
CI / secret-scan (push) Successful in 8s
CI / php-lint (push) Successful in 21s
CI / static-tests (push) Successful in 52s
CI / js-tests (push) Successful in 18s
CI / auto-merge (pull_request) Successful in 5s
CI / auto-merge (push) Has been skipped
Lot U du cycle P3 (Users/RBAC/Stats). Gestion complete des comptes back-office
(mlt domaine 10) : toutes les mutations sont des actions sensibles (RG-T13) avec
re-autorisation par PIN equipier + ligne audit_log dans la meme transaction
(RG-T14), throttle PIN par acteur agissant (RG-T22).
- UserRepository : all (JOIN role) / find / emailExists / activeRoleExists /
create / update (allowlist RG-T16) / setPasswordHash / clearPin / deactivate /
anonymise (RGPD mlt 10.5, tombstone idempotent) / activeAdminCount / isAdmin.
- UserController (user.read/create/update/deactivate) : index ; create/store ;
edit/update ; deactivate ; reset-pin ; erase-PII. Helper resolvePin mutualise
le flux throttle+verif+pin.failed. details JSON d'audit = noms de champs/role
(pas de PII). Conflit d'unicite email -> 409 (convention PR-0).
- Garde-fous d'integrite : pas d'auto-desactivation (mlt 10.3 PRE-2 -> 403) ; on
ne peut ni desactiver, ni retrograder, ni anonymiser le DERNIER admin actif
(anti-lockout) ; erase deja anonymise -> 409.
- Vues admin/users/{index,form,confirm} (PIN inline), 11 routes, nav Administration.
Tests : unit 251, integration 285 / 867 assertions (WAKDO_DB_TESTS=1, dont
UserControllerTest 18 + UserRepositoryDbTest 5), PHPStan L6 propre.
190 lines
7.2 KiB
PHP
190 lines
7.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Integration;
|
|
|
|
use PDOException;
|
|
use PHPUnit\Framework\TestCase;
|
|
use Throwable;
|
|
use App\Auth\PasswordHasher;
|
|
use App\Auth\UserRepository;
|
|
use App\Core\Config;
|
|
use App\Core\Database;
|
|
|
|
/**
|
|
* Ecriture du PIN (UserRepository) contre une vraie MariaDB. Auto-skip si
|
|
* WAKDO_DB_TESTS != 1. User jetable (.invalid), supprime en tearDown.
|
|
*/
|
|
final class UserRepositoryDbTest extends TestCase
|
|
{
|
|
private Database $db;
|
|
private Config $config;
|
|
private int $userId = 0;
|
|
private int $counterRoleId = 0;
|
|
private int $adminRoleId = 0;
|
|
/** @var list<int> ids des comptes crees par les tests CRUD (nettoyes par id). */
|
|
private array $createdIds = [];
|
|
|
|
protected function setUp(): void
|
|
{
|
|
if (getenv('WAKDO_DB_TESTS') !== '1') {
|
|
self::markTestSkipped('Tests DB desactives (definir WAKDO_DB_TESTS=1 + DB_*).');
|
|
}
|
|
|
|
$this->config = new Config();
|
|
$this->db = new Database($this->config);
|
|
|
|
try {
|
|
$this->db->fetch('SELECT 1');
|
|
} catch (Throwable $exception) {
|
|
self::markTestSkipped('Base injoignable: ' . $exception->getMessage());
|
|
}
|
|
|
|
$this->counterRoleId = (int) ($this->db->fetch("SELECT id FROM role WHERE code = 'counter'")['id'] ?? 0);
|
|
$this->adminRoleId = (int) ($this->db->fetch("SELECT id FROM role WHERE code = 'admin'")['id'] ?? 0);
|
|
$roleId = (int) ($this->db->fetch('SELECT id FROM role ORDER BY id LIMIT 1')['id'] ?? 0);
|
|
$hasher = new PasswordHasher($this->config);
|
|
$this->db->execute(
|
|
'INSERT INTO user (email, password_hash, first_name, last_name, role_id, is_active) '
|
|
. 'VALUES (:email, :pwd, :fn, :ln, :role, 1)',
|
|
[
|
|
'email' => 'it-userrepo-' . bin2hex(random_bytes(6)) . '@wakdo.invalid',
|
|
'pwd' => $hasher->hash('IntegrationPass1'),
|
|
'fn' => 'Integration',
|
|
'ln' => 'UserRepo',
|
|
'role' => $roleId,
|
|
],
|
|
);
|
|
$this->userId = (int) ($this->db->fetch('SELECT LAST_INSERT_ID() AS id')['id'] ?? 0);
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
if ($this->userId !== 0) {
|
|
$this->db->execute('DELETE FROM user WHERE id = :id', ['id' => $this->userId]);
|
|
$this->userId = 0;
|
|
}
|
|
foreach ($this->createdIds as $id) {
|
|
$this->db->execute('DELETE FROM user WHERE id = :id', ['id' => $id]);
|
|
}
|
|
$this->createdIds = [];
|
|
}
|
|
|
|
private function makeUser(UserRepository $repo, string $tag, int $roleId): int
|
|
{
|
|
$id = $repo->create([
|
|
'email' => 'it-user-' . $tag . '-' . bin2hex(random_bytes(3)) . '@wakdo.test',
|
|
'password_hash' => '$argon2id$placeholder',
|
|
'first_name' => 'Test',
|
|
'last_name' => 'User' . $tag,
|
|
'role_id' => $roleId,
|
|
]);
|
|
$this->createdIds[] = $id;
|
|
|
|
return $id;
|
|
}
|
|
|
|
public function testSetPinHashAndPinIsSet(): void
|
|
{
|
|
$repo = new UserRepository($this->db);
|
|
$hasher = new PasswordHasher($this->config);
|
|
|
|
// Aucun PIN au depart.
|
|
self::assertFalse($repo->pinIsSet($this->userId));
|
|
|
|
$repo->setPinHash($this->userId, $hasher->hash('4729'));
|
|
|
|
self::assertTrue($repo->pinIsSet($this->userId));
|
|
|
|
// Le hash stocke est verifiable et n'est pas le PIN en clair.
|
|
$stored = (string) ($this->db->fetch('SELECT pin_hash FROM user WHERE id = :id', ['id' => $this->userId])['pin_hash'] ?? '');
|
|
self::assertNotSame('4729', $stored);
|
|
self::assertTrue($hasher->verify('4729', $stored));
|
|
}
|
|
|
|
public function testCreateFindUpdate(): void
|
|
{
|
|
$repo = new UserRepository($this->db);
|
|
self::assertTrue($repo->activeRoleExists($this->counterRoleId));
|
|
self::assertFalse($repo->activeRoleExists(0));
|
|
|
|
$id = $this->makeUser($repo, 'a', $this->counterRoleId);
|
|
self::assertGreaterThan(0, $id);
|
|
|
|
$found = $repo->find($id);
|
|
self::assertNotNull($found);
|
|
self::assertSame($this->counterRoleId, (int) $found['role_id']);
|
|
self::assertSame(1, (int) $found['is_active']);
|
|
self::assertTrue($repo->emailExists((string) $found['email']));
|
|
self::assertFalse($repo->emailExists((string) $found['email'], $id)); // s'exclut lui-meme
|
|
|
|
$repo->update($id, [
|
|
'email' => (string) $found['email'],
|
|
'first_name' => 'Renamed',
|
|
'last_name' => 'Person',
|
|
'role_id' => $this->adminRoleId,
|
|
'is_active' => 0,
|
|
]);
|
|
$updated = $repo->find($id);
|
|
self::assertNotNull($updated);
|
|
self::assertSame('Renamed', (string) $updated['first_name']);
|
|
self::assertSame($this->adminRoleId, (int) $updated['role_id']);
|
|
self::assertSame(0, (int) $updated['is_active']);
|
|
|
|
$emails = array_map(static fn (array $r): string => (string) ($r['email'] ?? ''), $repo->all());
|
|
self::assertContains((string) $found['email'], $emails); // all() joint le libelle de role
|
|
}
|
|
|
|
public function testDuplicateEmailViolatesUnique(): void
|
|
{
|
|
$repo = new UserRepository($this->db);
|
|
$id = $this->makeUser($repo, 'dup', $this->counterRoleId);
|
|
$email = (string) ($repo->find($id)['email'] ?? '');
|
|
|
|
$violated = false;
|
|
try {
|
|
$newId = $repo->create(['email' => $email, 'password_hash' => 'x', 'first_name' => 'D', 'last_name' => 'U', 'role_id' => $this->counterRoleId]);
|
|
$this->createdIds[] = $newId;
|
|
} catch (PDOException $exception) {
|
|
$violated = (string) $exception->getCode() === '23000';
|
|
}
|
|
self::assertTrue($violated, 'uk_user_email doit rejeter un doublon (SQLSTATE 23000).');
|
|
}
|
|
|
|
public function testDeactivateThenAnonymiseIsIdempotent(): void
|
|
{
|
|
$repo = new UserRepository($this->db);
|
|
$id = $this->makeUser($repo, 'rgpd', $this->counterRoleId);
|
|
|
|
self::assertSame(1, $repo->deactivate($id));
|
|
self::assertSame(0, (int) ($repo->find($id)['is_active'] ?? -1));
|
|
|
|
self::assertSame(1, $repo->anonymise($id)); // vide la PII, garde la ligne (tombstone)
|
|
$anon = $repo->find($id);
|
|
self::assertNotNull($anon);
|
|
self::assertSame('', (string) $anon['first_name']);
|
|
self::assertSame('', (string) $anon['last_name']);
|
|
self::assertSame('anon-' . $id . '@wakdo.invalid', (string) $anon['email']);
|
|
self::assertNotNull($anon['anonymized_at']);
|
|
|
|
self::assertSame(0, $repo->anonymise($id)); // idempotent : deja anonymise
|
|
}
|
|
|
|
public function testActiveAdminCountAndIsAdmin(): void
|
|
{
|
|
$repo = new UserRepository($this->db);
|
|
$before = $repo->activeAdminCount();
|
|
self::assertGreaterThanOrEqual(1, $before); // le seed pose un admin actif
|
|
|
|
$adminId = $this->makeUser($repo, 'adm', $this->adminRoleId);
|
|
self::assertSame($before + 1, $repo->activeAdminCount());
|
|
self::assertTrue($repo->isAdmin($adminId));
|
|
|
|
$counterId = $this->makeUser($repo, 'cnt', $this->counterRoleId);
|
|
self::assertFalse($repo->isAdmin($counterId));
|
|
|
|
$repo->deactivate($adminId);
|
|
self::assertSame($before, $repo->activeAdminCount()); // redescend
|
|
}
|
|
}
|