logger = new InMemoryAuditLogger(); $this->clock = new SystemClock(); }); it('logs audit entries', function () { $entry = AuditEntry::create( clock: $this->clock, action: AuditableAction::CREATE, entityType: 'User', entityId: '123', userId: 'admin', metadata: ['name' => 'John Doe'] ); $this->logger->log($entry); $found = $this->logger->find($entry->id); expect($found)->not->toBeNull(); expect($found->action)->toBe(AuditableAction::CREATE); expect($found->entityType)->toBe('User'); expect($found->entityId)->toBe('123'); expect($found->userId)->toBe('admin'); expect($found->metadata)->toBe(['name' => 'John Doe']); }); it('logs failed entries', function () { $entry = AuditEntry::failed( clock: $this->clock, action: AuditableAction::DELETE, entityType: 'User', entityId: '123', errorMessage: 'Permission denied', userId: 'user' ); $this->logger->log($entry); $found = $this->logger->find($entry->id); expect($found->success)->toBeFalse(); expect($found->errorMessage)->toBe('Permission denied'); }); it('queries by action', function () { $this->logger->log(AuditEntry::create($this->clock, AuditableAction::CREATE, 'User')); $this->logger->log(AuditEntry::create($this->clock, AuditableAction::UPDATE, 'User')); $this->logger->log(AuditEntry::create($this->clock, AuditableAction::CREATE, 'Post')); $query = AuditQuery::forAction(AuditableAction::CREATE); $results = $this->logger->query($query); expect($results)->toHaveCount(2); expect($results[0]->action)->toBe(AuditableAction::CREATE); expect($results[1]->action)->toBe(AuditableAction::CREATE); }); it('queries by entity type', function () { $this->logger->log(AuditEntry::create($this->clock, AuditableAction::CREATE, 'User', '1')); $this->logger->log(AuditEntry::create($this->clock, AuditableAction::CREATE, 'User', '2')); $this->logger->log(AuditEntry::create($this->clock, AuditableAction::CREATE, 'Post', '1')); $query = AuditQuery::forEntity('User'); $results = $this->logger->query($query); expect($results)->toHaveCount(2); expect($results[0]->entityType)->toBe('User'); }); it('queries by entity id', function () { $this->logger->log(AuditEntry::create($this->clock, AuditableAction::CREATE, 'User', '123')); $this->logger->log(AuditEntry::create($this->clock, AuditableAction::UPDATE, 'User', '123')); $this->logger->log(AuditEntry::create($this->clock, AuditableAction::CREATE, 'User', '456')); $query = AuditQuery::forEntity('User', '123'); $results = $this->logger->query($query); expect($results)->toHaveCount(2); expect($results[0]->entityId)->toBe('123'); }); it('queries by user id', function () { $this->logger->log(AuditEntry::create($this->clock, AuditableAction::CREATE, 'User', userId: 'admin')); $this->logger->log(AuditEntry::create($this->clock, AuditableAction::UPDATE, 'User', userId: 'admin')); $this->logger->log(AuditEntry::create($this->clock, AuditableAction::CREATE, 'User', userId: 'user')); $query = AuditQuery::forUser('admin'); $results = $this->logger->query($query); expect($results)->toHaveCount(2); expect($results[0]->userId)->toBe('admin'); }); it('queries failed entries only', function () { $this->logger->log(AuditEntry::create($this->clock, AuditableAction::CREATE, 'User')); $this->logger->log(AuditEntry::failed($this->clock, AuditableAction::DELETE, 'User', errorMessage: 'Error')); $this->logger->log(AuditEntry::failed($this->clock, AuditableAction::UPDATE, 'User', errorMessage: 'Error')); $query = AuditQuery::failedOnly(); $results = $this->logger->query($query); expect($results)->toHaveCount(2); expect($results[0]->success)->toBeFalse(); expect($results[1]->success)->toBeFalse(); }); it('queries successful entries only', function () { $this->logger->log(AuditEntry::create($this->clock, AuditableAction::CREATE, 'User')); $this->logger->log(AuditEntry::create($this->clock, AuditableAction::UPDATE, 'User')); $this->logger->log(AuditEntry::failed($this->clock, AuditableAction::DELETE, 'User', errorMessage: 'Error')); $query = AuditQuery::successfulOnly(); $results = $this->logger->query($query); expect($results)->toHaveCount(2); expect($results[0]->success)->toBeTrue(); expect($results[1]->success)->toBeTrue(); }); it('queries by date range', function () { $now = new \DateTimeImmutable(); $yesterday = $now->modify('-1 day'); $tomorrow = $now->modify('+1 day'); $this->logger->log(AuditEntry::create($this->clock, AuditableAction::CREATE, 'User')); $query = AuditQuery::inDateRange($yesterday, $tomorrow); $results = $this->logger->query($query); expect($results)->toHaveCount(1); }); it('combines multiple filters', function () { $this->logger->log(AuditEntry::create($this->clock, AuditableAction::CREATE, 'User', '1', 'admin')); $this->logger->log(AuditEntry::create($this->clock, AuditableAction::UPDATE, 'User', '1', 'admin')); $this->logger->log(AuditEntry::create($this->clock, AuditableAction::CREATE, 'User', '2', 'user')); $query = AuditQuery::forEntity('User', '1') ->withAction(AuditableAction::CREATE) ->withUserId('admin'); $results = $this->logger->query($query); expect($results)->toHaveCount(1); expect($results[0]->action)->toBe(AuditableAction::CREATE); expect($results[0]->entityId)->toBe('1'); expect($results[0]->userId)->toBe('admin'); }); it('paginates results', function () { for ($i = 1; $i <= 10; $i++) { $this->logger->log(AuditEntry::create($this->clock, AuditableAction::CREATE, 'User', (string) $i)); } $query = AuditQuery::all()->withLimit(5)->withOffset(0); $page1 = $this->logger->query($query); $query = AuditQuery::all()->withLimit(5)->withOffset(5); $page2 = $this->logger->query($query); expect($page1)->toHaveCount(5); expect($page2)->toHaveCount(5); }); it('counts entries', function () { $this->logger->log(AuditEntry::create($this->clock, AuditableAction::CREATE, 'User')); $this->logger->log(AuditEntry::create($this->clock, AuditableAction::CREATE, 'User')); $this->logger->log(AuditEntry::create($this->clock, AuditableAction::UPDATE, 'User')); $query = AuditQuery::forAction(AuditableAction::CREATE); $count = $this->logger->count($query); expect($count)->toBe(2); }); it('purges old entries', function () { $this->logger->log(AuditEntry::create($this->clock, AuditableAction::CREATE, 'User')); $this->logger->log(AuditEntry::create($this->clock, AuditableAction::UPDATE, 'User')); $future = new \DateTimeImmutable('+1 day'); $deleted = $this->logger->purgeOlderThan($future); expect($deleted)->toBe(2); expect($this->logger->count(AuditQuery::all()))->toBe(0); }); it('stores ip address', function () { $entry = AuditEntry::create( clock: $this->clock, action: AuditableAction::LOGIN, entityType: 'Session', ipAddress: IpAddress::from('192.168.1.1') ); $this->logger->log($entry); $found = $this->logger->find($entry->id); expect($found->ipAddress)->not->toBeNull(); expect((string) $found->ipAddress)->toBe('192.168.1.1'); }); it('converts to array', function () { $entry = AuditEntry::create( clock: $this->clock, action: AuditableAction::CREATE, entityType: 'User', entityId: '123', userId: 'admin', metadata: ['test' => 'value'] ); $array = $entry->toArray(); expect($array)->toHaveKey('id'); expect($array)->toHaveKey('action'); expect($array)->toHaveKey('entity_type'); expect($array)->toHaveKey('entity_id'); expect($array)->toHaveKey('timestamp'); expect($array)->toHaveKey('user_id'); expect($array)->toHaveKey('metadata'); expect($array['action'])->toBe('crud.create'); expect($array['entity_type'])->toBe('User'); expect($array['metadata'])->toBe(['test' => 'value']); }); });