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
221 lines
7.5 KiB
PHP
221 lines
7.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Unit\Administration\Domain\Model\User;
|
|
|
|
use App\Administration\Domain\Event\CompteActive;
|
|
use App\Administration\Domain\Event\CompteCreated;
|
|
use App\Administration\Domain\Exception\CompteNonActivableException;
|
|
use App\Administration\Domain\Model\ConsentementParental\ConsentementParental;
|
|
use App\Administration\Domain\Model\User\Email;
|
|
use App\Administration\Domain\Model\User\Role;
|
|
use App\Administration\Domain\Model\User\StatutCompte;
|
|
use App\Administration\Domain\Model\User\User;
|
|
use App\Administration\Domain\Policy\ConsentementParentalPolicy;
|
|
use App\Shared\Domain\Clock;
|
|
use App\Shared\Infrastructure\Tenant\TenantId;
|
|
use DateTimeImmutable;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
final class UserTest extends TestCase
|
|
{
|
|
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440002';
|
|
private const string SCHOOL_NAME = 'École Alpha';
|
|
|
|
private Clock $clock;
|
|
private ConsentementParentalPolicy $consentementPolicy;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->clock = new class implements Clock {
|
|
public function now(): DateTimeImmutable
|
|
{
|
|
return new DateTimeImmutable('2026-01-31 10:00:00');
|
|
}
|
|
};
|
|
|
|
$this->consentementPolicy = new ConsentementParentalPolicy($this->clock);
|
|
}
|
|
|
|
#[Test]
|
|
public function creerCreatesUserWithPendingStatus(): void
|
|
{
|
|
$user = $this->createUser();
|
|
|
|
self::assertSame(StatutCompte::EN_ATTENTE, $user->statut);
|
|
self::assertNull($user->hashedPassword);
|
|
self::assertNull($user->activatedAt);
|
|
}
|
|
|
|
#[Test]
|
|
public function creerRecordsCompteCreatedEvent(): void
|
|
{
|
|
$user = $this->createUser();
|
|
|
|
$events = $user->pullDomainEvents();
|
|
|
|
self::assertCount(1, $events);
|
|
self::assertInstanceOf(CompteCreated::class, $events[0]);
|
|
}
|
|
|
|
#[Test]
|
|
public function activerSetsPasswordAndChangesStatusToActive(): void
|
|
{
|
|
$user = $this->createUser();
|
|
$hashedPassword = '$argon2id$hashed';
|
|
$activatedAt = new DateTimeImmutable('2026-01-31 10:00:00');
|
|
|
|
$user->activer($hashedPassword, $activatedAt, $this->consentementPolicy);
|
|
|
|
self::assertSame(StatutCompte::ACTIF, $user->statut);
|
|
self::assertSame($hashedPassword, $user->hashedPassword);
|
|
self::assertEquals($activatedAt, $user->activatedAt);
|
|
}
|
|
|
|
#[Test]
|
|
public function activerRecordsCompteActiveEvent(): void
|
|
{
|
|
$user = $this->createUser();
|
|
$user->pullDomainEvents();
|
|
|
|
$user->activer('$argon2id$hashed', new DateTimeImmutable(), $this->consentementPolicy);
|
|
|
|
$events = $user->pullDomainEvents();
|
|
self::assertCount(1, $events);
|
|
self::assertInstanceOf(CompteActive::class, $events[0]);
|
|
}
|
|
|
|
#[Test]
|
|
public function activerThrowsWhenStatusIsNotPending(): void
|
|
{
|
|
$user = $this->createUser();
|
|
$user->activer('$argon2id$hashed', new DateTimeImmutable(), $this->consentementPolicy);
|
|
|
|
$this->expectException(CompteNonActivableException::class);
|
|
|
|
$user->activer('$argon2id$another', new DateTimeImmutable(), $this->consentementPolicy);
|
|
}
|
|
|
|
#[Test]
|
|
public function activerThrowsForMinorWithoutConsent(): void
|
|
{
|
|
// Créer un utilisateur mineur (14 ans)
|
|
$user = User::creer(
|
|
email: new Email('eleve@example.com'),
|
|
role: Role::ELEVE,
|
|
tenantId: TenantId::fromString(self::TENANT_ID),
|
|
schoolName: self::SCHOOL_NAME,
|
|
dateNaissance: new DateTimeImmutable('2012-06-15'), // 13 ans
|
|
createdAt: new DateTimeImmutable('2026-01-15 10:00:00'),
|
|
);
|
|
|
|
$this->expectException(CompteNonActivableException::class);
|
|
$this->expectExceptionMessage('consentement parental manquant');
|
|
|
|
$user->activer('$argon2id$hashed', new DateTimeImmutable(), $this->consentementPolicy);
|
|
}
|
|
|
|
#[Test]
|
|
public function activerSucceedsForMinorWithConsent(): void
|
|
{
|
|
// Créer un utilisateur mineur (14 ans)
|
|
$user = User::creer(
|
|
email: new Email('eleve@example.com'),
|
|
role: Role::ELEVE,
|
|
tenantId: TenantId::fromString(self::TENANT_ID),
|
|
schoolName: self::SCHOOL_NAME,
|
|
dateNaissance: new DateTimeImmutable('2012-06-15'),
|
|
createdAt: new DateTimeImmutable('2026-01-15 10:00:00'),
|
|
);
|
|
|
|
// Enregistrer le consentement parental
|
|
$consentement = ConsentementParental::accorder(
|
|
parentId: 'parent-uuid',
|
|
eleveId: (string) $user->id,
|
|
at: new DateTimeImmutable('2026-01-20 10:00:00'),
|
|
ipAddress: '192.168.1.1',
|
|
);
|
|
$user->enregistrerConsentementParental($consentement);
|
|
|
|
// L'activation devrait maintenant réussir
|
|
$user->activer('$argon2id$hashed', new DateTimeImmutable(), $this->consentementPolicy);
|
|
|
|
self::assertSame(StatutCompte::ACTIF, $user->statut);
|
|
}
|
|
|
|
#[Test]
|
|
public function activerSucceedsForAdultWithoutConsent(): void
|
|
{
|
|
// Créer un utilisateur adulte (16 ans)
|
|
$user = User::creer(
|
|
email: new Email('eleve@example.com'),
|
|
role: Role::ELEVE,
|
|
tenantId: TenantId::fromString(self::TENANT_ID),
|
|
schoolName: self::SCHOOL_NAME,
|
|
dateNaissance: new DateTimeImmutable('2010-01-01'), // 16 ans
|
|
createdAt: new DateTimeImmutable('2026-01-15 10:00:00'),
|
|
);
|
|
|
|
// Pas de consentement nécessaire
|
|
$user->activer('$argon2id$hashed', new DateTimeImmutable(), $this->consentementPolicy);
|
|
|
|
self::assertSame(StatutCompte::ACTIF, $user->statut);
|
|
}
|
|
|
|
#[Test]
|
|
public function peutSeConnecterReturnsTrueOnlyWhenActive(): void
|
|
{
|
|
$user = $this->createUser();
|
|
|
|
self::assertFalse($user->peutSeConnecter());
|
|
|
|
$user->activer('$argon2id$hashed', new DateTimeImmutable(), $this->consentementPolicy);
|
|
|
|
self::assertTrue($user->peutSeConnecter());
|
|
}
|
|
|
|
#[Test]
|
|
public function necessiteConsentementParentalReturnsTrueForMinor(): void
|
|
{
|
|
$user = User::creer(
|
|
email: new Email('eleve@example.com'),
|
|
role: Role::ELEVE,
|
|
tenantId: TenantId::fromString(self::TENANT_ID),
|
|
schoolName: self::SCHOOL_NAME,
|
|
dateNaissance: new DateTimeImmutable('2012-06-15'), // 13 ans
|
|
createdAt: new DateTimeImmutable('2026-01-15 10:00:00'),
|
|
);
|
|
|
|
self::assertTrue($user->necessiteConsentementParental($this->consentementPolicy));
|
|
}
|
|
|
|
#[Test]
|
|
public function necessiteConsentementParentalReturnsFalseForAdult(): void
|
|
{
|
|
$user = User::creer(
|
|
email: new Email('parent@example.com'),
|
|
role: Role::PARENT,
|
|
tenantId: TenantId::fromString(self::TENANT_ID),
|
|
schoolName: self::SCHOOL_NAME,
|
|
dateNaissance: null, // Parents n'ont pas de date de naissance enregistrée
|
|
createdAt: new DateTimeImmutable('2026-01-15 10:00:00'),
|
|
);
|
|
|
|
self::assertFalse($user->necessiteConsentementParental($this->consentementPolicy));
|
|
}
|
|
|
|
private function createUser(): User
|
|
{
|
|
return User::creer(
|
|
email: new Email('user@example.com'),
|
|
role: Role::PARENT,
|
|
tenantId: TenantId::fromString(self::TENANT_ID),
|
|
schoolName: self::SCHOOL_NAME,
|
|
dateNaissance: null,
|
|
createdAt: new DateTimeImmutable('2026-01-15 10:00:00'),
|
|
);
|
|
}
|
|
}
|