Compare commits

..

4 Commits

4 changed files with 74 additions and 17 deletions

View File

@@ -50,7 +50,7 @@ class Router
* *
* @var callable $notFoundCallback * @var callable $notFoundCallback
*/ */
public static $notFoundCallback = Router::defaultNotFound(...); public static $notFoundCallback = 'Libs\Router::defaultNotFound';
/** /**
* Default callback function for when * Default callback function for when
@@ -72,7 +72,7 @@ class Router
* *
* @var callable $exceptionCallback * @var callable $exceptionCallback
*/ */
public static $exceptionCallback = Router::defaultException(...); public static $exceptionCallback = 'Libs\Router::defaultException';
/** /**
* Default callback function for exception responses. * Default callback function for exception responses.

View File

@@ -38,7 +38,7 @@ use Exception;
* | url | | Must be a valid URL | * | url | | Must be a valid URL |
* | date | | Must be a parseable date | * | date | | Must be a parseable date |
* | regex | :pattern | Must match a PCRE pattern (with delimiters) | * | regex | :pattern | Must match a PCRE pattern (with delimiters) |
* | enum | :a,b,c | Must be one of the listed values | * | in | :a,b,c | Must be one of the listed values |
* | min | :n | Length/number/count min (files: KB) | * | min | :n | Length/number/count min (files: KB) |
* | max | :n | Length/number/count max (files: KB) | * | max | :n | Length/number/count max (files: KB) |
* | between | :min,:max | Value within range (files: KB per upload) | * | between | :min,:max | Value within range (files: KB per upload) |
@@ -105,7 +105,7 @@ class Validator
* | :size | the size rule value | * | :size | the size rule value |
* | :value | the required_if expected value | * | :value | the required_if expected value |
* | :other | the required_if companion field / confirmation | * | :other | the required_if companion field / confirmation |
* | :values | a comma list (enum, mimes, required_with) | * | :values | a comma list (in, mimes, required_with) |
* |----------+----------------------------------------------------| * |----------+----------------------------------------------------|
* *
* @var array<string,string> * @var array<string,string>
@@ -122,7 +122,7 @@ class Validator
'array' => 'The :attribute must be an array.', 'array' => 'The :attribute must be an array.',
'email' => 'The :attribute must be a valid email address.', 'email' => 'The :attribute must be a valid email address.',
'url' => 'The :attribute must be a valid URL.', 'url' => 'The :attribute must be a valid URL.',
'enum' => 'The selected :attribute is invalid. Allowed: :values.', 'in' => 'The selected :attribute is invalid. Allowed: :values.',
'min' => 'The :attribute must be at least :min.', 'min' => 'The :attribute must be at least :min.',
'max' => 'The :attribute must not be greater than :max.', 'max' => 'The :attribute must not be greater than :max.',
'between' => 'The :attribute must be between :min and :max.', 'between' => 'The :attribute must be between :min and :max.',
@@ -243,7 +243,7 @@ class Validator
case 'size': case 'size':
$replace[':size'] = $args[0] ?? ''; $replace[':size'] = $args[0] ?? '';
break; break;
case 'enum': case 'in':
case 'mimes': case 'mimes':
case 'required_with': case 'required_with':
$replace[':values'] = implode(', ', $args); $replace[':values'] = implode(', ', $args);
@@ -305,7 +305,7 @@ class Validator
* arguments legitimately contain ':' or ',' - such as regex or mimes - * arguments legitimately contain ':' or ',' - such as regex or mimes -
* are preserved intact. The caller decides how to split the arguments. * are preserved intact. The caller decides how to split the arguments.
* *
* @param string $rule The rule to parse. Ex: "regex:/^a,b$/" or "enum:a,b". * @param string $rule The rule to parse. Ex: "regex:/^a,b$/" or "in:a,b".
* *
* @return array A two element array: [name, rawArguments]. When the rule * @return array A two element array: [name, rawArguments]. When the rule
* has no parameters, rawArguments is an empty string. * has no parameters, rawArguments is an empty string.
@@ -419,7 +419,7 @@ class Validator
* Splits a rule's raw argument string into the arguments to pass to the * Splits a rule's raw argument string into the arguments to pass to the
* rule's method (the subject is not included). * rule's method (the subject is not included).
* *
* Most rules take a comma-separated list of values (e.g. "enum:a,b"). * Most rules take a comma-separated list of values (e.g. "in:a,b").
* A few rules receive an argument that must be kept intact because it can * A few rules receive an argument that must be kept intact because it can
* legitimately contain commas or colons: "regex" (a pattern) and "not" * legitimately contain commas or colons: "regex" (a pattern) and "not"
* (a sub-rule that is itself parsed recursively). An empty argument string * (a sub-rule that is itself parsed recursively). An empty argument string
@@ -810,7 +810,7 @@ class Validator
* *
* @return bool * @return bool
*/ */
public static function enum(mixed $subject, ...$values): bool public static function in(mixed $subject, ...$values): bool
{ {
return in_array($subject, $values); return in_array($subject, $values);
} }

