- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
241 lines
9.1 KiB
PHP
241 lines
9.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Unit\Framework\Audit;
|
|
|
|
use App\Framework\Audit\InMemoryAuditLogger;
|
|
use App\Framework\Audit\ValueObjects\AuditableAction;
|
|
use App\Framework\Audit\ValueObjects\AuditEntry;
|
|
use App\Framework\Audit\ValueObjects\AuditQuery;
|
|
use App\Framework\DateTime\SystemClock;
|
|
use App\Framework\Http\IpAddress;
|
|
|
|
describe('AuditLogger', function () {
|
|
beforeEach(function () {
|
|
$this->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']);
|
|
});
|
|
});
|