diff --git a/tests/Unit/RouterTest.php b/tests/Unit/RouterTest.php index f294816..c7e12b7 100644 --- a/tests/Unit/RouterTest.php +++ b/tests/Unit/RouterTest.php @@ -8,7 +8,9 @@ use Libs\Request; use Libs\Router; use LogicException; use PDOException; +use PHPUnit\Framework\Attributes\RunInSeparateProcess; use PHPUnit\Framework\Attributes\Test; +use ReflectionClass; use ReflectionProperty; use Tests\TestCase; use TypeError; @@ -21,7 +23,10 @@ use TypeError; * negociacion por Accept entre representacion JSON y texto plano. Tambien la * frontera de Router::apply(): que toda excepcion de la cadena (middlewares, * callback final, notFound) llegue al $exceptionCallback deteniendo la - * ejecucion sin exit() y dejando intacto el flujo normal. + * ejecucion sin exit() y dejando intacto el flujo normal. Tambien que los + * valores por defecto declarados en las propiedades $notFoundCallback y + * $exceptionCallback (callables string) se resuelvan de punta a punta a + * traves de Synapsis::resolve() dentro de apply(). */ final class RouterTest extends TestCase { @@ -36,7 +41,7 @@ final class RouterTest extends TestCase $this->postBackup = $_POST; $_SERVER['REQUEST_METHOD'] = 'GET'; unset($_SERVER['HTTP_ACCEPT']); - http_response_code(200); + @http_response_code(200); // Ver tearDown: inofensivo mientras no haya un header('HTTP/...') activo. $this->resetRouterState(); } @@ -45,7 +50,11 @@ final class RouterTest extends TestCase $_SERVER = $this->serverBackup; $_GET = $this->getBackup; $_POST = $this->postBackup; - http_response_code(200); + // El @ cubre el warning de PHP 8.5 ("...has no effect") que dispara + // cualquier http_response_code() posterior a un header('HTTP/...'), + // inevitable en el proceso aislado del test de notFound (el valor + // se aplica igualmente, como confirma el getter). + @http_response_code(200); $this->resetRouterState(); } @@ -243,6 +252,46 @@ final class RouterTest extends TestCase $this->assertSame(400, $caught[0]->getCode(), 'The status channel must survive custom exception types'); $this->assertFalse($finalRan, 'A Request that throws during DI resolution must stop the route'); } + + #[Test] + public function declaredDefaultExceptionCallbackRendersThroughTheBoundary(): void + { + // Se fuerza el valor declarado en la clase, no el que reasigna + // resetRouterState(): es justo el camino property default -> + // Synapsis::resolve(string callable) que debe seguir funcionando. + Router::$exceptionCallback = (new ReflectionClass(Router::class)) + ->getDefaultProperties()['exceptionCallback']; + + Router::get('/default-exception', function (): void { + throw new Exception('rendered by the default handler', 422); + }); + + ob_start(); + Router::apply('/default-exception'); + $output = ob_get_clean(); + + $this->assertSame(422, http_response_code(), 'The declared default handler must derive the status from the code'); + $this->assertStringContainsString('rendered by the default handler', $output); + $this->assertStringContainsString('#0', $output, 'The declared default handler must render the plain text trace'); + } + + #[Test] + #[RunInSeparateProcess] + public function declaredDefaultNotFoundCallbackRendersThe404Body(): void + { + Router::$notFoundCallback = (new ReflectionClass(Router::class)) + ->getDefaultProperties()['notFoundCallback']; + + ob_start(); + Router::apply('/no-such-route'); + $output = ob_get_clean(); + + // Proceso aislado: defaultNotFound() emite header('HTTP/1.0 404 ...') y en + // PHP 8.5 eso hace que TODO http_response_code() posterior avise, sin forma + // de limpiar el estado en el mismo proceso. En CLI el header no toca + // http_response_code(), asi que la asercion util es el cuerpo renderizado. + $this->assertStringContainsString('Error 404 - Page Not Found', $output); + } } // phpcs:disable PSR1.Classes.ClassDeclaration.MultipleClasses