feat(core) : add doctrine audit listener and mark core entities auditable
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
<?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\Test\KernelTestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class AuditListenerTest extends KernelTestCase
|
||||
{
|
||||
private EntityManagerInterface $em;
|
||||
private Connection $auditConnection;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
$container = self::getContainer();
|
||||
$this->em = $container->get(EntityManagerInterface::class);
|
||||
$this->auditConnection = $container->get('doctrine.dbal.audit_connection');
|
||||
// Clean slate for deterministic assertions: these tests are not wrapped
|
||||
// in a rolled-back transaction, so remove any leftover rows from a
|
||||
// previous run before each test.
|
||||
$this->em->getConnection()->executeStatement("DELETE FROM \"user\" WHERE username LIKE 'audit\\_%'");
|
||||
$this->auditConnection->executeStatement('DELETE FROM audit_log');
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
parent::tearDown();
|
||||
unset($this->em, $this->auditConnection);
|
||||
}
|
||||
|
||||
public function testCreateUserIsAudited(): void
|
||||
{
|
||||
$user = $this->makeUser('audit_create_user');
|
||||
$this->em->persist($user);
|
||||
$this->em->flush();
|
||||
|
||||
$rows = $this->fetchLogs('core.User', (string) $user->getId());
|
||||
self::assertCount(1, $rows);
|
||||
self::assertSame('create', $rows[0]['action']);
|
||||
$changes = json_decode((string) $rows[0]['changes'], true);
|
||||
self::assertArrayHasKey('username', $changes);
|
||||
self::assertArrayNotHasKey('password', $changes, 'password must be excluded via #[AuditIgnore]');
|
||||
self::assertArrayNotHasKey('apiToken', $changes, 'apiToken must be excluded via #[AuditIgnore]');
|
||||
}
|
||||
|
||||
public function testUpdateUserIsAuditedWithDiff(): void
|
||||
{
|
||||
$user = $this->makeUser('audit_update_user');
|
||||
$this->em->persist($user);
|
||||
$this->em->flush();
|
||||
$this->auditConnection->executeStatement('DELETE FROM audit_log');
|
||||
|
||||
$user->setFirstName('Changed');
|
||||
$this->em->flush();
|
||||
|
||||
$rows = $this->fetchLogs('core.User', (string) $user->getId());
|
||||
self::assertCount(1, $rows);
|
||||
self::assertSame('update', $rows[0]['action']);
|
||||
$changes = json_decode((string) $rows[0]['changes'], true);
|
||||
self::assertArrayHasKey('firstName', $changes);
|
||||
self::assertSame('Changed', $changes['firstName']['new']);
|
||||
}
|
||||
|
||||
public function testDeleteUserIsAudited(): void
|
||||
{
|
||||
$user = $this->makeUser('audit_delete_user');
|
||||
$this->em->persist($user);
|
||||
$this->em->flush();
|
||||
$id = (string) $user->getId();
|
||||
$this->auditConnection->executeStatement('DELETE FROM audit_log');
|
||||
|
||||
$this->em->remove($user);
|
||||
$this->em->flush();
|
||||
|
||||
$rows = $this->fetchLogs('core.User', $id);
|
||||
self::assertCount(1, $rows);
|
||||
self::assertSame('delete', $rows[0]['action']);
|
||||
}
|
||||
|
||||
private function makeUser(string $username): User
|
||||
{
|
||||
$user = new User();
|
||||
$user->setUsername($username);
|
||||
$user->setPassword('hashed-secret');
|
||||
$user->setRoles(['ROLE_USER']);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
private function fetchLogs(string $entityType, string $entityId): array
|
||||
{
|
||||
return $this->auditConnection->fetchAllAssociative(
|
||||
'SELECT action, changes FROM audit_log WHERE entity_type = :t AND entity_id = :id ORDER BY performed_at ASC',
|
||||
['t' => $entityType, 'id' => $entityId],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user