Compare commits
10 Commits
master
...
48d3ed6b3f
| Author | SHA1 | Date | |
|---|---|---|---|
| 48d3ed6b3f | |||
| d9ac4c3f14 | |||
| d594aa3ec1 | |||
| bf03e10b54 | |||
| f1bda46723 | |||
| dbbde42fe7 | |||
| 7494ec5fda | |||
| c0e5cf79d0 | |||
| a4fcd21492 | |||
| a4611e8718 |
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/vendor/
|
||||
/.phpunit.cache/
|
||||
*.sqlite
|
||||
/.publish/
|
||||
96
Makefile
Normal file
96
Makefile
Normal file
@@ -0,0 +1,96 @@
|
||||
# DuckBrain development harness — Makefile
|
||||
#
|
||||
# This file (along with tests/, phpunit.xml, composer.*, .gitignore) lives
|
||||
# ONLY on the develop branch. master is a publish-only vitrine that contains
|
||||
# nothing but the readable artifact: nobody commits to master directly, and
|
||||
# no file outside the WHITELIST below ever reaches it.
|
||||
#
|
||||
# Daily loop:
|
||||
# make test run the unit suite (sqlite :memory:, no containers, no
|
||||
# network; installs dev dependencies first if missing)
|
||||
# ...then develop on develop and commit there as usual.
|
||||
#
|
||||
# Publishing ritual (human only — AI agents must never run it; it commits):
|
||||
# 1. make publish MSG="feat: ..." (MSG optional, defaults to "sync: <date>")
|
||||
# 2. review the new master commit (git log -1 refs/heads/master); if the
|
||||
# generated message needs polish, rewrite it BEFORE pushing, e.g.
|
||||
# from Emacs
|
||||
# 3. git push origin master (manual on purpose: pushing is a
|
||||
# decision, never a side effect)
|
||||
#
|
||||
# How publish builds master — wipe-and-rebuild mirror, no merges, no
|
||||
# cherry-picks (see design decisions behind establish-dev-publish-workflow):
|
||||
# - refuses to run off develop or with a dirty working tree
|
||||
# - checks master out into a disposable worktree under .publish/
|
||||
# (your current working tree is never touched)
|
||||
# - for each WHITELIST path: removes master's copy and checks out develop's,
|
||||
# so deleted/renamed artifact files never survive as ghosts in master
|
||||
# - BLACKLIST then subtracts noise found INSIDE whitelisted paths
|
||||
# - creates the commit only when the artifact actually changed; its body
|
||||
# lists the develop commits (after the last-sync tag) that touched
|
||||
# WHITELIST paths — pure-noise commits are filtered out automatically
|
||||
# - moves the last-sync tag to develop's tip, removes the worktree, and
|
||||
# prints any root entry left out of WHITELIST (add it if it belongs to
|
||||
# the artifact)
|
||||
|
||||
SHELL := /bin/bash
|
||||
|
||||
.PHONY: test publish
|
||||
|
||||
PHPUNIT := vendor/bin/phpunit
|
||||
|
||||
# --- publish configuration -------------------------------------------------
|
||||
WHITELIST := src config.php index.php autoload.php .htaccess readme.org
|
||||
BLACKLIST :=
|
||||
DEVELOP_BRANCH := develop
|
||||
DEVELOP_REF := refs/heads/$(DEVELOP_BRANCH)
|
||||
MASTER_BRANCH := master
|
||||
MASTER_REF := refs/heads/$(MASTER_BRANCH)
|
||||
LAST_SYNC_TAG := last-sync
|
||||
MSG ?= sync: $(shell date +%Y-%m-%d)
|
||||
|
||||
test: $(PHPUNIT)
|
||||
./$(PHPUNIT)
|
||||
|
||||
$(PHPUNIT): composer.json composer.lock
|
||||
composer install --no-interaction
|
||||
|
||||
publish:
|
||||
@if [ "$$(git branch --show-current)" != "$(DEVELOP_BRANCH)" ]; then \
|
||||
echo "ABORT: publish must be invoked from $(DEVELOP_BRANCH) (current: '$$(git branch --show-current)')"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@if [ -n "$$(git status --porcelain)" ]; then \
|
||||
echo "ABORT: working tree is dirty; commit your changes on $(DEVELOP_BRANCH) before publishing:"; \
|
||||
git status --short; \
|
||||
exit 1; \
|
||||
fi
|
||||
git worktree remove --force .publish/master 2>/dev/null || true
|
||||
git worktree add --quiet .publish/master $(MASTER_BRANCH)
|
||||
@test "$$(git -C .publish/master symbolic-ref HEAD)" = "$(MASTER_REF)" || { \
|
||||
echo "ABORT: publish worktree HEAD is not attached to $(MASTER_REF)"; \
|
||||
git worktree remove --force .publish/master; exit 1; }
|
||||
@for w in $(WHITELIST); do git -C .publish/master rm -rf --ignore-unmatch --quiet -- $$w; done
|
||||
git -C .publish/master checkout $(DEVELOP_REF) -- $(WHITELIST)
|
||||
@for b in $(BLACKLIST); do git -C .publish/master rm -rf --ignore-unmatch --quiet -- $$b; done
|
||||
git -C .publish/master diff --quiet $(DEVELOP_REF) -- $(WHITELIST)
|
||||
@if git -C .publish/master diff --cached --quiet; then \
|
||||
echo "nothing to publish: master already mirrors $(DEVELOP_BRANCH)"; \
|
||||
else \
|
||||
set -e; \
|
||||
if git rev-parse -q --verify tags/$(LAST_SYNC_TAG) >/dev/null; then \
|
||||
RANGE=$(LAST_SYNC_TAG)..$(DEVELOP_REF); \
|
||||
else \
|
||||
echo "note: no $(LAST_SYNC_TAG) tag yet; summarizing full $(DEVELOP_BRANCH) history"; \
|
||||
RANGE=$(DEVELOP_REF); \
|
||||
fi; \
|
||||
BODY=$$(git log $$RANGE --oneline -- $(WHITELIST)); \
|
||||
{ printf '%s\n' "$(MSG)"; printf '\n'; printf '%s\n' "$$BODY"; } > .publish/msg.txt; \
|
||||
git -C .publish/master commit --quiet -F "$$(pwd)/.publish/msg.txt"; \
|
||||
echo "publish commit created; review/edit it (before pushing) with your git tool of choice"; \
|
||||
fi
|
||||
git tag -f $(LAST_SYNC_TAG) $(DEVELOP_REF) >/dev/null
|
||||
git worktree remove .publish/master
|
||||
@echo "root entries left behind in $(DEVELOP_BRANCH) (add to WHITELIST if they belong to the artifact):"; \
|
||||
comm -23 <(git ls-tree --name-only $(DEVELOP_REF) | sort) \
|
||||
<(for x in $(WHITELIST) $(BLACKLIST); do echo $$x; done | sort) | sed 's/^/ - /'
|
||||
15
composer.json
Normal file
15
composer.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "kj/duckbrain",
|
||||
"description": "DuckBrain microframework - development harness (tests and tooling; never published to master).",
|
||||
"type": "project",
|
||||
"license": "MIT",
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.24",
|
||||
"phpunit/phpunit": "^11.0"
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
}
|
||||
}
|
||||
1970
composer.lock
generated
Normal file
1970
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
phpunit.xml
Normal file
22
phpunit.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||
bootstrap="tests/bootstrap.php"
|
||||
colors="true"
|
||||
cacheDirectory=".phpunit.cache"
|
||||
executionOrder="depends,defects"
|
||||
requireCoverageMetadata="false"
|
||||
beStrictAboutOutputDuringTests="true"
|
||||
failOnRisky="true"
|
||||
failOnWarning="true">
|
||||
<testsuites>
|
||||
<testsuite name="Unit">
|
||||
<directory>tests/Unit</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<source>
|
||||
<include>
|
||||
<directory suffix=".php">src</directory>
|
||||
</include>
|
||||
</source>
|
||||
</phpunit>
|
||||
55
tests/Factories/Factory.php
Normal file
55
tests/Factories/Factory.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Factories;
|
||||
|
||||
use Faker\Factory as FakerFactory;
|
||||
use Faker\Generator;
|
||||
|
||||
/**
|
||||
* Factory - DuckBrain testing
|
||||
*
|
||||
* Clase base para crear fábricas de datos de prueba. Cada subclase
|
||||
* define los atributos por defecto de un modelo y construye su
|
||||
* instancia.
|
||||
*
|
||||
* @author KJ
|
||||
* @website https://kj2.me
|
||||
* @license MIT
|
||||
*/
|
||||
abstract class Factory
|
||||
{
|
||||
protected static ?Generator $fake = null;
|
||||
|
||||
/**
|
||||
* Devuelve una instancia de Faker (singleton por fábrica).
|
||||
*/
|
||||
protected static function faker(): Generator
|
||||
{
|
||||
return static::$fake ??= FakerFactory::create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fusiona los atributos por defecto con los indicados.
|
||||
*
|
||||
* @param array<string, mixed> $attributes
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected static function attributes(array $attributes = []): array
|
||||
{
|
||||
return array_merge(static::definition(), $attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Atributos por defecto de la fábrica.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
abstract protected static function definition(): array;
|
||||
|
||||
/**
|
||||
* Crea una instancia del modelo con los atributos dados.
|
||||
*
|
||||
* @param array<string, mixed> $attributes
|
||||
*/
|
||||
abstract public static function create(array $attributes = []): object;
|
||||
}
|
||||
61
tests/TestCase.php
Normal file
61
tests/TestCase.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Libs\Database;
|
||||
use PDO;
|
||||
use PHPUnit\Framework\TestCase as FrameworkTestCase;
|
||||
|
||||
/**
|
||||
* TestCase - DuckBrain test harness
|
||||
*
|
||||
* Base class for the framework's own tests. Deliberately lightweight: this
|
||||
* repo has no migrations/ nor duckbrain-commands, so each test defines its
|
||||
* schema with raw DDL through createTable(), and tables registered there are
|
||||
* dropped automatically after the class finishes.
|
||||
*/
|
||||
abstract class TestCase extends FrameworkTestCase
|
||||
{
|
||||
/**
|
||||
* @var list<string> Tables created through createTable() for this class.
|
||||
*/
|
||||
private static array $tables = [];
|
||||
|
||||
/**
|
||||
* Returns the PDO connection to the in-memory test database.
|
||||
*
|
||||
* @return PDO
|
||||
*/
|
||||
protected static function db(): PDO
|
||||
{
|
||||
return Database::getInstance(DB_TYPE, DB_HOST, DB_NAME, DB_USER, DB_PASS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a table in the test database and registers it for cleanup.
|
||||
*
|
||||
* @param string $name
|
||||
* Table name.
|
||||
*
|
||||
* @param string $columns
|
||||
* Raw column definition, as accepted by the test
|
||||
* engine (sqlite): "id INTEGER PRIMARY KEY, x TEXT".
|
||||
*/
|
||||
protected static function createTable(string $name, string $columns): void
|
||||
{
|
||||
static::db()->exec("CREATE TABLE IF NOT EXISTS {$name} ({$columns})");
|
||||
static::$tables[] = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops every table registered through createTable() during this class.
|
||||
*/
|
||||
public static function tearDownAfterClass(): void
|
||||
{
|
||||
foreach (array_unique(static::$tables) as $table) {
|
||||
static::db()->exec("DROP TABLE IF EXISTS {$table}");
|
||||
}
|
||||
|
||||
static::$tables = [];
|
||||
}
|
||||
}
|
||||
0
tests/Unit/.gitkeep
Normal file
0
tests/Unit/.gitkeep
Normal file
51
tests/Unit/BootstrapTest.php
Normal file
51
tests/Unit/BootstrapTest.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use Libs\Neuron;
|
||||
use PDO;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* BootstrapTest - DuckBrain test harness
|
||||
*
|
||||
* Prueba de humo de la infraestructura: autoload de composer + framework,
|
||||
* constantes de la DB de prueba y conexión PDO realmente utilizable. Si
|
||||
* algo del entorno está mal, esta prueba falla primero con el diagnóstico.
|
||||
*/
|
||||
final class BootstrapTest extends TestCase
|
||||
{
|
||||
public function testFrameworkClassesAreAutoloaded(): void
|
||||
{
|
||||
$this->assertTrue(
|
||||
class_exists(Neuron::class),
|
||||
'MISSING AUTOLOAD: autoload.php did not resolve Libs\Neuron; check ROOT_CORE and run from the project root'
|
||||
);
|
||||
|
||||
$neuron = new Neuron(['key' => 'value']);
|
||||
$this->assertSame('value', $neuron->key);
|
||||
$this->assertNull($neuron->doesNotExist);
|
||||
}
|
||||
|
||||
public function testDatabaseConstantsPointToInMemorySqlite(): void
|
||||
{
|
||||
$this->assertSame(
|
||||
'sqlite',
|
||||
DB_TYPE,
|
||||
'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 should be :memory: so tests never touch disks or servers');
|
||||
}
|
||||
|
||||
public function testTestDatabaseIsUsable(): void
|
||||
{
|
||||
$db = self::db();
|
||||
$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(['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'])
|
||||
);
|
||||
}
|
||||
}
|
||||
16
tests/bootstrap.php
Normal file
16
tests/bootstrap.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
// Test bootstrap for the DuckBrain development harness.
|
||||
// Run PHPUnit from the project root (autoload.php resolves config.php via cwd).
|
||||
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
// Define DB constants BEFORE config.php so the test database wins over the
|
||||
// real configuration (config.php's define() warnings are suppressed by @).
|
||||
define('DB_TYPE', 'sqlite');
|
||||
define('DB_HOST', 'localhost');
|
||||
define('DB_NAME', ':memory:');
|
||||
define('DB_USER', '');
|
||||
define('DB_PASS', '');
|
||||
|
||||
@require_once __DIR__ . '/../autoload.php';
|
||||
Reference in New Issue
Block a user