Les administrateurs ont besoin d'un moyen simple pour inviter les parents à rejoindre la plateforme. Cette fonctionnalité permet de générer des codes d'invitation uniques (8 caractères alphanumériques) avec une validité de 48h, de les envoyer par email, et de les activer via une page publique dédiée qui crée automatiquement le compte parent. L'interface d'administration offre l'envoi unitaire et en masse, le renvoi, le filtrage par statut, ainsi que la visualisation de l'état de chaque invitation (en attente, activée, expirée).
350 lines
12 KiB
PHP
350 lines
12 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Unit\Administration\Domain\Model\Invitation;
|
|
|
|
use App\Administration\Domain\Event\InvitationParentActivee;
|
|
use App\Administration\Domain\Event\InvitationParentEnvoyee;
|
|
use App\Administration\Domain\Exception\InvitationDejaActiveeException;
|
|
use App\Administration\Domain\Exception\InvitationExpireeException;
|
|
use App\Administration\Domain\Exception\InvitationNonEnvoyeeException;
|
|
use App\Administration\Domain\Model\Invitation\InvitationCode;
|
|
use App\Administration\Domain\Model\Invitation\InvitationStatus;
|
|
use App\Administration\Domain\Model\Invitation\ParentInvitation;
|
|
use App\Administration\Domain\Model\Invitation\ParentInvitationId;
|
|
use App\Administration\Domain\Model\User\Email;
|
|
use App\Administration\Domain\Model\User\UserId;
|
|
use App\Shared\Domain\Tenant\TenantId;
|
|
use DateTimeImmutable;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
final class ParentInvitationTest extends TestCase
|
|
{
|
|
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
|
|
private const string STUDENT_ID = '550e8400-e29b-41d4-a716-446655440002';
|
|
private const string CREATED_BY_ID = '550e8400-e29b-41d4-a716-446655440003';
|
|
private const string PARENT_EMAIL = 'parent@example.com';
|
|
private const string CODE = 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4';
|
|
|
|
#[Test]
|
|
public function creerCreatesInvitationWithCorrectProperties(): void
|
|
{
|
|
$invitation = $this->creerInvitation();
|
|
|
|
self::assertInstanceOf(ParentInvitationId::class, $invitation->id);
|
|
self::assertTrue(TenantId::fromString(self::TENANT_ID)->equals($invitation->tenantId));
|
|
self::assertTrue(UserId::fromString(self::STUDENT_ID)->equals($invitation->studentId));
|
|
self::assertSame(self::PARENT_EMAIL, (string) $invitation->parentEmail);
|
|
self::assertSame(self::CODE, (string) $invitation->code);
|
|
self::assertSame(InvitationStatus::PENDING, $invitation->status);
|
|
self::assertNull($invitation->sentAt);
|
|
self::assertNull($invitation->activatedAt);
|
|
self::assertNull($invitation->activatedUserId);
|
|
}
|
|
|
|
#[Test]
|
|
public function creerSetsExpirationTo7DaysAfterCreation(): void
|
|
{
|
|
$createdAt = new DateTimeImmutable('2026-02-20 10:00:00');
|
|
$expectedExpiration = new DateTimeImmutable('2026-02-27 10:00:00');
|
|
|
|
$invitation = $this->creerInvitation(createdAt: $createdAt);
|
|
|
|
self::assertEquals($expectedExpiration, $invitation->expiresAt);
|
|
}
|
|
|
|
#[Test]
|
|
public function envoyerChangesStatusToSent(): void
|
|
{
|
|
$invitation = $this->creerInvitation();
|
|
$sentAt = new DateTimeImmutable('2026-02-20 11:00:00');
|
|
|
|
$invitation->envoyer($sentAt);
|
|
|
|
self::assertSame(InvitationStatus::SENT, $invitation->status);
|
|
self::assertEquals($sentAt, $invitation->sentAt);
|
|
}
|
|
|
|
#[Test]
|
|
public function envoyerRecordsInvitationParentEnvoyeeEvent(): void
|
|
{
|
|
$invitation = $this->creerInvitation();
|
|
$sentAt = new DateTimeImmutable('2026-02-20 11:00:00');
|
|
|
|
$invitation->envoyer($sentAt);
|
|
|
|
$events = $invitation->pullDomainEvents();
|
|
self::assertCount(1, $events);
|
|
self::assertInstanceOf(InvitationParentEnvoyee::class, $events[0]);
|
|
}
|
|
|
|
#[Test]
|
|
public function envoyerThrowsExceptionWhenAlreadyActivated(): void
|
|
{
|
|
$invitation = $this->creerInvitationActivee();
|
|
|
|
$this->expectException(InvitationDejaActiveeException::class);
|
|
|
|
$invitation->envoyer(new DateTimeImmutable('2026-02-21 10:00:00'));
|
|
}
|
|
|
|
#[Test]
|
|
public function activerChangesStatusToActivated(): void
|
|
{
|
|
$invitation = $this->creerInvitationEnvoyee();
|
|
$parentUserId = UserId::generate();
|
|
$activatedAt = new DateTimeImmutable('2026-02-21 10:00:00');
|
|
|
|
$invitation->activer($parentUserId, $activatedAt);
|
|
|
|
self::assertSame(InvitationStatus::ACTIVATED, $invitation->status);
|
|
self::assertEquals($activatedAt, $invitation->activatedAt);
|
|
self::assertTrue($parentUserId->equals($invitation->activatedUserId));
|
|
}
|
|
|
|
#[Test]
|
|
public function activerRecordsInvitationParentActiveeEvent(): void
|
|
{
|
|
$invitation = $this->creerInvitationEnvoyee();
|
|
$parentUserId = UserId::generate();
|
|
$activatedAt = new DateTimeImmutable('2026-02-21 10:00:00');
|
|
$invitation->pullDomainEvents();
|
|
|
|
$invitation->activer($parentUserId, $activatedAt);
|
|
|
|
$events = $invitation->pullDomainEvents();
|
|
self::assertCount(1, $events);
|
|
self::assertInstanceOf(InvitationParentActivee::class, $events[0]);
|
|
}
|
|
|
|
#[Test]
|
|
public function activerThrowsExceptionWhenAlreadyActivated(): void
|
|
{
|
|
$invitation = $this->creerInvitationActivee();
|
|
|
|
$this->expectException(InvitationDejaActiveeException::class);
|
|
|
|
$invitation->activer(UserId::generate(), new DateTimeImmutable('2026-02-22 10:00:00'));
|
|
}
|
|
|
|
#[Test]
|
|
public function activerThrowsExceptionWhenNotSent(): void
|
|
{
|
|
$invitation = $this->creerInvitation();
|
|
|
|
$this->expectException(InvitationNonEnvoyeeException::class);
|
|
|
|
$invitation->activer(UserId::generate(), new DateTimeImmutable('2026-02-21 10:00:00'));
|
|
}
|
|
|
|
#[Test]
|
|
public function activerThrowsExceptionWhenExpired(): void
|
|
{
|
|
$invitation = $this->creerInvitationEnvoyee(
|
|
createdAt: new DateTimeImmutable('2026-02-01 10:00:00'),
|
|
);
|
|
|
|
$this->expectException(InvitationExpireeException::class);
|
|
|
|
$invitation->activer(
|
|
UserId::generate(),
|
|
new DateTimeImmutable('2026-02-20 10:00:00'),
|
|
);
|
|
}
|
|
|
|
#[Test]
|
|
public function marquerExpireeChangesStatusWhenSent(): void
|
|
{
|
|
$invitation = $this->creerInvitationEnvoyee();
|
|
|
|
$invitation->marquerExpiree();
|
|
|
|
self::assertSame(InvitationStatus::EXPIRED, $invitation->status);
|
|
}
|
|
|
|
#[Test]
|
|
public function marquerExpireeDoesNothingWhenPending(): void
|
|
{
|
|
$invitation = $this->creerInvitation();
|
|
|
|
$invitation->marquerExpiree();
|
|
|
|
self::assertSame(InvitationStatus::PENDING, $invitation->status);
|
|
}
|
|
|
|
#[Test]
|
|
public function marquerExpireeDoesNothingWhenActivated(): void
|
|
{
|
|
$invitation = $this->creerInvitationActivee();
|
|
|
|
$invitation->marquerExpiree();
|
|
|
|
self::assertSame(InvitationStatus::ACTIVATED, $invitation->status);
|
|
}
|
|
|
|
#[Test]
|
|
public function renvoyerResetsCodeAndStatusAndExpiration(): void
|
|
{
|
|
$invitation = $this->creerInvitationEnvoyee(
|
|
createdAt: new DateTimeImmutable('2026-02-01 10:00:00'),
|
|
);
|
|
$nouveauCode = new InvitationCode('11111111111111111111111111111111');
|
|
$renvoyeAt = new DateTimeImmutable('2026-02-15 10:00:00');
|
|
$expectedExpiration = new DateTimeImmutable('2026-02-22 10:00:00');
|
|
|
|
$invitation->renvoyer($nouveauCode, $renvoyeAt);
|
|
|
|
self::assertSame(InvitationStatus::SENT, $invitation->status);
|
|
self::assertSame('11111111111111111111111111111111', (string) $invitation->code);
|
|
self::assertEquals($renvoyeAt, $invitation->sentAt);
|
|
self::assertEquals($expectedExpiration, $invitation->expiresAt);
|
|
}
|
|
|
|
#[Test]
|
|
public function renvoyerThrowsExceptionWhenActivated(): void
|
|
{
|
|
$invitation = $this->creerInvitationActivee();
|
|
|
|
$this->expectException(InvitationDejaActiveeException::class);
|
|
|
|
$invitation->renvoyer(
|
|
new InvitationCode('11111111111111111111111111111111'),
|
|
new DateTimeImmutable('2026-02-21 10:00:00'),
|
|
);
|
|
}
|
|
|
|
#[Test]
|
|
public function renvoyerWorksOnExpiredInvitation(): void
|
|
{
|
|
$invitation = $this->creerInvitationEnvoyee(
|
|
createdAt: new DateTimeImmutable('2026-02-01 10:00:00'),
|
|
);
|
|
$invitation->marquerExpiree();
|
|
$nouveauCode = new InvitationCode('22222222222222222222222222222222');
|
|
$renvoyeAt = new DateTimeImmutable('2026-02-15 10:00:00');
|
|
|
|
$invitation->renvoyer($nouveauCode, $renvoyeAt);
|
|
|
|
self::assertSame(InvitationStatus::SENT, $invitation->status);
|
|
}
|
|
|
|
#[Test]
|
|
public function estExpireeReturnsFalseBeforeExpiration(): void
|
|
{
|
|
$invitation = $this->creerInvitation(
|
|
createdAt: new DateTimeImmutable('2026-02-20 10:00:00'),
|
|
);
|
|
|
|
self::assertFalse($invitation->estExpiree(new DateTimeImmutable('2026-02-25 10:00:00')));
|
|
}
|
|
|
|
#[Test]
|
|
public function estExpireeReturnsTrueAfterExpiration(): void
|
|
{
|
|
$invitation = $this->creerInvitation(
|
|
createdAt: new DateTimeImmutable('2026-02-20 10:00:00'),
|
|
);
|
|
|
|
self::assertTrue($invitation->estExpiree(new DateTimeImmutable('2026-02-28 10:00:00')));
|
|
}
|
|
|
|
#[Test]
|
|
public function estExpireeReturnsTrueAtExactExpiration(): void
|
|
{
|
|
$invitation = $this->creerInvitation(
|
|
createdAt: new DateTimeImmutable('2026-02-20 10:00:00'),
|
|
);
|
|
|
|
self::assertTrue($invitation->estExpiree(new DateTimeImmutable('2026-02-27 10:00:00')));
|
|
}
|
|
|
|
#[Test]
|
|
public function estActiveeReturnsFalseForNewInvitation(): void
|
|
{
|
|
$invitation = $this->creerInvitation();
|
|
|
|
self::assertFalse($invitation->estActivee());
|
|
}
|
|
|
|
#[Test]
|
|
public function estActiveeReturnsTrueAfterActivation(): void
|
|
{
|
|
$invitation = $this->creerInvitationActivee();
|
|
|
|
self::assertTrue($invitation->estActivee());
|
|
}
|
|
|
|
#[Test]
|
|
public function reconstitutePreservesAllProperties(): void
|
|
{
|
|
$id = ParentInvitationId::generate();
|
|
$tenantId = TenantId::fromString(self::TENANT_ID);
|
|
$studentId = UserId::fromString(self::STUDENT_ID);
|
|
$parentEmail = new Email(self::PARENT_EMAIL);
|
|
$code = new InvitationCode(self::CODE);
|
|
$createdAt = new DateTimeImmutable('2026-02-20 10:00:00');
|
|
$createdBy = UserId::fromString(self::CREATED_BY_ID);
|
|
$sentAt = new DateTimeImmutable('2026-02-20 11:00:00');
|
|
$activatedAt = new DateTimeImmutable('2026-02-21 10:00:00');
|
|
$activatedUserId = UserId::generate();
|
|
|
|
$invitation = ParentInvitation::reconstitute(
|
|
id: $id,
|
|
tenantId: $tenantId,
|
|
studentId: $studentId,
|
|
parentEmail: $parentEmail,
|
|
code: $code,
|
|
status: InvitationStatus::ACTIVATED,
|
|
expiresAt: new DateTimeImmutable('2026-02-27 10:00:00'),
|
|
createdAt: $createdAt,
|
|
createdBy: $createdBy,
|
|
sentAt: $sentAt,
|
|
activatedAt: $activatedAt,
|
|
activatedUserId: $activatedUserId,
|
|
);
|
|
|
|
self::assertTrue($id->equals($invitation->id));
|
|
self::assertTrue($tenantId->equals($invitation->tenantId));
|
|
self::assertTrue($studentId->equals($invitation->studentId));
|
|
self::assertTrue($parentEmail->equals($invitation->parentEmail));
|
|
self::assertTrue($code->equals($invitation->code));
|
|
self::assertSame(InvitationStatus::ACTIVATED, $invitation->status);
|
|
self::assertEquals($sentAt, $invitation->sentAt);
|
|
self::assertEquals($activatedAt, $invitation->activatedAt);
|
|
self::assertTrue($activatedUserId->equals($invitation->activatedUserId));
|
|
}
|
|
|
|
private function creerInvitation(?DateTimeImmutable $createdAt = null): ParentInvitation
|
|
{
|
|
return ParentInvitation::creer(
|
|
tenantId: TenantId::fromString(self::TENANT_ID),
|
|
studentId: UserId::fromString(self::STUDENT_ID),
|
|
parentEmail: new Email(self::PARENT_EMAIL),
|
|
code: new InvitationCode(self::CODE),
|
|
createdAt: $createdAt ?? new DateTimeImmutable('2026-02-20 10:00:00'),
|
|
createdBy: UserId::fromString(self::CREATED_BY_ID),
|
|
);
|
|
}
|
|
|
|
private function creerInvitationEnvoyee(?DateTimeImmutable $createdAt = null): ParentInvitation
|
|
{
|
|
$invitation = $this->creerInvitation($createdAt);
|
|
$invitation->envoyer($createdAt ?? new DateTimeImmutable('2026-02-20 10:30:00'));
|
|
|
|
return $invitation;
|
|
}
|
|
|
|
private function creerInvitationActivee(): ParentInvitation
|
|
{
|
|
$invitation = $this->creerInvitationEnvoyee();
|
|
$invitation->activer(
|
|
UserId::generate(),
|
|
new DateTimeImmutable('2026-02-21 10:00:00'),
|
|
);
|
|
|
|
return $invitation;
|
|
}
|
|
}
|