Les événements métier (emails d'invitation, reset password, activation) bloquaient la réponse API en étant traités de manière synchrone. Ce commit route ces événements vers un transport AMQP asynchrone avec un worker dédié, garantissant des réponses API rapides et une gestion robuste des échecs. Le retry utilise une stratégie Fibonacci (1s, 1s, 2s, 3s, 5s, 8s, 13s) qui offre un bon compromis entre réactivité et protection des services externes. Les messages qui épuisent leurs tentatives arrivent dans une dead-letter queue Doctrine avec alerte email à l'admin. La commande console CreateTestActivationTokenCommand détecte désormais les comptes déjà actifs et génère un token de réinitialisation de mot de passe au lieu d'un token d'activation, évitant une erreur bloquante lors de la ré-invitation par un admin.
222 lines
7.5 KiB
PHP
222 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\Domain\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);
|
|
$this->expectExceptionMessage('Ce compte est déjà actif.');
|
|
|
|
$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'),
|
|
);
|
|
}
|
|
}
|