feat: Audit trail pour actions sensibles
Story 1.7 - Implémente un système complet d'audit trail pour tracer toutes les actions sensibles (authentification, modifications de données, exports) avec immuabilité garantie par PostgreSQL. Fonctionnalités principales: - Table audit_log append-only avec contraintes PostgreSQL (RULE) - AuditLogger centralisé avec injection automatique du contexte - Correlation ID pour traçabilité distribuée (HTTP + async) - Handlers pour événements d'authentification - Commande d'archivage des logs anciens - Pas de PII dans les logs (emails/IPs hashés) Infrastructure: - Middlewares Messenger pour propagation du Correlation ID - HTTP middleware pour génération/propagation du Correlation ID - Support multi-tenant avec TenantResolver
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Shared\Infrastructure\Audit;
|
||||
|
||||
use App\Shared\Infrastructure\Audit\AuditLogEntry;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
/**
|
||||
* @see Story 1.7 - T9: Requetes d'investigation
|
||||
*/
|
||||
final class AuditLogEntryTest extends TestCase
|
||||
{
|
||||
public function testFromDatabaseRow(): void
|
||||
{
|
||||
$id = Uuid::uuid4()->toString();
|
||||
$aggregateId = Uuid::uuid4()->toString();
|
||||
$row = [
|
||||
'id' => $id,
|
||||
'aggregate_type' => 'User',
|
||||
'aggregate_id' => $aggregateId,
|
||||
'event_type' => 'ConnexionReussie',
|
||||
'payload' => '{"email_hash":"abc123","result":"success"}',
|
||||
'metadata' => '{"tenant_id":"tenant-1","user_id":"user-1","correlation_id":"corr-1"}',
|
||||
'occurred_at' => '2026-02-03T10:30:00+00:00',
|
||||
'sequence_number' => '42',
|
||||
];
|
||||
|
||||
$entry = AuditLogEntry::fromDatabaseRow($row);
|
||||
|
||||
$this->assertEquals($id, $entry->id->toString());
|
||||
$this->assertSame('User', $entry->aggregateType);
|
||||
$this->assertEquals($aggregateId, $entry->aggregateId?->toString());
|
||||
$this->assertSame('ConnexionReussie', $entry->eventType);
|
||||
$this->assertSame(['email_hash' => 'abc123', 'result' => 'success'], $entry->payload);
|
||||
$this->assertSame('tenant-1', $entry->tenantId());
|
||||
$this->assertSame('user-1', $entry->userId());
|
||||
$this->assertSame('corr-1', $entry->correlationId());
|
||||
$this->assertSame(42, $entry->sequenceNumber);
|
||||
}
|
||||
|
||||
public function testFromDatabaseRowWithNullAggregateId(): void
|
||||
{
|
||||
$row = [
|
||||
'id' => Uuid::uuid4()->toString(),
|
||||
'aggregate_type' => 'Export',
|
||||
'aggregate_id' => null,
|
||||
'event_type' => 'ExportGenerated',
|
||||
'payload' => '{}',
|
||||
'metadata' => '{}',
|
||||
'occurred_at' => '2026-02-03T10:30:00+00:00',
|
||||
'sequence_number' => '1',
|
||||
];
|
||||
|
||||
$entry = AuditLogEntry::fromDatabaseRow($row);
|
||||
|
||||
$this->assertNull($entry->aggregateId);
|
||||
}
|
||||
|
||||
public function testMetadataAccessorsReturnNullWhenMissing(): void
|
||||
{
|
||||
$row = [
|
||||
'id' => Uuid::uuid4()->toString(),
|
||||
'aggregate_type' => 'User',
|
||||
'aggregate_id' => null,
|
||||
'event_type' => 'Test',
|
||||
'payload' => '{}',
|
||||
'metadata' => '{}',
|
||||
'occurred_at' => '2026-02-03T10:30:00+00:00',
|
||||
'sequence_number' => '1',
|
||||
];
|
||||
|
||||
$entry = AuditLogEntry::fromDatabaseRow($row);
|
||||
|
||||
$this->assertNull($entry->tenantId());
|
||||
$this->assertNull($entry->userId());
|
||||
$this->assertNull($entry->correlationId());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user