Les parents doivent pouvoir suivre la scolarité de leurs enfants (notes, emploi du temps, devoirs). Cela nécessite un lien formalisé entre le compte parent et le compte élève, géré par les administrateurs. Le lien est établi soit manuellement via l'interface d'administration, soit automatiquement lors de l'activation du compte parent lorsque l'invitation inclut un élève cible. Ce lien conditionne l'accès aux données scolaires de l'enfant (autorisations vérifiées par un voter dédié).
299 lines
10 KiB
PHP
299 lines
10 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Unit\Administration\Application\Command\LinkParentToStudent;
|
|
|
|
use App\Administration\Application\Command\LinkParentToStudent\LinkParentToStudentCommand;
|
|
use App\Administration\Application\Command\LinkParentToStudent\LinkParentToStudentHandler;
|
|
use App\Administration\Domain\Exception\InvalidGuardianRoleException;
|
|
use App\Administration\Domain\Exception\InvalidStudentRoleException;
|
|
use App\Administration\Domain\Exception\LiaisonDejaExistanteException;
|
|
use App\Administration\Domain\Exception\MaxGuardiansReachedException;
|
|
use App\Administration\Domain\Exception\TenantMismatchException;
|
|
use App\Administration\Domain\Model\StudentGuardian\RelationshipType;
|
|
use App\Administration\Domain\Model\StudentGuardian\StudentGuardian;
|
|
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\Domain\Repository\UserRepository;
|
|
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryStudentGuardianRepository;
|
|
use App\Shared\Domain\Clock;
|
|
use App\Shared\Domain\Tenant\TenantId;
|
|
use DateTimeImmutable;
|
|
use InvalidArgumentException;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
final class LinkParentToStudentHandlerTest 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 GUARDIAN_ID = '550e8400-e29b-41d4-a716-446655440003';
|
|
private const string GUARDIAN_2_ID = '550e8400-e29b-41d4-a716-446655440004';
|
|
private const string GUARDIAN_3_ID = '550e8400-e29b-41d4-a716-446655440005';
|
|
private const string ADMIN_ID = '550e8400-e29b-41d4-a716-446655440006';
|
|
private const string OTHER_TENANT_ID = '550e8400-e29b-41d4-a716-446655440099';
|
|
|
|
private InMemoryStudentGuardianRepository $repository;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->repository = new InMemoryStudentGuardianRepository();
|
|
}
|
|
|
|
private function createHandlerWithMockedUsers(
|
|
?Role $guardianRole = null,
|
|
?Role $studentRole = null,
|
|
?string $guardianTenantId = null,
|
|
?string $studentTenantId = null,
|
|
): LinkParentToStudentHandler {
|
|
$guardianRole ??= Role::PARENT;
|
|
$studentRole ??= Role::ELEVE;
|
|
|
|
$now = new DateTimeImmutable('2026-02-10 10:00:00');
|
|
|
|
$guardianUser = User::creer(
|
|
email: new Email('guardian@example.com'),
|
|
role: $guardianRole,
|
|
tenantId: TenantId::fromString($guardianTenantId ?? self::TENANT_ID),
|
|
schoolName: 'École Test',
|
|
dateNaissance: null,
|
|
createdAt: $now,
|
|
);
|
|
|
|
$studentUser = User::creer(
|
|
email: new Email('student@example.com'),
|
|
role: $studentRole,
|
|
tenantId: TenantId::fromString($studentTenantId ?? self::TENANT_ID),
|
|
schoolName: 'École Test',
|
|
dateNaissance: null,
|
|
createdAt: $now,
|
|
);
|
|
|
|
$userRepository = $this->createMock(UserRepository::class);
|
|
$userRepository->method('get')->willReturnCallback(
|
|
static function (UserId $id) use ($guardianUser, $studentUser): User {
|
|
if ((string) $id === self::GUARDIAN_ID || (string) $id === self::GUARDIAN_2_ID || (string) $id === self::GUARDIAN_3_ID) {
|
|
return $guardianUser;
|
|
}
|
|
|
|
return $studentUser;
|
|
},
|
|
);
|
|
|
|
$clock = new class implements Clock {
|
|
public function now(): DateTimeImmutable
|
|
{
|
|
return new DateTimeImmutable('2026-02-10 10:00:00');
|
|
}
|
|
};
|
|
|
|
return new LinkParentToStudentHandler($this->repository, $userRepository, $clock);
|
|
}
|
|
|
|
#[Test]
|
|
public function linkParentToStudentSuccessfully(): void
|
|
{
|
|
$handler = $this->createHandlerWithMockedUsers();
|
|
|
|
$command = new LinkParentToStudentCommand(
|
|
studentId: self::STUDENT_ID,
|
|
guardianId: self::GUARDIAN_ID,
|
|
relationshipType: RelationshipType::FATHER->value,
|
|
tenantId: self::TENANT_ID,
|
|
createdBy: self::ADMIN_ID,
|
|
);
|
|
|
|
$link = ($handler)($command);
|
|
|
|
self::assertInstanceOf(StudentGuardian::class, $link);
|
|
self::assertSame(RelationshipType::FATHER, $link->relationshipType);
|
|
self::assertNotNull($link->createdBy);
|
|
}
|
|
|
|
#[Test]
|
|
public function linkIsSavedToRepository(): void
|
|
{
|
|
$handler = $this->createHandlerWithMockedUsers();
|
|
|
|
$command = new LinkParentToStudentCommand(
|
|
studentId: self::STUDENT_ID,
|
|
guardianId: self::GUARDIAN_ID,
|
|
relationshipType: RelationshipType::MOTHER->value,
|
|
tenantId: self::TENANT_ID,
|
|
);
|
|
|
|
$link = ($handler)($command);
|
|
|
|
$saved = $this->repository->get($link->id, TenantId::fromString(self::TENANT_ID));
|
|
self::assertTrue($saved->id->equals($link->id));
|
|
}
|
|
|
|
#[Test]
|
|
public function throwsWhenLinkAlreadyExists(): void
|
|
{
|
|
$handler = $this->createHandlerWithMockedUsers();
|
|
|
|
$command = new LinkParentToStudentCommand(
|
|
studentId: self::STUDENT_ID,
|
|
guardianId: self::GUARDIAN_ID,
|
|
relationshipType: RelationshipType::FATHER->value,
|
|
tenantId: self::TENANT_ID,
|
|
);
|
|
|
|
($handler)($command);
|
|
|
|
$this->expectException(LiaisonDejaExistanteException::class);
|
|
($handler)($command);
|
|
}
|
|
|
|
#[Test]
|
|
public function throwsWhenMaxGuardiansReached(): void
|
|
{
|
|
$handler = $this->createHandlerWithMockedUsers();
|
|
|
|
($handler)(new LinkParentToStudentCommand(
|
|
studentId: self::STUDENT_ID,
|
|
guardianId: self::GUARDIAN_ID,
|
|
relationshipType: RelationshipType::FATHER->value,
|
|
tenantId: self::TENANT_ID,
|
|
));
|
|
|
|
($handler)(new LinkParentToStudentCommand(
|
|
studentId: self::STUDENT_ID,
|
|
guardianId: self::GUARDIAN_2_ID,
|
|
relationshipType: RelationshipType::MOTHER->value,
|
|
tenantId: self::TENANT_ID,
|
|
));
|
|
|
|
$this->expectException(MaxGuardiansReachedException::class);
|
|
($handler)(new LinkParentToStudentCommand(
|
|
studentId: self::STUDENT_ID,
|
|
guardianId: self::GUARDIAN_3_ID,
|
|
relationshipType: RelationshipType::TUTOR_M->value,
|
|
tenantId: self::TENANT_ID,
|
|
));
|
|
}
|
|
|
|
#[Test]
|
|
public function allowsTwoGuardiansForSameStudent(): void
|
|
{
|
|
$handler = $this->createHandlerWithMockedUsers();
|
|
|
|
($handler)(new LinkParentToStudentCommand(
|
|
studentId: self::STUDENT_ID,
|
|
guardianId: self::GUARDIAN_ID,
|
|
relationshipType: RelationshipType::FATHER->value,
|
|
tenantId: self::TENANT_ID,
|
|
));
|
|
|
|
$link2 = ($handler)(new LinkParentToStudentCommand(
|
|
studentId: self::STUDENT_ID,
|
|
guardianId: self::GUARDIAN_2_ID,
|
|
relationshipType: RelationshipType::MOTHER->value,
|
|
tenantId: self::TENANT_ID,
|
|
));
|
|
|
|
self::assertInstanceOf(StudentGuardian::class, $link2);
|
|
self::assertSame(2, $this->repository->countGuardiansForStudent(
|
|
$link2->studentId,
|
|
TenantId::fromString(self::TENANT_ID),
|
|
));
|
|
}
|
|
|
|
#[Test]
|
|
public function linkWithoutCreatedByAllowsNull(): void
|
|
{
|
|
$handler = $this->createHandlerWithMockedUsers();
|
|
|
|
$command = new LinkParentToStudentCommand(
|
|
studentId: self::STUDENT_ID,
|
|
guardianId: self::GUARDIAN_ID,
|
|
relationshipType: RelationshipType::FATHER->value,
|
|
tenantId: self::TENANT_ID,
|
|
);
|
|
|
|
$link = ($handler)($command);
|
|
|
|
self::assertNull($link->createdBy);
|
|
}
|
|
|
|
#[Test]
|
|
public function throwsWhenGuardianIsNotParent(): void
|
|
{
|
|
$handler = $this->createHandlerWithMockedUsers(guardianRole: Role::ELEVE);
|
|
|
|
$this->expectException(InvalidGuardianRoleException::class);
|
|
|
|
($handler)(new LinkParentToStudentCommand(
|
|
studentId: self::STUDENT_ID,
|
|
guardianId: self::GUARDIAN_ID,
|
|
relationshipType: RelationshipType::FATHER->value,
|
|
tenantId: self::TENANT_ID,
|
|
));
|
|
}
|
|
|
|
#[Test]
|
|
public function throwsWhenStudentIsNotEleve(): void
|
|
{
|
|
$handler = $this->createHandlerWithMockedUsers(studentRole: Role::PARENT);
|
|
|
|
$this->expectException(InvalidStudentRoleException::class);
|
|
|
|
($handler)(new LinkParentToStudentCommand(
|
|
studentId: self::STUDENT_ID,
|
|
guardianId: self::GUARDIAN_ID,
|
|
relationshipType: RelationshipType::FATHER->value,
|
|
tenantId: self::TENANT_ID,
|
|
));
|
|
}
|
|
|
|
#[Test]
|
|
public function throwsWhenGuardianBelongsToDifferentTenant(): void
|
|
{
|
|
$handler = $this->createHandlerWithMockedUsers(guardianTenantId: self::OTHER_TENANT_ID);
|
|
|
|
$this->expectException(TenantMismatchException::class);
|
|
|
|
($handler)(new LinkParentToStudentCommand(
|
|
studentId: self::STUDENT_ID,
|
|
guardianId: self::GUARDIAN_ID,
|
|
relationshipType: RelationshipType::FATHER->value,
|
|
tenantId: self::TENANT_ID,
|
|
));
|
|
}
|
|
|
|
#[Test]
|
|
public function throwsWhenStudentBelongsToDifferentTenant(): void
|
|
{
|
|
$handler = $this->createHandlerWithMockedUsers(studentTenantId: self::OTHER_TENANT_ID);
|
|
|
|
$this->expectException(TenantMismatchException::class);
|
|
|
|
($handler)(new LinkParentToStudentCommand(
|
|
studentId: self::STUDENT_ID,
|
|
guardianId: self::GUARDIAN_ID,
|
|
relationshipType: RelationshipType::FATHER->value,
|
|
tenantId: self::TENANT_ID,
|
|
));
|
|
}
|
|
|
|
#[Test]
|
|
public function throwsWhenRelationshipTypeIsInvalid(): void
|
|
{
|
|
$handler = $this->createHandlerWithMockedUsers();
|
|
|
|
$this->expectException(InvalidArgumentException::class);
|
|
$this->expectExceptionMessage('Type de relation invalide');
|
|
|
|
($handler)(new LinkParentToStudentCommand(
|
|
studentId: self::STUDENT_ID,
|
|
guardianId: self::GUARDIAN_ID,
|
|
relationshipType: 'invalide',
|
|
tenantId: self::TENANT_ID,
|
|
));
|
|
}
|
|
}
|