feat(core) : expose read-only audit-logs api with dbal provider and pagination
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user