Les parents doivent pouvoir suivre la scolarité de leurs enfants (notes, emploi du temps, devoirs). Cela nécessite un lien formalisé entre le compte parent et le compte élève, géré par les administrateurs. Le lien est établi soit manuellement via l'interface d'administration, soit automatiquement lors de l'activation du compte parent lorsque l'invitation inclut un élève cible. Ce lien conditionne l'accès aux données scolaires de l'enfant (autorisations vérifiées par un voter dédié).
268 lines
8.4 KiB
PHP
268 lines
8.4 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);
|
|
}
|
|
|
|
#[Test]
|
|
public function generateStoresStudentIdWhenProvided(): void
|
|
{
|
|
$studentId = '550e8400-e29b-41d4-a716-446655440099';
|
|
|
|
$token = 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'),
|
|
studentId: $studentId,
|
|
);
|
|
|
|
self::assertSame($studentId, $token->studentId);
|
|
}
|
|
|
|
#[Test]
|
|
public function generateHasNullStudentIdByDefault(): void
|
|
{
|
|
$token = $this->createToken();
|
|
|
|
self::assertNull($token->studentId);
|
|
}
|
|
|
|
#[Test]
|
|
public function reconstitutePreservesStudentId(): void
|
|
{
|
|
$studentId = '550e8400-e29b-41d4-a716-446655440099';
|
|
|
|
$token = ActivationToken::reconstitute(
|
|
id: ActivationTokenId::fromString('550e8400-e29b-41d4-a716-446655440010'),
|
|
tokenValue: 'some-token-value',
|
|
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'),
|
|
expiresAt: new DateTimeImmutable('2026-01-22 10:00:00'),
|
|
usedAt: null,
|
|
studentId: $studentId,
|
|
);
|
|
|
|
self::assertSame($studentId, $token->studentId);
|
|
}
|
|
|
|
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'),
|
|
);
|
|
}
|
|
}
|