validator = new Validator($reflectionProvider); }); test('validation passes for valid object', function () { $object = new ValidTestObject('test@example.com', 'John Doe'); $result = $this->validator->validate($object); expect($result->hasErrors())->toBeFalse(); }); test('validation fails for invalid email', function () { $object = new ValidTestObject('invalid-email', 'John Doe'); $result = $this->validator->validate($object); expect($result->hasErrors())->toBeTrue() ->and($result->getFieldErrors('email'))->not->toBeEmpty(); }); test('validation fails for empty required field', function () { $object = new ValidTestObject('test@example.com', ''); $result = $this->validator->validate($object); expect($result->hasErrors())->toBeTrue() ->and($result->getFieldErrors('name'))->not->toBeEmpty(); }); test('validation fails for too short string', function () { $object = new ValidTestObject('test@example.com', 'Jo'); // Too short $result = $this->validator->validate($object); expect($result->hasErrors())->toBeTrue() ->and($result->getFieldErrors('name'))->not->toBeEmpty(); }); test('validation with groups only validates specified group', function () { $object = new GroupedTestObject('test@example.com', ''); // Validate only 'basic' group - should pass $result = $this->validator->validate($object, 'basic'); expect($result->hasErrors())->toBeFalse(); // Validate 'extended' group - should fail due to empty name $result = $this->validator->validate($object, 'extended'); expect($result->hasErrors())->toBeTrue() ->and($result->getFieldErrors('name'))->not->toBeEmpty(); }); test('validation handles uninitialized non-nullable properties', function () { $object = new UninitializedTestObject(); $result = $this->validator->validate($object); expect($result->hasErrors())->toBeTrue() ->and($result->getFieldErrors('requiredField'))->not->toBeEmpty(); }); test('validation handles nullable properties correctly', function () { $object = new NullableTestObject(); $result = $this->validator->validate($object); expect($result->hasErrors())->toBeFalse(); }); test('multiple validation errors are collected', function () { $object = new ValidTestObject('invalid', ''); // Both email and name invalid $result = $this->validator->validate($object); expect($result->hasErrors())->toBeTrue() ->and($result->getFieldErrors('email'))->not->toBeEmpty() ->and($result->getFieldErrors('name'))->not->toBeEmpty() ->and($result->getAllErrorMessages())->toHaveCount(2); }); test('validation result can be merged', function () { $result1 = new ValidationResult(); $result1->addError('field1', 'Error 1'); $result2 = new ValidationResult(); $result2->addError('field2', 'Error 2'); $merged = $result1->merge($result2); expect($merged->getFieldErrors('field1'))->toContain('Error 1') ->and($merged->getFieldErrors('field2'))->toContain('Error 2') ->and($merged->getAllErrorMessages())->toHaveCount(2); }); // Test fixtures class ValidTestObject { public function __construct( #[Email] public string $email, #[Required] #[StringLength(min: 3, max: 50)] public string $name ) { } } class GroupedTestObject { public function __construct( #[Email] #[TestValidationGroup('basic')] public string $email, #[Required] #[TestValidationGroup('extended')] public string $name ) { } } class UninitializedTestObject { #[Required] public string $requiredField; } class NullableTestObject { #[Email] public ?string $optionalEmail = null; } // Custom validation rule for testing groups #[\Attribute(\Attribute::TARGET_PROPERTY)] class TestValidationGroup implements ValidationRule, GroupAware { public function __construct( private string $group ) { } public function validate(mixed $value): bool { return true; // Always pass - we're just testing group functionality } public function getErrorMessages(): array { return []; } public function belongsToGroup(string $group): bool { return $this->group === $group; } }