Skip to content

Commit a2cdbb0

Browse files
committed
refactor: reorganize test suite into focused files
- Split monolithic ValidatorTest.php into 8 focused test files - Create AssociativeValidatorTest.php for associative array validation - Create ObjectValidatorTest.php for stdClass object validation - Create ArrayValidatorTest.php for plain array validation - Create StringValidatorTest.php for string validation and formats - Create IntValidatorTest.php for integer validation - Create FloatValidatorTest.php for float validation - Create FieldValidatorTest.php for base validator functionality - Create NumericConstraintsTraitTest.php for trait-specific tests - Improve test organization and maintainability - All 57 tests pass with 154 assertions
1 parent e281077 commit a2cdbb0

9 files changed

Lines changed: 610 additions & 326 deletions

tests/ArrayValidatorTest.php

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
<?php
2+
3+
use Lemmon\Validator;
4+
5+
it('should validate plain arrays', function () {
6+
$validator = Validator::isArray();
7+
8+
$data = $validator->validate([1, 2, 3, 'foo']);
9+
expect($data)->toBe([1, 2, 3, 'foo']);
10+
11+
$data = $validator->validate(['a', 'b', 'c']);
12+
expect($data)->toBe(['a', 'b', 'c']);
13+
14+
$data = $validator->validate([]);
15+
expect($data)->toBe([]);
16+
});
17+
18+
it('should reject associative arrays', function () {
19+
$validator = Validator::isArray();
20+
21+
$validator->validate(['key' => 'value']);
22+
})->throws(Lemmon\ValidationException::class);
23+
24+
it('should validate array items with type validator', function () {
25+
$validator = Validator::isArray()->items(Validator::isString());
26+
27+
$data = $validator->validate(['foo', 'bar', 'baz']);
28+
expect($data)->toBe(['foo', 'bar', 'baz']);
29+
});
30+
31+
it('should reject array items that do not match type validator', function () {
32+
$validator = Validator::isArray()->items(Validator::isString());
33+
34+
$validator->validate(['foo', 123, 'baz']);
35+
})->throws(Lemmon\ValidationException::class);
36+
37+
it('should coerce string to single-item array', function () {
38+
$validator = Validator::isArray()->coerce();
39+
40+
// Any string becomes single-item array
41+
$data = $validator->validate('single');
42+
expect($data)->toBe(['single']);
43+
44+
$data = $validator->validate('[1,2,3]');
45+
expect($data)->toBe(['[1,2,3]']);
46+
47+
$data = $validator->validate('a,b,c');
48+
expect($data)->toBe(['a,b,c']);
49+
});
50+
51+
it('should coerce associative arrays to indexed arrays', function () {
52+
$validator = Validator::isArray()->coerce();
53+
54+
$data = $validator->validate(['key1' => 'value1', 'key2' => 'value2']);
55+
expect($data)->toBe(['value1', 'value2']);
56+
57+
$data = $validator->validate(['a' => 1, 'b' => 2, 'c' => 3]);
58+
expect($data)->toBe([1, 2, 3]);
59+
});
60+
61+
it('should coerce scalar values to array', function () {
62+
$validator = Validator::isArray()->coerce();
63+
64+
$data = $validator->validate(123);
65+
expect($data)->toBe([123]);
66+
67+
$data = $validator->validate(true);
68+
expect($data)->toBe([true]);
69+
});
70+
71+
it('should work with required and default values', function () {
72+
$validator = Validator::isArray()->default(['default']);
73+
74+
$data = $validator->validate(null, 'test', []);
75+
expect($data)->toBe(['default']);
76+
77+
// Test with required
78+
$validator = Validator::isArray()->required();
79+
$validator->validate(null, 'test', []);
80+
})->throws(Lemmon\ValidationException::class);
81+
82+
it('should work with oneOf constraint', function () {
83+
$validator = Validator::isArray()->oneOf([[1, 2], [3, 4]]);
84+
85+
$data = $validator->validate([1, 2]);
86+
expect($data)->toBe([1, 2]);
87+
88+
$validator->validate([1, 2, 3]);
89+
})->throws(Lemmon\ValidationException::class);
90+
91+
it('should handle null correctly with coercion', function () {
92+
// Without required - null should stay null
93+
$validator = Validator::isArray()->coerce();
94+
$data = $validator->validate(null, 'test', []);
95+
expect($data)->toBe(null);
96+
97+
// With default - null should use default
98+
$validator = Validator::isArray()->coerce()->default(['default']);
99+
$data = $validator->validate(null, 'test', []);
100+
expect($data)->toBe(['default']);
101+
102+
// With required - null should throw error
103+
$validator = Validator::isArray()->coerce()->required();
104+
$validator->validate(null, 'test', []);
105+
})->throws(Lemmon\ValidationException::class);
106+
107+
it('should allow null for optional array validator', function () {
108+
$validator = Validator::isArray();
109+
[$valid, $data, $errors] = $validator->tryValidate(null);
110+
expect($valid)->toBe(true);
111+
expect($data)->toBe(null);
112+
expect($errors)->toBe(null);
113+
});
114+
115+
it('should coerce empty string to empty array', function () {
116+
$validator = Validator::isArray()->coerce();
117+
$data = $validator->validate('');
118+
expect($data)->toBe([]);
119+
});
120+
121+
it('should nullify empty string and empty array when nullifyEmpty is called', function () {
122+
$arrayValidator = Validator::isArray()->nullifyEmpty();
123+
expect($arrayValidator->validate([]))->toBe(null);
124+
125+
// Should not nullify non-empty values
126+
expect($arrayValidator->validate([1, 2]))->toBe([1, 2]);
127+
});

