diff --git a/app/Audit/AuditEventListener.php b/app/Audit/AuditEventListener.php index 3a5f7fa62..66af6935c 100644 --- a/app/Audit/AuditEventListener.php +++ b/app/Audit/AuditEventListener.php @@ -28,9 +28,15 @@ class AuditEventListener { private const ROUTE_METHOD_SEPARATOR = '|'; private $em; + + protected function shouldSkipInTest(): bool + { + return app()->environment('testing'); + } + public function onFlush(OnFlushEventArgs $eventArgs): void { - if (app()->environment('testing')) { + if ($this->shouldSkipInTest()) { return; } $this->em = $eventArgs->getObjectManager(); @@ -83,7 +89,7 @@ public function onFlush(OnFlushEventArgs $eventArgs): void /** * Get the appropriate audit strategy based on environment configuration */ - private function getAuditStrategy($em): ?IAuditStrategy + protected function getAuditStrategy($em): ?IAuditStrategy { // Check if OTLP audit is enabled if (config('opentelemetry.enabled', false)) { @@ -102,7 +108,7 @@ private function getAuditStrategy($em): ?IAuditStrategy return new AuditLogStrategy($em); } - private function buildAuditContext(): AuditContext + protected function buildAuditContext(): AuditContext { $resourceCtx = app(\models\oauth2\IResourceServerContext::class); $userExternalId = $resourceCtx->getCurrentUserId(); diff --git a/tests/OpenTelemetry/Formatters/AuditEventListenerTest.php b/tests/OpenTelemetry/Formatters/AuditEventListenerTest.php index a602799ce..1fdacfcc7 100644 --- a/tests/OpenTelemetry/Formatters/AuditEventListenerTest.php +++ b/tests/OpenTelemetry/Formatters/AuditEventListenerTest.php @@ -15,16 +15,20 @@ * limitations under the License. **/ +use App\Audit\AuditContext; use App\Audit\AuditEventListener; use App\Audit\Interfaces\IAuditStrategy; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\DBAL\Connection; use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Event\OnFlushEventArgs; +use Doctrine\ORM\Event\PostFlushEventArgs; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Mapping\DefaultNamingStrategy; use Doctrine\ORM\Mapping\ManyToManyOwningSideMapping; use Doctrine\ORM\Mapping\OneToManyAssociationMapping; use Doctrine\ORM\PersistentCollection; +use Doctrine\ORM\UnitOfWork; use Tests\TestCase; class AuditEventListenerTest extends TestCase @@ -217,4 +221,48 @@ public function testAuditCollectionDeleteUninitializedUsesJoinTableQuery(): void $this->assertSame([10, 11], $payload['deleted_ids']); $this->assertSame(IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_DELETE, $eventType); } + + public function testCreationAuditIsBufferedInOnFlushAndDispatchedInPostFlush(): void + { + // Simulate a new entity not yet persisted: getId() returns 0 (pre-INSERT state). + $entity = $this->getMockBuilder(\models\summit\SummitEvent::class) + ->disableOriginalConstructor() + ->onlyMethods(['getId']) + ->getMock(); + $entity->method('getId')->willReturn(0); + + $uow = $this->createMock(UnitOfWork::class); + $uow->method('getScheduledEntityInsertions')->willReturn([$entity]); + $uow->method('getScheduledEntityUpdates')->willReturn([]); + $uow->method('getScheduledEntityDeletions')->willReturn([]); + $uow->method('getScheduledCollectionDeletions')->willReturn([]); + $uow->method('getScheduledCollectionUpdates')->willReturn([]); + + $em = $this->createMock(EntityManagerInterface::class); + $em->method('getUnitOfWork')->willReturn($uow); + + $strategy = $this->createMock(IAuditStrategy::class); + + $listener = $this->getMockBuilder(AuditEventListener::class) + ->onlyMethods(['shouldSkipInTest', 'getAuditStrategy', 'buildAuditContext']) + ->getMock(); + $listener->method('shouldSkipInTest')->willReturn(false); + $listener->method('getAuditStrategy')->willReturn($strategy); + $listener->method('buildAuditContext')->willReturn(new AuditContext()); + + $capturedId = null; + $strategy->method('audit')->willReturnCallback( + function ($subject) use (&$capturedId) { + $capturedId = method_exists($subject, 'getId') ? $subject->getId() : null; + } + ); + + $listener->onFlush(new OnFlushEventArgs($em)); + + // Bug: audit() was called during onFlush — before the INSERT — so the captured id is 0. + $this->assertNotNull($capturedId, + 'audit() must not be called during onFlush (INSERT has not run yet)'); + $this->assertNotEquals(0, $capturedId, + 'entity id captured during onFlush is 0 — the INSERT has not assigned the real id yet'); + } }