createHttpResponse($exception, null, isDebugMode: false); expect($response->status)->toBe(Status::INTERNAL_SERVER_ERROR); expect($response->headers->getFirst('Content-Type'))->toBe('application/json'); $body = json_decode($response->body, true); expect($body['error']['message'])->toBe('An error occurred while processing your request.'); expect($body['error']['type'])->toBe('ServerError'); expect($body['error']['code'])->toBe(500); }); it('creates JSON API error response with debug mode', function () { $errorKernel = new ErrorKernel(); $exception = new RuntimeException('Database connection failed', 500); $response = $errorKernel->createHttpResponse($exception, null, isDebugMode: true); expect($response->status)->toBe(Status::INTERNAL_SERVER_ERROR); $body = json_decode($response->body, true); expect($body['error']['message'])->toBe('Database connection failed'); expect($body['error']['type'])->toBe(RuntimeException::class); expect($body['error'])->toHaveKey('file'); expect($body['error'])->toHaveKey('line'); expect($body['error'])->toHaveKey('trace'); }); it('creates JSON API error response with WeakMap context', function () { $errorKernel = new ErrorKernel(); $contextProvider = new ExceptionContextProvider(); $exception = new RuntimeException('User operation failed', 500); // Enrich exception with context $contextData = new ExceptionContextData( operation: 'user.create', component: 'UserService', requestId: 'req-12345', occurredAt: new \DateTimeImmutable(), metadata: ['user_email' => 'test@example.com'] ); $contextProvider->attach($exception, $contextData); $response = $errorKernel->createHttpResponse($exception, $contextProvider, isDebugMode: true); $body = json_decode($response->body, true); expect($body['context']['operation'])->toBe('user.create'); expect($body['context']['component'])->toBe('UserService'); expect($body['context']['request_id'])->toBe('req-12345'); expect($body['context'])->toHaveKey('occurred_at'); }); }); describe('ResponseErrorRenderer', function () { beforeEach(function () { // Mock $_SERVER for API detection $_SERVER['HTTP_ACCEPT'] = 'application/json'; $_SERVER['REQUEST_URI'] = '/api/test'; }); afterEach(function () { // Cleanup unset($_SERVER['HTTP_ACCEPT'], $_SERVER['REQUEST_URI']); }); it('detects API requests correctly', function () { $renderer = new ResponseErrorRenderer(isDebugMode: false); $exception = new RuntimeException('Test error'); $response = $renderer->createResponse($exception, null); expect($response->headers->getFirst('Content-Type'))->toBe('application/json'); }); it('creates HTML response for non-API requests', function () { // Override to non-API request $_SERVER['HTTP_ACCEPT'] = 'text/html'; $_SERVER['REQUEST_URI'] = '/web/page'; $renderer = new ResponseErrorRenderer(isDebugMode: false); $exception = new RuntimeException('Page error'); $response = $renderer->createResponse($exception, null); expect($response->headers->getFirst('Content-Type'))->toBe('text/html; charset=utf-8'); expect($response->body)->toContain(''); expect($response->body)->toContain('An error occurred while processing your request.'); }); it('includes debug info in HTML response when enabled', function () { $_SERVER['HTTP_ACCEPT'] = 'text/html'; $_SERVER['REQUEST_URI'] = '/web/page'; $renderer = new ResponseErrorRenderer(isDebugMode: true); $contextProvider = new ExceptionContextProvider(); $exception = new RuntimeException('Debug test error'); $contextData = new ExceptionContextData( operation: 'page.render', component: 'PageController', requestId: 'req-67890', occurredAt: new \DateTimeImmutable() ); $contextProvider->attach($exception, $contextData); $response = $renderer->createResponse($exception, $contextProvider); expect($response->body)->toContain('Debug Information'); expect($response->body)->toContain('page.render'); expect($response->body)->toContain('PageController'); expect($response->body)->toContain('req-67890'); }); it('maps exception types to HTTP status codes correctly', function () { $renderer = new ResponseErrorRenderer(); // InvalidArgumentException → 400 $exception = new \InvalidArgumentException('Invalid input'); $response = $renderer->createResponse($exception, null); expect($response->status)->toBe(Status::BAD_REQUEST); // RuntimeException → 500 $exception = new RuntimeException('Runtime error'); $response = $renderer->createResponse($exception, null); expect($response->status)->toBe(Status::INTERNAL_SERVER_ERROR); // Custom code in valid range $exception = new RuntimeException('Not found', 404); $response = $renderer->createResponse($exception, null); expect($response->status)->toBe(Status::NOT_FOUND); }); }); describe('ExceptionContextProvider WeakMap functionality', function () { it('stores and retrieves exception context', function () { $contextProvider = new ExceptionContextProvider(); $exception = new RuntimeException('Test exception'); $contextData = new ExceptionContextData( operation: 'test.operation', component: 'TestComponent', requestId: 'test-123', occurredAt: new \DateTimeImmutable() ); $contextProvider->attach($exception, $contextData); $retrieved = $contextProvider->get($exception); expect($retrieved)->not->toBeNull(); expect($retrieved->operation)->toBe('test.operation'); expect($retrieved->component)->toBe('TestComponent'); expect($retrieved->requestId)->toBe('test-123'); }); it('returns null for exceptions without context', function () { $contextProvider = new ExceptionContextProvider(); $exception = new RuntimeException('No context'); $retrieved = $contextProvider->get($exception); expect($retrieved)->toBeNull(); }); it('uses WeakMap semantics - context garbage collected with exception', function () { $contextProvider = new ExceptionContextProvider(); $exception = new RuntimeException('Will be garbage collected'); $contextData = new ExceptionContextData( operation: 'test', component: 'Test', requestId: 'test', occurredAt: new \DateTimeImmutable() ); $contextProvider->attach($exception, $contextData); // Verify context exists expect($contextProvider->get($exception))->not->toBeNull(); // Remove all references to exception unset($exception); // Context is automatically garbage collected with exception // (WeakMap behavior - cannot directly test GC, but semantically correct) expect(true)->toBeTrue(); // Placeholder for semantic correctness }); }); describe('Context enrichment with boundary metadata', function () { it('enriches exception context with boundary metadata', function () { $contextProvider = new ExceptionContextProvider(); $exception = new RuntimeException('Boundary error'); // Simulate ErrorBoundary enrichment $initialContext = new ExceptionContextData( operation: 'user.operation', component: 'UserService', requestId: 'req-abc', occurredAt: new \DateTimeImmutable() ); $contextProvider->attach($exception, $initialContext); // ErrorBoundary enriches with boundary metadata $existingContext = $contextProvider->get($exception); $enrichedContext = $existingContext->addMetadata([ 'error_boundary' => 'user_boundary', 'boundary_failure' => true, 'fallback_executed' => true ]); $contextProvider->attach($exception, $enrichedContext); // Retrieve and verify enriched context $finalContext = $contextProvider->get($exception); expect($finalContext->metadata['error_boundary'])->toBe('user_boundary'); expect($finalContext->metadata['boundary_failure'])->toBeTrue(); expect($finalContext->metadata['fallback_executed'])->toBeTrue(); }); it('preserves original context when enriching with HTTP fields', function () { $contextProvider = new ExceptionContextProvider(); $exception = new RuntimeException('HTTP error'); $initialContext = new ExceptionContextData( operation: 'api.request', component: 'ApiController', requestId: 'req-http-123', occurredAt: new \DateTimeImmutable() ); $contextProvider->attach($exception, $initialContext); // Enrich with HTTP-specific fields $existingContext = $contextProvider->get($exception); $enrichedContext = $existingContext->addMetadata([ 'client_ip' => '192.168.1.100', 'user_agent' => 'Mozilla/5.0', 'http_method' => 'POST', 'request_uri' => '/api/users' ]); $contextProvider->attach($exception, $enrichedContext); // Verify both original and enriched data $finalContext = $contextProvider->get($exception); expect($finalContext->operation)->toBe('api.request'); expect($finalContext->component)->toBe('ApiController'); expect($finalContext->metadata['client_ip'])->toBe('192.168.1.100'); expect($finalContext->metadata['user_agent'])->toBe('Mozilla/5.0'); }); }); describe('End-to-end integration scenario', function () { beforeEach(function () { // Mock $_SERVER for API detection $_SERVER['HTTP_ACCEPT'] = 'application/json'; $_SERVER['REQUEST_URI'] = '/api/test'; }); afterEach(function () { // Cleanup unset($_SERVER['HTTP_ACCEPT'], $_SERVER['REQUEST_URI']); }); it('demonstrates full exception handling flow with context enrichment', function () { // Setup $errorKernel = new ErrorKernel(); $contextProvider = new ExceptionContextProvider(); // 1. Exception occurs in service layer $exception = new RuntimeException('User registration failed', 500); // 2. Service enriches with operation context $serviceContext = new ExceptionContextData( operation: 'user.register', component: 'UserService', requestId: 'req-registration-789', occurredAt: new \DateTimeImmutable(), metadata: ['user_email' => 'test@example.com'] ); $contextProvider->attach($exception, $serviceContext); // 3. ErrorBoundary catches and enriches with boundary metadata $boundaryContext = $contextProvider->get($exception)->addMetadata([ 'error_boundary' => 'user_registration_boundary', 'boundary_failure' => true, 'fallback_executed' => false ]); $contextProvider->attach($exception, $boundaryContext); // 4. HTTP layer enriches with request metadata $httpContext = $contextProvider->get($exception)->addMetadata([ 'client_ip' => '203.0.113.42', 'user_agent' => 'Mozilla/5.0 (Windows NT 10.0)', 'http_method' => 'POST', 'request_uri' => '/api/users/register' ]); $contextProvider->attach($exception, $httpContext); // 5. ErrorKernel generates HTTP response $response = $errorKernel->createHttpResponse($exception, $contextProvider, isDebugMode: true); // 6. Verify complete context propagation $body = json_decode($response->body, true); expect($body['context']['operation'])->toBe('user.register'); expect($body['context']['component'])->toBe('UserService'); expect($body['context']['request_id'])->toBe('req-registration-789'); // Note: Metadata is stored in ExceptionContextData but not automatically // included in ResponseErrorRenderer output (by design for production safety) // Metadata can be accessed programmatically via $contextProvider->get($exception)->metadata expect($response->status)->toBe(Status::INTERNAL_SERVER_ERROR); expect($response->headers->getFirst('Content-Type'))->toBe('application/json'); }); });