fix(error): harden logging, i18n, a11y and handler tests
This commit is contained in:
78
tests/Http/ErrorHandlerTest.php
Normal file
78
tests/Http/ErrorHandlerTest.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Http;
|
||||
|
||||
use MintyPHP\Debugger;
|
||||
use MintyPHP\Http\ErrorHandler;
|
||||
use MintyPHP\Http\RequestContext;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ErrorHandlerTest extends TestCase
|
||||
{
|
||||
private array $serverBackup = [];
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->serverBackup = $_SERVER;
|
||||
RequestContext::resetForTests();
|
||||
Debugger::$enabled = false;
|
||||
header_remove();
|
||||
http_response_code(200);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$_SERVER = $this->serverBackup;
|
||||
RequestContext::resetForTests();
|
||||
Debugger::$enabled = false;
|
||||
header_remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* @runInSeparateProcess
|
||||
*/
|
||||
public function testHandleExceptionReturnsJsonForApiRequestsWithRequestId(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||
$_SERVER['REQUEST_URI'] = '/api/v1/ping';
|
||||
$_SERVER['HTTP_X_REQUEST_ID'] = '123e4567-e89b-12d3-a456-426614174000';
|
||||
|
||||
$this->expectOutputRegex('/^(?:(?!<html).)*"error_code":"internal_error".*"request_id":"123e4567-e89b-12d3-a456-426614174000".*$/s');
|
||||
|
||||
ErrorHandler::handleException(new \RuntimeException('API failure'));
|
||||
|
||||
$this->assertSame(500, http_response_code());
|
||||
}
|
||||
|
||||
/**
|
||||
* @runInSeparateProcess
|
||||
*/
|
||||
public function testHandleExceptionRendersProdPageInWebWhenDebugDisabled(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||
$_SERVER['REQUEST_URI'] = '/admin/users';
|
||||
$_SERVER['HTTP_X_REQUEST_ID'] = '123e4567-e89b-12d3-a456-426614174111';
|
||||
|
||||
$this->expectOutputRegex('/^(?=.*<html lang="en">)(?=.*Back to start)(?=.*123e4567-e89b-12d3-a456-426614174111)(?!.*panel-exception).*$/s');
|
||||
|
||||
ErrorHandler::handleException(new \RuntimeException('Web failure'));
|
||||
|
||||
$this->assertSame(500, http_response_code());
|
||||
}
|
||||
|
||||
/**
|
||||
* @runInSeparateProcess
|
||||
*/
|
||||
public function testHandleExceptionKeepsExistingErrorStatusCode(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||
$_SERVER['REQUEST_URI'] = '/admin/settings';
|
||||
http_response_code(500);
|
||||
|
||||
$this->expectOutputRegex('/500/');
|
||||
|
||||
ErrorHandler::handleException(new \RuntimeException('Status flow'));
|
||||
|
||||
$this->assertSame(500, http_response_code());
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ class ErrorLoggerTest extends TestCase
|
||||
'request_id' => 'abc-123',
|
||||
'channel' => 'web',
|
||||
'method' => 'GET',
|
||||
'url' => '/admin/dashboard',
|
||||
'url' => '/admin/dashboard?token=very-secret',
|
||||
'ip' => '127.0.0.1',
|
||||
'user_id' => 5,
|
||||
'tenant_id' => 1,
|
||||
@@ -60,11 +60,41 @@ class ErrorLoggerTest extends TestCase
|
||||
$this->assertSame('error', $entry['level']);
|
||||
$this->assertSame('abc-123', $entry['request_id']);
|
||||
$this->assertSame('RuntimeException', $entry['exception']['class']);
|
||||
$this->assertSame('Test error', $entry['exception']['message']);
|
||||
$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
|
||||
|
||||
@@ -3,10 +3,28 @@
|
||||
namespace MintyPHP\Tests\Http;
|
||||
|
||||
use MintyPHP\Http\ErrorRenderer;
|
||||
use MintyPHP\I18n;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ErrorRendererTest extends TestCase
|
||||
{
|
||||
private string $localeBackup = '';
|
||||
private string $defaultLocaleBackup = '';
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->localeBackup = (string) (I18n::$locale ?? '');
|
||||
$this->defaultLocaleBackup = (string) (I18n::$defaultLocale ?? 'en');
|
||||
I18n::$defaultLocale = 'en';
|
||||
I18n::$locale = 'en';
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
I18n::$locale = $this->localeBackup;
|
||||
I18n::$defaultLocale = $this->defaultLocaleBackup;
|
||||
}
|
||||
|
||||
public function testRenderDevReturnsCompleteHtmlDocument(): void
|
||||
{
|
||||
$data = self::sampleData();
|
||||
@@ -16,6 +34,7 @@ class ErrorRendererTest extends TestCase
|
||||
$this->assertStringContainsString('</html>', $html);
|
||||
$this->assertStringContainsString('<style>', $html);
|
||||
$this->assertStringContainsString('<script>', $html);
|
||||
$this->assertStringContainsString('<html lang="en">', $html);
|
||||
}
|
||||
|
||||
public function testRenderDevContainsExceptionDetails(): void
|
||||
@@ -37,6 +56,21 @@ class ErrorRendererTest extends TestCase
|
||||
$this->assertStringContainsString('panel-request', $html);
|
||||
$this->assertStringContainsString('panel-environment', $html);
|
||||
$this->assertStringContainsString('panel-queries', $html);
|
||||
$this->assertStringContainsString('role="tablist"', $html);
|
||||
$this->assertStringContainsString('aria-controls="panel-exception"', $html);
|
||||
$this->assertStringContainsString('role="tabpanel"', $html);
|
||||
}
|
||||
|
||||
public function testRenderDevUsesAccessibleToggleButtons(): void
|
||||
{
|
||||
$data = self::sampleData();
|
||||
$html = ErrorRenderer::renderDev($data);
|
||||
|
||||
$this->assertStringContainsString('app-error-debug-frame__toggle', $html);
|
||||
$this->assertStringContainsString('app-error-debug-query__toggle', $html);
|
||||
$this->assertStringContainsString('aria-expanded="true"', $html);
|
||||
$this->assertStringContainsString('aria-controls="frame-code-0"', $html);
|
||||
$this->assertStringContainsString('aria-label="Copy request ID"', $html);
|
||||
}
|
||||
|
||||
public function testRenderDevEscapesHtmlInExceptionMessage(): void
|
||||
@@ -63,9 +97,12 @@ class ErrorRendererTest extends TestCase
|
||||
$html = ErrorRenderer::renderProd('abc-def-123');
|
||||
|
||||
$this->assertStringContainsString('<!DOCTYPE html>', $html);
|
||||
$this->assertStringContainsString('<html lang="en">', $html);
|
||||
$this->assertStringContainsString('abc-def-123', $html);
|
||||
$this->assertStringContainsString('An unexpected error occurred', $html);
|
||||
$this->assertStringContainsString('Back to start', $html);
|
||||
$this->assertStringContainsString('data-copy="abc-def-123"', $html);
|
||||
$this->assertStringContainsString('aria-label="Copy request ID"', $html);
|
||||
}
|
||||
|
||||
public function testRenderProdDoesNotContainDebugInfo(): void
|
||||
|
||||
Reference in New Issue
Block a user