logFile = 'storage/logs/errors/error-' . date('Y-m-d') . '.log'; // Remove the file so each test starts clean. if (is_file($this->logFile)) { @unlink($this->logFile); } } public function testLogWritesJsonLineEntry(): void { $exception = new \RuntimeException('Test error', 42); $data = [ 'exception' => [ 'class' => 'RuntimeException', 'message' => 'Test error', 'code' => 42, 'file' => '/app/web/index.php', 'line' => 10, 'trace' => [ ['file' => '/app/web/index.php', 'line' => 10], ], ], 'request' => [ 'request_id' => 'abc-123', 'channel' => 'web', 'method' => 'GET', 'url' => '/admin/dashboard?token=very-secret', 'ip' => '127.0.0.1', 'user_id' => 5, 'tenant_id' => 1, ], 'environment' => [ 'php_version' => '8.5.0', 'memory_peak' => 12 * 1024 * 1024, ], 'queries' => [], ]; ErrorLogger::log($exception, $data); $this->assertFileExists($this->logFile); $lines = array_filter(explode("\n", (string) file_get_contents($this->logFile))); $this->assertCount(1, $lines); $entry = json_decode($lines[0], true); $this->assertIsArray($entry); $this->assertSame('error', $entry['level']); $this->assertSame('abc-123', $entry['request_id']); $this->assertSame('RuntimeException', $entry['exception']['class']); $this->assertSame('[REDACTED]', $entry['exception']['message']); $this->assertArrayHasKey('message_hash', $entry['exception']); $this->assertStringStartsWith('sha256:', (string) $entry['exception']['message_hash']); $this->assertArrayHasKey('ip_hash', $entry['request']); $this->assertStringStartsWith('sha256:', $entry['request']['ip_hash']); $this->assertSame('/admin/dashboard', $entry['request']['path']); $this->assertArrayNotHasKey('url', $entry['request']); // Raw IP should NOT be in the log. $this->assertStringNotContainsString('127.0.0.1', json_encode($entry)); $this->assertStringNotContainsString('very-secret', json_encode($entry)); } public function testLogStoresPathWithoutQueryParameters(): void { $exception = new \RuntimeException('Query redaction'); $data = [ 'request' => [ 'request_id' => 'query-test', 'url' => '/auth/reset-password?email=user@example.com&token=abc123', ], 'exception' => [], 'environment' => [], ]; ErrorLogger::log($exception, $data); $this->assertFileExists($this->logFile); $lines = array_filter(explode("\n", (string) file_get_contents($this->logFile))); $this->assertCount(1, $lines); $entry = json_decode($lines[0], true); $this->assertIsArray($entry); $this->assertSame('/auth/reset-password', $entry['request']['path']); $this->assertStringNotContainsString('user@example.com', $lines[0]); $this->assertStringNotContainsString('token=abc123', $lines[0]); } public function testLogRedactsIpAddress(): void { $exception = new \RuntimeException('IP test'); $data = [ 'request' => ['ip' => '192.168.1.42', 'request_id' => 'test-id'], 'exception' => [], 'environment' => [], ]; ErrorLogger::log($exception, $data); $this->assertFileExists($this->logFile); $contents = (string) file_get_contents($this->logFile); $this->assertStringNotContainsString('192.168.1.42', $contents); $this->assertStringContainsString('sha256:', $contents); } public function testLogNeverThrowsOnFailure(): void { // Even with broken data, log() must not throw. $exception = new \RuntimeException('safe test'); ErrorLogger::log($exception, []); $this->assertTrue(true); // If we get here, no exception was thrown. } }