feat: Tracer automatiquement les événements notes et évaluations dans l'audit trail
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.
This commit is contained in:
@@ -38,7 +38,7 @@ project: classeo
|
|||||||
project_key: classeo
|
project_key: classeo
|
||||||
tracking_system: file-system
|
tracking_system: file-system
|
||||||
story_location: _bmad-output/implementation-artifacts
|
story_location: _bmad-output/implementation-artifacts
|
||||||
last_updated: 2026-04-17
|
last_updated: 2026-04-22
|
||||||
|
|
||||||
development_status:
|
development_status:
|
||||||
# Epic 1: Fondations, Auth & Observabilité (9 stories)
|
# Epic 1: Fondations, Auth & Observabilité (9 stories)
|
||||||
@@ -123,8 +123,8 @@ development_status:
|
|||||||
6-7-consultation-notes-par-le-parent: done
|
6-7-consultation-notes-par-le-parent: done
|
||||||
6-8-statistiques-enseignant: done
|
6-8-statistiques-enseignant: done
|
||||||
6-9-grade-voter-et-acces-notes-affectations: done # Débloque tâches différées de 2-6, 2-8, 2-9
|
6-9-grade-voter-et-acces-notes-affectations: done # Débloque tâches différées de 2-6, 2-8, 2-9
|
||||||
6-10-statistiques-notes-par-matiere-admin: review # Débloque tâches différées de 2-2
|
6-10-statistiques-notes-par-matiere-admin: done # Débloque tâches différées de 2-2
|
||||||
6-11-audit-trail-evenements-notes: ready-for-dev # Débloque tâches différées de 1-7
|
6-11-audit-trail-evenements-notes: done # Débloque tâches différées de 1-7
|
||||||
6-12-correctifs-mode-competences: ready-for-dev # Patches critiques review 6-5
|
6-12-correctifs-mode-competences: ready-for-dev # Patches critiques review 6-5
|
||||||
6-13-acces-evaluations-remplacant: ready-for-dev # UX : navigation évaluations pour le remplaçant (identifié en 6-9)
|
6-13-acces-evaluations-remplacant: ready-for-dev # UX : navigation évaluations pour le remplaçant (identifié en 6-9)
|
||||||
epic-6-retrospective: optional
|
epic-6-retrospective: optional
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ final readonly class EvaluationCreee implements DomainEvent
|
|||||||
public string $subjectId,
|
public string $subjectId,
|
||||||
public string $teacherId,
|
public string $teacherId,
|
||||||
public string $title,
|
public string $title,
|
||||||
|
public ?string $description,
|
||||||
public DateTimeImmutable $evaluationDate,
|
public DateTimeImmutable $evaluationDate,
|
||||||
|
public int $gradeScale,
|
||||||
|
public float $coefficient,
|
||||||
private DateTimeImmutable $occurredOn,
|
private DateTimeImmutable $occurredOn,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,16 @@ final readonly class EvaluationModifiee implements DomainEvent
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public EvaluationId $evaluationId,
|
public EvaluationId $evaluationId,
|
||||||
public string $title,
|
public string $oldTitle,
|
||||||
public DateTimeImmutable $evaluationDate,
|
public string $newTitle,
|
||||||
|
public ?string $oldDescription,
|
||||||
|
public ?string $newDescription,
|
||||||
|
public float $oldCoefficient,
|
||||||
|
public float $newCoefficient,
|
||||||
|
public DateTimeImmutable $oldEvaluationDate,
|
||||||
|
public DateTimeImmutable $newEvaluationDate,
|
||||||
|
public int $oldGradeScale,
|
||||||
|
public int $newGradeScale,
|
||||||
private DateTimeImmutable $occurredOn,
|
private DateTimeImmutable $occurredOn,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ final readonly class NoteModifiee implements DomainEvent
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
public GradeId $gradeId,
|
public GradeId $gradeId,
|
||||||
public string $evaluationId,
|
public string $evaluationId,
|
||||||
|
public string $studentId,
|
||||||
public ?float $oldValue,
|
public ?float $oldValue,
|
||||||
public ?float $newValue,
|
public ?float $newValue,
|
||||||
public string $oldStatus,
|
public string $oldStatus,
|
||||||
|
|||||||
@@ -73,7 +73,10 @@ final class Evaluation extends AggregateRoot
|
|||||||
subjectId: (string) $subjectId,
|
subjectId: (string) $subjectId,
|
||||||
teacherId: (string) $teacherId,
|
teacherId: (string) $teacherId,
|
||||||
title: $title,
|
title: $title,
|
||||||
|
description: $description,
|
||||||
evaluationDate: $evaluationDate,
|
evaluationDate: $evaluationDate,
|
||||||
|
gradeScale: $gradeScale->maxValue,
|
||||||
|
coefficient: $coefficient->value,
|
||||||
occurredOn: $now,
|
occurredOn: $now,
|
||||||
));
|
));
|
||||||
|
|
||||||
@@ -97,6 +100,12 @@ final class Evaluation extends AggregateRoot
|
|||||||
throw BaremeNonModifiableException::carNotesExistantes($this->id);
|
throw BaremeNonModifiableException::carNotesExistantes($this->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$oldTitle = $this->title;
|
||||||
|
$oldDescription = $this->description;
|
||||||
|
$oldCoefficient = $this->coefficient;
|
||||||
|
$oldEvaluationDate = $this->evaluationDate;
|
||||||
|
$oldGradeScale = $this->gradeScale;
|
||||||
|
|
||||||
$this->title = $title;
|
$this->title = $title;
|
||||||
$this->description = $description;
|
$this->description = $description;
|
||||||
$this->coefficient = $coefficient;
|
$this->coefficient = $coefficient;
|
||||||
@@ -110,8 +119,16 @@ final class Evaluation extends AggregateRoot
|
|||||||
|
|
||||||
$this->recordEvent(new EvaluationModifiee(
|
$this->recordEvent(new EvaluationModifiee(
|
||||||
evaluationId: $this->id,
|
evaluationId: $this->id,
|
||||||
title: $title,
|
oldTitle: $oldTitle,
|
||||||
evaluationDate: $evaluationDate,
|
newTitle: $this->title,
|
||||||
|
oldDescription: $oldDescription,
|
||||||
|
newDescription: $this->description,
|
||||||
|
oldCoefficient: $oldCoefficient->value,
|
||||||
|
newCoefficient: $this->coefficient->value,
|
||||||
|
oldEvaluationDate: $oldEvaluationDate,
|
||||||
|
newEvaluationDate: $this->evaluationDate,
|
||||||
|
oldGradeScale: $oldGradeScale->maxValue,
|
||||||
|
newGradeScale: $this->gradeScale->maxValue,
|
||||||
occurredOn: $now,
|
occurredOn: $now,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ final class Grade extends AggregateRoot
|
|||||||
$this->recordEvent(new NoteModifiee(
|
$this->recordEvent(new NoteModifiee(
|
||||||
gradeId: $this->id,
|
gradeId: $this->id,
|
||||||
evaluationId: (string) $this->evaluationId,
|
evaluationId: (string) $this->evaluationId,
|
||||||
|
studentId: (string) $this->studentId,
|
||||||
oldValue: $oldValue,
|
oldValue: $oldValue,
|
||||||
newValue: $value?->value,
|
newValue: $value?->value,
|
||||||
oldStatus: $oldStatus,
|
oldStatus: $oldStatus,
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Scolarite\Infrastructure\EventHandler;
|
||||||
|
|
||||||
|
use App\Scolarite\Domain\Event\EvaluationCreee;
|
||||||
|
use App\Scolarite\Domain\Event\EvaluationModifiee;
|
||||||
|
use App\Scolarite\Domain\Event\EvaluationSupprimee;
|
||||||
|
use App\Scolarite\Domain\Event\NotesPubliees;
|
||||||
|
use App\Shared\Application\Port\AuditLogger;
|
||||||
|
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||||
|
|
||||||
|
final readonly class AuditEvaluationEventsHandler
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private AuditLogger $auditLogger,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
#[AsMessageHandler(bus: 'event.bus')]
|
||||||
|
public function handleEvaluationCreee(EvaluationCreee $event): void
|
||||||
|
{
|
||||||
|
$this->auditLogger->logDataChange(
|
||||||
|
aggregateType: 'Evaluation',
|
||||||
|
aggregateId: $event->evaluationId->value,
|
||||||
|
eventType: 'EvaluationCreee',
|
||||||
|
oldValues: [],
|
||||||
|
newValues: [
|
||||||
|
'title' => $event->title,
|
||||||
|
'description' => $event->description,
|
||||||
|
'class_id' => $event->classId,
|
||||||
|
'subject_id' => $event->subjectId,
|
||||||
|
'teacher_id' => $event->teacherId,
|
||||||
|
'evaluation_date' => $event->evaluationDate->format('Y-m-d'),
|
||||||
|
'grade_scale' => $event->gradeScale,
|
||||||
|
'coefficient' => $event->coefficient,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[AsMessageHandler(bus: 'event.bus')]
|
||||||
|
public function handleEvaluationModifiee(EvaluationModifiee $event): void
|
||||||
|
{
|
||||||
|
$this->auditLogger->logDataChange(
|
||||||
|
aggregateType: 'Evaluation',
|
||||||
|
aggregateId: $event->evaluationId->value,
|
||||||
|
eventType: 'EvaluationModifiee',
|
||||||
|
oldValues: [
|
||||||
|
'title' => $event->oldTitle,
|
||||||
|
'description' => $event->oldDescription,
|
||||||
|
'coefficient' => $event->oldCoefficient,
|
||||||
|
'evaluation_date' => $event->oldEvaluationDate->format('Y-m-d'),
|
||||||
|
'grade_scale' => $event->oldGradeScale,
|
||||||
|
],
|
||||||
|
newValues: [
|
||||||
|
'title' => $event->newTitle,
|
||||||
|
'description' => $event->newDescription,
|
||||||
|
'coefficient' => $event->newCoefficient,
|
||||||
|
'evaluation_date' => $event->newEvaluationDate->format('Y-m-d'),
|
||||||
|
'grade_scale' => $event->newGradeScale,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[AsMessageHandler(bus: 'event.bus')]
|
||||||
|
public function handleEvaluationSupprimee(EvaluationSupprimee $event): void
|
||||||
|
{
|
||||||
|
$this->auditLogger->logDataChange(
|
||||||
|
aggregateType: 'Evaluation',
|
||||||
|
aggregateId: $event->evaluationId->value,
|
||||||
|
eventType: 'EvaluationSupprimee',
|
||||||
|
oldValues: [],
|
||||||
|
newValues: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[AsMessageHandler(bus: 'event.bus')]
|
||||||
|
public function handleNotesPubliees(NotesPubliees $event): void
|
||||||
|
{
|
||||||
|
$this->auditLogger->logDataChange(
|
||||||
|
aggregateType: 'Evaluation',
|
||||||
|
aggregateId: $event->evaluationId->value,
|
||||||
|
eventType: 'NotesPubliees',
|
||||||
|
oldValues: [],
|
||||||
|
newValues: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Scolarite\Infrastructure\EventHandler;
|
||||||
|
|
||||||
|
use App\Scolarite\Domain\Event\NoteModifiee;
|
||||||
|
use App\Scolarite\Domain\Event\NoteSaisie;
|
||||||
|
use App\Shared\Application\Port\AuditLogger;
|
||||||
|
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||||
|
|
||||||
|
final readonly class AuditGradeEventsHandler
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private AuditLogger $auditLogger,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
#[AsMessageHandler(bus: 'event.bus')]
|
||||||
|
public function handleNoteSaisie(NoteSaisie $event): void
|
||||||
|
{
|
||||||
|
$this->auditLogger->logDataChange(
|
||||||
|
aggregateType: 'Grade',
|
||||||
|
aggregateId: $event->gradeId->value,
|
||||||
|
eventType: 'NoteSaisie',
|
||||||
|
oldValues: [],
|
||||||
|
newValues: [
|
||||||
|
'evaluation_id' => $event->evaluationId,
|
||||||
|
'student_id' => $event->studentId,
|
||||||
|
'value' => $event->value,
|
||||||
|
'status' => $event->status,
|
||||||
|
'created_by' => $event->createdBy,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[AsMessageHandler(bus: 'event.bus')]
|
||||||
|
public function handleNoteModifiee(NoteModifiee $event): void
|
||||||
|
{
|
||||||
|
$this->auditLogger->logDataChange(
|
||||||
|
aggregateType: 'Grade',
|
||||||
|
aggregateId: $event->gradeId->value,
|
||||||
|
eventType: 'NoteModifiee',
|
||||||
|
oldValues: [
|
||||||
|
'value' => $event->oldValue,
|
||||||
|
'status' => $event->oldStatus,
|
||||||
|
],
|
||||||
|
newValues: [
|
||||||
|
'evaluation_id' => $event->evaluationId,
|
||||||
|
'student_id' => $event->studentId,
|
||||||
|
'value' => $event->newValue,
|
||||||
|
'status' => $event->newStatus,
|
||||||
|
'modified_by' => $event->modifiedBy,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Tests\Functional\Scolarite\Infrastructure\EventHandler;
|
||||||
|
|
||||||
|
use App\Scolarite\Domain\Event\EvaluationCreee;
|
||||||
|
use App\Scolarite\Domain\Event\EvaluationModifiee;
|
||||||
|
use App\Scolarite\Domain\Event\EvaluationSupprimee;
|
||||||
|
use App\Scolarite\Domain\Event\NotesPubliees;
|
||||||
|
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
|
||||||
|
use App\Scolarite\Infrastructure\EventHandler\AuditEvaluationEventsHandler;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use Doctrine\DBAL\Connection;
|
||||||
|
|
||||||
|
use const JSON_THROW_ON_ERROR;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use Ramsey\Uuid\Uuid;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie le round-trip DB de l'audit des événements Evaluation : le handler,
|
||||||
|
* connecté à l'AuditLogger réel, persiste `old_values`/`new_values` complets
|
||||||
|
* (title, description, coefficient, evaluation_date, grade_scale) dans `audit_log`.
|
||||||
|
*/
|
||||||
|
final class AuditEvaluationEventsHandlerFunctionalTest extends KernelTestCase
|
||||||
|
{
|
||||||
|
private Connection $connection;
|
||||||
|
private AuditEvaluationEventsHandler $handler;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
self::bootKernel();
|
||||||
|
|
||||||
|
/** @var Connection $connection */
|
||||||
|
$connection = static::getContainer()->get(Connection::class);
|
||||||
|
$this->connection = $connection;
|
||||||
|
|
||||||
|
/** @var AuditEvaluationEventsHandler $handler */
|
||||||
|
$handler = static::getContainer()->get(AuditEvaluationEventsHandler::class);
|
||||||
|
$this->handler = $handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function handleEvaluationCreeeWritesAuditEntryToDatabase(): void
|
||||||
|
{
|
||||||
|
$evaluationId = EvaluationId::generate();
|
||||||
|
$classId = Uuid::uuid4()->toString();
|
||||||
|
$subjectId = Uuid::uuid4()->toString();
|
||||||
|
$teacherId = Uuid::uuid4()->toString();
|
||||||
|
|
||||||
|
$event = new EvaluationCreee(
|
||||||
|
evaluationId: $evaluationId,
|
||||||
|
classId: $classId,
|
||||||
|
subjectId: $subjectId,
|
||||||
|
teacherId: $teacherId,
|
||||||
|
title: 'Contrôle chapitre 3',
|
||||||
|
description: 'Sur les parties 1 et 2',
|
||||||
|
evaluationDate: new DateTimeImmutable('2026-04-15'),
|
||||||
|
gradeScale: 20,
|
||||||
|
coefficient: 2.0,
|
||||||
|
occurredOn: new DateTimeImmutable(),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->handler->handleEvaluationCreee($event);
|
||||||
|
|
||||||
|
$entry = $this->connection->fetchAssociative(
|
||||||
|
'SELECT * FROM audit_log WHERE aggregate_id = ? AND event_type = ? ORDER BY occurred_at DESC LIMIT 1',
|
||||||
|
[$evaluationId->value->toString(), 'EvaluationCreee'],
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertNotFalse($entry, 'Audit log entry should exist after EvaluationCreee');
|
||||||
|
self::assertSame('Evaluation', $entry['aggregate_type']);
|
||||||
|
|
||||||
|
$payload = self::decodePayload($entry['payload']);
|
||||||
|
self::assertSame([], $payload['old_values']);
|
||||||
|
self::assertSame('Contrôle chapitre 3', $payload['new_values']['title']);
|
||||||
|
self::assertSame('Sur les parties 1 et 2', $payload['new_values']['description']);
|
||||||
|
self::assertSame($classId, $payload['new_values']['class_id']);
|
||||||
|
self::assertSame($subjectId, $payload['new_values']['subject_id']);
|
||||||
|
self::assertSame($teacherId, $payload['new_values']['teacher_id']);
|
||||||
|
self::assertSame('2026-04-15', $payload['new_values']['evaluation_date']);
|
||||||
|
self::assertSame(20, $payload['new_values']['grade_scale']);
|
||||||
|
self::assertSame(2.0, $payload['new_values']['coefficient']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function handleEvaluationModifieeWritesAuditEntryWithDiff(): void
|
||||||
|
{
|
||||||
|
$evaluationId = EvaluationId::generate();
|
||||||
|
|
||||||
|
$event = new EvaluationModifiee(
|
||||||
|
evaluationId: $evaluationId,
|
||||||
|
oldTitle: 'Ancien titre',
|
||||||
|
newTitle: 'Nouveau titre',
|
||||||
|
oldDescription: null,
|
||||||
|
newDescription: 'Description ajoutée',
|
||||||
|
oldCoefficient: 1.0,
|
||||||
|
newCoefficient: 3.0,
|
||||||
|
oldEvaluationDate: new DateTimeImmutable('2026-04-15'),
|
||||||
|
newEvaluationDate: new DateTimeImmutable('2026-05-02'),
|
||||||
|
oldGradeScale: 20,
|
||||||
|
newGradeScale: 100,
|
||||||
|
occurredOn: new DateTimeImmutable(),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->handler->handleEvaluationModifiee($event);
|
||||||
|
|
||||||
|
$entry = $this->connection->fetchAssociative(
|
||||||
|
'SELECT * FROM audit_log WHERE aggregate_id = ? AND event_type = ? ORDER BY occurred_at DESC LIMIT 1',
|
||||||
|
[$evaluationId->value->toString(), 'EvaluationModifiee'],
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertNotFalse($entry, 'Audit log entry should exist after EvaluationModifiee');
|
||||||
|
self::assertSame('Evaluation', $entry['aggregate_type']);
|
||||||
|
|
||||||
|
$payload = self::decodePayload($entry['payload']);
|
||||||
|
self::assertSame('Ancien titre', $payload['old_values']['title']);
|
||||||
|
self::assertNull($payload['old_values']['description']);
|
||||||
|
self::assertSame(1.0, $payload['old_values']['coefficient']);
|
||||||
|
self::assertSame('2026-04-15', $payload['old_values']['evaluation_date']);
|
||||||
|
self::assertSame(20, $payload['old_values']['grade_scale']);
|
||||||
|
|
||||||
|
self::assertSame('Nouveau titre', $payload['new_values']['title']);
|
||||||
|
self::assertSame('Description ajoutée', $payload['new_values']['description']);
|
||||||
|
self::assertSame(3.0, $payload['new_values']['coefficient']);
|
||||||
|
self::assertSame('2026-05-02', $payload['new_values']['evaluation_date']);
|
||||||
|
self::assertSame(100, $payload['new_values']['grade_scale']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function handleNotesPublieesWritesAuditEntryToDatabase(): void
|
||||||
|
{
|
||||||
|
$evaluationId = EvaluationId::generate();
|
||||||
|
|
||||||
|
$event = new NotesPubliees(
|
||||||
|
evaluationId: $evaluationId,
|
||||||
|
occurredOn: new DateTimeImmutable(),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->handler->handleNotesPubliees($event);
|
||||||
|
|
||||||
|
$entry = $this->connection->fetchAssociative(
|
||||||
|
'SELECT * FROM audit_log WHERE aggregate_id = ? AND event_type = ? ORDER BY occurred_at DESC LIMIT 1',
|
||||||
|
[$evaluationId->value->toString(), 'NotesPubliees'],
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertNotFalse($entry, 'Audit log entry should exist after NotesPubliees');
|
||||||
|
self::assertSame('Evaluation', $entry['aggregate_type']);
|
||||||
|
self::assertSame($evaluationId->value->toString(), $entry['aggregate_id']);
|
||||||
|
|
||||||
|
$payload = self::decodePayload($entry['payload']);
|
||||||
|
self::assertSame([], $payload['old_values']);
|
||||||
|
self::assertSame([], $payload['new_values']);
|
||||||
|
|
||||||
|
$metadata = self::decodePayload($entry['metadata']);
|
||||||
|
self::assertArrayHasKey('correlation_id', $metadata);
|
||||||
|
self::assertArrayHasKey('occurred_at', $metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function handleEvaluationSupprimeeWritesAuditEntryToDatabase(): void
|
||||||
|
{
|
||||||
|
$evaluationId = EvaluationId::generate();
|
||||||
|
|
||||||
|
$event = new EvaluationSupprimee(
|
||||||
|
evaluationId: $evaluationId,
|
||||||
|
occurredOn: new DateTimeImmutable(),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->handler->handleEvaluationSupprimee($event);
|
||||||
|
|
||||||
|
$entry = $this->connection->fetchAssociative(
|
||||||
|
'SELECT * FROM audit_log WHERE aggregate_id = ? AND event_type = ? ORDER BY occurred_at DESC LIMIT 1',
|
||||||
|
[$evaluationId->value->toString(), 'EvaluationSupprimee'],
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertNotFalse($entry, 'Audit log entry should exist after EvaluationSupprimee');
|
||||||
|
self::assertSame('Evaluation', $entry['aggregate_type']);
|
||||||
|
self::assertSame($evaluationId->value->toString(), $entry['aggregate_id']);
|
||||||
|
|
||||||
|
$payload = self::decodePayload($entry['payload']);
|
||||||
|
self::assertSame([], $payload['old_values']);
|
||||||
|
self::assertSame([], $payload['new_values']);
|
||||||
|
|
||||||
|
$metadata = self::decodePayload($entry['metadata']);
|
||||||
|
self::assertArrayHasKey('correlation_id', $metadata);
|
||||||
|
self::assertArrayHasKey('occurred_at', $metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private static function decodePayload(mixed $raw): array
|
||||||
|
{
|
||||||
|
self::assertIsString($raw);
|
||||||
|
|
||||||
|
/** @var array<string, mixed> $decoded */
|
||||||
|
$decoded = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
|
||||||
|
return $decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Tests\Functional\Scolarite\Infrastructure\EventHandler;
|
||||||
|
|
||||||
|
use App\Scolarite\Domain\Event\NoteModifiee;
|
||||||
|
use App\Scolarite\Domain\Event\NoteSaisie;
|
||||||
|
use App\Scolarite\Domain\Model\Grade\GradeId;
|
||||||
|
use App\Scolarite\Infrastructure\EventHandler\AuditGradeEventsHandler;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use Doctrine\DBAL\Connection;
|
||||||
|
|
||||||
|
use const JSON_THROW_ON_ERROR;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use Ramsey\Uuid\Uuid;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie le bout-en-bout de l'audit des événements Grade : le handler,
|
||||||
|
* connecté à l'AuditLogger réel et à la base, écrit une ligne immuable
|
||||||
|
* dans `audit_log` avec le bon payload (création / diff).
|
||||||
|
*/
|
||||||
|
final class AuditGradeEventsHandlerFunctionalTest extends KernelTestCase
|
||||||
|
{
|
||||||
|
private Connection $connection;
|
||||||
|
private AuditGradeEventsHandler $handler;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
self::bootKernel();
|
||||||
|
|
||||||
|
/** @var Connection $connection */
|
||||||
|
$connection = static::getContainer()->get(Connection::class);
|
||||||
|
$this->connection = $connection;
|
||||||
|
|
||||||
|
/** @var AuditGradeEventsHandler $handler */
|
||||||
|
$handler = static::getContainer()->get(AuditGradeEventsHandler::class);
|
||||||
|
$this->handler = $handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
// audit_log est append-only : pas de DELETE possible, on filtre par UUID unique dans chaque test
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function handleNoteSaisieWritesAuditEntryToDatabase(): void
|
||||||
|
{
|
||||||
|
$gradeId = GradeId::generate();
|
||||||
|
$evaluationId = Uuid::uuid4()->toString();
|
||||||
|
$studentId = Uuid::uuid4()->toString();
|
||||||
|
$createdBy = Uuid::uuid4()->toString();
|
||||||
|
|
||||||
|
$event = new NoteSaisie(
|
||||||
|
gradeId: $gradeId,
|
||||||
|
evaluationId: $evaluationId,
|
||||||
|
studentId: $studentId,
|
||||||
|
value: 15.5,
|
||||||
|
status: 'draft',
|
||||||
|
createdBy: $createdBy,
|
||||||
|
occurredOn: new DateTimeImmutable(),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->handler->handleNoteSaisie($event);
|
||||||
|
|
||||||
|
$entry = $this->connection->fetchAssociative(
|
||||||
|
'SELECT * FROM audit_log WHERE aggregate_id = ? AND event_type = ? ORDER BY occurred_at DESC LIMIT 1',
|
||||||
|
[$gradeId->value->toString(), 'NoteSaisie'],
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertNotFalse($entry, 'Audit log entry should exist after NoteSaisie');
|
||||||
|
self::assertSame('Grade', $entry['aggregate_type']);
|
||||||
|
|
||||||
|
$payload = self::decodePayload($entry['payload']);
|
||||||
|
self::assertSame([], $payload['old_values']);
|
||||||
|
self::assertSame($evaluationId, $payload['new_values']['evaluation_id']);
|
||||||
|
self::assertSame($studentId, $payload['new_values']['student_id']);
|
||||||
|
self::assertSame(15.5, $payload['new_values']['value']);
|
||||||
|
self::assertSame('draft', $payload['new_values']['status']);
|
||||||
|
self::assertSame($createdBy, $payload['new_values']['created_by']);
|
||||||
|
|
||||||
|
self::assertArrayHasKey('metadata', $entry);
|
||||||
|
$metadata = self::decodePayload($entry['metadata']);
|
||||||
|
self::assertArrayHasKey('correlation_id', $metadata);
|
||||||
|
self::assertArrayHasKey('occurred_at', $metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function handleNoteModifieeWritesAuditEntryWithDiff(): void
|
||||||
|
{
|
||||||
|
$gradeId = GradeId::generate();
|
||||||
|
$evaluationId = Uuid::uuid4()->toString();
|
||||||
|
$studentId = Uuid::uuid4()->toString();
|
||||||
|
$modifiedBy = Uuid::uuid4()->toString();
|
||||||
|
|
||||||
|
$event = new NoteModifiee(
|
||||||
|
gradeId: $gradeId,
|
||||||
|
evaluationId: $evaluationId,
|
||||||
|
studentId: $studentId,
|
||||||
|
oldValue: 12.0,
|
||||||
|
newValue: 14.5,
|
||||||
|
oldStatus: 'draft',
|
||||||
|
newStatus: 'published',
|
||||||
|
modifiedBy: $modifiedBy,
|
||||||
|
occurredOn: new DateTimeImmutable(),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->handler->handleNoteModifiee($event);
|
||||||
|
|
||||||
|
$entry = $this->connection->fetchAssociative(
|
||||||
|
'SELECT * FROM audit_log WHERE aggregate_id = ? AND event_type = ? ORDER BY occurred_at DESC LIMIT 1',
|
||||||
|
[$gradeId->value->toString(), 'NoteModifiee'],
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertNotFalse($entry, 'Audit log entry should exist after NoteModifiee');
|
||||||
|
self::assertSame('Grade', $entry['aggregate_type']);
|
||||||
|
|
||||||
|
$payload = self::decodePayload($entry['payload']);
|
||||||
|
self::assertSame(['value' => 12.0, 'status' => 'draft'], $payload['old_values']);
|
||||||
|
self::assertSame(14.5, $payload['new_values']['value']);
|
||||||
|
self::assertSame('published', $payload['new_values']['status']);
|
||||||
|
self::assertSame($modifiedBy, $payload['new_values']['modified_by']);
|
||||||
|
self::assertSame($evaluationId, $payload['new_values']['evaluation_id']);
|
||||||
|
self::assertSame($studentId, $payload['new_values']['student_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private static function decodePayload(mixed $raw): array
|
||||||
|
{
|
||||||
|
self::assertIsString($raw);
|
||||||
|
|
||||||
|
/** @var array<string, mixed> $decoded */
|
||||||
|
$decoded = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
|
||||||
|
return $decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,6 +47,10 @@ final class EvaluationTest extends TestCase
|
|||||||
self::assertCount(1, $events);
|
self::assertCount(1, $events);
|
||||||
self::assertInstanceOf(EvaluationCreee::class, $events[0]);
|
self::assertInstanceOf(EvaluationCreee::class, $events[0]);
|
||||||
self::assertSame($evaluation->id, $events[0]->evaluationId);
|
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]
|
#[Test]
|
||||||
@@ -136,6 +140,16 @@ final class EvaluationTest extends TestCase
|
|||||||
self::assertCount(1, $events);
|
self::assertCount(1, $events);
|
||||||
self::assertInstanceOf(EvaluationModifiee::class, $events[0]);
|
self::assertInstanceOf(EvaluationModifiee::class, $events[0]);
|
||||||
self::assertSame($evaluation->id, $events[0]->evaluationId);
|
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]
|
#[Test]
|
||||||
|
|||||||
@@ -179,6 +179,7 @@ final class GradeTest extends TestCase
|
|||||||
$events = $grade->pullDomainEvents();
|
$events = $grade->pullDomainEvents();
|
||||||
self::assertCount(1, $events);
|
self::assertCount(1, $events);
|
||||||
self::assertInstanceOf(NoteModifiee::class, $events[0]);
|
self::assertInstanceOf(NoteModifiee::class, $events[0]);
|
||||||
|
self::assertSame(self::STUDENT_ID, $events[0]->studentId);
|
||||||
self::assertSame(15.5, $events[0]->oldValue);
|
self::assertSame(15.5, $events[0]->oldValue);
|
||||||
self::assertSame(18.0, $events[0]->newValue);
|
self::assertSame(18.0, $events[0]->newValue);
|
||||||
self::assertSame('graded', $events[0]->oldStatus);
|
self::assertSame('graded', $events[0]->oldStatus);
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Tests\Unit\Scolarite\Infrastructure\EventHandler;
|
||||||
|
|
||||||
|
use App\Scolarite\Domain\Event\EvaluationCreee;
|
||||||
|
use App\Scolarite\Domain\Event\EvaluationModifiee;
|
||||||
|
use App\Scolarite\Domain\Event\EvaluationSupprimee;
|
||||||
|
use App\Scolarite\Domain\Event\NotesPubliees;
|
||||||
|
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
|
||||||
|
use App\Scolarite\Infrastructure\EventHandler\AuditEvaluationEventsHandler;
|
||||||
|
use App\Shared\Application\Port\AuditLogger;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Ramsey\Uuid\Uuid;
|
||||||
|
|
||||||
|
final class AuditEvaluationEventsHandlerTest extends TestCase
|
||||||
|
{
|
||||||
|
private AuditLogger&MockObject $auditLogger;
|
||||||
|
private AuditEvaluationEventsHandler $handler;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->auditLogger = $this->createMock(AuditLogger::class);
|
||||||
|
$this->handler = new AuditEvaluationEventsHandler($this->auditLogger);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHandleEvaluationCreeeLogsAuditEntryWithFullPayload(): void
|
||||||
|
{
|
||||||
|
$evaluationId = EvaluationId::generate();
|
||||||
|
$classId = Uuid::uuid4()->toString();
|
||||||
|
$subjectId = Uuid::uuid4()->toString();
|
||||||
|
$teacherId = Uuid::uuid4()->toString();
|
||||||
|
$evaluationDate = new DateTimeImmutable('2026-04-15');
|
||||||
|
|
||||||
|
$event = new EvaluationCreee(
|
||||||
|
evaluationId: $evaluationId,
|
||||||
|
classId: $classId,
|
||||||
|
subjectId: $subjectId,
|
||||||
|
teacherId: $teacherId,
|
||||||
|
title: 'Contrôle chapitre 3',
|
||||||
|
description: 'Sur le chapitre 3 uniquement',
|
||||||
|
evaluationDate: $evaluationDate,
|
||||||
|
gradeScale: 20,
|
||||||
|
coefficient: 2.0,
|
||||||
|
occurredOn: new DateTimeImmutable(),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->auditLogger->expects($this->once())
|
||||||
|
->method('logDataChange')
|
||||||
|
->with(
|
||||||
|
$this->equalTo('Evaluation'),
|
||||||
|
$this->callback(static fn ($uuid) => $uuid->toString() === $evaluationId->value->toString()),
|
||||||
|
$this->equalTo('EvaluationCreee'),
|
||||||
|
$this->equalTo([]),
|
||||||
|
$this->callback(static fn ($new) => $new['title'] === 'Contrôle chapitre 3'
|
||||||
|
&& $new['description'] === 'Sur le chapitre 3 uniquement'
|
||||||
|
&& $new['class_id'] === $classId
|
||||||
|
&& $new['subject_id'] === $subjectId
|
||||||
|
&& $new['teacher_id'] === $teacherId
|
||||||
|
&& $new['evaluation_date'] === '2026-04-15'
|
||||||
|
&& $new['grade_scale'] === 20
|
||||||
|
&& $new['coefficient'] === 2.0
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->handler->handleEvaluationCreee($event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHandleEvaluationModifieeLogsAuditEntryWithDiff(): void
|
||||||
|
{
|
||||||
|
$evaluationId = EvaluationId::generate();
|
||||||
|
|
||||||
|
$event = new EvaluationModifiee(
|
||||||
|
evaluationId: $evaluationId,
|
||||||
|
oldTitle: 'Ancien titre',
|
||||||
|
newTitle: 'Nouveau titre',
|
||||||
|
oldDescription: 'Ancienne description',
|
||||||
|
newDescription: 'Nouvelle description',
|
||||||
|
oldCoefficient: 1.0,
|
||||||
|
newCoefficient: 2.0,
|
||||||
|
oldEvaluationDate: new DateTimeImmutable('2026-04-15'),
|
||||||
|
newEvaluationDate: new DateTimeImmutable('2026-05-01'),
|
||||||
|
oldGradeScale: 20,
|
||||||
|
newGradeScale: 100,
|
||||||
|
occurredOn: new DateTimeImmutable(),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->auditLogger->expects($this->once())
|
||||||
|
->method('logDataChange')
|
||||||
|
->with(
|
||||||
|
$this->equalTo('Evaluation'),
|
||||||
|
$this->callback(static fn ($uuid) => $uuid->toString() === $evaluationId->value->toString()),
|
||||||
|
$this->equalTo('EvaluationModifiee'),
|
||||||
|
$this->callback(static fn ($old) => $old['title'] === 'Ancien titre'
|
||||||
|
&& $old['description'] === 'Ancienne description'
|
||||||
|
&& $old['coefficient'] === 1.0
|
||||||
|
&& $old['evaluation_date'] === '2026-04-15'
|
||||||
|
&& $old['grade_scale'] === 20
|
||||||
|
),
|
||||||
|
$this->callback(static fn ($new) => $new['title'] === 'Nouveau titre'
|
||||||
|
&& $new['description'] === 'Nouvelle description'
|
||||||
|
&& $new['coefficient'] === 2.0
|
||||||
|
&& $new['evaluation_date'] === '2026-05-01'
|
||||||
|
&& $new['grade_scale'] === 100
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->handler->handleEvaluationModifiee($event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHandleEvaluationSupprimeeLogsAuditEntry(): void
|
||||||
|
{
|
||||||
|
$evaluationId = EvaluationId::generate();
|
||||||
|
|
||||||
|
$event = new EvaluationSupprimee(
|
||||||
|
evaluationId: $evaluationId,
|
||||||
|
occurredOn: new DateTimeImmutable(),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->auditLogger->expects($this->once())
|
||||||
|
->method('logDataChange')
|
||||||
|
->with(
|
||||||
|
$this->equalTo('Evaluation'),
|
||||||
|
$this->callback(static fn ($uuid) => $uuid->toString() === $evaluationId->value->toString()),
|
||||||
|
$this->equalTo('EvaluationSupprimee'),
|
||||||
|
$this->equalTo([]),
|
||||||
|
$this->equalTo([]),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->handler->handleEvaluationSupprimee($event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHandleNotesPublieesLogsAuditEntry(): void
|
||||||
|
{
|
||||||
|
$evaluationId = EvaluationId::generate();
|
||||||
|
|
||||||
|
$event = new NotesPubliees(
|
||||||
|
evaluationId: $evaluationId,
|
||||||
|
occurredOn: new DateTimeImmutable(),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->auditLogger->expects($this->once())
|
||||||
|
->method('logDataChange')
|
||||||
|
->with(
|
||||||
|
$this->equalTo('Evaluation'),
|
||||||
|
$this->callback(static fn ($uuid) => $uuid->toString() === $evaluationId->value->toString()),
|
||||||
|
$this->equalTo('NotesPubliees'),
|
||||||
|
$this->equalTo([]),
|
||||||
|
$this->equalTo([]),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->handler->handleNotesPubliees($event);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Tests\Unit\Scolarite\Infrastructure\EventHandler;
|
||||||
|
|
||||||
|
use App\Scolarite\Domain\Event\NoteModifiee;
|
||||||
|
use App\Scolarite\Domain\Event\NoteSaisie;
|
||||||
|
use App\Scolarite\Domain\Model\Grade\GradeId;
|
||||||
|
use App\Scolarite\Infrastructure\EventHandler\AuditGradeEventsHandler;
|
||||||
|
use App\Shared\Application\Port\AuditLogger;
|
||||||
|
|
||||||
|
use function array_key_exists;
|
||||||
|
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Ramsey\Uuid\Uuid;
|
||||||
|
|
||||||
|
final class AuditGradeEventsHandlerTest extends TestCase
|
||||||
|
{
|
||||||
|
private AuditLogger&MockObject $auditLogger;
|
||||||
|
private AuditGradeEventsHandler $handler;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->auditLogger = $this->createMock(AuditLogger::class);
|
||||||
|
$this->handler = new AuditGradeEventsHandler($this->auditLogger);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHandleNoteSaisieLogsAuditEntryWithCreationPayload(): void
|
||||||
|
{
|
||||||
|
$gradeId = GradeId::generate();
|
||||||
|
$evaluationId = Uuid::uuid4()->toString();
|
||||||
|
$studentId = Uuid::uuid4()->toString();
|
||||||
|
$createdBy = Uuid::uuid4()->toString();
|
||||||
|
|
||||||
|
$event = new NoteSaisie(
|
||||||
|
gradeId: $gradeId,
|
||||||
|
evaluationId: $evaluationId,
|
||||||
|
studentId: $studentId,
|
||||||
|
value: 15.5,
|
||||||
|
status: 'draft',
|
||||||
|
createdBy: $createdBy,
|
||||||
|
occurredOn: new DateTimeImmutable(),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->auditLogger->expects($this->once())
|
||||||
|
->method('logDataChange')
|
||||||
|
->with(
|
||||||
|
$this->equalTo('Grade'),
|
||||||
|
$this->callback(static fn ($uuid) => $uuid->toString() === $gradeId->value->toString()),
|
||||||
|
$this->equalTo('NoteSaisie'),
|
||||||
|
$this->equalTo([]),
|
||||||
|
$this->callback(static fn ($new) => $new['evaluation_id'] === $evaluationId
|
||||||
|
&& $new['student_id'] === $studentId
|
||||||
|
&& $new['value'] === 15.5
|
||||||
|
&& $new['status'] === 'draft'
|
||||||
|
&& $new['created_by'] === $createdBy
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->handler->handleNoteSaisie($event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHandleNoteSaisieSupportsNullValue(): void
|
||||||
|
{
|
||||||
|
$event = new NoteSaisie(
|
||||||
|
gradeId: GradeId::generate(),
|
||||||
|
evaluationId: Uuid::uuid4()->toString(),
|
||||||
|
studentId: Uuid::uuid4()->toString(),
|
||||||
|
value: null,
|
||||||
|
status: 'absent',
|
||||||
|
createdBy: Uuid::uuid4()->toString(),
|
||||||
|
occurredOn: new DateTimeImmutable(),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->auditLogger->expects($this->once())
|
||||||
|
->method('logDataChange')
|
||||||
|
->with(
|
||||||
|
$this->equalTo('Grade'),
|
||||||
|
$this->anything(),
|
||||||
|
$this->equalTo('NoteSaisie'),
|
||||||
|
$this->equalTo([]),
|
||||||
|
$this->callback(static fn ($new) => $new['value'] === null
|
||||||
|
&& $new['status'] === 'absent'
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->handler->handleNoteSaisie($event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHandleNoteModifieeLogsAuditEntryWithDiff(): void
|
||||||
|
{
|
||||||
|
$gradeId = GradeId::generate();
|
||||||
|
$evaluationId = Uuid::uuid4()->toString();
|
||||||
|
$studentId = Uuid::uuid4()->toString();
|
||||||
|
$modifiedBy = Uuid::uuid4()->toString();
|
||||||
|
|
||||||
|
$event = new NoteModifiee(
|
||||||
|
gradeId: $gradeId,
|
||||||
|
evaluationId: $evaluationId,
|
||||||
|
studentId: $studentId,
|
||||||
|
oldValue: 12.0,
|
||||||
|
newValue: 14.5,
|
||||||
|
oldStatus: 'draft',
|
||||||
|
newStatus: 'published',
|
||||||
|
modifiedBy: $modifiedBy,
|
||||||
|
occurredOn: new DateTimeImmutable(),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->auditLogger->expects($this->once())
|
||||||
|
->method('logDataChange')
|
||||||
|
->with(
|
||||||
|
$this->equalTo('Grade'),
|
||||||
|
$this->callback(static fn ($uuid) => $uuid->toString() === $gradeId->value->toString()),
|
||||||
|
$this->equalTo('NoteModifiee'),
|
||||||
|
$this->callback(static fn ($old) => $old['value'] === 12.0
|
||||||
|
&& $old['status'] === 'draft'
|
||||||
|
),
|
||||||
|
$this->callback(static fn ($new) => $new['value'] === 14.5
|
||||||
|
&& $new['status'] === 'published'
|
||||||
|
&& $new['modified_by'] === $modifiedBy
|
||||||
|
&& $new['evaluation_id'] === $evaluationId
|
||||||
|
&& $new['student_id'] === $studentId
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->handler->handleNoteModifiee($event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHandleNoteModifieeSupportsNullValues(): void
|
||||||
|
{
|
||||||
|
$event = new NoteModifiee(
|
||||||
|
gradeId: GradeId::generate(),
|
||||||
|
evaluationId: Uuid::uuid4()->toString(),
|
||||||
|
studentId: Uuid::uuid4()->toString(),
|
||||||
|
oldValue: 10.0,
|
||||||
|
newValue: null,
|
||||||
|
oldStatus: 'published',
|
||||||
|
newStatus: 'absent',
|
||||||
|
modifiedBy: Uuid::uuid4()->toString(),
|
||||||
|
occurredOn: new DateTimeImmutable(),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->auditLogger->expects($this->once())
|
||||||
|
->method('logDataChange')
|
||||||
|
->with(
|
||||||
|
$this->equalTo('Grade'),
|
||||||
|
$this->anything(),
|
||||||
|
$this->equalTo('NoteModifiee'),
|
||||||
|
$this->callback(static fn ($old) => $old['value'] === 10.0),
|
||||||
|
$this->callback(static fn ($new) => $new['value'] === null
|
||||||
|
&& $new['status'] === 'absent'
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->handler->handleNoteModifiee($event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHandleNoteSaisieSupportsZeroValue(): void
|
||||||
|
{
|
||||||
|
$event = new NoteSaisie(
|
||||||
|
gradeId: GradeId::generate(),
|
||||||
|
evaluationId: Uuid::uuid4()->toString(),
|
||||||
|
studentId: Uuid::uuid4()->toString(),
|
||||||
|
value: 0.0,
|
||||||
|
status: 'published',
|
||||||
|
createdBy: Uuid::uuid4()->toString(),
|
||||||
|
occurredOn: new DateTimeImmutable(),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->auditLogger->expects($this->once())
|
||||||
|
->method('logDataChange')
|
||||||
|
->with(
|
||||||
|
$this->equalTo('Grade'),
|
||||||
|
$this->anything(),
|
||||||
|
$this->equalTo('NoteSaisie'),
|
||||||
|
$this->equalTo([]),
|
||||||
|
$this->callback(static fn ($new) => array_key_exists('value', $new)
|
||||||
|
&& $new['value'] === 0.0
|
||||||
|
&& $new['status'] === 'published'
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->handler->handleNoteSaisie($event);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -100,8 +100,16 @@ final class RecalculerMoyennesOnEvaluationModifieeHandlerTest extends TestCase
|
|||||||
|
|
||||||
($this->handler)(new EvaluationModifiee(
|
($this->handler)(new EvaluationModifiee(
|
||||||
evaluationId: $evaluationId,
|
evaluationId: $evaluationId,
|
||||||
title: 'Titre modifié',
|
oldTitle: 'Test Evaluation',
|
||||||
evaluationDate: new DateTimeImmutable('2026-02-15'),
|
newTitle: 'Titre modifié',
|
||||||
|
oldDescription: null,
|
||||||
|
newDescription: null,
|
||||||
|
oldCoefficient: 1.0,
|
||||||
|
newCoefficient: 1.0,
|
||||||
|
oldEvaluationDate: new DateTimeImmutable('2026-02-15'),
|
||||||
|
newEvaluationDate: new DateTimeImmutable('2026-02-15'),
|
||||||
|
oldGradeScale: 20,
|
||||||
|
newGradeScale: 20,
|
||||||
occurredOn: new DateTimeImmutable(),
|
occurredOn: new DateTimeImmutable(),
|
||||||
));
|
));
|
||||||
|
|
||||||
@@ -126,8 +134,16 @@ final class RecalculerMoyennesOnEvaluationModifieeHandlerTest extends TestCase
|
|||||||
|
|
||||||
($this->handler)(new EvaluationModifiee(
|
($this->handler)(new EvaluationModifiee(
|
||||||
evaluationId: $evaluationId,
|
evaluationId: $evaluationId,
|
||||||
title: 'Titre modifié',
|
oldTitle: 'Test Evaluation',
|
||||||
evaluationDate: new DateTimeImmutable('2026-02-15'),
|
newTitle: 'Titre modifié',
|
||||||
|
oldDescription: null,
|
||||||
|
newDescription: null,
|
||||||
|
oldCoefficient: 1.0,
|
||||||
|
newCoefficient: 1.0,
|
||||||
|
oldEvaluationDate: new DateTimeImmutable('2026-02-15'),
|
||||||
|
newEvaluationDate: new DateTimeImmutable('2026-02-15'),
|
||||||
|
oldGradeScale: 20,
|
||||||
|
newGradeScale: 20,
|
||||||
occurredOn: new DateTimeImmutable(),
|
occurredOn: new DateTimeImmutable(),
|
||||||
));
|
));
|
||||||
|
|
||||||
@@ -163,8 +179,16 @@ final class RecalculerMoyennesOnEvaluationModifieeHandlerTest extends TestCase
|
|||||||
|
|
||||||
($this->handler)(new EvaluationModifiee(
|
($this->handler)(new EvaluationModifiee(
|
||||||
evaluationId: $evaluationId,
|
evaluationId: $evaluationId,
|
||||||
title: 'Titre modifié',
|
oldTitle: 'Test Evaluation',
|
||||||
evaluationDate: new DateTimeImmutable('2026-02-15'),
|
newTitle: 'Titre modifié',
|
||||||
|
oldDescription: null,
|
||||||
|
newDescription: null,
|
||||||
|
oldCoefficient: 1.0,
|
||||||
|
newCoefficient: 1.0,
|
||||||
|
oldEvaluationDate: new DateTimeImmutable('2026-02-15'),
|
||||||
|
newEvaluationDate: new DateTimeImmutable('2026-02-15'),
|
||||||
|
oldGradeScale: 20,
|
||||||
|
newGradeScale: 20,
|
||||||
occurredOn: new DateTimeImmutable(),
|
occurredOn: new DateTimeImmutable(),
|
||||||
));
|
));
|
||||||
|
|
||||||
@@ -188,8 +212,16 @@ final class RecalculerMoyennesOnEvaluationModifieeHandlerTest extends TestCase
|
|||||||
|
|
||||||
($this->handler)(new EvaluationModifiee(
|
($this->handler)(new EvaluationModifiee(
|
||||||
evaluationId: $evaluationId,
|
evaluationId: $evaluationId,
|
||||||
title: 'Titre modifié',
|
oldTitle: 'Test Evaluation',
|
||||||
evaluationDate: new DateTimeImmutable('2026-02-15'),
|
newTitle: 'Titre modifié',
|
||||||
|
oldDescription: null,
|
||||||
|
newDescription: null,
|
||||||
|
oldCoefficient: 1.0,
|
||||||
|
newCoefficient: 1.0,
|
||||||
|
oldEvaluationDate: new DateTimeImmutable('2026-02-15'),
|
||||||
|
newEvaluationDate: new DateTimeImmutable('2026-02-15'),
|
||||||
|
oldGradeScale: 20,
|
||||||
|
newGradeScale: 20,
|
||||||
occurredOn: new DateTimeImmutable(),
|
occurredOn: new DateTimeImmutable(),
|
||||||
));
|
));
|
||||||
|
|
||||||
@@ -219,8 +251,16 @@ final class RecalculerMoyennesOnEvaluationModifieeHandlerTest extends TestCase
|
|||||||
|
|
||||||
($this->handler)(new EvaluationModifiee(
|
($this->handler)(new EvaluationModifiee(
|
||||||
evaluationId: $evaluationId,
|
evaluationId: $evaluationId,
|
||||||
title: 'Titre modifié',
|
oldTitle: 'Test Evaluation',
|
||||||
evaluationDate: new DateTimeImmutable('2026-02-15'),
|
newTitle: 'Titre modifié',
|
||||||
|
oldDescription: null,
|
||||||
|
newDescription: null,
|
||||||
|
oldCoefficient: 1.0,
|
||||||
|
newCoefficient: 1.0,
|
||||||
|
oldEvaluationDate: new DateTimeImmutable('2026-02-15'),
|
||||||
|
newEvaluationDate: new DateTimeImmutable('2026-02-15'),
|
||||||
|
oldGradeScale: 20,
|
||||||
|
newGradeScale: 20,
|
||||||
occurredOn: new DateTimeImmutable(),
|
occurredOn: new DateTimeImmutable(),
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ final class RecalculerMoyennesOnNoteModifieeHandlerTest extends TestCase
|
|||||||
$event = new NoteModifiee(
|
$event = new NoteModifiee(
|
||||||
gradeId: $grade1->id,
|
gradeId: $grade1->id,
|
||||||
evaluationId: (string) $evaluation->id,
|
evaluationId: (string) $evaluation->id,
|
||||||
|
studentId: self::STUDENT_ID,
|
||||||
oldValue: 14.0,
|
oldValue: 14.0,
|
||||||
newValue: 18.0,
|
newValue: 18.0,
|
||||||
oldStatus: 'graded',
|
oldStatus: 'graded',
|
||||||
@@ -174,6 +175,7 @@ final class RecalculerMoyennesOnNoteModifieeHandlerTest extends TestCase
|
|||||||
$event = new NoteModifiee(
|
$event = new NoteModifiee(
|
||||||
gradeId: $grade->id,
|
gradeId: $grade->id,
|
||||||
evaluationId: (string) $evaluation->id,
|
evaluationId: (string) $evaluation->id,
|
||||||
|
studentId: self::STUDENT_ID,
|
||||||
oldValue: 10.0,
|
oldValue: 10.0,
|
||||||
newValue: 14.0,
|
newValue: 14.0,
|
||||||
oldStatus: 'graded',
|
oldStatus: 'graded',
|
||||||
@@ -233,6 +235,7 @@ final class RecalculerMoyennesOnNoteModifieeHandlerTest extends TestCase
|
|||||||
$event = new NoteModifiee(
|
$event = new NoteModifiee(
|
||||||
gradeId: GradeId::generate(),
|
gradeId: GradeId::generate(),
|
||||||
evaluationId: (string) $evaluation->id,
|
evaluationId: (string) $evaluation->id,
|
||||||
|
studentId: self::STUDENT_ID,
|
||||||
oldValue: 10.0,
|
oldValue: 10.0,
|
||||||
newValue: 14.0,
|
newValue: 14.0,
|
||||||
oldStatus: 'graded',
|
oldStatus: 'graded',
|
||||||
@@ -264,6 +267,7 @@ final class RecalculerMoyennesOnNoteModifieeHandlerTest extends TestCase
|
|||||||
$event = new NoteModifiee(
|
$event = new NoteModifiee(
|
||||||
gradeId: GradeId::generate(),
|
gradeId: GradeId::generate(),
|
||||||
evaluationId: (string) $unknownEvalId,
|
evaluationId: (string) $unknownEvalId,
|
||||||
|
studentId: self::STUDENT_ID,
|
||||||
oldValue: 10.0,
|
oldValue: 10.0,
|
||||||
newValue: 14.0,
|
newValue: 14.0,
|
||||||
oldStatus: 'graded',
|
oldStatus: 'graded',
|
||||||
|
|||||||
@@ -335,10 +335,12 @@ test.describe('Student Grade Consultation (Story 6.6)', () => {
|
|||||||
await loginAsStudent(page);
|
await loginAsStudent(page);
|
||||||
await page.goto(`${ALPHA_URL}/dashboard/student-grades`);
|
await page.goto(`${ALPHA_URL}/dashboard/student-grades`);
|
||||||
|
|
||||||
await expect(page.locator('.grade-card').first()).toBeVisible({ timeout: 15000 });
|
// Le badge disparaît 3 s après le chargement (markGradesSeen). On attend
|
||||||
|
// directement le badge dans un seul expect pour éviter une fenêtre de
|
||||||
// Badges should be visible on new grades
|
// course entre « grade-card visible » et « badge encore affiché ».
|
||||||
await expect(page.locator('.badge-new').first()).toBeVisible({ timeout: 5000 });
|
await expect(page.locator('.grade-card .badge-new').first()).toBeVisible({
|
||||||
|
timeout: 15000,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user