Files
Classeo/backend/tests/Unit/Administration/Domain/Model/ActivationToken/ActivationTokenTest.php
Mathias STRASSER affad287f9 feat: Réinitialisation de mot de passe avec tokens sécurisés
Implémentation complète du flux de réinitialisation de mot de passe (Story 1.5):

Backend:
- Aggregate PasswordResetToken avec TTL 1h, UUID v7, usage unique
- Endpoint POST /api/password/forgot avec rate limiting (3/h par email, 10/h par IP)
- Endpoint POST /api/password/reset avec validation token
- Templates email (demande + confirmation)
- Repository Redis avec TTL 2h pour distinguer expiré/invalide

Frontend:
- Page /mot-de-passe-oublie avec message générique (anti-énumération)
- Page /reset-password/[token] avec validation temps réel des critères
- Gestion erreurs: token invalide, expiré, déjà utilisé

Tests:
- 14 tests unitaires PasswordResetToken
- 7 tests unitaires RequestPasswordResetHandler
- 7 tests unitaires ResetPasswordHandler
- Tests E2E Playwright pour le flux complet
2026-02-02 09:45:15 +01:00

220 lines
6.9 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\Unit\Administration\Domain\Model\ActivationToken;
use App\Administration\Domain\Event\ActivationTokenGenerated;
use App\Administration\Domain\Event\ActivationTokenUsed;
use App\Administration\Domain\Exception\ActivationTokenAlreadyUsedException;
use App\Administration\Domain\Exception\ActivationTokenExpiredException;
use App\Administration\Domain\Model\ActivationToken\ActivationToken;
use App\Administration\Domain\Model\ActivationToken\ActivationTokenId;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class ActivationTokenTest extends TestCase
{
private const string USER_ID = '550e8400-e29b-41d4-a716-446655440001';
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440002';
private const string EMAIL = 'user@example.com';
private const string ROLE = 'ROLE_PARENT';
private const string SCHOOL_NAME = 'École Alpha';
#[Test]
public function generateCreatesTokenWithCorrectProperties(): void
{
$userId = self::USER_ID;
$email = self::EMAIL;
$tenantId = TenantId::fromString(self::TENANT_ID);
$role = self::ROLE;
$schoolName = self::SCHOOL_NAME;
$now = new DateTimeImmutable('2026-01-15 10:00:00');
$token = ActivationToken::generate(
userId: $userId,
email: $email,
tenantId: $tenantId,
role: $role,
schoolName: $schoolName,
createdAt: $now,
);
self::assertInstanceOf(ActivationTokenId::class, $token->id);
self::assertSame($userId, $token->userId);
self::assertSame($email, $token->email);
self::assertTrue($tenantId->equals($token->tenantId));
self::assertSame($role, $token->role);
self::assertSame($schoolName, $token->schoolName);
self::assertEquals($now, $token->createdAt);
self::assertFalse($token->isUsed());
}
#[Test]
public function generateRecordsActivationTokenGeneratedEvent(): void
{
$token = $this->createToken();
$events = $token->pullDomainEvents();
self::assertCount(1, $events);
self::assertInstanceOf(ActivationTokenGenerated::class, $events[0]);
}
#[Test]
public function tokenValueIsUuidV7Format(): void
{
$token = $this->createToken();
self::assertMatchesRegularExpression(
'/^[a-f0-9]{8}-[a-f0-9]{4}-7[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i',
$token->tokenValue,
);
}
#[Test]
public function expiresAtIs7DaysAfterCreation(): void
{
$createdAt = new DateTimeImmutable('2026-01-15 10:00:00');
$expectedExpiration = new DateTimeImmutable('2026-01-22 10:00:00');
$token = ActivationToken::generate(
userId: self::USER_ID,
email: self::EMAIL,
tenantId: TenantId::fromString(self::TENANT_ID),
role: self::ROLE,
schoolName: self::SCHOOL_NAME,
createdAt: $createdAt,
);
self::assertEquals($expectedExpiration, $token->expiresAt);
}
#[Test]
public function isExpiredReturnsFalseWhenNotExpired(): void
{
$createdAt = new DateTimeImmutable('2026-01-15 10:00:00');
$checkAt = new DateTimeImmutable('2026-01-20 10:00:00');
$token = ActivationToken::generate(
userId: self::USER_ID,
email: self::EMAIL,
tenantId: TenantId::fromString(self::TENANT_ID),
role: self::ROLE,
schoolName: self::SCHOOL_NAME,
createdAt: $createdAt,
);
self::assertFalse($token->isExpired($checkAt));
}
#[Test]
public function isExpiredReturnsTrueWhenExpired(): void
{
$createdAt = new DateTimeImmutable('2026-01-15 10:00:00');
$checkAt = new DateTimeImmutable('2026-01-25 10:00:00');
$token = ActivationToken::generate(
userId: self::USER_ID,
email: self::EMAIL,
tenantId: TenantId::fromString(self::TENANT_ID),
role: self::ROLE,
schoolName: self::SCHOOL_NAME,
createdAt: $createdAt,
);
self::assertTrue($token->isExpired($checkAt));
}
#[Test]
public function isExpiredReturnsTrueAtExactExpirationMoment(): void
{
$createdAt = new DateTimeImmutable('2026-01-15 10:00:00');
$checkAt = new DateTimeImmutable('2026-01-22 10:00:00');
$token = ActivationToken::generate(
userId: self::USER_ID,
email: self::EMAIL,
tenantId: TenantId::fromString(self::TENANT_ID),
role: self::ROLE,
schoolName: self::SCHOOL_NAME,
createdAt: $createdAt,
);
self::assertTrue($token->isExpired($checkAt));
}
#[Test]
public function useMarksTokenAsUsed(): void
{
$token = $this->createToken();
$usedAt = new DateTimeImmutable('2026-01-16 10:00:00');
$token->use($usedAt);
self::assertTrue($token->isUsed());
self::assertEquals($usedAt, $token->usedAt);
}
#[Test]
public function useRecordsActivationTokenUsedEvent(): void
{
$token = $this->createToken();
$token->pullDomainEvents();
$usedAt = new DateTimeImmutable('2026-01-16 10:00:00');
$token->use($usedAt);
$events = $token->pullDomainEvents();
self::assertCount(1, $events);
self::assertInstanceOf(ActivationTokenUsed::class, $events[0]);
}
#[Test]
public function useThrowsExceptionWhenTokenAlreadyUsed(): void
{
$token = $this->createToken();
$firstUse = new DateTimeImmutable('2026-01-16 10:00:00');
$token->use($firstUse);
$this->expectException(ActivationTokenAlreadyUsedException::class);
$secondUse = new DateTimeImmutable('2026-01-17 10:00:00');
$token->use($secondUse);
}
#[Test]
public function useThrowsExceptionWhenTokenExpired(): void
{
$createdAt = new DateTimeImmutable('2026-01-15 10:00:00');
$usedAt = new DateTimeImmutable('2026-01-25 10:00:00');
$token = ActivationToken::generate(
userId: self::USER_ID,
email: self::EMAIL,
tenantId: TenantId::fromString(self::TENANT_ID),
role: self::ROLE,
schoolName: self::SCHOOL_NAME,
createdAt: $createdAt,
);
$this->expectException(ActivationTokenExpiredException::class);
$token->use($usedAt);
}
private function createToken(): ActivationToken
{
return ActivationToken::generate(
userId: self::USER_ID,
email: self::EMAIL,
tenantId: TenantId::fromString(self::TENANT_ID),
role: self::ROLE,
schoolName: self::SCHOOL_NAME,
createdAt: new DateTimeImmutable('2026-01-15 10:00:00'),
);
}
}