tests/AssociativeValidatorTest.php

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
<?php
2+
3+
use Lemmon\Validator;
4+
5+
it('should validate a correct payload', function () {
6+
$schema = Validator::isAssociative([
7+
'required' => Validator::isString()->required(),
8+
'optional' => Validator::isString(),
9+
'forced' => Validator::isString()->default('Hello!'),
10+
'level' => Validator::isInt()->coerce()->oneOf([3, 5, 8])->default(3),
11+
'override' => Validator::isBool()->coerce()->default(false),
12+
])->coerceAll();
13+
14+
$input = [
15+
'required' => 'test',
16+
'level' => '5',
17+
];
18+
19+
$data = $schema->validate($input);
20+
21+
expect($data)->toBe([
22+
'required' => 'test',
23+
'optional' => null,
24+
'forced' => 'Hello!',
25+
'level' => 5,
26+
'override' => false,
27+
]);
28+
});
29+
30+
it('should throw a validation exception for invalid payload', function () {
31+
$schema = Validator::isAssociative([
32+
'required' => Validator::isString()->required(),
33+
'level' => Validator::isInt()->oneOf([3, 5, 8]),
34+
]);
35+
36+
$input = [
37+
'level' => 10,
38+
];
39+
40+
$schema->validate($input);
41+
})->throws(Lemmon\ValidationException::class);
42+
43+
it('should throw a validation exception for non-array input in AssociativeValidator', function () {
44+
$schema = Validator::isAssociative([]);
45+
46+
try {
47+
$schema->validate('not an array');
48+
} catch (Lemmon\ValidationException $e) {
49+
expect($e->getErrors())->toBe(['Input must be an associative array.']);
50+
}
51+
});
52+
53+
it('should validate an empty associative array with an empty schema', function () {
54+
$schema = Validator::isAssociative([]);
55+
$data = $schema->validate([]);
56+
expect($data)->toBe([]);
57+
});
58+
59+
it('should allow Validator::isAssociative() to be called without arguments', function () {
60+
$schema = Validator::isAssociative();
61+
$data = $schema->validate([]);
62+
expect($data)->toBe([]);
63+
});
64+
65+
it('should handle null input for AssociativeValidator created without arguments gracefully', function () {
66+
$schema = Validator::isAssociative();
67+
[$valid, $data, $errors] = $schema->tryValidate(null);
68+
expect($valid)->toBe(true);
69+
expect($data)->toBe(null);
70+
expect($errors)->toBe(null);
71+
});
72+
73+
it('should allow null for optional associative array validator', function () {
74+
$validator = Validator::isAssociative();
75+
$data = $validator->validate(null);
76+
expect($data)->toBe(null);
77+
});
78+
79+
it('should allow null for optional nested associative array in schema', function () {
80+
$schema = Validator::isAssociative([
81+
'nested' => Validator::isAssociative(),
82+
]);
83+
84+
$input = [
85+
'nested' => null,
86+
];
87+
88+
$data = $schema->validate($input);
89+
expect($data)->toBe(['nested' => null]);
90+
});
91+
92+
it('should coerce stdClass object to associative array when coerce is enabled', function () {
93+
$schema = Lemmon\Validator::isAssociative([
94+
'name' => Lemmon\Validator::isString(),
95+
'age' => Lemmon\Validator::isInt(),
96+
])->coerce();
97+
98+
$object = new stdClass();
99+
$object->name = 'John Doe';
100+
$object->age = 42;
101+
102+
$validated = $schema->validate($object);
103+
104+
expect($validated)->toBe([
105+
'name' => 'John Doe',
106+
'age' => 42,
107+
]);
108+
});
109+
110+
it('should fail to validate stdClass object when coerce is not enabled', function () {
111+
$schema = Lemmon\Validator::isAssociative([
112+
'name' => Lemmon\Validator::isString(),
113+
]);
114+
115+
$object = new stdClass();
116+
$object->name = 'John Doe';
117+
118+
$schema->validate($object);
119+
})->throws(Lemmon\ValidationException::class, 'Input must be an associative array.');

