mirror of
https://github.com/Vateron-Media/XC_VM.git
synced 2026-10-03 20:02:29 +02:00
Introduce the source-of-truth classes for the bootstrap testability refactor. Purely additive — nothing is wired to them yet. - ConstantsInitializer: pure value maps (paths/appConfig/binaries/statuses) plus the single define() site (init/initStatus). The maps evaluate with different MAIN_HOME/BIN_PATH in one process, which the one-shot define() constants they feed cannot — this is what makes them testable. - ErrorResponder: the generateError()/generate404() logic extracted into pure codes()/renderDebug()/render404()/respond*() plus a single side-effecting emit(). A test-mode toggle throws ErrorResponseException instead of exit(). - ErrorResponseException: value carrier for a resolved error response. OPENSSL_EXTRA is now sourced per-install via ConfigReader with a mandatory fallback to the historical literal, so existing installs (whose persisted data derives from it) keep decrypting; generation-at-install is left to the installer. Verified byte-for-byte against the legacy prelude before wiring: 53/53 constants, debug/404 HTML frozen as sha256 goldens, 65 error codes.
124 lines
4.4 KiB
PHP
124 lines
4.4 KiB
PHP
<?php
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
use XcVm\Core\Error\ErrorResponder;
|
|
use XcVm\Core\Error\ErrorResponseException;
|
|
|
|
/**
|
|
* Golden + behavioural coverage for the error responder. The HTML hashes freeze
|
|
* the exact bytes the legacy generateError()/generate404() emitted (verified
|
|
* byte-for-byte against Core/Error/ErrorHandler.php before it became a shim), so
|
|
* the PR2 cutover cannot silently change the rendered page.
|
|
*/
|
|
final class ErrorResponderTest extends TestCase {
|
|
private const DEBUG_HTML_LEN = 1651;
|
|
private const DEBUG_HTML_SHA = 'e6ff18792b0e26027c23b013b8fd35874c34baa8260f20ecbe5c56c5a8c47d6e';
|
|
private const NOT_FOUND_LEN = 546;
|
|
private const NOT_FOUND_SHA = 'dd21fa922cb6133c73a795e7b42164baeddd4ae53597cbd3405346f8f3a2f871';
|
|
|
|
protected function tearDown(): void {
|
|
ErrorResponder::$throwInsteadOfExit = false;
|
|
}
|
|
|
|
public function testDebugHtmlGolden(): void {
|
|
$html = ErrorResponder::renderDebug('BANNED', ErrorResponder::codes()['BANNED']);
|
|
|
|
$this->assertSame(self::DEBUG_HTML_LEN, strlen($html));
|
|
$this->assertSame(self::DEBUG_HTML_SHA, hash('sha256', $html));
|
|
$this->assertStringContainsString('<h2>BANNED</h2>', $html);
|
|
$this->assertStringContainsString('Line has been banned.', $html);
|
|
}
|
|
|
|
public function test404HtmlGolden(): void {
|
|
$html = ErrorResponder::render404();
|
|
|
|
$this->assertSame(self::NOT_FOUND_LEN, strlen($html));
|
|
$this->assertSame(self::NOT_FOUND_SHA, hash('sha256', $html));
|
|
$this->assertStringContainsString('404 Not Found', $html);
|
|
}
|
|
|
|
public function testCodesCatalogueSize(): void {
|
|
$codes = ErrorResponder::codes();
|
|
|
|
$this->assertCount(65, $codes);
|
|
$this->assertSame('Domain name not recognised.', $codes['INVALID_HOST']);
|
|
$this->assertArrayHasKey('CACHE_INCOMPLETE', $codes);
|
|
}
|
|
|
|
/** Debug on: styled page is emitted, no HTTP code, terminates only when $kill. */
|
|
public function testDebugOutcomeEmitsBodyWithoutHttpCode(): void {
|
|
$outcome = ErrorResponder::respondError('BANNED', ['debug_show_errors' => true], true, null);
|
|
|
|
$this->assertFalse($outcome->is404);
|
|
$this->assertNull($outcome->httpCode);
|
|
$this->assertTrue($outcome->shouldExit);
|
|
$this->assertNotSame('', $outcome->body);
|
|
}
|
|
|
|
public function testDebugNoKillDoesNotExitButStillEmitsBody(): void {
|
|
$outcome = ErrorResponder::respondError('BANNED', ['debug_show_errors' => true], false, null);
|
|
|
|
$this->assertFalse($outcome->shouldExit);
|
|
$this->assertNotSame('', $outcome->body);
|
|
}
|
|
|
|
/** Production + kill + no explicit code: falls through to a bare 404. */
|
|
public function testProductionKillNoCodeFallsTo404(): void {
|
|
$outcome = ErrorResponder::respondError('BANNED', ['debug_show_errors' => false], true, null);
|
|
|
|
$this->assertTrue($outcome->is404);
|
|
$this->assertSame(404, $outcome->httpCode);
|
|
$this->assertTrue($outcome->shouldExit);
|
|
$this->assertSame(self::NOT_FOUND_SHA, hash('sha256', $outcome->body));
|
|
}
|
|
|
|
/** Production + kill + explicit code: just the status, no body. */
|
|
public function testProductionKillWithCodeSetsStatusOnly(): void {
|
|
$outcome = ErrorResponder::respondError('BANNED', ['debug_show_errors' => false], true, 403);
|
|
|
|
$this->assertSame(403, $outcome->httpCode);
|
|
$this->assertSame('', $outcome->body);
|
|
$this->assertTrue($outcome->shouldExit);
|
|
}
|
|
|
|
/** Production + no kill: emit nothing at all. */
|
|
public function testProductionNoKillEmitsNothing(): void {
|
|
$outcome = ErrorResponder::respondError('BANNED', ['debug_show_errors' => false], false, null);
|
|
|
|
$this->assertSame('', $outcome->body);
|
|
$this->assertNull($outcome->httpCode);
|
|
$this->assertFalse($outcome->shouldExit);
|
|
}
|
|
|
|
public function testMissingSettingsTreatedAsProduction(): void {
|
|
$outcome = ErrorResponder::respondError('BANNED', [], true, null);
|
|
|
|
$this->assertTrue($outcome->is404);
|
|
}
|
|
|
|
/** In test mode a terminating outcome throws instead of exit()-ing. */
|
|
public function testEmitThrowsInTestModeForTerminatingOutcome(): void {
|
|
ErrorResponder::$throwInsteadOfExit = true;
|
|
$outcome = ErrorResponder::respond404(true);
|
|
|
|
$this->expectException(ErrorResponseException::class);
|
|
ob_start();
|
|
try {
|
|
ErrorResponder::emit($outcome);
|
|
} finally {
|
|
ob_end_clean();
|
|
}
|
|
}
|
|
|
|
public function testEmitIsSilentForNonTerminatingOutcome(): void {
|
|
ErrorResponder::$throwInsteadOfExit = true;
|
|
$outcome = ErrorResponder::respondError('BANNED', ['debug_show_errors' => false], false, null);
|
|
|
|
ob_start();
|
|
ErrorResponder::emit($outcome);
|
|
$output = ob_get_clean();
|
|
|
|
$this->assertSame('', $output);
|
|
}
|
|
}
|