feat(core) : expose read-only audit-logs api with dbal provider and pagination

This commit is contained in:
Matthieu
2026-06-19 21:09:55 +02:00
parent 8c3699a9b0
commit 90b8ca15cd
9 changed files with 623 additions and 0 deletions
@@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
namespace App\Tests\Functional\Module\Core;
use App\Module\Core\Domain\Entity\User;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\Uid\Uuid;
/**
* @internal
*/
final class AuditLogApiTest extends WebTestCase
{
public function testGetCollectionRequiresAuthentication(): void
{
$client = self::createClient();
$this->seedAuditLog();
$client->request('GET', '/api/audit-logs');
self::assertResponseStatusCodeSame(401);
}
public function testUserWithoutPermissionIsForbidden(): void
{
$client = self::createClient();
$this->seedAuditLog();
$this->loginUser($client, 'alice');
$client->request('GET', '/api/audit-logs');
self::assertResponseStatusCodeSame(403);
}
public function testAdminCanListAuditLogs(): void
{
$client = self::createClient();
$this->seedAuditLog();
$this->loginUser($client, 'admin');
$client->request('GET', '/api/audit-logs');
self::assertResponseIsSuccessful();
$data = json_decode($client->getResponse()->getContent(), true);
self::assertArrayHasKey('member', $data);
self::assertArrayHasKey('totalItems', $data);
self::assertGreaterThanOrEqual(3, $data['totalItems']);
}
public function testFilterByActionReturnsOnlyMatchingEntries(): void
{
$client = self::createClient();
$this->seedAuditLog();
$this->loginUser($client, 'admin');
$client->request('GET', '/api/audit-logs?action=update');
self::assertResponseIsSuccessful();
$data = json_decode($client->getResponse()->getContent(), true);
self::assertNotEmpty($data['member']);
foreach ($data['member'] as $entry) {
self::assertSame('update', $entry['action']);
}
}
public function testFilterByEntityType(): void
{
$client = self::createClient();
$this->seedAuditLog();
$this->loginUser($client, 'admin');
$client->request('GET', '/api/audit-logs?entity_type=core.User');
self::assertResponseIsSuccessful();
$data = json_decode($client->getResponse()->getContent(), true);
self::assertNotEmpty($data['member']);
foreach ($data['member'] as $entry) {
self::assertSame('core.User', $entry['entityType']);
}
}
public function testInvalidActionFilterIsRejected(): void
{
$client = self::createClient();
$this->seedAuditLog();
$this->loginUser($client, 'admin');
$client->request('GET', '/api/audit-logs?action=bogus');
self::assertResponseStatusCodeSame(400);
}
/**
* Insert deterministic audit rows directly through the audit connection.
*
* The table audit_log has no ORM entity (written via raw DBAL), so we
* clean and seed it via the dedicated connection. These tests are not
* wrapped in a rolled-back transaction, hence the upfront DELETE.
*/
private function seedAuditLog(): void
{
/** @var Connection $audit */
$audit = self::getContainer()->get('doctrine.dbal.audit_connection');
$audit->executeStatement('DELETE FROM audit_log');
$rows = [
['core.User', 'create', '{"username":"seed-create"}'],
['core.User', 'update', '{"firstName":{"old":"a","new":"b"}}'],
['core.Role', 'delete', '{}'],
];
$performedAt = '2026-04-22 10:00:00';
foreach ($rows as $i => [$entityType, $action, $changes]) {
$audit->insert('audit_log', [
'id' => Uuid::v7()->toRfc4122(),
'entity_type' => $entityType,
'entity_id' => (string) ($i + 1),
'action' => $action,
'changes' => $changes,
'performed_by' => 'admin',
'performed_at' => $performedAt,
'ip_address' => '127.0.0.1',
'request_id' => 'req-'.$i,
]);
}
}
private function loginUser(KernelBrowser $client, string $username): void
{
$em = self::getContainer()->get(EntityManagerInterface::class);
$user = $em->getRepository(User::class)->findOneBy(['username' => $username]);
self::assertInstanceOf(User::class, $user);
$client->loginUser($user);
}
}
@@ -32,4 +32,12 @@ final class CoreModuleTest extends TestCase
self::assertArrayHasKey('label', $permission);
}
}
public function testPermissionsExposeAuditLogView(): void
{
$codes = array_column(CoreModule::permissions(), 'code');
self::assertCount(6, $codes);
self::assertContains('core.audit_log.view', $codes);
}
}