tests/FieldValidatorTest.php

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
use Lemmon\Validator;
4+
5+
it('should throw a validation exception with a generic message for standalone validators', function () {
6+
try {
7+
Validator::isString()->validate(123);
8+
} catch (Lemmon\ValidationException $e) {
9+
expect($e->getErrors())->toBe(['Value must be a string.']);
10+
}
11+
});
12+
13+
it('should return a result tuple for standalone validators', function () {
14+
[$valid, $data, $errors] = Validator::isString()->tryValidate('hello');
15+
expect($valid)->toBe(true);
16+
expect($data)->toBe('hello');
17+
expect($errors)->toBe(null);
18+
19+
[$valid, $data, $errors] = Validator::isString()->tryValidate(123);
20+
expect($valid)->toBe(false);
21+
expect($data)->toBe(123);
22+
expect($errors)->toBe(['Value must be a string.']);
23+
});
24+
25+
it('should collect all validation errors', function () {
26+
$validator = Validator::isString()->minLength(10)->maxLength(3)->email();
27+
28+
[$valid, $data, $errors] = $validator->tryValidate('short');
29+
30+
expect($valid)->toBe(false);
31+
expect($errors)->toHaveCount(3);
32+
expect($errors)->toContain('Value must be at least 10 characters long.');
33+
expect($errors)->toContain('Value must be at most 3 characters long.');
34+
expect($errors)->toContain('Value must be a valid email address.');
35+
});
36+
37+
it('should pass context to custom validators', function () {
38+
$validator = Validator::isString()->addValidation(
39+
function ($value, $key, $input) {
40+
return $key === 'test' && is_array($input) && isset($input['other']);
41+
},
42+
'Custom validation failed.'
43+
);
44+
45+
[$valid, $data, $errors] = $validator->tryValidate('value', 'test', ['other' => 'data']);
46+
expect($valid)->toBe(true);
47+
48+
[$valid, $data, $errors] = $validator->tryValidate('value', 'wrong', ['other' => 'data']);
49+
expect($valid)->toBe(false);
50+
expect($errors)->toContain('Custom validation failed.');
51+
});
52+
53+
it('should validate allOf combinator', function () {
54+
$validator = Validator::isString()->allOf([
55+
Validator::isString()->minLength(3),
56+
Validator::isString()->maxLength(10),
57+
Validator::isString()->pattern('/^[a-z]+$/')
58+
]);
59+
60+
expect($validator->validate('hello'))->toBe('hello');
61+
62+
$validator->validate('hi'); // Too short
63+
})->throws(Lemmon\ValidationException::class, 'Value must satisfy all validation rules.');
64+
65+
it('should validate anyOf combinator', function () {
66+
$validator = Validator::isString()->anyOf([
67+
Validator::isString()->email(),
68+
Validator::isString()->url(),
69+
Validator::isString()->uuid()
70+
]);
71+
72+
expect($validator->validate('test@example.com'))->toBe('test@example.com');
73+
expect($validator->validate('https://example.com'))->toBe('https://example.com');
74+
expect($validator->validate('550e8400-e29b-41d4-a716-446655440000'))->toBe('550e8400-e29b-41d4-a716-446655440000');
75+
76+
$validator->validate('invalid-value');
77+
})->throws(Lemmon\ValidationException::class, 'Value must satisfy at least one validation rule.');
78+
79+
it('should validate not combinator', function () {
80+
$validator = Validator::isString()->not(Validator::isString()->email());
81+
82+
expect($validator->validate('not-an-email'))->toBe('not-an-email');
83+
expect($validator->validate('hello world'))->toBe('hello world');
84+
85+
$validator->validate('test@example.com');
86+
})->throws(Lemmon\ValidationException::class, 'Value must not satisfy the validation rule.');

tests/FloatValidatorTest.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
use Lemmon\Validator;
4+
5+
it('should validate floats', function () {
6+
$validator = Validator::isFloat();
7+
8+
expect($validator->validate(42.5))->toBe(42.5);
9+
expect($validator->validate(100))->toBe(100.0);
10+
expect($validator->validate('123.45'))->toBe(123.45);
11+
12+
$validator->validate('not-a-float');
13+
})->throws(Lemmon\ValidationException::class, 'Value must be a float.');
14+
15+
it('should validate float ranges', function () {
16+
$rangeValidator = Validator::isFloat()->min(10)->max(100);
17+
18+
expect($rangeValidator->validate(50))->toBe(50.0);
19+
expect($rangeValidator->validate(10))->toBe(10.0);
20+
expect($rangeValidator->validate(100))->toBe(100.0);
21+
22+
$rangeValidator->validate(5);
23+
})->throws(Lemmon\ValidationException::class);
24+
25+
it('should validate float multiples', function () {
26+
$multipleValidator = Validator::isFloat()->multipleOf(5);
27+
28+
expect($multipleValidator->validate(15))->toBe(15.0);
29+
expect($multipleValidator->validate(20))->toBe(20.0);
30+
31+
$multipleValidator->validate(13);
32+
})->throws(Lemmon\ValidationException::class, 'Value must be a multiple of 5.');
33+
34+
it('should validate positive floats', function () {
35+
$positiveValidator = Validator::isFloat()->positive();
36+
37+
expect($positiveValidator->validate(1))->toBe(1.0);
38+
expect($positiveValidator->validate(0.1))->toBe(0.1);
39+
40+
$positiveValidator->validate(-1);
41+
})->throws(Lemmon\ValidationException::class, 'Value must be positive.');

0 commit comments

Comments
 (0)