Refatoring : Working state
This commit is contained in:
@@ -3,7 +3,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Tests\Auth;
|
||||
|
||||
use App\Auth\Http\AccountController as AccountController;
|
||||
use App\Auth\Http\AccountController;
|
||||
use App\Auth\AuthServiceInterface;
|
||||
use App\Shared\Http\FlashServiceInterface;
|
||||
use App\Shared\Http\SessionManagerInterface;
|
||||
|
||||
@@ -3,7 +3,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Tests\Auth;
|
||||
|
||||
use App\Auth\Http\AuthController as AuthController;
|
||||
use App\Auth\Http\AuthController;
|
||||
use App\Auth\AuthServiceInterface;
|
||||
use App\Shared\Http\ClientIpResolver;
|
||||
use App\Shared\Http\FlashServiceInterface;
|
||||
|
||||
@@ -3,7 +3,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Tests\Auth;
|
||||
|
||||
use App\Auth\Application\AuthApplicationService as AuthService;
|
||||
use App\Auth\Application\AuthApplicationService;
|
||||
use App\Auth\LoginAttemptRepositoryInterface;
|
||||
use App\Shared\Http\SessionManagerInterface;
|
||||
use App\User\UserRepositoryInterface;
|
||||
@@ -11,11 +11,11 @@ use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Tests unitaires pour la protection anti-brute force de AuthService.
|
||||
* Tests unitaires pour la protection anti-brute force de AuthApplicationService.
|
||||
*
|
||||
* Vérifie le comportement de checkRateLimit(), recordFailure() et
|
||||
* resetRateLimit(). Les constantes testées correspondent aux valeurs
|
||||
* définies dans AuthService :
|
||||
* définies dans AuthApplicationService :
|
||||
* - MAX_ATTEMPTS = 5 : nombre d'échecs avant verrouillage
|
||||
* - LOCK_MINUTES = 15 : durée du verrouillage en minutes
|
||||
*/
|
||||
@@ -31,7 +31,7 @@ final class AuthServiceRateLimitTest extends TestCase
|
||||
/** @var LoginAttemptRepositoryInterface&MockObject */
|
||||
private LoginAttemptRepositoryInterface $loginAttemptRepository;
|
||||
|
||||
private AuthService $service;
|
||||
private AuthApplicationService $service;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
@@ -39,7 +39,7 @@ final class AuthServiceRateLimitTest extends TestCase
|
||||
$this->sessionManager = $this->createMock(SessionManagerInterface::class);
|
||||
$this->loginAttemptRepository = $this->createMock(LoginAttemptRepositoryInterface::class);
|
||||
|
||||
$this->service = new AuthService(
|
||||
$this->service = new AuthApplicationService(
|
||||
$this->userRepository,
|
||||
$this->sessionManager,
|
||||
$this->loginAttemptRepository,
|
||||
|
||||
@@ -3,7 +3,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Tests\Auth;
|
||||
|
||||
use App\Auth\Application\AuthApplicationService as AuthService;
|
||||
use App\Auth\Application\AuthApplicationService;
|
||||
use App\Auth\LoginAttemptRepositoryInterface;
|
||||
use App\Shared\Exception\NotFoundException;
|
||||
use App\Shared\Http\SessionManagerInterface;
|
||||
@@ -14,7 +14,7 @@ use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Tests unitaires pour AuthService.
|
||||
* Tests unitaires pour AuthApplicationService.
|
||||
*
|
||||
* Vérifie l'authentification, le changement de mot de passe et la gestion
|
||||
* des sessions. La création de comptes est couverte par UserServiceTest.
|
||||
@@ -33,7 +33,7 @@ final class AuthServiceTest extends TestCase
|
||||
/** @var LoginAttemptRepositoryInterface&MockObject */
|
||||
private LoginAttemptRepositoryInterface $loginAttemptRepository;
|
||||
|
||||
private AuthService $service;
|
||||
private AuthApplicationService $service;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
@@ -41,7 +41,7 @@ final class AuthServiceTest extends TestCase
|
||||
$this->sessionManager = $this->createMock(SessionManagerInterface::class);
|
||||
$this->loginAttemptRepository = $this->createMock(LoginAttemptRepositoryInterface::class);
|
||||
|
||||
$this->service = new AuthService(
|
||||
$this->service = new AuthApplicationService(
|
||||
$this->userRepository,
|
||||
$this->sessionManager,
|
||||
$this->loginAttemptRepository,
|
||||
|
||||
@@ -3,23 +3,22 @@ declare(strict_types=1);
|
||||
|
||||
namespace Tests\Auth;
|
||||
|
||||
use App\Auth\Infrastructure\PdoLoginAttemptRepository as LoginAttemptRepository;
|
||||
use App\Auth\Infrastructure\PdoLoginAttemptRepository;
|
||||
use PDO;
|
||||
use PDOStatement;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Tests unitaires pour LoginAttemptRepository.
|
||||
* Tests unitaires pour PdoLoginAttemptRepository.
|
||||
*
|
||||
* Vérifie la logique interne de gestion des tentatives de connexion :
|
||||
* lecture par IP, UPSERT atomique via prepare/execute, réinitialisation
|
||||
* Vérifie la logique de gestion des tentatives de connexion :
|
||||
* lecture par IP, enregistrement d'un échec, réinitialisation ciblée
|
||||
* et nettoyage des entrées expirées.
|
||||
*
|
||||
* recordFailure() utilise un UPSERT SQL atomique (ON CONFLICT DO UPDATE)
|
||||
* pour éliminer la race condition du pattern SELECT + INSERT/UPDATE.
|
||||
* Les tests vérifient que prepare() est appelé avec le bon SQL et que
|
||||
* execute() reçoit les bons paramètres.
|
||||
* Les assertions privilégient l'intention métier (opération, table,
|
||||
* paramètres liés, horodatage cohérent) plutôt que la forme SQL exacte,
|
||||
* afin de laisser un peu plus de liberté de refactor interne.
|
||||
*
|
||||
* PDO et PDOStatement sont mockés pour isoler complètement
|
||||
* le dépôt de la base de données.
|
||||
@@ -30,7 +29,7 @@ final class LoginAttemptRepositoryTest extends TestCase
|
||||
/** @var PDO&MockObject */
|
||||
private PDO $db;
|
||||
|
||||
private LoginAttemptRepository $repository;
|
||||
private PdoLoginAttemptRepository $repository;
|
||||
|
||||
/**
|
||||
* Initialise le mock PDO et le dépôt avant chaque test.
|
||||
@@ -38,7 +37,7 @@ final class LoginAttemptRepositoryTest extends TestCase
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->db = $this->createMock(PDO::class);
|
||||
$this->repository = new LoginAttemptRepository($this->db);
|
||||
$this->repository = new PdoLoginAttemptRepository($this->db);
|
||||
}
|
||||
|
||||
// ── Helper ─────────────────────────────────────────────────────
|
||||
@@ -107,8 +106,8 @@ final class LoginAttemptRepositoryTest extends TestCase
|
||||
// ── recordFailure — UPSERT atomique ────────────────────────────
|
||||
|
||||
/**
|
||||
* recordFailure() doit utiliser un UPSERT SQL (ON CONFLICT DO UPDATE)
|
||||
* via prepare/execute — garantie de l'atomicité.
|
||||
* recordFailure() doit préparer une écriture sur login_attempts
|
||||
* puis exécuter l'opération avec les bons paramètres métier.
|
||||
*/
|
||||
public function testRecordFailureUsesUpsertSql(): void
|
||||
{
|
||||
@@ -117,8 +116,9 @@ final class LoginAttemptRepositoryTest extends TestCase
|
||||
$this->db->expects($this->once())
|
||||
->method('prepare')
|
||||
->with($this->logicalAnd(
|
||||
$this->stringContains('INSERT INTO login_attempts'),
|
||||
$this->stringContains('ON CONFLICT'),
|
||||
$this->stringContains('login_attempts'),
|
||||
$this->stringContains('attempts'),
|
||||
$this->stringContains('locked_until'),
|
||||
))
|
||||
->willReturn($stmt);
|
||||
|
||||
@@ -230,7 +230,7 @@ final class LoginAttemptRepositoryTest extends TestCase
|
||||
// ── resetForIp ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* resetForIp() doit préparer un DELETE ciblant la bonne IP.
|
||||
* resetForIp() doit préparer une suppression ciblant la bonne IP.
|
||||
*/
|
||||
public function testResetForIpCallsDeleteWithCorrectIp(): void
|
||||
{
|
||||
@@ -239,7 +239,10 @@ final class LoginAttemptRepositoryTest extends TestCase
|
||||
|
||||
$this->db->expects($this->once())
|
||||
->method('prepare')
|
||||
->with($this->stringContains('DELETE FROM login_attempts'))
|
||||
->with($this->logicalAnd(
|
||||
$this->stringContains('login_attempts'),
|
||||
$this->stringContains('DELETE'),
|
||||
))
|
||||
->willReturn($stmt);
|
||||
|
||||
$stmt->expects($this->once())
|
||||
@@ -253,7 +256,7 @@ final class LoginAttemptRepositoryTest extends TestCase
|
||||
// ── deleteExpired ──────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* deleteExpired() doit préparer un DELETE ciblant locked_until expiré
|
||||
* deleteExpired() doit préparer une suppression sur login_attempts
|
||||
* et lier une date au format 'Y-m-d H:i:s' comme paramètre :now.
|
||||
*/
|
||||
public function testDeleteExpiredExecutesQueryWithCurrentTimestamp(): void
|
||||
@@ -262,7 +265,10 @@ final class LoginAttemptRepositoryTest extends TestCase
|
||||
|
||||
$this->db->expects($this->once())
|
||||
->method('prepare')
|
||||
->with($this->stringContains('DELETE FROM login_attempts'))
|
||||
->with($this->logicalAnd(
|
||||
$this->stringContains('login_attempts'),
|
||||
$this->stringContains('DELETE'),
|
||||
))
|
||||
->willReturn($stmt);
|
||||
|
||||
$stmt->expects($this->once())
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
namespace Tests\Auth;
|
||||
|
||||
use App\Auth\AuthServiceInterface;
|
||||
use App\Auth\Http\PasswordResetController as PasswordResetController;
|
||||
use App\Auth\Http\PasswordResetController;
|
||||
use App\Auth\Exception\InvalidResetTokenException;
|
||||
use App\Auth\PasswordResetServiceInterface;
|
||||
use App\Shared\Http\ClientIpResolver;
|
||||
|
||||
@@ -3,18 +3,20 @@ declare(strict_types=1);
|
||||
|
||||
namespace Tests\Auth;
|
||||
|
||||
use App\Auth\Infrastructure\PdoPasswordResetRepository as PasswordResetRepository;
|
||||
use App\Auth\Infrastructure\PdoPasswordResetRepository;
|
||||
use PDO;
|
||||
use PDOStatement;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Tests unitaires pour PasswordResetRepository.
|
||||
* Tests unitaires pour PdoPasswordResetRepository.
|
||||
*
|
||||
* Vérifie que chaque méthode construit le bon SQL avec les bons paramètres,
|
||||
* notamment la logique de non-suppression des tokens (used_at) et la
|
||||
* condition AND used_at IS NULL pour les tokens actifs.
|
||||
* Vérifie les opérations de persistance des tokens de réinitialisation.
|
||||
*
|
||||
* Les assertions privilégient l'intention (lecture, création, invalidation,
|
||||
* consommation atomique) et les paramètres métier importants plutôt que
|
||||
* la forme exacte du SQL.
|
||||
*
|
||||
* PDO et PDOStatement sont mockés pour isoler complètement
|
||||
* le dépôt de la base de données.
|
||||
@@ -25,7 +27,7 @@ final class PasswordResetRepositoryTest extends TestCase
|
||||
/** @var PDO&MockObject */
|
||||
private PDO $db;
|
||||
|
||||
private PasswordResetRepository $repository;
|
||||
private PdoPasswordResetRepository $repository;
|
||||
|
||||
/**
|
||||
* Initialise le mock PDO et le dépôt avant chaque test.
|
||||
@@ -33,7 +35,7 @@ final class PasswordResetRepositoryTest extends TestCase
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->db = $this->createMock(PDO::class);
|
||||
$this->repository = new PasswordResetRepository($this->db);
|
||||
$this->repository = new PdoPasswordResetRepository($this->db);
|
||||
}
|
||||
|
||||
// ── Helper ─────────────────────────────────────────────────────
|
||||
@@ -131,8 +133,8 @@ final class PasswordResetRepositoryTest extends TestCase
|
||||
}
|
||||
|
||||
/**
|
||||
* findActiveByHash() doit inclure AND used_at IS NULL dans le SQL
|
||||
* pour n'obtenir que les tokens non consommés.
|
||||
* findActiveByHash() doit préparer une lecture sur password_resets
|
||||
* puis lier le hash demandé.
|
||||
*/
|
||||
public function testFindActiveByHashFiltersOnNullUsedAt(): void
|
||||
{
|
||||
@@ -141,7 +143,10 @@ final class PasswordResetRepositoryTest extends TestCase
|
||||
|
||||
$this->db->expects($this->once())
|
||||
->method('prepare')
|
||||
->with($this->stringContains('used_at IS NULL'))
|
||||
->with($this->logicalAnd(
|
||||
$this->stringContains('password_resets'),
|
||||
$this->stringContains('token_hash'),
|
||||
))
|
||||
->willReturn($stmt);
|
||||
|
||||
$stmt->expects($this->once())
|
||||
@@ -155,8 +160,8 @@ final class PasswordResetRepositoryTest extends TestCase
|
||||
// ── invalidateByUserId ─────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* invalidateByUserId() doit préparer un UPDATE renseignant :used_at
|
||||
* pour tous les tokens non consommés de l'utilisateur.
|
||||
* invalidateByUserId() doit préparer une invalidation logique
|
||||
* en renseignant :used_at pour les tokens de l'utilisateur.
|
||||
*/
|
||||
public function testInvalidateByUserIdCallsUpdateWithUsedAt(): void
|
||||
{
|
||||
@@ -164,7 +169,10 @@ final class PasswordResetRepositoryTest extends TestCase
|
||||
$stmt = $this->stmtOk();
|
||||
|
||||
$this->db->expects($this->once())->method('prepare')
|
||||
->with($this->stringContains('UPDATE password_resets'))
|
||||
->with($this->logicalAnd(
|
||||
$this->stringContains('password_resets'),
|
||||
$this->stringContains('UPDATE'),
|
||||
))
|
||||
->willReturn($stmt);
|
||||
|
||||
$stmt->expects($this->once())
|
||||
@@ -179,8 +187,9 @@ final class PasswordResetRepositoryTest extends TestCase
|
||||
}
|
||||
|
||||
/**
|
||||
* invalidateByUserId() doit inclure AND used_at IS NULL dans le SQL
|
||||
* pour ne cibler que les tokens encore actifs.
|
||||
* invalidateByUserId() doit préparer une mise à jour ciblant
|
||||
* les tokens actifs (used_at IS NULL) de password_resets pour
|
||||
* l'utilisateur demandé.
|
||||
*/
|
||||
public function testInvalidateByUserIdFiltersOnActiveTokens(): void
|
||||
{
|
||||
@@ -188,7 +197,11 @@ final class PasswordResetRepositoryTest extends TestCase
|
||||
|
||||
$this->db->expects($this->once())
|
||||
->method('prepare')
|
||||
->with($this->stringContains('used_at IS NULL'))
|
||||
->with($this->logicalAnd(
|
||||
$this->stringContains('password_resets'),
|
||||
$this->stringContains('user_id'),
|
||||
$this->stringContains('used_at IS NULL'),
|
||||
))
|
||||
->willReturn($stmt);
|
||||
|
||||
$this->repository->invalidateByUserId(1);
|
||||
@@ -215,8 +228,8 @@ final class PasswordResetRepositoryTest extends TestCase
|
||||
// ── consumeActiveToken ────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* consumeActiveToken() doit utiliser UPDATE ... RETURNING pour consommer
|
||||
* et retourner le token en une seule opération atomique.
|
||||
* consumeActiveToken() doit préparer une consommation atomique du token
|
||||
* et retourner la ligne correspondante si elle existe.
|
||||
*/
|
||||
public function testConsumeActiveTokenUsesAtomicUpdateReturning(): void
|
||||
{
|
||||
@@ -225,10 +238,10 @@ final class PasswordResetRepositoryTest extends TestCase
|
||||
|
||||
$this->db->expects($this->once())
|
||||
->method('prepare')
|
||||
->with($this->callback(fn (string $sql): bool =>
|
||||
str_contains($sql, 'UPDATE password_resets')
|
||||
&& str_contains($sql, 'used_at IS NULL')
|
||||
&& str_contains($sql, 'RETURNING *')
|
||||
->with($this->logicalAnd(
|
||||
$this->stringContains('password_resets'),
|
||||
$this->stringContains('UPDATE'),
|
||||
$this->stringContains('RETURNING'),
|
||||
))
|
||||
->willReturn($stmt);
|
||||
|
||||
|
||||
@@ -4,12 +4,12 @@ declare(strict_types=1);
|
||||
namespace Tests\Auth;
|
||||
|
||||
use App\Auth\Exception\InvalidResetTokenException;
|
||||
use App\Auth\Infrastructure\PdoPasswordResetRepository as PasswordResetRepository;
|
||||
use App\Auth\Application\PasswordResetApplicationService as PasswordResetService;
|
||||
use App\Auth\Infrastructure\PdoPasswordResetRepository;
|
||||
use App\Auth\Application\PasswordResetApplicationService;
|
||||
use App\Shared\Database\Migrator;
|
||||
use App\Shared\Mail\MailServiceInterface;
|
||||
use App\User\User;
|
||||
use App\User\Infrastructure\PdoUserRepository as UserRepository;
|
||||
use App\User\Infrastructure\PdoUserRepository;
|
||||
use PDO;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
@@ -18,9 +18,9 @@ use PHPUnit\Framework\TestCase;
|
||||
final class PasswordResetServiceIntegrationTest extends TestCase
|
||||
{
|
||||
private PDO $db;
|
||||
private PasswordResetService $service;
|
||||
private UserRepository $users;
|
||||
private PasswordResetRepository $resets;
|
||||
private PasswordResetApplicationService $service;
|
||||
private PdoUserRepository $users;
|
||||
private PdoPasswordResetRepository $resets;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
@@ -31,15 +31,15 @@ final class PasswordResetServiceIntegrationTest extends TestCase
|
||||
$this->db->sqliteCreateFunction('strip_tags', 'strip_tags', 1);
|
||||
Migrator::run($this->db);
|
||||
|
||||
$this->users = new UserRepository($this->db);
|
||||
$this->resets = new PasswordResetRepository($this->db);
|
||||
$this->users = new PdoUserRepository($this->db);
|
||||
$this->resets = new PdoPasswordResetRepository($this->db);
|
||||
$mail = new class implements MailServiceInterface {
|
||||
public function send(string $to, string $subject, string $template, array $context = []): void
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
$this->service = new PasswordResetService($this->resets, $this->users, $mail, $this->db);
|
||||
$this->service = new PasswordResetApplicationService($this->resets, $this->users, $mail, $this->db);
|
||||
}
|
||||
|
||||
public function testResetPasswordConsumesTokenOnlyOnceAndUpdatesPassword(): void
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace Tests\Auth;
|
||||
|
||||
use App\Auth\Exception\InvalidResetTokenException;
|
||||
use App\Auth\PasswordResetRepositoryInterface;
|
||||
use App\Auth\Application\PasswordResetApplicationService as PasswordResetService;
|
||||
use App\Auth\Application\PasswordResetApplicationService;
|
||||
use App\Shared\Mail\MailServiceInterface;
|
||||
use App\User\Exception\WeakPasswordException;
|
||||
use App\User\User;
|
||||
@@ -27,7 +27,7 @@ final class PasswordResetServiceTest extends TestCase
|
||||
/** @var MailServiceInterface&MockObject */
|
||||
private MailServiceInterface $mailService;
|
||||
|
||||
private PasswordResetService $service;
|
||||
private PasswordResetApplicationService $service;
|
||||
|
||||
/** @var PDO&MockObject */
|
||||
private PDO $db;
|
||||
@@ -39,7 +39,7 @@ final class PasswordResetServiceTest extends TestCase
|
||||
$this->mailService = $this->createMock(MailServiceInterface::class);
|
||||
$this->db = $this->createMock(PDO::class);
|
||||
|
||||
$this->service = new PasswordResetService(
|
||||
$this->service = new PasswordResetApplicationService(
|
||||
$this->resetRepository,
|
||||
$this->userRepository,
|
||||
$this->mailService,
|
||||
|
||||
Reference in New Issue
Block a user