corentin_wakdo/tests/Unit/Auth/PasswordHasherTest.php
Imugiii 5835be0e66 feat(auth): authentification back-office (login, logout, reinitialisation mot de passe)
Implemente mlt.md section 12 : AUTHENTICATE_USER (12.1), LOGOUT_USER (12.2),
RESET_PASSWORD (12.3). Sessions PHP + argon2id, regeneration d'ID a la connexion,
idle 4h / absolu 10h via SessionGuard (cable en P3), jeton CSRF synchroniseur, backoff
degressif anti brute-force par compte et par IP source (login_throttle), audit_log
append-only (login_success/failed, password_reset), defenses anti-enumeration d'email
(timing + profil d'ecritures identique), fail-closed sur erreur base. Vues login/forgot/reset
rendues serveur. Routes posees sur le vhost admin (pas de prefixe /admin : docroot =
public/admin). PHPUnit sans Composer (unit + integration DB auto-skippee sans base) et
PHPStan L6 restent verts.
2026-06-15 18:15:32 +00:00

87 lines
2.5 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\Unit\Auth;
use PHPUnit\Framework\TestCase;
use App\Auth\PasswordHasher;
use App\Core\Config;
/**
* Verifie le hash argon2id (cout pilote par l'environnement) et le leurre de
* timing. Les couts sont volontairement abaisses ici pour garder la suite rapide.
*/
final class PasswordHasherTest extends TestCase
{
/** @var list<string> */
private array $touchedKeys = [];
protected function setUp(): void
{
// Cout reduit : les tests ne valident pas la robustesse cryptographique
// (couverte par les valeurs de prod) mais la mecanique hash/verify/cout.
$this->setEnv('ARGON2_MEMORY_COST', '1024');
$this->setEnv('ARGON2_TIME_COST', '1');
$this->setEnv('ARGON2_THREADS', '1');
}
protected function tearDown(): void
{
foreach ($this->touchedKeys as $key) {
putenv($key);
}
$this->touchedKeys = [];
}
private function setEnv(string $key, string $value): void
{
$this->touchedKeys[] = $key;
putenv($key . '=' . $value);
}
private function hasher(): PasswordHasher
{
return new PasswordHasher(new Config());
}
public function testHashIsVerifiable(): void
{
$hasher = $this->hasher();
$hash = $hasher->hash('WakdoAdmin2026!');
self::assertTrue($hasher->verify('WakdoAdmin2026!', $hash));
}
public function testWrongPasswordIsRejected(): void
{
$hasher = $this->hasher();
$hash = $hasher->hash('correct horse');
self::assertFalse($hasher->verify('battery staple', $hash));
}
public function testHashUsesArgon2idAlgorithm(): void
{
$info = password_get_info($this->hasher()->hash('x'));
self::assertSame('argon2id', $info['algoName']);
}
public function testHashEmbedsConfiguredCost(): void
{
$info = password_get_info($this->hasher()->hash('x'));
self::assertSame(1024, $info['options']['memory_cost'] ?? null);
self::assertSame(1, $info['options']['time_cost'] ?? null);
self::assertSame(1, $info['options']['threads'] ?? null);
}
public function testVerifyDecoyRunsWithoutThrowing(): void
{
// Le leurre ne doit jamais lever ni valider un mot de passe : il ne sert
// qu'a consommer un temps CPU comparable au chemin nominal.
$this->expectNotToPerformAssertions();
$this->hasher()->verifyDecoy('any-submitted-password');
}
}