View File

@@ -8,7 +8,9 @@ use Libs\Request;
use Libs\Router; use Libs\Router;
use LogicException; use LogicException;
use PDOException; use PDOException;
use PHPUnit\Framework\Attributes\RunInSeparateProcess;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
use ReflectionClass;
use ReflectionProperty; use ReflectionProperty;
use Tests\TestCase; use Tests\TestCase;
use TypeError; use TypeError;
@@ -21,7 +23,10 @@ use TypeError;
* negociacion por Accept entre representacion JSON y texto plano. Tambien la * negociacion por Accept entre representacion JSON y texto plano. Tambien la
* frontera de Router::apply(): que toda excepcion de la cadena (middlewares, * frontera de Router::apply(): que toda excepcion de la cadena (middlewares,
* callback final, notFound) llegue al $exceptionCallback deteniendo la * 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 final class RouterTest extends TestCase
{ {
@@ -36,7 +41,7 @@ final class RouterTest extends TestCase
$this->postBackup = $_POST; $this->postBackup = $_POST;
$_SERVER['REQUEST_METHOD'] = 'GET'; $_SERVER['REQUEST_METHOD'] = 'GET';
unset($_SERVER['HTTP_ACCEPT']); unset($_SERVER['HTTP_ACCEPT']);
http_response_code(200); @http_response_code(200); // Ver tearDown: inofensivo mientras no haya un header('HTTP/...') activo.
$this->resetRouterState(); $this->resetRouterState();
} }
@@ -45,7 +50,11 @@ final class RouterTest extends TestCase
$_SERVER = $this->serverBackup; $_SERVER = $this->serverBackup;
$_GET = $this->getBackup; $_GET = $this->getBackup;
$_POST = $this->postBackup; $_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(); $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->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'); $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 // phpcs:disable PSR1.Classes.ClassDeclaration.MultipleClasses

View File

@@ -81,10 +81,18 @@ final class ValidatorTest extends TestCase
#[Test] #[Test]
public function enumIsLooseComparison(): void public function inIsLooseComparison(): void
{ {
$this->assertTrue(Validator::checkRule('1', 'enum:1,2,3')); $this->assertTrue(Validator::checkRule('1', 'in:1,2,3'));
$this->assertFalse(Validator::checkRule('9', 'enum:1,2,3')); $this->assertFalse(Validator::checkRule('9', 'in:1,2,3'));
}
#[Test]
public function enumRuleIsNoLongerRecognized(): void
{
$this->expectException(\Exception::class);
Validator::checkRule('1', 'enum:1,2,3');
} }
#[Test] #[Test]
@@ -100,7 +108,7 @@ final class ValidatorTest extends TestCase
public function parseRule(): void public function parseRule(): void
{ {
$this->assertSame(['required', ''], Validator::parseRule('required')); $this->assertSame(['required', ''], Validator::parseRule('required'));
$this->assertSame(['enum', 'a,b'], Validator::parseRule('enum:a,b')); $this->assertSame(['in', 'a,b'], Validator::parseRule('in:a,b'));
$this->assertSame(['regex', '/^a,b$/'], Validator::parseRule('regex:/^a,b$/')); $this->assertSame(['regex', '/^a,b$/'], Validator::parseRule('regex:/^a,b$/'));
} }
@@ -168,7 +176,7 @@ final class ValidatorTest extends TestCase
); );
$this->assertSame( $this->assertSame(
'The selected status is invalid. Allowed: a, b, c.', 'The selected status is invalid. Allowed: a, b, c.',
Validator::message('status.enum:a,b,c') Validator::message('status.in:a,b,c')
); );
$this->assertSame( $this->assertSame(
'The reason field is required when mode is other.', 'The reason field is required when mode is other.',