Files
Classeo/backend/tests/Unit/Administration/Domain/Model/User/UserTest.php
Mathias STRASSER 9ccad77bf0 feat: Messaging asynchrone fiable avec retry, dead-letter et métriques
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.
2026-02-08 21:38:20 +01:00

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'),
);
}
}