Files
Classeo/backend/tests/Unit/Administration/Application/Query/GetParentInvitations/GetParentInvitationsHandlerTest.php
Mathias STRASSER be1b0b60a6 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).
2026-02-28 16:37:10 +01:00

208 lines
7.3 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\Unit\Administration\Application\Query\GetParentInvitations;
use App\Administration\Application\Query\GetParentInvitations\GetParentInvitationsHandler;
use App\Administration\Application\Query\GetParentInvitations\GetParentInvitationsQuery;
use App\Administration\Domain\Model\Invitation\InvitationCode;
use App\Administration\Domain\Model\Invitation\ParentInvitation;
use App\Administration\Domain\Model\User\Email;
use App\Administration\Domain\Model\User\Role;
use App\Administration\Domain\Model\User\User;
use App\Administration\Domain\Model\User\UserId;
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryParentInvitationRepository;
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryUserRepository;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class GetParentInvitationsHandlerTest extends TestCase
{
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440002';
private const string OTHER_TENANT_ID = '550e8400-e29b-41d4-a716-446655440099';
private InMemoryParentInvitationRepository $invitationRepository;
private InMemoryUserRepository $userRepository;
private GetParentInvitationsHandler $handler;
protected function setUp(): void
{
$this->invitationRepository = new InMemoryParentInvitationRepository();
$this->userRepository = new InMemoryUserRepository();
$this->handler = new GetParentInvitationsHandler(
$this->invitationRepository,
$this->userRepository,
);
}
#[Test]
public function itReturnsAllInvitationsForTenant(): void
{
$student = $this->createAndSaveStudent('Alice', 'Dupont');
$this->createAndSaveInvitation($student->id, 'parent1@example.com');
$this->createAndSaveInvitation($student->id, 'parent2@example.com');
$result = ($this->handler)(new GetParentInvitationsQuery(
tenantId: self::TENANT_ID,
));
self::assertSame(2, $result->total);
self::assertCount(2, $result->items);
}
#[Test]
public function itFiltersInvitationsByStatus(): void
{
$student = $this->createAndSaveStudent('Bob', 'Martin');
$invitation = $this->createAndSaveInvitation($student->id, 'parent@example.com');
$this->createPendingInvitation($student->id, 'parent2@example.com');
$result = ($this->handler)(new GetParentInvitationsQuery(
tenantId: self::TENANT_ID,
status: 'sent',
));
self::assertSame(1, $result->total);
self::assertSame('parent@example.com', $result->items[0]->parentEmail);
}
#[Test]
public function itFiltersInvitationsByStudentId(): void
{
$student1 = $this->createAndSaveStudent('Alice', 'Dupont');
$student2 = $this->createAndSaveStudent('Bob', 'Martin');
$this->createAndSaveInvitation($student1->id, 'parent1@example.com');
$this->createAndSaveInvitation($student2->id, 'parent2@example.com');
$result = ($this->handler)(new GetParentInvitationsQuery(
tenantId: self::TENANT_ID,
studentId: (string) $student1->id,
));
self::assertSame(1, $result->total);
self::assertSame('parent1@example.com', $result->items[0]->parentEmail);
}
#[Test]
public function itSearchesByParentEmailOrStudentName(): void
{
$student = $this->createAndSaveStudent('Alice', 'Dupont');
$this->createAndSaveInvitation($student->id, 'parent@example.com');
$this->createAndSaveInvitation($student->id, 'other@example.com');
$result = ($this->handler)(new GetParentInvitationsQuery(
tenantId: self::TENANT_ID,
search: 'Alice',
));
self::assertSame(2, $result->total);
$result = ($this->handler)(new GetParentInvitationsQuery(
tenantId: self::TENANT_ID,
search: 'parent@',
));
self::assertSame(1, $result->total);
}
#[Test]
public function itPaginatesResults(): void
{
$student = $this->createAndSaveStudent('Alice', 'Dupont');
for ($i = 0; $i < 5; ++$i) {
$this->createAndSaveInvitation($student->id, "parent{$i}@example.com");
}
$result = ($this->handler)(new GetParentInvitationsQuery(
tenantId: self::TENANT_ID,
page: 1,
limit: 2,
));
self::assertSame(5, $result->total);
self::assertCount(2, $result->items);
}
#[Test]
public function itEnrichesResultsWithStudentNames(): void
{
$student = $this->createAndSaveStudent('Alice', 'Dupont');
$this->createAndSaveInvitation($student->id, 'parent@example.com');
$result = ($this->handler)(new GetParentInvitationsQuery(
tenantId: self::TENANT_ID,
));
self::assertSame('Alice', $result->items[0]->studentFirstName);
self::assertSame('Dupont', $result->items[0]->studentLastName);
}
#[Test]
public function itIsolatesByTenant(): void
{
$student = $this->createAndSaveStudent('Alice', 'Dupont');
$this->createAndSaveInvitation($student->id, 'parent@example.com');
$result = ($this->handler)(new GetParentInvitationsQuery(
tenantId: self::OTHER_TENANT_ID,
));
self::assertSame(0, $result->total);
}
private function createAndSaveStudent(string $firstName, string $lastName): User
{
$student = User::inviter(
email: new Email($firstName . '@example.com'),
role: Role::ELEVE,
tenantId: TenantId::fromString(self::TENANT_ID),
schoolName: 'École Alpha',
firstName: $firstName,
lastName: $lastName,
invitedAt: new DateTimeImmutable('2026-02-07 10:00:00'),
);
$student->pullDomainEvents();
$this->userRepository->save($student);
return $student;
}
private function createAndSaveInvitation(UserId $studentId, string $parentEmail): ParentInvitation
{
$code = bin2hex(random_bytes(16));
$invitation = ParentInvitation::creer(
tenantId: TenantId::fromString(self::TENANT_ID),
studentId: $studentId,
parentEmail: new Email($parentEmail),
code: new InvitationCode($code),
createdAt: new DateTimeImmutable('2026-02-07 10:00:00'),
createdBy: UserId::generate(),
);
$invitation->envoyer(new DateTimeImmutable('2026-02-07 10:00:00'));
$invitation->pullDomainEvents();
$this->invitationRepository->save($invitation);
return $invitation;
}
private function createPendingInvitation(UserId $studentId, string $parentEmail): ParentInvitation
{
$code = bin2hex(random_bytes(16));
$invitation = ParentInvitation::creer(
tenantId: TenantId::fromString(self::TENANT_ID),
studentId: $studentId,
parentEmail: new Email($parentEmail),
code: new InvitationCode($code),
createdAt: new DateTimeImmutable('2026-02-07 10:00:00'),
createdBy: UserId::generate(),
);
$invitation->pullDomainEvents();
$this->invitationRepository->save($invitation);
return $invitation;
}
}