<?php
declare(strict_types=1);
require __DIR__ . '/../src/ContextualValidator.php';
use ContextValidate\ContextualValidator;
$failures = 0;
$passed = 0;
/**
* @param mixed $expected
* @param mixed $actual
*/
function assertSame_($expected, $actual, string $label): void
{
global $failures, $passed;
if ($expected === $actual) {
$passed++;
echo " [OK] {$label}\n";
} else {
$failures++;
echo " [FAIL] {$label}\n";
echo ' expected: ' . var_export($expected, true) . "\n";
echo ' actual: ' . var_export($actual, true) . "\n";
}
}
function assertTrue_(bool $condition, string $label): void
{
assertSame_(true, $condition, $label);
}
// --- Test 1: postal code / country coherence --------------------------------
echo "Test 1: postal code vs country\n";
$v = new ContextualValidator(['zip' => '90210', 'country' => 'US']);
$v->postalCodeMatchesCountry('zip', 'country');
assertTrue_($v->passes(), 'valid US zip passes');
$v = new ContextualValidator(['zip' => '90210-1234', 'country' => 'FR']);
$v->postalCodeMatchesCountry('zip', 'country');
assertTrue_($v->fails(), 'US zip+4 fails for FR (FR expects exactly 5 digits)');
assertTrue_(isset($v->errors()['zip']), 'error attached to zip field');
$v = new ContextualValidator(['zip' => '75008', 'country' => 'FR']);
$v->postalCodeMatchesCountry('zip', 'country');
assertTrue_($v->passes(), 'valid FR postal code passes');
$v = new ContextualValidator(['zip' => 'anything', 'country' => 'ZZ']);
$v->postalCodeMatchesCountry('zip', 'country');
assertTrue_($v->passes(), 'unknown country is skipped, not flagged invalid');
$v = new ContextualValidator(['zip' => '', 'country' => 'FR']);
$v->postalCodeMatchesCountry('zip', 'country');
assertTrue_($v->passes(), 'empty postal code is skipped (not this rule\'s job)');
// --- Test 2: date range -----------------------------------------------------
echo "\nTest 2: date range\n";
$v = new ContextualValidator(['start_date' => '2026-01-01', 'end_date' => '2026-01-10']);
$v->dateRange('start_date', 'end_date');
assertTrue_($v->passes(), 'start before end passes');
$v = new ContextualValidator(['start_date' => '2026-01-10', 'end_date' => '2026-01-01']);
$v->dateRange('start_date', 'end_date');
assertTrue_($v->fails(), 'start after end fails');
$v = new ContextualValidator(['start_date' => '2026-01-01', 'end_date' => '2026-01-01']);
$v->dateRange('start_date', 'end_date');
assertTrue_($v->passes(), 'equal dates pass by default');
$v = new ContextualValidator(['start_date' => '2026-01-01', 'end_date' => '2026-01-01']);
$v->dateRange('start_date', 'end_date', false);
assertTrue_($v->fails(), 'equal dates fail when allowEqual=false');
// --- Test 3: age vs birthdate ------------------------------------------------
echo "\nTest 3: age vs birthdate\n";
$ref = new \DateTimeImmutable('2026-07-20');
$v = new ContextualValidator(['age' => '36', 'birthdate' => '1990-01-15']);
$v->ageMatchesBirthdate('age', 'birthdate', 0, $ref);
assertTrue_($v->passes(), 'age matches birthdate as of reference date');
$v = new ContextualValidator(['age' => '15', 'birthdate' => '1990-01-15']);
$v->ageMatchesBirthdate('age', 'birthdate', 0, $ref);
assertTrue_($v->fails(), 'mismatched age flagged');
$v = new ContextualValidator(['age' => '37', 'birthdate' => '1990-01-15']);
$v->ageMatchesBirthdate('age', 'birthdate', 1, $ref);
assertTrue_($v->passes(), 'off-by-one tolerated when toleranceYears=1');
// --- Test 4: phone vs country -------------------------------------------------
echo "\nTest 4: phone vs country\n";
$v = new ContextualValidator(['phone' => '212-555-0100', 'country' => 'US']);
$v->phoneMatchesCountry('phone', 'country');
assertTrue_($v->passes(), 'plausible US phone passes');
$v = new ContextualValidator(['phone' => '5550100', 'country' => 'US']);
$v->phoneMatchesCountry('phone', 'country');
assertTrue_($v->fails(), 'too-short number for US fails');
$v = new ContextualValidator(['phone' => '+1 212 555 0100', 'country' => 'US']);
$v->phoneMatchesCountry('phone', 'country');
assertTrue_($v->passes(), 'phone with country calling code prefix still passes');
// --- Test 5: fields match -----------------------------------------------------
echo "\nTest 5: fields match\n";
$v = new ContextualValidator(['password' => 'secret123', 'password_confirmation' => 'secret123']);
$v->fieldsMatch('password', 'password_confirmation');
assertTrue_($v->passes(), 'matching passwords pass');
$v = new ContextualValidator(['password' => 'secret123', 'password_confirmation' => 'different']);
$v->fieldsMatch('password', 'password_confirmation');
assertTrue_($v->fails(), 'mismatched passwords fail');
// --- Test 6: numeric range ----------------------------------------------------
echo "\nTest 6: numeric range\n";
$v = new ContextualValidator(['min_price' => '10', 'max_price' => '50']);
$v->numericRange('min_price', 'max_price');
assertTrue_($v->passes(), 'min <= max passes');
$v = new ContextualValidator(['min_price' => '50', 'max_price' => '10']);
$v->numericRange('min_price', 'max_price');
assertTrue_($v->fails(), 'min > max fails');
// --- Test 7: custom rule -------------------------------------------------------
echo "\nTest 7: custom rule\n";
$v = new ContextualValidator(['discount' => 20, 'price' => 15]);
$v->custom('discount', function (array $data) {
return $data['discount'] <= $data['price'] ? true : 'Discount cannot exceed price.';
});
assertTrue_($v->fails(), 'custom rule can fail');
$v = new ContextualValidator(['discount' => 5, 'price' => 15]);
$v->custom('discount', function (array $data) {
return $data['discount'] <= $data['price'] ? true : 'Discount cannot exceed price.';
});
assertTrue_($v->passes(), 'custom rule can pass');
// --- Test 8: chaining and multiple errors -------------------------------------
echo "\nTest 8: chaining multiple rules\n";
$v = new ContextualValidator([
'zip' => '90210-1234',
'country' => 'FR',
'start_date' => '2026-05-10',
'end_date' => '2026-05-01',
'password' => 'a',
'password_confirmation' => 'b',
]);
$v->postalCodeMatchesCountry('zip', 'country')
->dateRange('start_date', 'end_date')
->fieldsMatch('password', 'password_confirmation');
assertTrue_($v->fails(), 'multiple broken rules fail overall');
assertSame_(3, count($v->errors()), 'three distinct fields carry errors');
assertSame_(3, count($v->allMessages()), 'three flattened messages');
// --- Test 9: extending postal code patterns -----------------------------------
echo "\nTest 9: custom postal code pattern\n";
ContextualValidator::addPostalCodePattern('CM', '/^\d{5}$/');
$v = new ContextualValidator(['zip' => '12345', 'country' => 'CM']);
$v->postalCodeMatchesCountry('zip', 'country');
assertTrue_($v->passes(), 'custom pattern overrides built-in permissive default');
$v = new ContextualValidator(['zip' => 'abc', 'country' => 'CM']);
$v->postalCodeMatchesCountry('zip', 'country');
assertTrue_($v->fails(), 'custom pattern correctly rejects non-matching value');
echo "\n---\n{$passed} passed, {$failures} failed\n";
exit($failures > 0 ? 1 : 0);
|