Files
Classeo/backend/tests/Unit/Administration/Application/Command/CreateStudent/CreateStudentHandlerTest.php
Mathias STRASSER ba80e8cb57 fix: Permettre l'activation des comptes élèves de moins de 15 ans créés par l'admin
Lorsqu'un admin créait un élève de moins de 15 ans avec une date de
naissance, le compte ne pouvait pas être activé car le consentement
parental RGPD n'avait jamais été enregistré — aucun mécanisme ne le
permettait dans le parcours admin.

Ajout d'une case « Consentement parental obtenu » dans le formulaire
de création d'élève, affichée conditionnellement quand la date de
naissance indique un âge < 15 ans. L'admin confirme que l'établissement
a recueilli le consentement, qui est alors enregistré côté backend
lors de la création du compte.
2026-03-07 21:14:04 +01:00

305 lines
11 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\Unit\Administration\Application\Command\CreateStudent;
use App\Administration\Application\Command\CreateStudent\CreateStudentCommand;
use App\Administration\Application\Command\CreateStudent\CreateStudentHandler;
use App\Administration\Domain\Exception\ClasseNotFoundException;
use App\Administration\Domain\Exception\EmailDejaUtiliseeException;
use App\Administration\Domain\Model\SchoolClass\AcademicYearId;
use App\Administration\Domain\Model\SchoolClass\ClassId;
use App\Administration\Domain\Model\SchoolClass\ClassName;
use App\Administration\Domain\Model\SchoolClass\ClassStatus;
use App\Administration\Domain\Model\SchoolClass\SchoolClass;
use App\Administration\Domain\Model\SchoolClass\SchoolId;
use App\Administration\Domain\Model\SchoolClass\SchoolLevel;
use App\Administration\Domain\Model\User\Email;
use App\Administration\Domain\Model\User\Role;
use App\Administration\Domain\Model\User\StatutCompte;
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\InMemoryClassAssignmentRepository;
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryClassRepository;
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryUserRepository;
use App\Shared\Domain\Clock;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
use Doctrine\DBAL\Connection;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use RuntimeException;
final class CreateStudentHandlerTest extends TestCase
{
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
private const string CLASS_ID = '550e8400-e29b-41d4-a716-446655440020';
private const string ACADEMIC_YEAR_ID = '550e8400-e29b-41d4-a716-446655440040';
private const string SCHOOL_ID = '550e8400-e29b-41d4-a716-446655440050';
private InMemoryUserRepository $userRepository;
private InMemoryClassAssignmentRepository $classAssignmentRepository;
private InMemoryClassRepository $classRepository;
private Connection $connection;
private Clock $clock;
protected function setUp(): void
{
$this->userRepository = new InMemoryUserRepository();
$this->classAssignmentRepository = new InMemoryClassAssignmentRepository();
$this->classRepository = new InMemoryClassRepository();
$this->connection = $this->createMock(Connection::class);
$this->connection->method('beginTransaction');
$this->connection->method('commit');
$this->clock = new class implements Clock {
public function now(): DateTimeImmutable
{
return new DateTimeImmutable('2026-02-21 10:00:00');
}
};
$this->seedTestData();
}
#[Test]
public function itCreatesStudentWithEmail(): void
{
$handler = $this->createHandler();
$command = $this->createCommand(email: 'eleve@example.com');
$user = $handler($command);
self::assertSame('Marie', $user->firstName);
self::assertSame('Dupont', $user->lastName);
self::assertSame(StatutCompte::EN_ATTENTE, $user->statut);
self::assertSame('eleve@example.com', (string) $user->email);
self::assertTrue($user->aLeRole(Role::ELEVE));
}
#[Test]
public function itCreatesStudentWithoutEmail(): void
{
$handler = $this->createHandler();
$command = $this->createCommand(email: null);
$user = $handler($command);
self::assertSame('Marie', $user->firstName);
self::assertSame('Dupont', $user->lastName);
self::assertSame(StatutCompte::INSCRIT, $user->statut);
self::assertNull($user->email);
self::assertTrue($user->aLeRole(Role::ELEVE));
}
#[Test]
public function itAssignsStudentToClass(): void
{
$handler = $this->createHandler();
$command = $this->createCommand();
$user = $handler($command);
$assignment = $this->classAssignmentRepository->findByStudent(
$user->id,
AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
TenantId::fromString(self::TENANT_ID),
);
self::assertNotNull($assignment);
self::assertTrue($assignment->classId->equals(ClassId::fromString(self::CLASS_ID)));
}
#[Test]
public function itSetsStudentNumber(): void
{
$handler = $this->createHandler();
$command = $this->createCommand(studentNumber: '12345678901');
$user = $handler($command);
self::assertSame('12345678901', $user->studentNumber);
}
#[Test]
public function itSetsDateNaissance(): void
{
$handler = $this->createHandler();
$command = $this->createCommand(dateNaissance: '2015-06-15');
$user = $handler($command);
self::assertNotNull($user->dateNaissance);
self::assertSame('2015-06-15', $user->dateNaissance->format('Y-m-d'));
}
#[Test]
public function itThrowsWhenClassDoesNotExist(): void
{
$handler = $this->createHandler();
$command = $this->createCommand(classId: '550e8400-e29b-41d4-a716-446655440099');
$this->expectException(ClasseNotFoundException::class);
$handler($command);
}
#[Test]
public function itThrowsWhenEmailAlreadyUsedInTenant(): void
{
// Pre-populate with a user having the same email
$existing = User::reconstitute(
id: UserId::fromString('550e8400-e29b-41d4-a716-446655440060'),
email: new Email('existing@example.com'),
roles: [Role::ELEVE],
tenantId: TenantId::fromString(self::TENANT_ID),
schoolName: 'École Test',
statut: StatutCompte::EN_ATTENTE,
dateNaissance: null,
createdAt: new DateTimeImmutable('2026-01-15'),
hashedPassword: null,
activatedAt: null,
consentementParental: null,
);
$this->userRepository->save($existing);
$handler = $this->createHandler();
$command = $this->createCommand(email: 'existing@example.com');
$this->expectException(EmailDejaUtiliseeException::class);
$handler($command);
}
#[Test]
public function itRollsBackTransactionOnFailure(): void
{
$connection = $this->createMock(Connection::class);
$connection->expects(self::once())->method('beginTransaction');
$connection->expects(self::once())->method('rollBack');
$connection->expects(self::never())->method('commit');
// Use a mock UserRepository that throws during save (inside the transaction)
$failingUserRepo = $this->createMock(UserRepository::class);
$failingUserRepo->method('findByEmail')->willReturn(null);
$failingUserRepo->method('save')->willThrowException(new RuntimeException('DB write failed'));
$handler = new CreateStudentHandler(
$failingUserRepo,
$this->classAssignmentRepository,
$this->classRepository,
$connection,
$this->clock,
new \App\Administration\Domain\Policy\ConsentementParentalPolicy($this->clock),
);
$command = $this->createCommand();
$this->expectException(RuntimeException::class);
$handler($command);
}
#[Test]
public function itThrowsWhenClassBelongsToAnotherTenant(): void
{
$otherTenantClassId = '550e8400-e29b-41d4-a716-446655440080';
$otherClass = SchoolClass::reconstitute(
id: ClassId::fromString($otherTenantClassId),
tenantId: TenantId::fromString('550e8400-e29b-41d4-a716-446655440099'),
schoolId: SchoolId::fromString(self::SCHOOL_ID),
academicYearId: AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
name: new ClassName('Autre tenant'),
level: SchoolLevel::SIXIEME,
capacity: 30,
status: ClassStatus::ACTIVE,
description: null,
createdAt: new DateTimeImmutable('2026-01-15'),
updatedAt: new DateTimeImmutable('2026-01-15'),
deletedAt: null,
);
$this->classRepository->save($otherClass);
$handler = $this->createHandler();
$command = $this->createCommand(classId: $otherTenantClassId);
$this->expectException(ClasseNotFoundException::class);
$handler($command);
}
#[Test]
public function itThrowsWhenClassIsArchived(): void
{
$archivedClassId = '550e8400-e29b-41d4-a716-446655440081';
$archivedClass = SchoolClass::reconstitute(
id: ClassId::fromString($archivedClassId),
tenantId: TenantId::fromString(self::TENANT_ID),
schoolId: SchoolId::fromString(self::SCHOOL_ID),
academicYearId: AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
name: new ClassName('Archivée'),
level: SchoolLevel::SIXIEME,
capacity: 30,
status: ClassStatus::ARCHIVED,
description: null,
createdAt: new DateTimeImmutable('2026-01-15'),
updatedAt: new DateTimeImmutable('2026-01-15'),
deletedAt: null,
);
$this->classRepository->save($archivedClass);
$handler = $this->createHandler();
$command = $this->createCommand(classId: $archivedClassId);
$this->expectException(ClasseNotFoundException::class);
$handler($command);
}
private function seedTestData(): void
{
$class = SchoolClass::reconstitute(
id: ClassId::fromString(self::CLASS_ID),
tenantId: TenantId::fromString(self::TENANT_ID),
schoolId: SchoolId::fromString(self::SCHOOL_ID),
academicYearId: AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
name: new ClassName('6ème A'),
level: SchoolLevel::SIXIEME,
capacity: 30,
status: ClassStatus::ACTIVE,
description: null,
createdAt: new DateTimeImmutable('2026-01-15'),
updatedAt: new DateTimeImmutable('2026-01-15'),
deletedAt: null,
);
$this->classRepository->save($class);
}
private function createHandler(): CreateStudentHandler
{
return new CreateStudentHandler(
$this->userRepository,
$this->classAssignmentRepository,
$this->classRepository,
$this->connection,
$this->clock,
new \App\Administration\Domain\Policy\ConsentementParentalPolicy($this->clock),
);
}
private function createCommand(
?string $email = 'eleve@example.com',
?string $classId = null,
?string $dateNaissance = null,
?string $studentNumber = null,
): CreateStudentCommand {
return new CreateStudentCommand(
tenantId: self::TENANT_ID,
schoolName: 'École Test',
firstName: 'Marie',
lastName: 'Dupont',
classId: $classId ?? self::CLASS_ID,
academicYearId: self::ACADEMIC_YEAR_ID,
email: $email,
dateNaissance: $dateNaissance,
studentNumber: $studentNumber,
);
}
}