feat: Permettre la génération et l'envoi de codes d'invitation aux parents
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).
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Administration\Infrastructure\Console;
|
||||
|
||||
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\User\Email;
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Administration\Infrastructure\Console\ExpireInvitationsCommand;
|
||||
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryParentInvitationRepository;
|
||||
use App\Shared\Domain\Clock;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
final class ExpireInvitationsCommandTest 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 InMemoryParentInvitationRepository $repository;
|
||||
private Clock $clock;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->repository = new InMemoryParentInvitationRepository();
|
||||
$this->clock = new class implements Clock {
|
||||
public function now(): DateTimeImmutable
|
||||
{
|
||||
return new DateTimeImmutable('2026-02-28 10:00:00');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itExpiresInvitationsPastExpirationDate(): void
|
||||
{
|
||||
$invitation = $this->creerInvitation(
|
||||
createdAt: new DateTimeImmutable('2026-02-01 10:00:00'),
|
||||
code: 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4',
|
||||
);
|
||||
$invitation->envoyer(new DateTimeImmutable('2026-02-01 11:00:00'));
|
||||
$this->repository->save($invitation);
|
||||
|
||||
$tester = $this->executeCommand();
|
||||
|
||||
self::assertSame(0, $tester->getStatusCode());
|
||||
self::assertStringContainsString('1 invitation(s) expirée(s) trouvée(s)', $tester->getDisplay());
|
||||
self::assertStringContainsString('1 invitation(s) marquée(s) comme expirée(s)', $tester->getDisplay());
|
||||
self::assertSame(InvitationStatus::EXPIRED, $invitation->status);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itHandlesNoExpiredInvitations(): void
|
||||
{
|
||||
$tester = $this->executeCommand();
|
||||
|
||||
self::assertSame(0, $tester->getStatusCode());
|
||||
self::assertStringContainsString('Aucune invitation expirée à traiter.', $tester->getDisplay());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itDoesNotExpirePendingInvitations(): void
|
||||
{
|
||||
$invitation = $this->creerInvitation(
|
||||
createdAt: new DateTimeImmutable('2026-02-01 10:00:00'),
|
||||
code: 'b1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4',
|
||||
);
|
||||
$this->repository->save($invitation);
|
||||
|
||||
$tester = $this->executeCommand();
|
||||
|
||||
self::assertSame(0, $tester->getStatusCode());
|
||||
self::assertStringContainsString('Aucune invitation expirée à traiter.', $tester->getDisplay());
|
||||
self::assertSame(InvitationStatus::PENDING, $invitation->status);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itDoesNotExpireNonExpiredSentInvitations(): void
|
||||
{
|
||||
$invitation = $this->creerInvitation(
|
||||
createdAt: new DateTimeImmutable('2026-02-25 10:00:00'),
|
||||
code: 'c1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4',
|
||||
);
|
||||
$invitation->envoyer(new DateTimeImmutable('2026-02-25 11:00:00'));
|
||||
$this->repository->save($invitation);
|
||||
|
||||
$tester = $this->executeCommand();
|
||||
|
||||
self::assertSame(0, $tester->getStatusCode());
|
||||
self::assertStringContainsString('Aucune invitation expirée à traiter.', $tester->getDisplay());
|
||||
self::assertSame(InvitationStatus::SENT, $invitation->status);
|
||||
}
|
||||
|
||||
private function executeCommand(): CommandTester
|
||||
{
|
||||
$command = new ExpireInvitationsCommand(
|
||||
$this->repository,
|
||||
$this->clock,
|
||||
new NullLogger(),
|
||||
);
|
||||
|
||||
$tester = new CommandTester($command);
|
||||
$tester->execute([]);
|
||||
|
||||
return $tester;
|
||||
}
|
||||
|
||||
private function creerInvitation(
|
||||
DateTimeImmutable $createdAt,
|
||||
string $code,
|
||||
): ParentInvitation {
|
||||
return ParentInvitation::creer(
|
||||
tenantId: TenantId::fromString(self::TENANT_ID),
|
||||
studentId: UserId::fromString(self::STUDENT_ID),
|
||||
parentEmail: new Email('parent@example.com'),
|
||||
code: new InvitationCode($code),
|
||||
createdAt: $createdAt,
|
||||
createdBy: UserId::fromString(self::CREATED_BY_ID),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user