serverBackup = $_SERVER; RequestContext::resetForTests(); } protected function tearDown(): void { $_SERVER = $this->serverBackup; RequestContext::resetForTests(); } public function testCollectReturnsAllFourSections(): void { $exception = new \RuntimeException('Test exception', 99); $result = ErrorDataCollector::collect($exception); $this->assertArrayHasKey('exception', $result); $this->assertArrayHasKey('request', $result); $this->assertArrayHasKey('environment', $result); $this->assertArrayHasKey('queries', $result); } public function testExceptionSectionContainsCorrectDetails(): void { $exception = new \LogicException('Something wrong', 7); $result = ErrorDataCollector::collect($exception); $exc = $result['exception']; $this->assertSame('LogicException', $exc['class']); $this->assertSame('Something wrong', $exc['message']); $this->assertSame(7, $exc['code']); $this->assertStringContainsString('ErrorDataCollectorTest.php', $exc['file']); $this->assertIsInt($exc['line']); $this->assertIsArray($exc['trace']); $this->assertNotEmpty($exc['trace']); } public function testChainedExceptionsAreCollected(): void { $inner = new \InvalidArgumentException('inner cause'); $outer = new \RuntimeException('outer', 0, $inner); $result = ErrorDataCollector::collect($outer); $this->assertArrayHasKey('previous', $result['exception']); $this->assertSame('InvalidArgumentException', $result['exception']['previous']['class']); $this->assertSame('inner cause', $result['exception']['previous']['message']); } public function testCodeSnippetIsExtractedForKnownFile(): void { $exception = new \RuntimeException('snippet test'); $result = ErrorDataCollector::collect($exception); $firstFrame = $result['exception']['trace'][0] ?? null; $this->assertNotNull($firstFrame); $this->assertArrayHasKey('code_snippet', $firstFrame); // The snippet should contain lines around the throw site. $this->assertNotEmpty($firstFrame['code_snippet']); } public function testRequestSectionContainsBasicData(): void { $_SERVER['REQUEST_METHOD'] = 'POST'; $_SERVER['REQUEST_URI'] = '/admin/users/edit?id=5'; $_SERVER['REMOTE_ADDR'] = '10.0.0.1'; $_SERVER['HTTP_USER_AGENT'] = 'TestAgent/1.0'; $exception = new \RuntimeException('request test'); $result = ErrorDataCollector::collect($exception); $req = $result['request']; $this->assertSame('POST', $req['method']); $this->assertSame('/admin/users/edit?id=5', $req['url']); $this->assertSame('10.0.0.1', $req['ip']); $this->assertSame('TestAgent/1.0', $req['user_agent']); } public function testHeadersAreRedacted(): void { $_SERVER['HTTP_AUTHORIZATION'] = 'Bearer secret-token'; $_SERVER['HTTP_COOKIE'] = 'session=abc123'; $_SERVER['HTTP_X_API_KEY'] = 'my-key'; $_SERVER['HTTP_ACCEPT'] = 'text/html'; $exception = new \RuntimeException('header redaction'); $result = ErrorDataCollector::collect($exception); $headers = $result['request']['headers']; $this->assertSame('[REDACTED]', $headers['authorization']); $this->assertSame('[REDACTED]', $headers['cookie']); $this->assertSame('[REDACTED]', $headers['x-api-key']); $this->assertSame('text/html', $headers['accept']); } public function testPostParamsAreRedacted(): void { $_POST = [ 'username' => 'admin', 'password' => 'secret123', 'csrf_token' => 'abc', 'email' => 'test@example.com', ]; $exception = new \RuntimeException('post redaction'); $result = ErrorDataCollector::collect($exception); $post = $result['request']['post']; $this->assertSame('admin', $post['username']); $this->assertSame('[REDACTED]', $post['password']); $this->assertSame('[REDACTED]', $post['csrf_token']); $this->assertSame('test@example.com', $post['email']); $_POST = []; } public function testEnvironmentSectionContainsPhpInfo(): void { $exception = new \RuntimeException('env test'); $result = ErrorDataCollector::collect($exception); $env = $result['environment']; $this->assertSame(PHP_VERSION, $env['php_version']); $this->assertSame(PHP_SAPI, $env['php_sapi']); $this->assertIsArray($env['extensions']); $this->assertNotEmpty($env['extensions']); $this->assertIsInt($env['memory_current']); $this->assertIsInt($env['memory_peak']); } public function testArgsAreSummarizedByTypeOnly(): void { // Trace frames should contain type summaries, not actual argument values. $exception = new \RuntimeException('args test'); $result = ErrorDataCollector::collect($exception); foreach ($result['exception']['trace'] as $frame) { if (isset($frame['args_summary'])) { foreach ($frame['args_summary'] as $summary) { $this->assertIsString($summary); // Should be type descriptions, not raw values. $this->assertMatchesRegularExpression( '/^(null|true|false|int|float|string\(\d+\)|array\(\d+\)|resource|unknown|[A-Z][\w\\\\]*)$/', $summary, ); } } } } }