Story 1-7 avait posé les fondations d'audit trail mais laissé en dehors du
périmètre initial les événements notes/évaluations, qui étaient alors non
couverts par les domaines. Avec la clôture des epics notation, ces actions
sensibles (création/modification/suppression d'évaluation, saisie/modification
de note, publication) doivent maintenant être tracées pour répondre aux
exigences RGPD et faciliter la résolution des litiges parent/enseignant.
Les événements de domaine existants ne transportaient pas tous les champs
nécessaires à l'audit (ancien/nouveau titre, description, barème, coefficient,
date, studentId). L'enrichissement de leur payload permet aux handlers d'audit
de journaliser les diffs complets via AuditLogger, sans que les autres
consommateurs (recalcul de moyennes) n'aient besoin de changer leur logique.
Au passage, le test E2E student-grades AC5 ("Nouveau" badge) visait
séquentiellement '.grade-card' puis '.badge-new' : la fenêtre de 3 s avant
markGradesSeen pouvait se refermer entre les deux attentes sur Firefox CI.
Un seul expect combiné '.grade-card .badge-new' élimine cette course.
303 lines
12 KiB
PHP
303 lines
12 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Unit\Scolarite\Domain\Model\Evaluation;
|
|
|
|
use App\Administration\Domain\Model\SchoolClass\ClassId;
|
|
use App\Administration\Domain\Model\Subject\SubjectId;
|
|
use App\Administration\Domain\Model\User\UserId;
|
|
use App\Scolarite\Domain\Event\EvaluationCreee;
|
|
use App\Scolarite\Domain\Event\EvaluationModifiee;
|
|
use App\Scolarite\Domain\Event\EvaluationSupprimee;
|
|
use App\Scolarite\Domain\Exception\BaremeNonModifiableException;
|
|
use App\Scolarite\Domain\Exception\EvaluationDejaSupprimeeException;
|
|
use App\Scolarite\Domain\Model\Evaluation\Coefficient;
|
|
use App\Scolarite\Domain\Model\Evaluation\Evaluation;
|
|
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
|
|
use App\Scolarite\Domain\Model\Evaluation\EvaluationStatus;
|
|
use App\Scolarite\Domain\Model\Evaluation\GradeScale;
|
|
use App\Shared\Domain\Tenant\TenantId;
|
|
use DateTimeImmutable;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
final class EvaluationTest 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 SUBJECT_ID = '550e8400-e29b-41d4-a716-446655440030';
|
|
private const string TEACHER_ID = '550e8400-e29b-41d4-a716-446655440010';
|
|
|
|
#[Test]
|
|
public function creerCreatesPublishedEvaluation(): void
|
|
{
|
|
$evaluation = $this->createEvaluation();
|
|
|
|
self::assertSame(EvaluationStatus::PUBLISHED, $evaluation->status);
|
|
}
|
|
|
|
#[Test]
|
|
public function creerRecordsEvaluationCreeeEvent(): void
|
|
{
|
|
$evaluation = $this->createEvaluation();
|
|
|
|
$events = $evaluation->pullDomainEvents();
|
|
|
|
self::assertCount(1, $events);
|
|
self::assertInstanceOf(EvaluationCreee::class, $events[0]);
|
|
self::assertSame($evaluation->id, $events[0]->evaluationId);
|
|
self::assertSame($evaluation->title, $events[0]->title);
|
|
self::assertSame($evaluation->description, $events[0]->description);
|
|
self::assertSame($evaluation->gradeScale->maxValue, $events[0]->gradeScale);
|
|
self::assertSame($evaluation->coefficient->value, $events[0]->coefficient);
|
|
}
|
|
|
|
#[Test]
|
|
public function creerSetsAllProperties(): void
|
|
{
|
|
$tenantId = TenantId::fromString(self::TENANT_ID);
|
|
$classId = ClassId::fromString(self::CLASS_ID);
|
|
$subjectId = SubjectId::fromString(self::SUBJECT_ID);
|
|
$teacherId = UserId::fromString(self::TEACHER_ID);
|
|
$evaluationDate = new DateTimeImmutable('2026-04-15');
|
|
$now = new DateTimeImmutable('2026-03-12 10:00:00');
|
|
$gradeScale = new GradeScale(20);
|
|
$coefficient = new Coefficient(1.5);
|
|
|
|
$evaluation = Evaluation::creer(
|
|
tenantId: $tenantId,
|
|
classId: $classId,
|
|
subjectId: $subjectId,
|
|
teacherId: $teacherId,
|
|
title: 'Contrôle chapitre 5',
|
|
description: 'Évaluation sur les fonctions',
|
|
evaluationDate: $evaluationDate,
|
|
gradeScale: $gradeScale,
|
|
coefficient: $coefficient,
|
|
now: $now,
|
|
);
|
|
|
|
self::assertTrue($evaluation->tenantId->equals($tenantId));
|
|
self::assertTrue($evaluation->classId->equals($classId));
|
|
self::assertTrue($evaluation->subjectId->equals($subjectId));
|
|
self::assertTrue($evaluation->teacherId->equals($teacherId));
|
|
self::assertSame('Contrôle chapitre 5', $evaluation->title);
|
|
self::assertSame('Évaluation sur les fonctions', $evaluation->description);
|
|
self::assertEquals($evaluationDate, $evaluation->evaluationDate);
|
|
self::assertSame(20, $evaluation->gradeScale->maxValue);
|
|
self::assertSame(1.5, $evaluation->coefficient->value);
|
|
self::assertSame(EvaluationStatus::PUBLISHED, $evaluation->status);
|
|
self::assertEquals($now, $evaluation->createdAt);
|
|
self::assertEquals($now, $evaluation->updatedAt);
|
|
}
|
|
|
|
#[Test]
|
|
public function creerAllowsNullDescription(): void
|
|
{
|
|
$evaluation = Evaluation::creer(
|
|
tenantId: TenantId::fromString(self::TENANT_ID),
|
|
classId: ClassId::fromString(self::CLASS_ID),
|
|
subjectId: SubjectId::fromString(self::SUBJECT_ID),
|
|
teacherId: UserId::fromString(self::TEACHER_ID),
|
|
title: 'Évaluation sans description',
|
|
description: null,
|
|
evaluationDate: new DateTimeImmutable('2026-04-15'),
|
|
gradeScale: new GradeScale(20),
|
|
coefficient: new Coefficient(1.0),
|
|
now: new DateTimeImmutable('2026-03-12 10:00:00'),
|
|
);
|
|
|
|
self::assertNull($evaluation->description);
|
|
}
|
|
|
|
#[Test]
|
|
public function modifierUpdatesFieldsAndRecordsEvent(): void
|
|
{
|
|
$evaluation = $this->createEvaluation();
|
|
$evaluation->pullDomainEvents();
|
|
$modifiedAt = new DateTimeImmutable('2026-03-13 14:00:00');
|
|
$newDate = new DateTimeImmutable('2026-04-20');
|
|
$newCoefficient = new Coefficient(2.0);
|
|
|
|
$evaluation->modifier(
|
|
title: 'Titre modifié',
|
|
description: 'Nouvelle description',
|
|
coefficient: $newCoefficient,
|
|
evaluationDate: $newDate,
|
|
gradeScale: null,
|
|
hasGrades: false,
|
|
now: $modifiedAt,
|
|
);
|
|
|
|
self::assertSame('Titre modifié', $evaluation->title);
|
|
self::assertSame('Nouvelle description', $evaluation->description);
|
|
self::assertSame(2.0, $evaluation->coefficient->value);
|
|
self::assertEquals($newDate, $evaluation->evaluationDate);
|
|
self::assertEquals($modifiedAt, $evaluation->updatedAt);
|
|
|
|
$events = $evaluation->pullDomainEvents();
|
|
self::assertCount(1, $events);
|
|
self::assertInstanceOf(EvaluationModifiee::class, $events[0]);
|
|
self::assertSame($evaluation->id, $events[0]->evaluationId);
|
|
self::assertSame('Contrôle chapitre 5', $events[0]->oldTitle);
|
|
self::assertSame('Titre modifié', $events[0]->newTitle);
|
|
self::assertSame('Évaluation sur les fonctions', $events[0]->oldDescription);
|
|
self::assertSame('Nouvelle description', $events[0]->newDescription);
|
|
self::assertSame(1.0, $events[0]->oldCoefficient);
|
|
self::assertSame(2.0, $events[0]->newCoefficient);
|
|
self::assertEquals(new DateTimeImmutable('2026-04-15'), $events[0]->oldEvaluationDate);
|
|
self::assertEquals($newDate, $events[0]->newEvaluationDate);
|
|
self::assertSame(20, $events[0]->oldGradeScale);
|
|
self::assertSame(20, $events[0]->newGradeScale);
|
|
}
|
|
|
|
#[Test]
|
|
public function modifierAllowsGradeScaleChangeWhenNoGrades(): void
|
|
{
|
|
$evaluation = $this->createEvaluation();
|
|
$evaluation->pullDomainEvents();
|
|
$newGradeScale = new GradeScale(10);
|
|
|
|
$evaluation->modifier(
|
|
title: $evaluation->title,
|
|
description: $evaluation->description,
|
|
coefficient: $evaluation->coefficient,
|
|
evaluationDate: $evaluation->evaluationDate,
|
|
gradeScale: $newGradeScale,
|
|
hasGrades: false,
|
|
now: new DateTimeImmutable('2026-03-13 14:00:00'),
|
|
);
|
|
|
|
self::assertSame(10, $evaluation->gradeScale->maxValue);
|
|
}
|
|
|
|
#[Test]
|
|
public function modifierBlocksGradeScaleChangeWhenGradesExist(): void
|
|
{
|
|
$evaluation = $this->createEvaluation();
|
|
|
|
$this->expectException(BaremeNonModifiableException::class);
|
|
|
|
$evaluation->modifier(
|
|
title: $evaluation->title,
|
|
description: $evaluation->description,
|
|
coefficient: $evaluation->coefficient,
|
|
evaluationDate: $evaluation->evaluationDate,
|
|
gradeScale: new GradeScale(10),
|
|
hasGrades: true,
|
|
now: new DateTimeImmutable('2026-03-13 14:00:00'),
|
|
);
|
|
}
|
|
|
|
#[Test]
|
|
public function modifierThrowsWhenDeleted(): void
|
|
{
|
|
$evaluation = $this->createEvaluation();
|
|
$evaluation->supprimer(new DateTimeImmutable('2026-03-13'));
|
|
|
|
$this->expectException(EvaluationDejaSupprimeeException::class);
|
|
|
|
$evaluation->modifier(
|
|
title: 'Titre',
|
|
description: null,
|
|
coefficient: new Coefficient(1.0),
|
|
evaluationDate: new DateTimeImmutable('2026-04-20'),
|
|
gradeScale: null,
|
|
hasGrades: false,
|
|
now: new DateTimeImmutable('2026-03-14'),
|
|
);
|
|
}
|
|
|
|
#[Test]
|
|
public function supprimerChangesStatusAndRecordsEvent(): void
|
|
{
|
|
$evaluation = $this->createEvaluation();
|
|
$evaluation->pullDomainEvents();
|
|
$deletedAt = new DateTimeImmutable('2026-03-14 08:00:00');
|
|
|
|
$evaluation->supprimer($deletedAt);
|
|
|
|
self::assertSame(EvaluationStatus::DELETED, $evaluation->status);
|
|
self::assertEquals($deletedAt, $evaluation->updatedAt);
|
|
|
|
$events = $evaluation->pullDomainEvents();
|
|
self::assertCount(1, $events);
|
|
self::assertInstanceOf(EvaluationSupprimee::class, $events[0]);
|
|
self::assertSame($evaluation->id, $events[0]->evaluationId);
|
|
}
|
|
|
|
#[Test]
|
|
public function supprimerThrowsWhenAlreadyDeleted(): void
|
|
{
|
|
$evaluation = $this->createEvaluation();
|
|
$evaluation->supprimer(new DateTimeImmutable('2026-03-14'));
|
|
|
|
$this->expectException(EvaluationDejaSupprimeeException::class);
|
|
|
|
$evaluation->supprimer(new DateTimeImmutable('2026-03-15'));
|
|
}
|
|
|
|
#[Test]
|
|
public function reconstituteRestoresAllPropertiesWithoutEvents(): void
|
|
{
|
|
$id = EvaluationId::generate();
|
|
$tenantId = TenantId::fromString(self::TENANT_ID);
|
|
$classId = ClassId::fromString(self::CLASS_ID);
|
|
$subjectId = SubjectId::fromString(self::SUBJECT_ID);
|
|
$teacherId = UserId::fromString(self::TEACHER_ID);
|
|
$evaluationDate = new DateTimeImmutable('2026-04-15');
|
|
$createdAt = new DateTimeImmutable('2026-03-12 10:00:00');
|
|
$updatedAt = new DateTimeImmutable('2026-03-13 14:00:00');
|
|
$gradeScale = new GradeScale(20);
|
|
$coefficient = new Coefficient(1.5);
|
|
|
|
$evaluation = Evaluation::reconstitute(
|
|
id: $id,
|
|
tenantId: $tenantId,
|
|
classId: $classId,
|
|
subjectId: $subjectId,
|
|
teacherId: $teacherId,
|
|
title: 'Contrôle chapitre 5',
|
|
description: 'Évaluation sur les fonctions',
|
|
evaluationDate: $evaluationDate,
|
|
gradeScale: $gradeScale,
|
|
coefficient: $coefficient,
|
|
status: EvaluationStatus::PUBLISHED,
|
|
createdAt: $createdAt,
|
|
updatedAt: $updatedAt,
|
|
);
|
|
|
|
self::assertTrue($evaluation->id->equals($id));
|
|
self::assertTrue($evaluation->tenantId->equals($tenantId));
|
|
self::assertTrue($evaluation->classId->equals($classId));
|
|
self::assertTrue($evaluation->subjectId->equals($subjectId));
|
|
self::assertTrue($evaluation->teacherId->equals($teacherId));
|
|
self::assertSame('Contrôle chapitre 5', $evaluation->title);
|
|
self::assertSame('Évaluation sur les fonctions', $evaluation->description);
|
|
self::assertEquals($evaluationDate, $evaluation->evaluationDate);
|
|
self::assertSame(20, $evaluation->gradeScale->maxValue);
|
|
self::assertSame(1.5, $evaluation->coefficient->value);
|
|
self::assertSame(EvaluationStatus::PUBLISHED, $evaluation->status);
|
|
self::assertEquals($createdAt, $evaluation->createdAt);
|
|
self::assertEquals($updatedAt, $evaluation->updatedAt);
|
|
self::assertEmpty($evaluation->pullDomainEvents());
|
|
}
|
|
|
|
private function createEvaluation(): Evaluation
|
|
{
|
|
return Evaluation::creer(
|
|
tenantId: TenantId::fromString(self::TENANT_ID),
|
|
classId: ClassId::fromString(self::CLASS_ID),
|
|
subjectId: SubjectId::fromString(self::SUBJECT_ID),
|
|
teacherId: UserId::fromString(self::TEACHER_ID),
|
|
title: 'Contrôle chapitre 5',
|
|
description: 'Évaluation sur les fonctions',
|
|
evaluationDate: new DateTimeImmutable('2026-04-15'),
|
|
gradeScale: new GradeScale(20),
|
|
coefficient: new Coefficient(1.0),
|
|
now: new DateTimeImmutable('2026-03-12 10:00:00'),
|
|
);
|
|
}
|
|
}
|