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,219 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Shared\Infrastructure\Audit;
|
||||
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use App\Shared\Infrastructure\Audit\AuditLogEntry;
|
||||
use App\Shared\Infrastructure\Audit\AuditLogRepository;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Query\QueryBuilder;
|
||||
use Doctrine\DBAL\Result;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
/**
|
||||
* @see Story 1.7 - T9: Requetes d'investigation
|
||||
*/
|
||||
final class AuditLogRepositoryTest extends TestCase
|
||||
{
|
||||
private Connection&MockObject $connection;
|
||||
private AuditLogRepository $repository;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->connection = $this->createMock(Connection::class);
|
||||
$this->repository = new AuditLogRepository($this->connection);
|
||||
}
|
||||
|
||||
public function testFindByUserReturnsAuditLogEntries(): void
|
||||
{
|
||||
$userId = Uuid::uuid4();
|
||||
$tenantId = TenantId::generate();
|
||||
|
||||
$rows = [
|
||||
$this->createRowData(),
|
||||
$this->createRowData(),
|
||||
];
|
||||
|
||||
$queryBuilder = $this->createQueryBuilderMock($rows);
|
||||
$this->connection->method('createQueryBuilder')->willReturn($queryBuilder);
|
||||
|
||||
$result = $this->repository->findByUser($userId, $tenantId);
|
||||
|
||||
$this->assertCount(2, $result);
|
||||
$this->assertContainsOnlyInstancesOf(AuditLogEntry::class, $result);
|
||||
}
|
||||
|
||||
public function testFindByUserAppliesFilters(): void
|
||||
{
|
||||
$userId = Uuid::uuid4();
|
||||
$tenantId = TenantId::generate();
|
||||
$from = new DateTimeImmutable('2026-01-01');
|
||||
$to = new DateTimeImmutable('2026-02-01');
|
||||
|
||||
$queryBuilder = $this->createMock(QueryBuilder::class);
|
||||
$queryBuilder->method('select')->willReturnSelf();
|
||||
$queryBuilder->method('from')->willReturnSelf();
|
||||
$queryBuilder->method('where')->willReturnSelf();
|
||||
$queryBuilder->method('orderBy')->willReturnSelf();
|
||||
|
||||
// Verify andWhere is called for each filter
|
||||
$queryBuilder->expects($this->atLeast(4))
|
||||
->method('andWhere')
|
||||
->willReturnSelf();
|
||||
|
||||
// Verify setParameter is called with expected parameters
|
||||
$capturedParams = [];
|
||||
$queryBuilder->method('setParameter')
|
||||
->willReturnCallback(static function (string $key, mixed $value) use ($queryBuilder, &$capturedParams) {
|
||||
$capturedParams[$key] = $value;
|
||||
|
||||
return $queryBuilder;
|
||||
});
|
||||
|
||||
// Verify pagination
|
||||
$queryBuilder->expects($this->once())
|
||||
->method('setMaxResults')
|
||||
->with(50)
|
||||
->willReturnSelf();
|
||||
|
||||
$queryBuilder->expects($this->once())
|
||||
->method('setFirstResult')
|
||||
->with(10)
|
||||
->willReturnSelf();
|
||||
|
||||
$result = $this->createMock(Result::class);
|
||||
$result->method('fetchAllAssociative')->willReturn([]);
|
||||
$queryBuilder->method('executeQuery')->willReturn($result);
|
||||
|
||||
$this->connection->method('createQueryBuilder')->willReturn($queryBuilder);
|
||||
|
||||
$this->repository->findByUser(
|
||||
$userId,
|
||||
$tenantId,
|
||||
$from,
|
||||
$to,
|
||||
'ConnexionReussie',
|
||||
50,
|
||||
10,
|
||||
);
|
||||
|
||||
// Verify the parameters were captured
|
||||
$this->assertArrayHasKey('user_id', $capturedParams);
|
||||
$this->assertArrayHasKey('tenant_id', $capturedParams);
|
||||
$this->assertArrayHasKey('from', $capturedParams);
|
||||
$this->assertArrayHasKey('to', $capturedParams);
|
||||
$this->assertArrayHasKey('event_type', $capturedParams);
|
||||
$this->assertSame('ConnexionReussie', $capturedParams['event_type']);
|
||||
}
|
||||
|
||||
public function testFindByResourceReturnsResults(): void
|
||||
{
|
||||
$aggregateId = Uuid::uuid4();
|
||||
$tenantId = TenantId::generate();
|
||||
|
||||
$rows = [$this->createRowData()];
|
||||
|
||||
$queryBuilder = $this->createQueryBuilderMock($rows);
|
||||
$this->connection->method('createQueryBuilder')->willReturn($queryBuilder);
|
||||
|
||||
$result = $this->repository->findByResource('Note', $aggregateId, $tenantId);
|
||||
|
||||
$this->assertCount(1, $result);
|
||||
}
|
||||
|
||||
public function testFindByCorrelationIdReturnsResults(): void
|
||||
{
|
||||
$correlationId = Uuid::uuid4()->toString();
|
||||
$tenantId = TenantId::generate();
|
||||
|
||||
$rows = [$this->createRowData()];
|
||||
|
||||
$queryBuilder = $this->createQueryBuilderMock($rows);
|
||||
$this->connection->method('createQueryBuilder')->willReturn($queryBuilder);
|
||||
|
||||
$result = $this->repository->findByCorrelationId($correlationId, $tenantId);
|
||||
|
||||
$this->assertCount(1, $result);
|
||||
}
|
||||
|
||||
public function testSearchWithAllFilters(): void
|
||||
{
|
||||
$tenantId = TenantId::generate();
|
||||
$from = new DateTimeImmutable('2026-01-01');
|
||||
$to = new DateTimeImmutable('2026-02-01');
|
||||
|
||||
$rows = [$this->createRowData()];
|
||||
|
||||
$queryBuilder = $this->createQueryBuilderMock($rows);
|
||||
$this->connection->method('createQueryBuilder')->willReturn($queryBuilder);
|
||||
|
||||
$result = $this->repository->search(
|
||||
$tenantId,
|
||||
$from,
|
||||
$to,
|
||||
'ConnexionReussie',
|
||||
'User',
|
||||
100,
|
||||
0,
|
||||
);
|
||||
|
||||
$this->assertCount(1, $result);
|
||||
}
|
||||
|
||||
public function testEmptyResultReturnsEmptyArray(): void
|
||||
{
|
||||
$userId = Uuid::uuid4();
|
||||
$tenantId = TenantId::generate();
|
||||
|
||||
$queryBuilder = $this->createQueryBuilderMock([]);
|
||||
$this->connection->method('createQueryBuilder')->willReturn($queryBuilder);
|
||||
|
||||
$result = $this->repository->findByUser($userId, $tenantId);
|
||||
|
||||
$this->assertSame([], $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function createRowData(): array
|
||||
{
|
||||
return [
|
||||
'id' => Uuid::uuid4()->toString(),
|
||||
'aggregate_type' => 'User',
|
||||
'aggregate_id' => Uuid::uuid4()->toString(),
|
||||
'event_type' => 'ConnexionReussie',
|
||||
'payload' => '{"email_hash":"abc123"}',
|
||||
'metadata' => '{"tenant_id":"tenant-1","user_id":"user-1"}',
|
||||
'occurred_at' => '2026-02-03T10:30:00+00:00',
|
||||
'sequence_number' => '1',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array<string, mixed>> $rows
|
||||
*/
|
||||
private function createQueryBuilderMock(array $rows): QueryBuilder&MockObject
|
||||
{
|
||||
$queryBuilder = $this->createMock(QueryBuilder::class);
|
||||
$queryBuilder->method('select')->willReturnSelf();
|
||||
$queryBuilder->method('from')->willReturnSelf();
|
||||
$queryBuilder->method('where')->willReturnSelf();
|
||||
$queryBuilder->method('andWhere')->willReturnSelf();
|
||||
$queryBuilder->method('setParameter')->willReturnSelf();
|
||||
$queryBuilder->method('orderBy')->willReturnSelf();
|
||||
$queryBuilder->method('setMaxResults')->willReturnSelf();
|
||||
$queryBuilder->method('setFirstResult')->willReturnSelf();
|
||||
|
||||
$result = $this->createMock(Result::class);
|
||||
$result->method('fetchAllAssociative')->willReturn($rows);
|
||||
$queryBuilder->method('executeQuery')->willReturn($result);
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user