Files
michaelschiemer/tests/Unit/Framework/ExceptionHandling/ExceptionContextIntegrationTest.php
Michael Schiemer 95147ff23e refactor(deployment): Remove WireGuard VPN dependency and restore public service access
Remove WireGuard integration from production deployment to simplify infrastructure:
- Remove docker-compose-direct-access.yml (VPN-bound services)
- Remove VPN-only middlewares from Grafana, Prometheus, Portainer
- Remove WireGuard middleware definitions from Traefik
- Remove WireGuard IPs (10.8.0.0/24) from Traefik forwarded headers

All monitoring services now publicly accessible via subdomains:
- grafana.michaelschiemer.de (with Grafana native auth)
- prometheus.michaelschiemer.de (with Basic Auth)
- portainer.michaelschiemer.de (with Portainer native auth)

All services use Let's Encrypt SSL certificates via Traefik.
2025-11-05 12:48:25 +01:00

224 lines
7.5 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('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')
);
}