Files
Classeo/backend/tests/Unit/Administration/Domain/Model/ActivationToken/ActivationTokenTest.php
Mathias STRASSER c5e6c1d810 feat: Activation de compte utilisateur avec validation token
L'inscription Classeo se fait via invitation : un admin crée un compte,
l'utilisateur reçoit un lien d'activation par email pour définir son
mot de passe. Ce flow sécurisé évite les inscriptions non autorisées
et garantit que seuls les utilisateurs légitimes accèdent au système.

Points clés de l'implémentation :
- Tokens d'activation à usage unique stockés en cache (Redis/filesystem)
- Validation du consentement parental pour les mineurs < 15 ans (RGPD)
- L'échec d'activation ne consume pas le token (retry possible)
- Users dans un cache séparé sans TTL (pas d'expiration)
- Hot reload en dev (FrankenPHP sans mode worker)

Story: 1.3 - Inscription et activation de compte
2026-01-31 19:34:03 +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\Infrastructure\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 tokenValueIsUuidV4Format(): void
{
$token = $this->createToken();
self::assertMatchesRegularExpression(
'/^[a-f0-9]{8}-[a-f0-9]{4}-4[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'),
);
}
}