fix: Gitea Traefik routing and connection pool optimization
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
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
- 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
This commit is contained in:
@@ -0,0 +1,301 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\Audit\AuditLogger;
|
||||
use App\Framework\Audit\InMemoryAuditLogger;
|
||||
use App\Framework\Audit\ValueObjects\AuditEntry;
|
||||
use App\Framework\Audit\ValueObjects\AuditableAction;
|
||||
use App\Framework\DateTime\Clock;
|
||||
use App\Framework\ExceptionHandling\Audit\ExceptionAuditLogger;
|
||||
use App\Framework\ExceptionHandling\Context\ExceptionContextData;
|
||||
use App\Framework\ExceptionHandling\Context\ExceptionContextProvider;
|
||||
|
||||
describe('ExceptionAuditLogger', 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::createFromTimestamp($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
|
||||
);
|
||||
});
|
||||
|
||||
it('logs auditable exception as audit entry', function () {
|
||||
$exception = new RuntimeException('User not found');
|
||||
$context = ExceptionContextData::forOperation('user.lookup', 'UserRepository')
|
||||
->addData(['user_id' => '123'])
|
||||
->withUserId('user-456')
|
||||
->withAuditable(true);
|
||||
|
||||
$this->contextProvider->attach($exception, $context);
|
||||
|
||||
$this->exceptionAuditLogger->logIfAuditable($exception);
|
||||
|
||||
// Check that audit entry was created
|
||||
$entries = $this->getAllAuditEntries($this->auditLogger);
|
||||
expect($entries)->toHaveCount(1);
|
||||
|
||||
$entry = $entries[0];
|
||||
expect($entry->success)->toBeFalse();
|
||||
expect($entry->errorMessage)->toBe('User not found');
|
||||
expect($entry->userId)->toBe('user-456');
|
||||
expect($entry->entityType)->toBe('userrepository');
|
||||
expect($entry->action)->toBeInstanceOf(AuditableAction::class);
|
||||
});
|
||||
|
||||
it('does not log non-auditable exception', function () {
|
||||
$exception = new RuntimeException('Validation error');
|
||||
$context = ExceptionContextData::forOperation('validation.check')
|
||||
->withAuditable(false);
|
||||
|
||||
$this->contextProvider->attach($exception, $context);
|
||||
|
||||
$this->exceptionAuditLogger->logIfAuditable($exception);
|
||||
|
||||
// Check that no audit entry was created
|
||||
$entries = $this->getAllAuditEntries($this->auditLogger);
|
||||
expect($entries)->toHaveCount(0);
|
||||
});
|
||||
|
||||
it('logs exception without context as auditable by default', function () {
|
||||
$exception = new RuntimeException('Generic error');
|
||||
|
||||
$this->exceptionAuditLogger->logIfAuditable($exception);
|
||||
|
||||
// Check that audit entry was created (default: auditable)
|
||||
$entries = $this->getAllAuditEntries($this->auditLogger);
|
||||
expect($entries)->toHaveCount(1);
|
||||
});
|
||||
|
||||
it('includes context data in audit entry metadata', function () {
|
||||
$exception = new RuntimeException('Operation failed');
|
||||
$context = ExceptionContextData::forOperation('order.process', 'OrderService')
|
||||
->addData(['order_id' => 'order-789', 'amount' => 1000])
|
||||
->withUserId('user-123')
|
||||
->withRequestId('req-456')
|
||||
->withTags('payment', 'external_api');
|
||||
|
||||
$this->contextProvider->attach($exception, $context);
|
||||
|
||||
$this->exceptionAuditLogger->logIfAuditable($exception);
|
||||
|
||||
$entries = $this->getAllAuditEntries($this->auditLogger);
|
||||
$entry = $entries[0];
|
||||
|
||||
expect($entry->metadata)->toHaveKey('operation');
|
||||
expect($entry->metadata['operation'])->toBe('order.process');
|
||||
expect($entry->metadata)->toHaveKey('component');
|
||||
expect($entry->metadata['component'])->toBe('OrderService');
|
||||
expect($entry->metadata)->toHaveKey('context_data');
|
||||
expect($entry->metadata['context_data']['order_id'])->toBe('order-789');
|
||||
expect($entry->metadata)->toHaveKey('tags');
|
||||
expect($entry->metadata['tags'])->toBe(['payment', 'external_api']);
|
||||
});
|
||||
|
||||
it('determines audit action from operation name', function () {
|
||||
$testCases = [
|
||||
['operation' => 'user.create', 'expected' => AuditableAction::CREATE],
|
||||
['operation' => 'order.update', 'expected' => AuditableAction::UPDATE],
|
||||
['operation' => 'product.delete', 'expected' => AuditableAction::DELETE],
|
||||
['operation' => 'data.read', 'expected' => AuditableAction::READ],
|
||||
];
|
||||
|
||||
foreach ($testCases as $testCase) {
|
||||
$exception = new RuntimeException('Test');
|
||||
$context = ExceptionContextData::forOperation($testCase['operation'])
|
||||
->withAuditable(true);
|
||||
|
||||
$this->contextProvider->attach($exception, $context);
|
||||
$this->exceptionAuditLogger->logIfAuditable($exception);
|
||||
|
||||
$entries = $this->getAllAuditEntries($this->auditLogger);
|
||||
$entry = end($entries);
|
||||
|
||||
expect($entry->action)->toBe($testCase['expected']);
|
||||
}
|
||||
});
|
||||
|
||||
it('extracts entity ID from context data', function () {
|
||||
$exception = new RuntimeException('Entity not found');
|
||||
$context = ExceptionContextData::forOperation('entity.get')
|
||||
->addData(['entity_id' => 'entity-123'])
|
||||
->withAuditable(true);
|
||||
|
||||
$this->contextProvider->attach($exception, $context);
|
||||
|
||||
$this->exceptionAuditLogger->logIfAuditable($exception);
|
||||
|
||||
$entries = $this->getAllAuditEntries($this->auditLogger);
|
||||
$entry = $entries[0];
|
||||
|
||||
expect($entry->entityId)->toBe('entity-123');
|
||||
});
|
||||
|
||||
it('handles IP address and user agent from context with Value Objects', function () {
|
||||
$exception = new RuntimeException('Security violation');
|
||||
$context = ExceptionContextData::forOperation('security.check')
|
||||
->withClientIp(\App\Framework\Http\IpAddress::from('192.168.1.1'))
|
||||
->withUserAgent(\App\Framework\UserAgent\UserAgent::fromString('Mozilla/5.0'))
|
||||
->withAuditable(true);
|
||||
|
||||
$this->contextProvider->attach($exception, $context);
|
||||
|
||||
$this->exceptionAuditLogger->logIfAuditable($exception);
|
||||
|
||||
$entries = $this->getAllAuditEntries($this->auditLogger);
|
||||
$entry = $entries[0];
|
||||
|
||||
expect($entry->ipAddress)->not->toBeNull();
|
||||
expect((string) $entry->ipAddress)->toBe('192.168.1.1');
|
||||
expect($entry->userAgent)->not->toBeNull();
|
||||
expect($entry->userAgent->value)->toBe('Mozilla/5.0');
|
||||
});
|
||||
|
||||
it('handles IP address and user agent from context with strings (backward compatibility)', function () {
|
||||
$exception = new RuntimeException('Security violation');
|
||||
$context = ExceptionContextData::forOperation('security.check')
|
||||
->withClientIp('192.168.1.1')
|
||||
->withUserAgent('Mozilla/5.0')
|
||||
->withAuditable(true);
|
||||
|
||||
$this->contextProvider->attach($exception, $context);
|
||||
|
||||
$this->exceptionAuditLogger->logIfAuditable($exception);
|
||||
|
||||
$entries = $this->getAllAuditEntries($this->auditLogger);
|
||||
$entry = $entries[0];
|
||||
|
||||
expect($entry->ipAddress)->not->toBeNull();
|
||||
expect((string) $entry->ipAddress)->toBe('192.168.1.1');
|
||||
expect($entry->userAgent)->not->toBeNull();
|
||||
expect($entry->userAgent->value)->toBe('Mozilla/5.0');
|
||||
});
|
||||
|
||||
it('does not throw when audit logging fails', function () {
|
||||
$failingAuditLogger = new class implements AuditLogger {
|
||||
public function log(AuditEntry $entry): void
|
||||
{
|
||||
throw new RuntimeException('Audit logging failed');
|
||||
}
|
||||
|
||||
public function find(\App\Framework\Audit\ValueObjects\AuditId $id): ?AuditEntry
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function query(\App\Framework\Audit\ValueObjects\AuditQuery $query): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function count(\App\Framework\Audit\ValueObjects\AuditQuery $query): int
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function purgeOlderThan(DateTimeImmutable $date): int
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
$logger = new ExceptionAuditLogger(
|
||||
$failingAuditLogger,
|
||||
$this->clock,
|
||||
$this->contextProvider
|
||||
);
|
||||
|
||||
$exception = new RuntimeException('Test error');
|
||||
$context = ExceptionContextData::forOperation('test')
|
||||
->withAuditable(true);
|
||||
|
||||
$this->contextProvider->attach($exception, $context);
|
||||
|
||||
// Should not throw
|
||||
expect(fn() => $logger->logIfAuditable($exception))->not->toThrow();
|
||||
});
|
||||
|
||||
it('includes previous exception in metadata', function () {
|
||||
$previous = new InvalidArgumentException('Invalid input');
|
||||
$exception = new RuntimeException('Operation failed', 0, $previous);
|
||||
$context = ExceptionContextData::forOperation('operation.execute')
|
||||
->withAuditable(true);
|
||||
|
||||
$this->contextProvider->attach($exception, $context);
|
||||
|
||||
$this->exceptionAuditLogger->logIfAuditable($exception);
|
||||
|
||||
$entries = $this->getAllAuditEntries($this->auditLogger);
|
||||
$entry = $entries[0];
|
||||
|
||||
expect($entry->metadata)->toHaveKey('previous_exception');
|
||||
expect($entry->metadata['previous_exception']['class'])->toBe(InvalidArgumentException::class);
|
||||
expect($entry->metadata['previous_exception']['message'])->toBe('Invalid input');
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
@@ -174,6 +174,36 @@ describe('Exception Context Integration', function () {
|
||||
expect($context2->data['query'])->toBe('SELECT * FROM users');
|
||||
});
|
||||
|
||||
it('serializes Value Objects to strings in toArray()', function () {
|
||||
$context = ExceptionContextData::forOperation('test.operation')
|
||||
->withClientIp(\App\Framework\Http\IpAddress::from('192.168.1.1'))
|
||||
->withUserAgent(\App\Framework\UserAgent\UserAgent::fromString('Mozilla/5.0'))
|
||||
->withSessionId(\App\Framework\Http\Session\SessionId::fromString('session-12345678901234567890123456789012'))
|
||||
->withRequestId('req-123');
|
||||
|
||||
$array = $context->toArray();
|
||||
|
||||
expect($array['client_ip'])->toBe('192.168.1.1');
|
||||
expect($array['user_agent'])->toBe('Mozilla/5.0');
|
||||
expect($array['session_id'])->toBe('session-12345678901234567890123456789012');
|
||||
expect($array['request_id'])->toBe('req-123');
|
||||
});
|
||||
|
||||
it('serializes strings to strings in toArray() (backward compatibility)', function () {
|
||||
$context = ExceptionContextData::forOperation('test.operation')
|
||||
->withClientIp('192.168.1.1')
|
||||
->withUserAgent('Mozilla/5.0')
|
||||
->withSessionId('session-123')
|
||||
->withRequestId('req-123');
|
||||
|
||||
$array = $context->toArray();
|
||||
|
||||
expect($array['client_ip'])->toBe('192.168.1.1');
|
||||
expect($array['user_agent'])->toBe('Mozilla/5.0');
|
||||
expect($array['session_id'])->toBe('session-123');
|
||||
expect($array['request_id'])->toBe('req-123');
|
||||
});
|
||||
|
||||
it('handles nested error scopes correctly', function () {
|
||||
// Outer scope
|
||||
$outerScope = ErrorScopeContext::http(
|
||||
|
||||
Reference in New Issue
Block a user