Files
michaelschiemer/tests/Integration/ExceptionAuditIntegrationTest.php
Michael Schiemer 36ef2a1e2c
Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
fix: Gitea Traefik routing and connection pool optimization
- Remove middleware reference from Gitea Traefik labels (caused routing issues)
- Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s)
- Add explicit service reference in Traefik labels
- Fix intermittent 504 timeouts by improving PostgreSQL connection handling

Fixes Gitea unreachability via git.michaelschiemer.de
2025-11-09 14:46:15 +01:00

254 lines
9.0 KiB
PHP

<?php
declare(strict_types=1);
use App\Framework\Audit\AuditLogger;
use App\Framework\Audit\InMemoryAuditLogger;
use App\Framework\Audit\ValueObjects\AuditEntry;
use App\Framework\DateTime\Clock;
use App\Framework\ExceptionHandling\Audit\ExceptionAuditLogger;
use App\Framework\ExceptionHandling\Context\ExceptionContextData;
use App\Framework\ExceptionHandling\Context\ExceptionContextProvider;
use App\Framework\ExceptionHandling\ErrorKernel;
use App\Framework\ExceptionHandling\Factory\ExceptionFactory;
use App\Framework\ExceptionHandling\Reporter\Reporter;
use App\Framework\ExceptionHandling\Scope\ErrorScope;
use App\Framework\ExceptionHandling\Renderers\ErrorRendererFactory;
describe('Exception Audit Integration', function () {
beforeEach(function () {
$this->auditLogger = new InMemoryAuditLogger();
$this->clock = new class implements Clock {
private DateTimeImmutable $now;
public function __construct()
{
$this->now = new DateTimeImmutable('2024-01-01 12:00:00');
}
public function now(): DateTimeImmutable
{
return $this->now;
}
public function fromTimestamp(\App\Framework\Core\ValueObjects\Timestamp $timestamp): DateTimeImmutable
{
return DateTimeImmutable::createFromFormat('U', (string) $timestamp->value);
}
public function fromString(string $dateTime, ?string $format = null): DateTimeImmutable
{
return DateTimeImmutable::createFromFormat($format ?? 'Y-m-d H:i:s', $dateTime);
}
public function today(): DateTimeImmutable
{
return $this->now;
}
public function yesterday(): DateTimeImmutable
{
return $this->now->modify('-1 day');
}
public function tomorrow(): DateTimeImmutable
{
return $this->now->modify('+1 day');
}
public function time(): \App\Framework\Core\ValueObjects\Timestamp
{
return new \App\Framework\Core\ValueObjects\Timestamp($this->now->getTimestamp());
}
};
$this->contextProvider = new ExceptionContextProvider();
$this->exceptionAuditLogger = new ExceptionAuditLogger(
$this->auditLogger,
$this->clock,
$this->contextProvider
);
$this->errorScope = new ErrorScope();
$this->factory = new ExceptionFactory($this->contextProvider, $this->errorScope);
});
it('automatically logs auditable exceptions through ErrorKernel', function () {
$reporter = new class implements Reporter {
public function report(\Throwable $exception): void
{
// Mock reporter
}
};
$rendererFactory = new ErrorRendererFactory(false);
$errorKernel = new ErrorKernel(
rendererFactory: $rendererFactory,
reporter: $reporter,
contextProvider: $this->contextProvider,
auditLogger: $this->exceptionAuditLogger
);
// Create auditable exception
$exception = $this->factory->createAuditable(
RuntimeException::class,
'User creation failed',
'user.create',
'UserService',
['user_id' => '123']
);
// Handle exception through ErrorKernel
$errorKernel->handle($exception);
// Check that audit entry was created
$entries = getAllAuditEntries($this->auditLogger);
expect($entries)->toHaveCount(1);
$entry = $entries[0];
expect($entry->success)->toBeFalse();
expect($entry->errorMessage)->toBe('User creation failed');
expect($entry->entityType)->toBe('userservice');
});
it('does not log non-auditable exceptions through ErrorKernel', function () {
$reporter = new class implements Reporter {
public function report(\Throwable $exception): void
{
// Mock reporter
}
};
$rendererFactory = new ErrorRendererFactory(false);
$errorKernel = new ErrorKernel(
rendererFactory: $rendererFactory,
reporter: $reporter,
contextProvider: $this->contextProvider,
auditLogger: $this->exceptionAuditLogger
);
// Create non-auditable exception
$exception = $this->factory->createNonAuditable(
RuntimeException::class,
'Validation error'
);
// Handle exception through ErrorKernel
$errorKernel->handle($exception);
// Check that no audit entry was created
$entries = getAllAuditEntries($this->auditLogger);
expect($entries)->toHaveCount(0);
});
it('logs exception with audit level through factory', function () {
$reporter = new class implements Reporter {
public function report(\Throwable $exception): void
{
// Mock reporter
}
};
$rendererFactory = new ErrorRendererFactory(false);
$errorKernel = new ErrorKernel(
rendererFactory: $rendererFactory,
reporter: $reporter,
contextProvider: $this->contextProvider,
auditLogger: $this->exceptionAuditLogger
);
// Create exception with audit level
$exception = $this->factory->withAuditLevel(
RuntimeException::class,
'Warning: Resource limit reached',
'WARNING'
);
// Handle exception
$errorKernel->handle($exception);
// Check that audit entry was created with level
$entries = getAllAuditEntries($this->auditLogger);
expect($entries)->toHaveCount(1);
$entry = $entries[0];
$context = $this->contextProvider->get($exception);
expect($context)->not->toBeNull();
expect($context->auditLevel)->toBe('WARNING');
});
it('preserves all context information in audit entry', function () {
$reporter = new class implements Reporter {
public function report(\Throwable $exception): void
{
// Mock reporter
}
};
$rendererFactory = new ErrorRendererFactory(false);
$errorKernel = new ErrorKernel(
rendererFactory: $rendererFactory,
reporter: $reporter,
contextProvider: $this->contextProvider,
auditLogger: $this->exceptionAuditLogger
);
// Create exception with full context (using Value Objects)
$context = ExceptionContextData::forOperation('payment.process', 'PaymentGateway')
->addData(['order_id' => 'order-123', 'amount' => 5000])
->withUserId('user-456')
->withRequestId('req-789') // String for backward compatibility (RequestId needs secret)
->withSessionId(\App\Framework\Http\Session\SessionId::fromString('session-abc'))
->withClientIp(\App\Framework\Http\IpAddress::from('192.168.1.100'))
->withUserAgent(\App\Framework\UserAgent\UserAgent::fromString('Mozilla/5.0'))
->withTags('payment', 'external_api', 'critical')
->withAuditable(true);
$exception = $this->factory->create(
RuntimeException::class,
'Payment processing failed',
$context
);
// Handle exception
$errorKernel->handle($exception);
// Check audit entry
$entries = getAllAuditEntries($this->auditLogger);
expect($entries)->toHaveCount(1);
$entry = $entries[0];
expect($entry->userId)->toBe('user-456');
expect($entry->entityId)->toBe('order-123');
expect($entry->ipAddress)->not->toBeNull();
expect((string) $entry->ipAddress)->toBe('192.168.1.100');
expect($entry->userAgent)->not->toBeNull();
expect($entry->userAgent->value)->toBe('Mozilla/5.0');
expect($entry->metadata)->toHaveKey('request_id');
expect($entry->metadata['request_id'])->toBe('req-789');
expect($entry->metadata)->toHaveKey('session_id');
expect($entry->metadata['session_id'])->toBe('session-abc');
expect($entry->metadata)->toHaveKey('tags');
expect($entry->metadata['tags'])->toContain('payment', 'external_api', 'critical');
});
});
/**
* Helper to get all audit entries from InMemoryAuditLogger
*
* @return array<AuditEntry>
*/
function getAllAuditEntries(AuditLogger $logger): array
{
if ($logger instanceof InMemoryAuditLogger) {
// Use reflection to access private entries property
$reflection = new ReflectionClass($logger);
$property = $reflection->getProperty('entries');
$property->setAccessible(true);
return array_values($property->getValue($logger));
}
// For other implementations, query all entries
$query = new \App\Framework\Audit\ValueObjects\AuditQuery();
return $logger->query($query);
}