test(unit): Add Neuron and Validator regression tests
This commit is contained in:
@@ -19,12 +19,12 @@ final class BootstrapTest extends TestCase
|
||||
{
|
||||
$this->assertTrue(
|
||||
class_exists(Neuron::class),
|
||||
'FALTA AUTOLOAD: autoload.php no resolvio Libs\Neuron; revisa ROOT_CORE y ejecuta desde la raiz'
|
||||
'MISSING AUTOLOAD: autoload.php did not resolve Libs\Neuron; check ROOT_CORE and run from the project root'
|
||||
);
|
||||
|
||||
$neuron = new Neuron(['clave' => 'valor']);
|
||||
$this->assertSame('valor', $neuron->clave);
|
||||
$this->assertNull($neuron->inexistente);
|
||||
$neuron = new Neuron(['key' => 'value']);
|
||||
$this->assertSame('value', $neuron->key);
|
||||
$this->assertNull($neuron->doesNotExist);
|
||||
}
|
||||
|
||||
public function testDatabaseConstantsPointToInMemorySqlite(): void
|
||||
@@ -32,20 +32,20 @@ final class BootstrapTest extends TestCase
|
||||
$this->assertSame(
|
||||
'sqlite',
|
||||
DB_TYPE,
|
||||
'FALTA OVERRIDE: DB_TYPE viene de config.php; el bootstrap debe definir las constantes ANTES de require autoload.php'
|
||||
'MISSING OVERRIDE: DB_TYPE comes from config.php; the bootstrap must define the DB_* constants BEFORE requiring autoload.php'
|
||||
);
|
||||
$this->assertSame(':memory:', DB_NAME, 'DB_NAME deberia ser :memory: para no tocar discos ni servidores');
|
||||
$this->assertSame(':memory:', DB_NAME, 'DB_NAME should be :memory: so tests never touch disks or servers');
|
||||
}
|
||||
|
||||
public function testTestDatabaseIsUsable(): void
|
||||
{
|
||||
$db = self::db();
|
||||
$this->assertInstanceOf(PDO::class, $db, 'No se pudo obtener la PDO de prueba');
|
||||
$this->assertSame($db, self::db(), 'Database::getInstance deberia devolver el mismo singleton');
|
||||
$this->assertInstanceOf(PDO::class, $db, 'Could not obtain the test PDO connection');
|
||||
$this->assertSame($db, self::db(), 'Database::getInstance should return the same singleton');
|
||||
|
||||
$db->exec('CREATE TABLE smoke (id INTEGER PRIMARY KEY, v TEXT)');
|
||||
$db->prepare('INSERT INTO smoke (v) VALUES (?)')->execute(['cuac']);
|
||||
$this->assertSame('cuac', $db->query('SELECT v FROM smoke')->fetchColumn());
|
||||
$db->prepare('INSERT INTO smoke (v) VALUES (?)')->execute(['quack']);
|
||||
$this->assertSame('quack', $db->query('SELECT v FROM smoke')->fetchColumn());
|
||||
$db->exec('DROP TABLE smoke');
|
||||
}
|
||||
}
|
||||
|
||||
59
tests/Unit/NeuronTest.php
Normal file
59
tests/Unit/NeuronTest.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use Libs\Neuron;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* NeuronTest - DuckBrain test harness
|
||||
*
|
||||
* Regression net para el contenedor de valores del core: construcción
|
||||
* desde array/objeto, propiedades dinámicas y null en inexistentes.
|
||||
*/
|
||||
final class NeuronTest extends TestCase
|
||||
{
|
||||
public function testConstructFromAssociativeArray(): void
|
||||
{
|
||||
$n = new Neuron(['username' => 'kj', 'level' => 3]);
|
||||
|
||||
$this->assertSame('kj', $n->username);
|
||||
$this->assertSame(3, $n->level);
|
||||
}
|
||||
|
||||
public function testConstructFromObjectCopiesPublicProperties(): void
|
||||
{
|
||||
$source = new \stdClass();
|
||||
$source->id = 7;
|
||||
$source->email = 'kj@example.com';
|
||||
|
||||
$n = new Neuron($source);
|
||||
|
||||
$this->assertSame(7, $n->id);
|
||||
$this->assertSame('kj@example.com', $n->email);
|
||||
}
|
||||
|
||||
public function testUndefinedPropertyIsNullWithoutNotice(): void
|
||||
{
|
||||
$n = new Neuron();
|
||||
|
||||
$this->assertNull($n->thisDoesNotExist);
|
||||
}
|
||||
|
||||
public function testDynamicPropertiesCanBeAssignedAndRead(): void
|
||||
{
|
||||
$n = new Neuron();
|
||||
$n->fresh = ['a', 'b'];
|
||||
|
||||
$this->assertSame(['a', 'b'], $n->fresh);
|
||||
}
|
||||
|
||||
public function testNestedValuesArePreservedVerbatim(): void
|
||||
{
|
||||
$payload = ['meta' => ['tags' => ['x', 'y'], 'n' => null]];
|
||||
$n = new Neuron($payload);
|
||||
|
||||
$this->assertSame($payload['meta'], $n->meta);
|
||||
$this->assertNull($n->meta['n']);
|
||||
}
|
||||
}
|
||||
166
tests/Unit/ValidatorTest.php
Normal file
166
tests/Unit/ValidatorTest.php
Normal file
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use Libs\Neuron;
|
||||
use Libs\Validator;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* ValidatorTest - DuckBrain test harness
|
||||
*
|
||||
* Regression net de las reglas escalares, el batch validateList()
|
||||
* (paro en el primer fallo + $lastFailed) y message(). Las reglas de
|
||||
* archivo (file/image/mimes) requieren fixtures de $_FILES y se dejan
|
||||
* para la suite multi-motor de integración.
|
||||
*/
|
||||
final class ValidatorTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
Validator::$lastFailed = '';
|
||||
}
|
||||
|
||||
public static function scalarRuleProvider(): array
|
||||
{
|
||||
return [
|
||||
'valid email' => ['email', 'kj@duckbrain.dev', true],
|
||||
'invalid email' => ['email', 'not-an-email', false],
|
||||
'valid url' => ['url', 'https://kj2.me', true],
|
||||
'invalid url' => ['url', 'kj2.me', false],
|
||||
'int from string' => ['int', '42', true],
|
||||
'int with decimals' => ['int', '42.5', false],
|
||||
'float' => ['float', '3.14', true],
|
||||
'number e-notation' => ['number', '1e3', true],
|
||||
'bool yes' => ['bool', 'yes', true],
|
||||
'bool garbage' => ['bool', 'quizas', false],
|
||||
'strict string' => ['string', '42', true],
|
||||
'string rejects int' => ['string', 42, false],
|
||||
'array' => ['array', [1], true],
|
||||
'array rejects string' => ['array', 'x', false],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('scalarRuleProvider')]
|
||||
public function testScalarRules(string $rule, mixed $value, bool $expected): void
|
||||
{
|
||||
$this->assertSame($expected, Validator::checkRule($value, $rule), "rule {$rule} with " . var_export($value, true));
|
||||
}
|
||||
|
||||
public function testExistsAndRequiredDisagreeOnEmptyString(): void
|
||||
{
|
||||
$this->assertTrue(Validator::checkRule('', 'exists'));
|
||||
$this->assertFalse(Validator::checkRule('', 'required'));
|
||||
$this->assertFalse(Validator::checkRule(null, 'exists'));
|
||||
}
|
||||
|
||||
public function testSizeRulesMeasureStringNumberAndArray(): void
|
||||
{
|
||||
$this->assertTrue(Validator::checkRule('abc', 'min:3'));
|
||||
$this->assertFalse(Validator::checkRule('abc', 'min:4'));
|
||||
$this->assertTrue(Validator::checkRule(10, 'max:10'));
|
||||
$this->assertTrue(Validator::checkRule([1, 2, 3], 'between:2,3'));
|
||||
$this->assertTrue(Validator::checkRule('hey', 'size:3'));
|
||||
$this->assertFalse(Validator::checkRule('hey', 'size:4'));
|
||||
}
|
||||
|
||||
public function testRegexKeepsColonsAndCommasInsidePattern(): void
|
||||
{
|
||||
$this->assertTrue(Validator::checkRule('a,b', 'regex:/^a,b$/'));
|
||||
$this->assertFalse(Validator::checkRule('a;b', 'regex:/^a,b$/'));
|
||||
}
|
||||
|
||||
public function testEnumIsLooseComparison(): void
|
||||
{
|
||||
$this->assertTrue(Validator::checkRule('1', 'enum:1,2,3'));
|
||||
$this->assertFalse(Validator::checkRule('9', 'enum:1,2,3'));
|
||||
}
|
||||
|
||||
public function testNotNegatesNextRule(): void
|
||||
{
|
||||
$this->assertTrue(Validator::checkRule('3.5', 'not:int'));
|
||||
$this->assertFalse(Validator::checkRule('42', 'not:int'));
|
||||
}
|
||||
|
||||
public function testParseRule(): void
|
||||
{
|
||||
$this->assertSame(['required', ''], Validator::parseRule('required'));
|
||||
$this->assertSame(['enum', 'a,b'], Validator::parseRule('enum:a,b'));
|
||||
$this->assertSame(['regex', '/^a,b$/'], Validator::parseRule('regex:/^a,b$/'));
|
||||
}
|
||||
|
||||
public function testValidateListPassesFullyValidBatch(): void
|
||||
{
|
||||
$data = new Neuron(['username' => 'kj', 'email' => 'kj@duckbrain.dev']);
|
||||
|
||||
$this->assertTrue(Validator::validateList(
|
||||
['username' => 'required|string|min:2', 'email' => 'required|email'],
|
||||
$data
|
||||
));
|
||||
$this->assertSame('', Validator::$lastFailed);
|
||||
}
|
||||
|
||||
public function testValidateListStopsAtFirstFailure(): void
|
||||
{
|
||||
$data = new Neuron(['email' => 'not-an-email', 'name' => null]);
|
||||
|
||||
$this->assertFalse(Validator::validateList(
|
||||
['email' => 'required|email', 'name' => 'required'],
|
||||
$data
|
||||
));
|
||||
$this->assertSame('email.email', Validator::$lastFailed);
|
||||
}
|
||||
|
||||
public function testValidateListNullableSkipsRestWhenEmpty(): void
|
||||
{
|
||||
$rules = ['bio' => 'nullable|min:10'];
|
||||
|
||||
$this->assertTrue(Validator::validateList($rules, new Neuron(['bio' => null])));
|
||||
$this->assertFalse(Validator::validateList($rules, new Neuron(['bio' => 'too-short'])));
|
||||
}
|
||||
|
||||
public function testValidateListConfirmedUsesSiblingField(): void
|
||||
{
|
||||
$rules = ['password' => 'required|confirmed'];
|
||||
|
||||
$ok = new Neuron(['password' => 'secret', 'password_confirmation' => 'secret']);
|
||||
$bad = new Neuron(['password' => 'secret', 'password_confirmation' => 'different']);
|
||||
|
||||
$this->assertTrue(Validator::validateList($rules, $ok));
|
||||
$this->assertFalse(Validator::validateList($rules, $bad));
|
||||
$this->assertSame('password.confirmed', Validator::$lastFailed);
|
||||
}
|
||||
|
||||
public function testMessageBuildsHumanTextFromLastFailed(): void
|
||||
{
|
||||
$this->assertSame(
|
||||
'The email must be a valid email address.',
|
||||
Validator::message('email.email')
|
||||
);
|
||||
$this->assertSame(
|
||||
'The age must be between 5 and 10.',
|
||||
Validator::message('age.between:5,10')
|
||||
);
|
||||
$this->assertSame(
|
||||
'The selected status is invalid. Allowed: a, b, c.',
|
||||
Validator::message('status.enum:a,b,c')
|
||||
);
|
||||
$this->assertSame(
|
||||
'The reason field is required when mode is other.',
|
||||
Validator::message('reason.required_if:mode,other')
|
||||
);
|
||||
}
|
||||
|
||||
public function testMessageRespectsAttributesAndOverrides(): void
|
||||
{
|
||||
$this->assertSame(
|
||||
'The user name field is required.',
|
||||
Validator::message('username.required', [], ['username' => 'user name'])
|
||||
);
|
||||
$this->assertSame(
|
||||
'custom',
|
||||
Validator::message('username.min:3', ['username.min:3' => 'custom'])
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user