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 */ private static function decodePayload(mixed $raw): array { self::assertIsString($raw); /** @var array $decoded */ $decoded = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); return $decoded; } }