['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')] #[Test] public function scalarRules(string $rule, mixed $value, bool $expected): void { $this->assertSame($expected, Validator::checkRule($value, $rule), "rule {$rule} with " . var_export($value, true)); } #[Test] public function existsAndRequiredDisagreeOnEmptyString(): void { $this->assertTrue(Validator::checkRule('', 'exists')); $this->assertFalse(Validator::checkRule('', 'required')); $this->assertFalse(Validator::checkRule(null, 'exists')); } #[Test] public function sizeRulesMeasureStringNumberAndArray(): 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')); } #[Test] public function regexKeepsColonsAndCommasInsidePattern(): void { $this->assertTrue(Validator::checkRule('a,b', 'regex:/^a,b$/')); $this->assertFalse(Validator::checkRule('a;b', 'regex:/^a,b$/')); } #[Test] public function enumIsLooseComparison(): void { $this->assertTrue(Validator::checkRule('1', 'enum:1,2,3')); $this->assertFalse(Validator::checkRule('9', 'enum:1,2,3')); } #[Test] public function notNegatesNextRule(): void { $this->assertTrue(Validator::checkRule('3.5', 'not:int')); $this->assertFalse(Validator::checkRule('42', 'not:int')); } #[Test] public function parseRule(): 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$/')); } #[Test] public function validateListPassesFullyValidBatch(): 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); } #[Test] public function validateListStopsAtFirstFailure(): 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); } #[Test] public function validateListNullableSkipsRestWhenEmpty(): 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']))); } #[Test] public function validateListConfirmedUsesSiblingField(): 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); } #[Test] public function messageBuildsHumanTextFromLastFailed(): 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') ); } #[Test] public function messageRespectsAttributesAndOverrides(): 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']) ); } }