feat: Liaison parents-enfants avec gestion des tuteurs
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é).
This commit is contained in:
@@ -164,6 +164,46 @@ final class ActivateAccountHandlerTest extends TestCase
|
||||
($this->handler)($command);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function activateAccountCarriesStudentIdFromToken(): void
|
||||
{
|
||||
$studentId = '550e8400-e29b-41d4-a716-446655440099';
|
||||
$token = ActivationToken::generate(
|
||||
userId: self::USER_ID,
|
||||
email: self::EMAIL,
|
||||
tenantId: TenantId::fromString(self::TENANT_ID),
|
||||
role: self::ROLE,
|
||||
schoolName: self::SCHOOL_NAME,
|
||||
createdAt: new DateTimeImmutable('2026-01-15 10:00:00'),
|
||||
studentId: $studentId,
|
||||
);
|
||||
$this->tokenRepository->save($token);
|
||||
|
||||
$command = new ActivateAccountCommand(
|
||||
tokenValue: $token->tokenValue,
|
||||
password: self::PASSWORD,
|
||||
);
|
||||
|
||||
$result = ($this->handler)($command);
|
||||
|
||||
self::assertSame($studentId, $result->studentId);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function activateAccountReturnsNullStudentIdWhenNotSet(): void
|
||||
{
|
||||
$token = $this->createAndSaveToken();
|
||||
|
||||
$command = new ActivateAccountCommand(
|
||||
tokenValue: $token->tokenValue,
|
||||
password: self::PASSWORD,
|
||||
);
|
||||
|
||||
$result = ($this->handler)($command);
|
||||
|
||||
self::assertNull($result->studentId);
|
||||
}
|
||||
|
||||
private function createAndSaveToken(?DateTimeImmutable $createdAt = null): ActivationToken
|
||||
{
|
||||
$token = ActivationToken::generate(
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Administration\Application\Command\AssignRole;
|
||||
|
||||
use App\Administration\Application\Command\AssignRole\AssignRoleCommand;
|
||||
use App\Administration\Application\Command\AssignRole\AssignRoleHandler;
|
||||
use App\Administration\Domain\Exception\RoleDejaAttribueException;
|
||||
use App\Administration\Domain\Exception\UserNotFoundException;
|
||||
use App\Administration\Domain\Model\User\Email;
|
||||
use App\Administration\Domain\Model\User\Role;
|
||||
use App\Administration\Domain\Model\User\User;
|
||||
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryUserRepository;
|
||||
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 AssignRoleHandlerTest extends TestCase
|
||||
{
|
||||
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440002';
|
||||
private const string SCHOOL_NAME = 'École Alpha';
|
||||
|
||||
private InMemoryUserRepository $userRepository;
|
||||
private Clock $clock;
|
||||
private AssignRoleHandler $handler;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->userRepository = new InMemoryUserRepository();
|
||||
$this->clock = new class implements Clock {
|
||||
public function now(): DateTimeImmutable
|
||||
{
|
||||
return new DateTimeImmutable('2026-02-07 10:00:00');
|
||||
}
|
||||
};
|
||||
$this->handler = new AssignRoleHandler($this->userRepository, $this->clock);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function assignsRoleSuccessfully(): void
|
||||
{
|
||||
$user = $this->createAndSaveUser(Role::PROF);
|
||||
|
||||
$command = new AssignRoleCommand(
|
||||
userId: (string) $user->id,
|
||||
role: Role::VIE_SCOLAIRE->value,
|
||||
);
|
||||
|
||||
$result = ($this->handler)($command);
|
||||
|
||||
self::assertTrue($result->aLeRole(Role::PROF));
|
||||
self::assertTrue($result->aLeRole(Role::VIE_SCOLAIRE));
|
||||
self::assertCount(2, $result->roles);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function savesUserAfterAssignment(): void
|
||||
{
|
||||
$user = $this->createAndSaveUser(Role::PROF);
|
||||
|
||||
$command = new AssignRoleCommand(
|
||||
userId: (string) $user->id,
|
||||
role: Role::ADMIN->value,
|
||||
);
|
||||
|
||||
($this->handler)($command);
|
||||
|
||||
$found = $this->userRepository->get($user->id);
|
||||
self::assertTrue($found->aLeRole(Role::ADMIN));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function throwsWhenRoleAlreadyAssigned(): void
|
||||
{
|
||||
$user = $this->createAndSaveUser(Role::PROF);
|
||||
|
||||
$command = new AssignRoleCommand(
|
||||
userId: (string) $user->id,
|
||||
role: Role::PROF->value,
|
||||
);
|
||||
|
||||
$this->expectException(RoleDejaAttribueException::class);
|
||||
|
||||
($this->handler)($command);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function throwsWhenUserNotFound(): void
|
||||
{
|
||||
$command = new AssignRoleCommand(
|
||||
userId: '550e8400-e29b-41d4-a716-446655440099',
|
||||
role: Role::PROF->value,
|
||||
);
|
||||
|
||||
$this->expectException(UserNotFoundException::class);
|
||||
|
||||
($this->handler)($command);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function throwsWhenRoleIsInvalid(): void
|
||||
{
|
||||
$user = $this->createAndSaveUser(Role::PROF);
|
||||
|
||||
$command = new AssignRoleCommand(
|
||||
userId: (string) $user->id,
|
||||
role: 'ROLE_INEXISTANT',
|
||||
);
|
||||
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Rôle invalide');
|
||||
|
||||
($this->handler)($command);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function throwsWhenTenantIdDoesNotMatch(): void
|
||||
{
|
||||
$user = $this->createAndSaveUser(Role::PROF);
|
||||
|
||||
$command = new AssignRoleCommand(
|
||||
userId: (string) $user->id,
|
||||
role: Role::ADMIN->value,
|
||||
tenantId: '550e8400-e29b-41d4-a716-446655440099',
|
||||
);
|
||||
|
||||
$this->expectException(UserNotFoundException::class);
|
||||
|
||||
($this->handler)($command);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function allowsAssignmentWhenTenantIdMatches(): void
|
||||
{
|
||||
$user = $this->createAndSaveUser(Role::PROF);
|
||||
|
||||
$command = new AssignRoleCommand(
|
||||
userId: (string) $user->id,
|
||||
role: Role::ADMIN->value,
|
||||
tenantId: self::TENANT_ID,
|
||||
);
|
||||
|
||||
$result = ($this->handler)($command);
|
||||
|
||||
self::assertTrue($result->aLeRole(Role::ADMIN));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function allowsAssignmentWhenTenantIdIsEmpty(): void
|
||||
{
|
||||
$user = $this->createAndSaveUser(Role::PROF);
|
||||
|
||||
$command = new AssignRoleCommand(
|
||||
userId: (string) $user->id,
|
||||
role: Role::SECRETARIAT->value,
|
||||
tenantId: '',
|
||||
);
|
||||
|
||||
$result = ($this->handler)($command);
|
||||
|
||||
self::assertTrue($result->aLeRole(Role::SECRETARIAT));
|
||||
}
|
||||
|
||||
private function createAndSaveUser(Role $role): User
|
||||
{
|
||||
$user = User::inviter(
|
||||
email: new Email('user@example.com'),
|
||||
role: $role,
|
||||
tenantId: TenantId::fromString(self::TENANT_ID),
|
||||
schoolName: self::SCHOOL_NAME,
|
||||
firstName: 'Jean',
|
||||
lastName: 'Dupont',
|
||||
invitedAt: new DateTimeImmutable('2026-02-01 10:00:00'),
|
||||
);
|
||||
$user->pullDomainEvents();
|
||||
$this->userRepository->save($user);
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
<?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,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Administration\Application\Command\RemoveRole;
|
||||
|
||||
use App\Administration\Application\Command\RemoveRole\RemoveRoleCommand;
|
||||
use App\Administration\Application\Command\RemoveRole\RemoveRoleHandler;
|
||||
use App\Administration\Domain\Exception\DernierRoleNonRetirableException;
|
||||
use App\Administration\Domain\Exception\RoleNonAttribueException;
|
||||
use App\Administration\Domain\Exception\UserNotFoundException;
|
||||
use App\Administration\Domain\Model\User\Email;
|
||||
use App\Administration\Domain\Model\User\Role;
|
||||
use App\Administration\Domain\Model\User\User;
|
||||
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryUserRepository;
|
||||
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 RemoveRoleHandlerTest extends TestCase
|
||||
{
|
||||
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440002';
|
||||
private const string SCHOOL_NAME = 'École Alpha';
|
||||
|
||||
private InMemoryUserRepository $userRepository;
|
||||
private Clock $clock;
|
||||
private RemoveRoleHandler $handler;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->userRepository = new InMemoryUserRepository();
|
||||
$this->clock = new class implements Clock {
|
||||
public function now(): DateTimeImmutable
|
||||
{
|
||||
return new DateTimeImmutable('2026-02-07 10:00:00');
|
||||
}
|
||||
};
|
||||
$this->handler = new RemoveRoleHandler($this->userRepository, $this->clock);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function removesRoleSuccessfully(): void
|
||||
{
|
||||
$user = $this->createUserWithMultipleRoles();
|
||||
|
||||
$command = new RemoveRoleCommand(
|
||||
userId: (string) $user->id,
|
||||
role: Role::VIE_SCOLAIRE->value,
|
||||
);
|
||||
|
||||
$result = ($this->handler)($command);
|
||||
|
||||
self::assertTrue($result->aLeRole(Role::PROF));
|
||||
self::assertFalse($result->aLeRole(Role::VIE_SCOLAIRE));
|
||||
self::assertCount(1, $result->roles);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function savesUserAfterRemoval(): void
|
||||
{
|
||||
$user = $this->createUserWithMultipleRoles();
|
||||
|
||||
$command = new RemoveRoleCommand(
|
||||
userId: (string) $user->id,
|
||||
role: Role::VIE_SCOLAIRE->value,
|
||||
);
|
||||
|
||||
($this->handler)($command);
|
||||
|
||||
$found = $this->userRepository->get($user->id);
|
||||
self::assertFalse($found->aLeRole(Role::VIE_SCOLAIRE));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function throwsWhenRemovingLastRole(): void
|
||||
{
|
||||
$user = $this->createAndSaveUser(Role::PROF);
|
||||
|
||||
$command = new RemoveRoleCommand(
|
||||
userId: (string) $user->id,
|
||||
role: Role::PROF->value,
|
||||
);
|
||||
|
||||
$this->expectException(DernierRoleNonRetirableException::class);
|
||||
|
||||
($this->handler)($command);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function throwsWhenRoleNotAssigned(): void
|
||||
{
|
||||
$user = $this->createAndSaveUser(Role::PROF);
|
||||
|
||||
$command = new RemoveRoleCommand(
|
||||
userId: (string) $user->id,
|
||||
role: Role::ADMIN->value,
|
||||
);
|
||||
|
||||
$this->expectException(RoleNonAttribueException::class);
|
||||
|
||||
($this->handler)($command);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function throwsWhenUserNotFound(): void
|
||||
{
|
||||
$command = new RemoveRoleCommand(
|
||||
userId: '550e8400-e29b-41d4-a716-446655440099',
|
||||
role: Role::PROF->value,
|
||||
);
|
||||
|
||||
$this->expectException(UserNotFoundException::class);
|
||||
|
||||
($this->handler)($command);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function throwsWhenRoleIsInvalid(): void
|
||||
{
|
||||
$user = $this->createAndSaveUser(Role::PROF);
|
||||
|
||||
$command = new RemoveRoleCommand(
|
||||
userId: (string) $user->id,
|
||||
role: 'ROLE_INEXISTANT',
|
||||
);
|
||||
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Rôle invalide');
|
||||
|
||||
($this->handler)($command);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function throwsWhenTenantIdDoesNotMatch(): void
|
||||
{
|
||||
$user = $this->createUserWithMultipleRoles();
|
||||
|
||||
$command = new RemoveRoleCommand(
|
||||
userId: (string) $user->id,
|
||||
role: Role::VIE_SCOLAIRE->value,
|
||||
tenantId: '550e8400-e29b-41d4-a716-446655440099',
|
||||
);
|
||||
|
||||
$this->expectException(UserNotFoundException::class);
|
||||
|
||||
($this->handler)($command);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function allowsRemovalWhenTenantIdMatches(): void
|
||||
{
|
||||
$user = $this->createUserWithMultipleRoles();
|
||||
|
||||
$command = new RemoveRoleCommand(
|
||||
userId: (string) $user->id,
|
||||
role: Role::VIE_SCOLAIRE->value,
|
||||
tenantId: self::TENANT_ID,
|
||||
);
|
||||
|
||||
$result = ($this->handler)($command);
|
||||
|
||||
self::assertFalse($result->aLeRole(Role::VIE_SCOLAIRE));
|
||||
}
|
||||
|
||||
private function createAndSaveUser(Role $role): User
|
||||
{
|
||||
$user = User::inviter(
|
||||
email: new Email('user@example.com'),
|
||||
role: $role,
|
||||
tenantId: TenantId::fromString(self::TENANT_ID),
|
||||
schoolName: self::SCHOOL_NAME,
|
||||
firstName: 'Jean',
|
||||
lastName: 'Dupont',
|
||||
invitedAt: new DateTimeImmutable('2026-02-01 10:00:00'),
|
||||
);
|
||||
$user->pullDomainEvents();
|
||||
$this->userRepository->save($user);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
private function createUserWithMultipleRoles(): User
|
||||
{
|
||||
$user = $this->createAndSaveUser(Role::PROF);
|
||||
$user->attribuerRole(Role::VIE_SCOLAIRE, new DateTimeImmutable('2026-02-02 10:00:00'));
|
||||
$user->pullDomainEvents();
|
||||
$this->userRepository->save($user);
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Administration\Application\Command\UnlinkParentFromStudent;
|
||||
|
||||
use App\Administration\Application\Command\UnlinkParentFromStudent\UnlinkParentFromStudentCommand;
|
||||
use App\Administration\Application\Command\UnlinkParentFromStudent\UnlinkParentFromStudentHandler;
|
||||
use App\Administration\Domain\Event\ParentDelieDEleve;
|
||||
use App\Administration\Domain\Exception\StudentGuardianNotFoundException;
|
||||
use App\Administration\Domain\Model\StudentGuardian\RelationshipType;
|
||||
use App\Administration\Domain\Model\StudentGuardian\StudentGuardian;
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryStudentGuardianRepository;
|
||||
use App\Shared\Domain\Clock;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class UnlinkParentFromStudentHandlerTest 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 InMemoryStudentGuardianRepository $repository;
|
||||
private UnlinkParentFromStudentHandler $handler;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->repository = new InMemoryStudentGuardianRepository();
|
||||
$clock = new class implements Clock {
|
||||
public function now(): DateTimeImmutable
|
||||
{
|
||||
return new DateTimeImmutable('2026-02-10 10:00:00');
|
||||
}
|
||||
};
|
||||
$this->handler = new UnlinkParentFromStudentHandler($this->repository, $clock);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function unlinkRemovesExistingLink(): void
|
||||
{
|
||||
$link = $this->createAndSaveLink();
|
||||
|
||||
($this->handler)(new UnlinkParentFromStudentCommand(
|
||||
linkId: (string) $link->id,
|
||||
tenantId: self::TENANT_ID,
|
||||
));
|
||||
|
||||
self::assertSame(0, $this->repository->countGuardiansForStudent(
|
||||
$link->studentId,
|
||||
$link->tenantId,
|
||||
));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function unlinkRecordsParentDelieDEleveEvent(): void
|
||||
{
|
||||
$link = $this->createAndSaveLink();
|
||||
|
||||
$result = ($this->handler)(new UnlinkParentFromStudentCommand(
|
||||
linkId: (string) $link->id,
|
||||
tenantId: self::TENANT_ID,
|
||||
));
|
||||
|
||||
$events = $result->pullDomainEvents();
|
||||
self::assertCount(1, $events);
|
||||
self::assertInstanceOf(ParentDelieDEleve::class, $events[0]);
|
||||
self::assertTrue($events[0]->studentId->equals($link->studentId));
|
||||
self::assertTrue($events[0]->guardianId->equals($link->guardianId));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function throwsWhenLinkNotFound(): void
|
||||
{
|
||||
$this->expectException(StudentGuardianNotFoundException::class);
|
||||
|
||||
($this->handler)(new UnlinkParentFromStudentCommand(
|
||||
linkId: '550e8400-e29b-41d4-a716-446655440099',
|
||||
tenantId: self::TENANT_ID,
|
||||
));
|
||||
}
|
||||
|
||||
private function createAndSaveLink(): StudentGuardian
|
||||
{
|
||||
$link = StudentGuardian::lier(
|
||||
studentId: UserId::fromString(self::STUDENT_ID),
|
||||
guardianId: UserId::fromString(self::GUARDIAN_ID),
|
||||
relationshipType: RelationshipType::FATHER,
|
||||
tenantId: TenantId::fromString(self::TENANT_ID),
|
||||
createdAt: new DateTimeImmutable('2026-02-10 10:00:00'),
|
||||
);
|
||||
// Drain lier() events so only delier() events are tested
|
||||
$link->pullDomainEvents();
|
||||
$this->repository->save($link);
|
||||
|
||||
return $link;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Administration\Application\Command\UpdateUserRoles;
|
||||
|
||||
use App\Administration\Application\Command\UpdateUserRoles\UpdateUserRolesCommand;
|
||||
use App\Administration\Application\Command\UpdateUserRoles\UpdateUserRolesHandler;
|
||||
use App\Administration\Application\Port\ActiveRoleStore;
|
||||
use App\Administration\Domain\Exception\UserNotFoundException;
|
||||
use App\Administration\Domain\Model\User\Email;
|
||||
use App\Administration\Domain\Model\User\Role;
|
||||
use App\Administration\Domain\Model\User\User;
|
||||
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryUserRepository;
|
||||
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 UpdateUserRolesHandlerTest extends TestCase
|
||||
{
|
||||
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440002';
|
||||
private const string SCHOOL_NAME = 'École Alpha';
|
||||
|
||||
private InMemoryUserRepository $userRepository;
|
||||
private Clock $clock;
|
||||
private ActiveRoleStore $activeRoleStore;
|
||||
private UpdateUserRolesHandler $handler;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->userRepository = new InMemoryUserRepository();
|
||||
$this->clock = new class implements Clock {
|
||||
public function now(): DateTimeImmutable
|
||||
{
|
||||
return new DateTimeImmutable('2026-02-07 10:00:00');
|
||||
}
|
||||
};
|
||||
$this->activeRoleStore = new class implements ActiveRoleStore {
|
||||
public bool $cleared = false;
|
||||
|
||||
public function store(User $user, Role $role): void
|
||||
{
|
||||
}
|
||||
|
||||
public function get(User $user): ?Role
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function clear(User $user): void
|
||||
{
|
||||
$this->cleared = true;
|
||||
}
|
||||
};
|
||||
$this->handler = new UpdateUserRolesHandler(
|
||||
$this->userRepository,
|
||||
$this->clock,
|
||||
$this->activeRoleStore,
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function replacesAllRolesSuccessfully(): void
|
||||
{
|
||||
$user = $this->createAndSaveUser(Role::PROF);
|
||||
|
||||
$command = new UpdateUserRolesCommand(
|
||||
userId: (string) $user->id,
|
||||
roles: [Role::ADMIN->value, Role::SECRETARIAT->value],
|
||||
);
|
||||
|
||||
$result = ($this->handler)($command);
|
||||
|
||||
self::assertTrue($result->aLeRole(Role::ADMIN));
|
||||
self::assertTrue($result->aLeRole(Role::SECRETARIAT));
|
||||
self::assertFalse($result->aLeRole(Role::PROF));
|
||||
self::assertCount(2, $result->roles);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function addsNewRolesWithoutRemovingExisting(): void
|
||||
{
|
||||
$user = $this->createAndSaveUser(Role::PROF);
|
||||
|
||||
$command = new UpdateUserRolesCommand(
|
||||
userId: (string) $user->id,
|
||||
roles: [Role::PROF->value, Role::ADMIN->value],
|
||||
);
|
||||
|
||||
$result = ($this->handler)($command);
|
||||
|
||||
self::assertTrue($result->aLeRole(Role::PROF));
|
||||
self::assertTrue($result->aLeRole(Role::ADMIN));
|
||||
self::assertCount(2, $result->roles);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function throwsWhenRolesArrayIsEmpty(): void
|
||||
{
|
||||
$user = $this->createAndSaveUser(Role::PROF);
|
||||
|
||||
$command = new UpdateUserRolesCommand(
|
||||
userId: (string) $user->id,
|
||||
roles: [],
|
||||
);
|
||||
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Au moins un rôle est requis.');
|
||||
|
||||
($this->handler)($command);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function throwsWhenRoleIsInvalid(): void
|
||||
{
|
||||
$user = $this->createAndSaveUser(Role::PROF);
|
||||
|
||||
$command = new UpdateUserRolesCommand(
|
||||
userId: (string) $user->id,
|
||||
roles: ['ROLE_INEXISTANT'],
|
||||
);
|
||||
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Rôle invalide');
|
||||
|
||||
($this->handler)($command);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function throwsWhenUserNotFound(): void
|
||||
{
|
||||
$command = new UpdateUserRolesCommand(
|
||||
userId: '550e8400-e29b-41d4-a716-446655440099',
|
||||
roles: [Role::PROF->value],
|
||||
);
|
||||
|
||||
$this->expectException(UserNotFoundException::class);
|
||||
|
||||
($this->handler)($command);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function throwsWhenTenantIdDoesNotMatch(): void
|
||||
{
|
||||
$user = $this->createAndSaveUser(Role::PROF);
|
||||
|
||||
$command = new UpdateUserRolesCommand(
|
||||
userId: (string) $user->id,
|
||||
roles: [Role::ADMIN->value],
|
||||
tenantId: '550e8400-e29b-41d4-a716-446655440099',
|
||||
);
|
||||
|
||||
$this->expectException(UserNotFoundException::class);
|
||||
|
||||
($this->handler)($command);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function clearsActiveRoleStoreAfterUpdate(): void
|
||||
{
|
||||
$user = $this->createAndSaveUser(Role::PROF);
|
||||
|
||||
$command = new UpdateUserRolesCommand(
|
||||
userId: (string) $user->id,
|
||||
roles: [Role::ADMIN->value],
|
||||
);
|
||||
|
||||
($this->handler)($command);
|
||||
|
||||
self::assertTrue($this->activeRoleStore->cleared);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function savesUserToRepositoryAfterUpdate(): void
|
||||
{
|
||||
$user = $this->createAndSaveUser(Role::PROF);
|
||||
|
||||
$command = new UpdateUserRolesCommand(
|
||||
userId: (string) $user->id,
|
||||
roles: [Role::ADMIN->value, Role::VIE_SCOLAIRE->value],
|
||||
);
|
||||
|
||||
($this->handler)($command);
|
||||
|
||||
$found = $this->userRepository->get($user->id);
|
||||
self::assertTrue($found->aLeRole(Role::ADMIN));
|
||||
self::assertTrue($found->aLeRole(Role::VIE_SCOLAIRE));
|
||||
self::assertFalse($found->aLeRole(Role::PROF));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function keepsOnlySpecifiedRolesWhenUserHasMultiple(): void
|
||||
{
|
||||
$user = $this->createAndSaveUser(Role::PROF);
|
||||
$user->attribuerRole(Role::VIE_SCOLAIRE, new DateTimeImmutable('2026-02-02'));
|
||||
$user->attribuerRole(Role::SECRETARIAT, new DateTimeImmutable('2026-02-03'));
|
||||
$user->pullDomainEvents();
|
||||
$this->userRepository->save($user);
|
||||
|
||||
$command = new UpdateUserRolesCommand(
|
||||
userId: (string) $user->id,
|
||||
roles: [Role::ADMIN->value],
|
||||
);
|
||||
|
||||
$result = ($this->handler)($command);
|
||||
|
||||
self::assertCount(1, $result->roles);
|
||||
self::assertTrue($result->aLeRole(Role::ADMIN));
|
||||
}
|
||||
|
||||
private function createAndSaveUser(Role $role): User
|
||||
{
|
||||
$user = User::inviter(
|
||||
email: new Email('user@example.com'),
|
||||
role: $role,
|
||||
tenantId: TenantId::fromString(self::TENANT_ID),
|
||||
schoolName: self::SCHOOL_NAME,
|
||||
firstName: 'Jean',
|
||||
lastName: 'Dupont',
|
||||
invitedAt: new DateTimeImmutable('2026-02-01 10:00:00'),
|
||||
);
|
||||
$user->pullDomainEvents();
|
||||
$this->userRepository->save($user);
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user