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
254 lines
8.8 KiB
PHP
254 lines
8.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\ExceptionHandling\Context\ExceptionContextData;
|
|
use App\Framework\ExceptionHandling\Context\ExceptionContextProvider;
|
|
use App\Framework\ExceptionHandling\Factory\ExceptionFactory;
|
|
use App\Framework\ExceptionHandling\Scope\ErrorScope;
|
|
use App\Framework\ExceptionHandling\Scope\ErrorScopeContext;
|
|
|
|
describe('Exception Context Integration', function () {
|
|
beforeEach(function () {
|
|
$this->contextProvider = ExceptionContextProvider::instance();
|
|
$this->errorScope = new ErrorScope();
|
|
$this->factory = new ExceptionFactory($this->contextProvider, $this->errorScope);
|
|
|
|
// Clear any existing contexts
|
|
$this->contextProvider->clear();
|
|
});
|
|
|
|
it('creates slim exception with external context via WeakMap', function () {
|
|
$context = ExceptionContextData::forOperation('user.create', 'UserService')
|
|
->addData(['user_id' => '123', 'email' => 'test@example.com']);
|
|
|
|
$exception = $this->factory->create(
|
|
RuntimeException::class,
|
|
'User creation failed',
|
|
$context
|
|
);
|
|
|
|
// Exception is slim (pure PHP)
|
|
expect($exception)->toBeInstanceOf(RuntimeException::class);
|
|
expect($exception->getMessage())->toBe('User creation failed');
|
|
|
|
// Context is stored externally
|
|
$storedContext = $this->contextProvider->get($exception);
|
|
expect($storedContext)->not->toBeNull();
|
|
expect($storedContext->operation)->toBe('user.create');
|
|
expect($storedContext->component)->toBe('UserService');
|
|
expect($storedContext->data)->toBe([
|
|
'user_id' => '123',
|
|
'email' => 'test@example.com'
|
|
]);
|
|
});
|
|
|
|
it('automatically enriches context from error scope', function () {
|
|
// Enter HTTP scope
|
|
$scopeContext = ErrorScopeContext::http(
|
|
request: createMockRequest(),
|
|
operation: 'api.request',
|
|
component: 'ApiController'
|
|
)->withUserId('user-456');
|
|
|
|
$this->errorScope->enter($scopeContext);
|
|
|
|
// Create exception without explicit context
|
|
$exception = $this->factory->create(
|
|
RuntimeException::class,
|
|
'API request failed'
|
|
);
|
|
|
|
// Context is enriched from scope
|
|
$storedContext = $this->contextProvider->get($exception);
|
|
expect($storedContext)->not->toBeNull();
|
|
expect($storedContext->userId)->toBe('user-456');
|
|
expect($storedContext->metadata)->toHaveKey('scope_type');
|
|
expect($storedContext->metadata['scope_type'])->toBe('http');
|
|
});
|
|
|
|
it('supports WeakMap automatic garbage collection', function () {
|
|
$exception = new RuntimeException('Test exception');
|
|
$context = ExceptionContextData::forOperation('test.operation');
|
|
|
|
$this->contextProvider->attach($exception, $context);
|
|
|
|
// Context exists
|
|
expect($this->contextProvider->has($exception))->toBeTrue();
|
|
|
|
// Unset exception reference
|
|
unset($exception);
|
|
|
|
// Force garbage collection
|
|
gc_collect_cycles();
|
|
|
|
// WeakMap automatically cleaned up (we can't directly test this,
|
|
// but stats should reflect fewer contexts after GC)
|
|
$stats = $this->contextProvider->getStats();
|
|
expect($stats)->toHaveKey('total_contexts');
|
|
});
|
|
|
|
it('enhances existing exception with additional context', function () {
|
|
$exception = new RuntimeException('Original error');
|
|
$originalContext = ExceptionContextData::forOperation('operation.1')
|
|
->addData(['step' => 1]);
|
|
|
|
$this->contextProvider->attach($exception, $originalContext);
|
|
|
|
// Enhance with additional context
|
|
$additionalContext = ExceptionContextData::empty()
|
|
->addData(['step' => 2, 'error_code' => 'E001']);
|
|
|
|
$this->factory->enhance($exception, $additionalContext);
|
|
|
|
// Context is merged
|
|
$storedContext = $this->contextProvider->get($exception);
|
|
expect($storedContext->data)->toBe([
|
|
'step' => 2, // Overwrites
|
|
'error_code' => 'E001' // Adds
|
|
]);
|
|
});
|
|
|
|
it('supports fiber-aware error scopes', function () {
|
|
// Main scope
|
|
$mainScope = ErrorScopeContext::generic(
|
|
'main',
|
|
'main.operation'
|
|
);
|
|
$this->errorScope->enter($mainScope);
|
|
|
|
// Create fiber scope
|
|
$fiber = new Fiber(function () {
|
|
$fiberScope = ErrorScopeContext::generic(
|
|
'fiber',
|
|
'fiber.operation'
|
|
);
|
|
$this->errorScope->enter($fiberScope);
|
|
|
|
$exception = $this->factory->create(
|
|
RuntimeException::class,
|
|
'Fiber error'
|
|
);
|
|
|
|
// Context from fiber scope
|
|
$context = $this->contextProvider->get($exception);
|
|
expect($context->operation)->toBe('fiber.operation');
|
|
|
|
$this->errorScope->exit();
|
|
});
|
|
|
|
$fiber->start();
|
|
|
|
// Main scope still active
|
|
$exception = $this->factory->create(
|
|
RuntimeException::class,
|
|
'Main error'
|
|
);
|
|
$context = $this->contextProvider->get($exception);
|
|
expect($context->operation)->toBe('main.operation');
|
|
});
|
|
|
|
it('creates exception with convenience factory methods', function () {
|
|
// forOperation
|
|
$exception1 = $this->factory->forOperation(
|
|
InvalidArgumentException::class,
|
|
'Invalid user data',
|
|
'user.validate',
|
|
'UserValidator',
|
|
['email' => 'invalid']
|
|
);
|
|
|
|
$context1 = $this->contextProvider->get($exception1);
|
|
expect($context1->operation)->toBe('user.validate');
|
|
expect($context1->component)->toBe('UserValidator');
|
|
expect($context1->data['email'])->toBe('invalid');
|
|
|
|
// withData
|
|
$exception2 = $this->factory->withData(
|
|
RuntimeException::class,
|
|
'Database error',
|
|
['query' => 'SELECT * FROM users']
|
|
);
|
|
|
|
$context2 = $this->contextProvider->get($exception2);
|
|
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(
|
|
request: createMockRequest(),
|
|
operation: 'outer.operation'
|
|
);
|
|
$this->errorScope->enter($outerScope);
|
|
|
|
// Inner scope
|
|
$innerScope = ErrorScopeContext::generic(
|
|
'inner',
|
|
'inner.operation'
|
|
);
|
|
$this->errorScope->enter($innerScope);
|
|
|
|
// Exception gets inner scope context
|
|
$exception = $this->factory->create(
|
|
RuntimeException::class,
|
|
'Inner error'
|
|
);
|
|
|
|
$context = $this->contextProvider->get($exception);
|
|
expect($context->operation)->toBe('inner.operation');
|
|
|
|
// Exit inner scope
|
|
$this->errorScope->exit();
|
|
|
|
// New exception gets outer scope context
|
|
$exception2 = $this->factory->create(
|
|
RuntimeException::class,
|
|
'Outer error'
|
|
);
|
|
|
|
$context2 = $this->contextProvider->get($exception2);
|
|
expect($context2->operation)->toBe('outer.operation');
|
|
});
|
|
});
|
|
|
|
// Helper function to create mock request
|
|
function createMockRequest(): \App\Framework\Http\HttpRequest
|
|
{
|
|
return new \App\Framework\Http\HttpRequest(
|
|
method: \App\Framework\Http\Method::GET,
|
|
path: '/test',
|
|
id: new \App\Framework\Http\RequestId('test-secret')
|
|
);
|
|
}
|