Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
211
tests/Framework/Validation/Rules/DateFormatTest.php
Normal file
211
tests/Framework/Validation/Rules/DateFormatTest.php
Normal file
@@ -0,0 +1,211 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\Validation\Rules\DateFormat;
|
||||
|
||||
test('validates empty values as true (handled by Required rule)', function () {
|
||||
$rule = new DateFormat('Y-m-d');
|
||||
|
||||
expect($rule->validate(null))->toBeTrue()
|
||||
->and($rule->validate(''))->toBeTrue();
|
||||
});
|
||||
|
||||
test('validates non-string values as false', function () {
|
||||
$rule = new DateFormat('Y-m-d');
|
||||
|
||||
expect($rule->validate(123))->toBeFalse()
|
||||
->and($rule->validate(['not', 'a', 'date']))->toBeFalse()
|
||||
->and($rule->validate(new stdClass()))->toBeFalse()
|
||||
->and($rule->validate(true))->toBeFalse()
|
||||
->and($rule->validate(12.34))->toBeFalse();
|
||||
});
|
||||
|
||||
test('validates Y-m-d format correctly', function () {
|
||||
$rule = new DateFormat('Y-m-d');
|
||||
|
||||
// Valid dates
|
||||
expect($rule->validate('2024-01-15'))->toBeTrue()
|
||||
->and($rule->validate('2023-12-31'))->toBeTrue()
|
||||
->and($rule->validate('2000-02-29'))->toBeTrue() // Leap year
|
||||
->and($rule->validate('1999-01-01'))->toBeTrue();
|
||||
|
||||
// Invalid dates
|
||||
expect($rule->validate('2024-13-01'))->toBeFalse() // Invalid month
|
||||
->and($rule->validate('2024-01-32'))->toBeFalse() // Invalid day
|
||||
->and($rule->validate('2023-02-29'))->toBeFalse() // Not a leap year
|
||||
->and($rule->validate('24-01-15'))->toBeFalse() // Wrong year format
|
||||
->and($rule->validate('2024/01/15'))->toBeFalse() // Wrong separator
|
||||
->and($rule->validate('15-01-2024'))->toBeFalse() // Wrong order
|
||||
->and($rule->validate('not-a-date'))->toBeFalse();
|
||||
});
|
||||
|
||||
test('validates d.m.Y format correctly (German format)', function () {
|
||||
$rule = new DateFormat('d.m.Y');
|
||||
|
||||
// Valid dates
|
||||
expect($rule->validate('15.01.2024'))->toBeTrue()
|
||||
->and($rule->validate('31.12.2023'))->toBeTrue()
|
||||
->and($rule->validate('29.02.2000'))->toBeTrue() // Leap year
|
||||
->and($rule->validate('01.01.1999'))->toBeTrue();
|
||||
|
||||
// Invalid dates
|
||||
expect($rule->validate('32.01.2024'))->toBeFalse() // Invalid day
|
||||
->and($rule->validate('15.13.2024'))->toBeFalse() // Invalid month
|
||||
->and($rule->validate('29.02.2023'))->toBeFalse() // Not a leap year
|
||||
->and($rule->validate('15-01-2024'))->toBeFalse() // Wrong separator
|
||||
->and($rule->validate('2024.01.15'))->toBeFalse() // Wrong order
|
||||
->and($rule->validate('15/01/2024'))->toBeFalse(); // Wrong separator
|
||||
});
|
||||
|
||||
test('validates Y-m-d H:i:s format correctly (datetime)', function () {
|
||||
$rule = new DateFormat('Y-m-d H:i:s');
|
||||
|
||||
// Valid datetimes
|
||||
expect($rule->validate('2024-01-15 14:30:25'))->toBeTrue()
|
||||
->and($rule->validate('2023-12-31 23:59:59'))->toBeTrue()
|
||||
->and($rule->validate('2000-01-01 00:00:00'))->toBeTrue();
|
||||
|
||||
// Invalid datetimes
|
||||
expect($rule->validate('2024-01-15 25:30:25'))->toBeFalse() // Invalid hour
|
||||
->and($rule->validate('2024-01-15 14:60:25'))->toBeFalse() // Invalid minute
|
||||
->and($rule->validate('2024-01-15 14:30:60'))->toBeFalse() // Invalid second
|
||||
->and($rule->validate('2024-01-15'))->toBeFalse() // Missing time
|
||||
->and($rule->validate('2024-01-15T14:30:25'))->toBeFalse(); // Wrong separator
|
||||
});
|
||||
|
||||
test('validates H:i format correctly (time only)', function () {
|
||||
$rule = new DateFormat('H:i');
|
||||
|
||||
// Valid times
|
||||
expect($rule->validate('14:30'))->toBeTrue()
|
||||
->and($rule->validate('00:00'))->toBeTrue()
|
||||
->and($rule->validate('23:59'))->toBeTrue();
|
||||
|
||||
// Invalid times
|
||||
expect($rule->validate('24:00'))->toBeFalse() // Invalid hour
|
||||
->and($rule->validate('14:60'))->toBeFalse() // Invalid minute
|
||||
->and($rule->validate('14:30:25'))->toBeFalse() // Too many parts
|
||||
->and($rule->validate('14'))->toBeFalse(); // Missing minute
|
||||
});
|
||||
|
||||
test('validates custom formats correctly', function () {
|
||||
// Month/Year format
|
||||
$monthYearRule = new DateFormat('m/Y');
|
||||
expect($monthYearRule->validate('01/2024'))->toBeTrue()
|
||||
->and($monthYearRule->validate('12/2023'))->toBeTrue()
|
||||
->and($monthYearRule->validate('13/2024'))->toBeFalse() // Invalid month
|
||||
->and($monthYearRule->validate('1/2024'))->toBeFalse(); // Single digit month (strict mode)
|
||||
|
||||
// Year only format
|
||||
$yearRule = new DateFormat('Y');
|
||||
expect($yearRule->validate('2024'))->toBeTrue()
|
||||
->and($yearRule->validate('1999'))->toBeTrue()
|
||||
->and($yearRule->validate('24'))->toBeFalse() // Wrong length
|
||||
->and($yearRule->validate('abcd'))->toBeFalse(); // Not a number
|
||||
});
|
||||
|
||||
test('strict mode validates exact format match', function () {
|
||||
$strictRule = new DateFormat('d.m.Y', strict: true);
|
||||
$nonStrictRule = new DateFormat('d.m.Y', strict: false);
|
||||
|
||||
// Diese Inputs sind technisch parsebar, aber nicht exakt im erwarteten Format
|
||||
$ambiguousInputs = [
|
||||
'5.1.2024', // Single digit day/month
|
||||
'05.1.2024', // Mixed format
|
||||
'5.01.2024', // Mixed format
|
||||
];
|
||||
|
||||
foreach ($ambiguousInputs as $input) {
|
||||
expect($strictRule->validate($input))->toBeFalse("Strict mode should reject: $input");
|
||||
expect($nonStrictRule->validate($input))->toBeTrue("Non-strict mode should accept: $input");
|
||||
}
|
||||
|
||||
// Exakt formatierte Inputs sollten in beiden Modi funktionieren
|
||||
expect($strictRule->validate('05.01.2024'))->toBeTrue()
|
||||
->and($nonStrictRule->validate('05.01.2024'))->toBeTrue();
|
||||
});
|
||||
|
||||
test('timezone parameter works correctly', function () {
|
||||
$utcRule = new DateFormat('Y-m-d H:i:s', timezone: 'UTC');
|
||||
$berlinRule = new DateFormat('Y-m-d H:i:s', timezone: 'Europe/Berlin');
|
||||
|
||||
$datetime = '2024-01-15 14:30:25';
|
||||
|
||||
// Beide sollten die gleiche Eingabe als gültig akzeptieren
|
||||
expect($utcRule->validate($datetime))->toBeTrue()
|
||||
->and($berlinRule->validate($datetime))->toBeTrue();
|
||||
});
|
||||
|
||||
test('default error message includes format and example', function () {
|
||||
$rule = new DateFormat('d.m.Y');
|
||||
$messages = $rule->getErrorMessages();
|
||||
|
||||
expect($messages)->toHaveCount(1);
|
||||
$message = $messages[0];
|
||||
|
||||
expect($message)->toContain('d.m.Y')
|
||||
->and($message)->toContain('15.01.2024'); // Should contain example
|
||||
});
|
||||
|
||||
test('custom error message overrides default', function () {
|
||||
$customRule = new DateFormat('Y-m-d', message: 'Falsches Datumsformat!');
|
||||
$messages = $customRule->getErrorMessages();
|
||||
|
||||
expect($messages)->toHaveCount(1)
|
||||
->and($messages[0])->toBe('Falsches Datumsformat!');
|
||||
});
|
||||
|
||||
test('validates common international date formats', function () {
|
||||
$formats = [
|
||||
'Y-m-d' => ['2024-01-15', '2024-12-31'], // ISO format
|
||||
'd/m/Y' => ['15/01/2024', '31/12/2024'], // UK format
|
||||
'm/d/Y' => ['01/15/2024', '12/31/2024'], // US format
|
||||
'd-m-Y' => ['15-01-2024', '31-12-2024'], // Alternative EU format
|
||||
'Y/m/d' => ['2024/01/15', '2024/12/31'], // Alternative ISO format
|
||||
'j.n.Y' => ['15.1.2024', '31.12.2024'], // German without leading zeros
|
||||
'F j, Y' => ['January 15, 2024', 'December 31, 2024'], // English text format
|
||||
'j F Y' => ['15 January 2024', '31 December 2024'], // Alternative English
|
||||
];
|
||||
|
||||
foreach ($formats as $format => $validDates) {
|
||||
$rule = new DateFormat($format, strict: false); // Non-strict for text formats
|
||||
|
||||
foreach ($validDates as $date) {
|
||||
expect($rule->validate($date))->toBeTrue("Format '$format' should accept '$date'");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('validates edge cases and special dates', function () {
|
||||
$rule = new DateFormat('Y-m-d');
|
||||
|
||||
// Leap years
|
||||
expect($rule->validate('2024-02-29'))->toBeTrue() // 2024 is leap year
|
||||
->and($rule->validate('2023-02-29'))->toBeFalse() // 2023 is not leap year
|
||||
->and($rule->validate('2000-02-29'))->toBeTrue() // 2000 is leap year
|
||||
->and($rule->validate('1900-02-29'))->toBeFalse(); // 1900 is not leap year
|
||||
|
||||
// Month boundaries
|
||||
expect($rule->validate('2024-04-31'))->toBeFalse() // April has 30 days
|
||||
->and($rule->validate('2024-04-30'))->toBeTrue()
|
||||
->and($rule->validate('2024-02-30'))->toBeFalse() // February never has 30 days
|
||||
->and($rule->validate('2024-02-28'))->toBeTrue();
|
||||
});
|
||||
|
||||
test('performance with many date validations', function () {
|
||||
$rule = new DateFormat('Y-m-d');
|
||||
$validDate = '2024-01-15';
|
||||
|
||||
$startTime = microtime(true);
|
||||
|
||||
for ($i = 0; $i < 1000; $i++) {
|
||||
$rule->validate($validDate);
|
||||
}
|
||||
|
||||
$endTime = microtime(true);
|
||||
$duration = $endTime - $startTime;
|
||||
|
||||
// Should complete 1000 validations in less than 100ms
|
||||
expect($duration)->toBeLessThan(0.1);
|
||||
});
|
||||
127
tests/Framework/Validation/Rules/PhoneTest.php
Normal file
127
tests/Framework/Validation/Rules/PhoneTest.php
Normal file
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\Validation\Rules\Phone;
|
||||
|
||||
describe('Phone Validation Rule', function () {
|
||||
it('passes validation for valid phone numbers', function () {
|
||||
$rule = new Phone();
|
||||
|
||||
$validNumbers = [
|
||||
'+49 123 456789',
|
||||
'+1 (555) 123-4567',
|
||||
'0123 456789',
|
||||
'+49-123-456-789',
|
||||
'+33 1 23 45 67 89',
|
||||
'12345678901',
|
||||
];
|
||||
|
||||
foreach ($validNumbers as $number) {
|
||||
$errors = $rule->validate('phone', $number);
|
||||
expect($errors)->toBeEmpty("Failed for: {$number}");
|
||||
}
|
||||
});
|
||||
|
||||
it('fails validation for invalid phone numbers', function () {
|
||||
$rule = new Phone();
|
||||
|
||||
$invalidNumbers = [
|
||||
'abc',
|
||||
'123', // too short
|
||||
'123456789012345678901', // too long
|
||||
'+49#123#456', // invalid characters
|
||||
'not-a-phone',
|
||||
'++49123456789', // double plus
|
||||
];
|
||||
|
||||
foreach ($invalidNumbers as $number) {
|
||||
$errors = $rule->validate('phone', $number);
|
||||
expect($errors)->not->toBeEmpty("Should fail for: {$number}");
|
||||
expect($errors)->toHaveKey('phone');
|
||||
}
|
||||
});
|
||||
|
||||
it('allows null and empty values', function () {
|
||||
$rule = new Phone();
|
||||
|
||||
expect($rule->validate('phone', null))->toBeEmpty();
|
||||
expect($rule->validate('phone', ''))->toBeEmpty();
|
||||
});
|
||||
|
||||
it('fails validation for non-string values', function () {
|
||||
$rule = new Phone();
|
||||
|
||||
$nonStringValues = [
|
||||
123456789,
|
||||
12.34,
|
||||
true,
|
||||
[],
|
||||
new stdClass(),
|
||||
];
|
||||
|
||||
foreach ($nonStringValues as $value) {
|
||||
$errors = $rule->validate('phone', $value);
|
||||
expect($errors)->not->toBeEmpty();
|
||||
expect($errors['phone'])->toBe('Phone number must be a string.');
|
||||
}
|
||||
});
|
||||
|
||||
it('uses custom error message', function () {
|
||||
$customMessage = 'Please enter a valid phone number.';
|
||||
$rule = new Phone($customMessage);
|
||||
|
||||
$errors = $rule->validate('phone', 'invalid');
|
||||
expect($errors['phone'])->toBe($customMessage);
|
||||
});
|
||||
|
||||
it('returns correct default message', function () {
|
||||
$rule = new Phone();
|
||||
expect($rule->getMessage())->toBe('The field must be a valid phone number.');
|
||||
});
|
||||
|
||||
it('validates real-world phone number formats', function () {
|
||||
$rule = new Phone();
|
||||
|
||||
$realWorldNumbers = [
|
||||
// German numbers
|
||||
'+49 30 12345678', // Berlin landline
|
||||
'+49 151 12345678', // Mobile
|
||||
'030 12345678', // National format
|
||||
|
||||
// US numbers
|
||||
'+1 212 555 1234', // New York
|
||||
'(212) 555-1234', // National format
|
||||
'212-555-1234', // Alternative format
|
||||
|
||||
// UK numbers
|
||||
'+44 20 7946 0958', // London
|
||||
|
||||
// French numbers
|
||||
'+33 1 23 45 67 89', // Paris
|
||||
|
||||
// International toll-free
|
||||
'+800 12345678', // International toll-free
|
||||
];
|
||||
|
||||
foreach ($realWorldNumbers as $number) {
|
||||
$errors = $rule->validate('phone', $number);
|
||||
expect($errors)->toBeEmpty("Failed for real-world number: {$number}");
|
||||
}
|
||||
});
|
||||
|
||||
it('handles edge cases gracefully', function () {
|
||||
$rule = new Phone();
|
||||
|
||||
$edgeCases = [
|
||||
'+1234567890123456789', // Very long but valid
|
||||
'+1 12345', // Short but valid
|
||||
' +49 123 456789 ', // With whitespace
|
||||
];
|
||||
|
||||
foreach ($edgeCases as $case) {
|
||||
$errors = $rule->validate('phone', $case);
|
||||
expect($errors)->toBeEmpty("Failed for edge case: {$case}");
|
||||
}
|
||||
});
|
||||
});
|
||||
150
tests/Framework/Validation/Rules/UlidTest.php
Normal file
150
tests/Framework/Validation/Rules/UlidTest.php
Normal file
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\Validation\Rules\Ulid;
|
||||
|
||||
beforeEach(function () {
|
||||
$this->rule = new Ulid();
|
||||
});
|
||||
|
||||
test('validates empty values as true (handled by Required rule)', function () {
|
||||
expect($this->rule->validate(null))->toBeTrue()
|
||||
->and($this->rule->validate(''))->toBeTrue();
|
||||
});
|
||||
|
||||
test('validates non-string values as false', function () {
|
||||
expect($this->rule->validate(123))->toBeFalse()
|
||||
->and($this->rule->validate(['not', 'a', 'ulid']))->toBeFalse()
|
||||
->and($this->rule->validate(new stdClass()))->toBeFalse()
|
||||
->and($this->rule->validate(true))->toBeFalse()
|
||||
->and($this->rule->validate(12.34))->toBeFalse();
|
||||
});
|
||||
|
||||
test('validates valid ULIDs', function () {
|
||||
$validUlids = [
|
||||
'01ARZ3NDEKTSV4RRFFQ69G5FAV', // Standard ULID format
|
||||
'01BX5ZZKBKACTAV9WEVGEMMVRY', // Another valid ULID
|
||||
'01CA2ZZKBKACTAV9WEVGEMMVRY', // Year 2022 timestamp
|
||||
'7ZZZZZZZZZZZZZZZZZZZZZZZZZ', // Max timestamp, max random
|
||||
'00000000000000000000000000', // Min ULID (all zeros)
|
||||
'0123456789ABCDEFGHJKMNPQRS', // Using all valid Crockford Base32 chars
|
||||
];
|
||||
|
||||
foreach ($validUlids as $ulid) {
|
||||
expect($this->rule->validate($ulid))->toBeTrue("ULID should be valid: $ulid");
|
||||
}
|
||||
});
|
||||
|
||||
test('validates invalid ULIDs as false', function () {
|
||||
$invalidUlids = [
|
||||
// Wrong length
|
||||
'01ARZ3NDEK', // Too short
|
||||
'01ARZ3NDEKTSV4RRFFQ69G5FAVX', // Too long (27 chars)
|
||||
'01ARZ3NDEKTSV4RRFFQ69G5FA', // Too short (25 chars)
|
||||
|
||||
// Invalid characters (not in Crockford Base32)
|
||||
'01ARZ3NDEKTSV4RRFFQ69G5FI0', // Contains 'I' (invalid)
|
||||
'01ARZ3NDEKTSV4RRFFQ69G5FL0', // Contains 'L' (invalid)
|
||||
'01ARZ3NDEKTSV4RRFFQ69G5FO0', // Contains 'O' (invalid)
|
||||
'01ARZ3NDEKTSV4RRFFQ69G5FU0', // Contains 'U' (invalid)
|
||||
'01ARZ3NDEKTSV4RRFFQ69G5f@V', // Contains '@' (invalid)
|
||||
'01ARZ3NDEKTSV4RRFFQ69G5f_V', // Contains '_' (invalid)
|
||||
'01ARZ3NDEKTSV4RRFFQ69G5f-V', // Contains '-' (invalid)
|
||||
|
||||
// Lowercase (ULIDs should be uppercase)
|
||||
'01arz3ndektsv4rrffq69g5fav', // All lowercase
|
||||
'01ARZ3ndektsv4rrffq69g5fav', // Mixed case
|
||||
|
||||
// Completely invalid formats
|
||||
'not-a-ulid-at-all',
|
||||
'01ARZ3NDEKTSV4RRFFQ69G5F V', // Contains space
|
||||
'01ARZ3NDEKTSV4RRFFQ69G5F\tV', // Contains tab
|
||||
'01ARZ3NDEKTSV4RRFFQ69G5F\nV', // Contains newline
|
||||
|
||||
// Empty variations
|
||||
' ', // Space only
|
||||
'\t', // Tab only
|
||||
'\n', // Newline only
|
||||
|
||||
// Common mistakes
|
||||
'01ARZ3NDEKTSV4RRFFQ69G5FAV ', // Trailing space
|
||||
' 01ARZ3NDEKTSV4RRFFQ69G5FAV', // Leading space
|
||||
'01ARZ3NDEKTSV4RRFFQ69G5FAV\n', // Trailing newline
|
||||
];
|
||||
|
||||
foreach ($invalidUlids as $ulid) {
|
||||
expect($this->rule->validate($ulid))->toBeFalse("ULID should be invalid: '$ulid'");
|
||||
}
|
||||
});
|
||||
|
||||
test('validates ULIDs with edge case timestamps', function () {
|
||||
// ULIDs with specific timestamp patterns that should be valid (all 26 chars)
|
||||
$edgeCaseUlids = [
|
||||
'00000000000000000000000000', // Min ULID (all zeros)
|
||||
'7ZZZZZZZZZZZZZZZZZZZZZZZZZ', // Max timestamp (~10889 AD)
|
||||
'01FXYZ0000000000000000000V', // Year 2021 with min random
|
||||
'01FXYZ999999999999999999ZZ', // Year 2021 with high random
|
||||
'GGGGGGGGGGGGGGGGGGGGGGGGGG', // All G's (valid in Crockford Base32)
|
||||
];
|
||||
|
||||
foreach ($edgeCaseUlids as $ulid) {
|
||||
expect($this->rule->validate($ulid))->toBeTrue("Edge case ULID should be valid: $ulid");
|
||||
}
|
||||
|
||||
// Test Y case separately (26 characters)
|
||||
$yUlid = 'YYYYYYYYYYYYYYYYYYYYYYYYYY';
|
||||
expect(strlen($yUlid))->toBe(26, "Y ULID should be 26 chars");
|
||||
expect($this->rule->validate($yUlid))->toBeTrue("Y ULID should be valid: $yUlid");
|
||||
});
|
||||
|
||||
test('validates strict Crockford Base32 alphabet', function () {
|
||||
// Test each valid character
|
||||
$validChars = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
||||
$baseUlid = '01ARZ3NDEKTSV4RRFFQ69G5FA';
|
||||
|
||||
for ($i = 0; $i < strlen($validChars); $i++) {
|
||||
$char = $validChars[$i];
|
||||
$testUlid = $baseUlid . $char;
|
||||
expect($this->rule->validate($testUlid))->toBeTrue("ULID with char '$char' should be valid");
|
||||
}
|
||||
|
||||
// Test invalid characters from standard Base32 that are excluded in Crockford
|
||||
$invalidChars = 'ILOU'; // These are excluded in Crockford Base32
|
||||
foreach (str_split($invalidChars) as $char) {
|
||||
$testUlid = $baseUlid . $char;
|
||||
expect($this->rule->validate($testUlid))->toBeFalse("ULID with invalid char '$char' should be invalid");
|
||||
}
|
||||
});
|
||||
|
||||
test('default error message', function () {
|
||||
$messages = $this->rule->getErrorMessages();
|
||||
|
||||
expect($messages)->toHaveCount(1)
|
||||
->and($messages[0])->toBe('Bitte geben Sie eine gültige ULID ein (26 Zeichen, Crockford Base32).');
|
||||
});
|
||||
|
||||
test('custom error message', function () {
|
||||
$customRule = new Ulid(message: 'Diese ULID ist nicht korrekt!');
|
||||
$messages = $customRule->getErrorMessages();
|
||||
|
||||
expect($messages)->toHaveCount(1)
|
||||
->and($messages[0])->toBe('Diese ULID ist nicht korrekt!');
|
||||
});
|
||||
|
||||
test('validates performance with many ULIDs', function () {
|
||||
// Performance test - should validate many ULIDs quickly
|
||||
$validUlid = '01ARZ3NDEKTSV4RRFFQ69G5FAV';
|
||||
|
||||
$startTime = microtime(true);
|
||||
|
||||
for ($i = 0; $i < 1000; $i++) {
|
||||
$this->rule->validate($validUlid);
|
||||
}
|
||||
|
||||
$endTime = microtime(true);
|
||||
$duration = $endTime - $startTime;
|
||||
|
||||
// Should complete 1000 validations in less than 100ms
|
||||
expect($duration)->toBeLessThan(0.1);
|
||||
});
|
||||
131
tests/Framework/Validation/Rules/UrlTest.php
Normal file
131
tests/Framework/Validation/Rules/UrlTest.php
Normal file
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\Validation\Rules\Url;
|
||||
|
||||
beforeEach(function () {
|
||||
$this->rule = new Url();
|
||||
});
|
||||
|
||||
test('validates empty values as true (handled by Required rule)', function () {
|
||||
expect($this->rule->validate(null))->toBeTrue()
|
||||
->and($this->rule->validate(''))->toBeTrue();
|
||||
});
|
||||
|
||||
test('validates non-string values as false', function () {
|
||||
expect($this->rule->validate(123))->toBeFalse()
|
||||
->and($this->rule->validate(['not', 'a', 'url']))->toBeFalse()
|
||||
->and($this->rule->validate(new stdClass()))->toBeFalse();
|
||||
});
|
||||
|
||||
test('validates valid HTTP URLs', function () {
|
||||
$validUrls = [
|
||||
'http://example.com',
|
||||
'https://example.com',
|
||||
'http://www.example.com',
|
||||
'https://www.example.com',
|
||||
'https://subdomain.example.com',
|
||||
'https://example.com/path',
|
||||
'https://example.com/path?query=value',
|
||||
'https://example.com/path?query=value#fragment',
|
||||
'http://localhost',
|
||||
'https://192.168.1.1',
|
||||
'https://127.0.0.1:8080',
|
||||
];
|
||||
|
||||
foreach ($validUrls as $url) {
|
||||
expect($this->rule->validate($url))->toBeTrue("URL should be valid: $url");
|
||||
}
|
||||
});
|
||||
|
||||
test('validates invalid URLs as false', function () {
|
||||
$invalidUrls = [
|
||||
'not-a-url',
|
||||
'ftp://example.com', // Not HTTP/HTTPS
|
||||
'javascript:alert("xss")',
|
||||
'mailto:test@example.com',
|
||||
'invalid://url',
|
||||
'http://',
|
||||
'https://',
|
||||
'://example.com',
|
||||
'just-text-no-protocol',
|
||||
'www.example.com', // Missing protocol (unless autoAddProtocol is true)
|
||||
' ', // Whitespace only
|
||||
'http://256.256.256.256', // Invalid IP
|
||||
'https://example..com', // Double dots
|
||||
];
|
||||
|
||||
foreach ($invalidUrls as $url) {
|
||||
expect($this->rule->validate($url))->toBeFalse("URL should be invalid: $url");
|
||||
}
|
||||
});
|
||||
|
||||
test('requireSecure option only allows HTTPS URLs', function () {
|
||||
$secureRule = new Url(requireSecure: true);
|
||||
|
||||
expect($secureRule->validate('https://example.com'))->toBeTrue()
|
||||
->and($secureRule->validate('http://example.com'))->toBeFalse();
|
||||
});
|
||||
|
||||
test('allowLocal option controls local URL validation', function () {
|
||||
$noLocalRule = new Url(allowLocal: false);
|
||||
|
||||
expect($noLocalRule->validate('https://example.com'))->toBeTrue()
|
||||
->and($noLocalRule->validate('http://localhost'))->toBeFalse()
|
||||
->and($noLocalRule->validate('https://127.0.0.1'))->toBeFalse()
|
||||
->and($noLocalRule->validate('https://192.168.1.1'))->toBeFalse()
|
||||
->and($noLocalRule->validate('https://10.0.0.1'))->toBeFalse()
|
||||
->and($noLocalRule->validate('https://172.16.0.1'))->toBeFalse();
|
||||
});
|
||||
|
||||
test('autoAddProtocol option automatically adds https prefix', function () {
|
||||
$autoProtocolRule = new Url(autoAddProtocol: true);
|
||||
|
||||
expect($autoProtocolRule->validate('example.com'))->toBeTrue()
|
||||
->and($autoProtocolRule->validate('www.example.com'))->toBeTrue()
|
||||
->and($autoProtocolRule->validate('subdomain.example.com/path'))->toBeTrue();
|
||||
});
|
||||
|
||||
test('combined options work together', function () {
|
||||
$strictRule = new Url(requireSecure: true, allowLocal: false, autoAddProtocol: true);
|
||||
|
||||
// Should work - gets https:// prefix and is secure and not local
|
||||
expect($strictRule->validate('example.com'))->toBeTrue();
|
||||
|
||||
// Should fail - local URL not allowed
|
||||
expect($strictRule->validate('localhost'))->toBeFalse();
|
||||
|
||||
// Should work - already secure
|
||||
expect($strictRule->validate('https://example.org'))->toBeTrue();
|
||||
|
||||
// Should fail - not secure (even with auto protocol, we can't change existing http://)
|
||||
expect($strictRule->validate('http://example.com'))->toBeFalse();
|
||||
});
|
||||
|
||||
test('default error message', function () {
|
||||
$messages = $this->rule->getErrorMessages();
|
||||
|
||||
expect($messages)->toHaveCount(1)
|
||||
->and($messages[0])->toBe('Bitte geben Sie eine gültige URL ein.');
|
||||
});
|
||||
|
||||
test('custom error message', function () {
|
||||
$customRule = new Url(message: 'Diese URL ist nicht gültig!');
|
||||
$messages = $customRule->getErrorMessages();
|
||||
|
||||
expect($messages)->toHaveCount(1)
|
||||
->and($messages[0])->toBe('Diese URL ist nicht gültig!');
|
||||
});
|
||||
|
||||
test('specific error messages for different configurations', function () {
|
||||
$secureRule = new Url(requireSecure: true);
|
||||
expect($secureRule->getErrorMessages()[0])->toContain('HTTPS-URL');
|
||||
|
||||
$noLocalRule = new Url(allowLocal: false);
|
||||
expect($noLocalRule->getErrorMessages()[0])->toContain('lokale URLs nicht erlaubt');
|
||||
|
||||
$strictRule = new Url(requireSecure: true, allowLocal: false);
|
||||
expect($strictRule->getErrorMessages()[0])->toContain('HTTPS-URL')
|
||||
->and($strictRule->getErrorMessages()[0])->toContain('lokale URLs nicht erlaubt');
|
||||
});
|
||||
163
tests/Framework/Validation/ValidatorTest.php
Normal file
163
tests/Framework/Validation/ValidatorTest.php
Normal file
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\Reflection\CachedReflectionProvider;
|
||||
use App\Framework\Validation\GroupAware;
|
||||
use App\Framework\Validation\Rules\Email;
|
||||
use App\Framework\Validation\Rules\Required;
|
||||
use App\Framework\Validation\Rules\StringLength;
|
||||
use App\Framework\Validation\ValidationResult;
|
||||
use App\Framework\Validation\ValidationRule;
|
||||
use App\Framework\Validation\Validator;
|
||||
|
||||
beforeEach(function () {
|
||||
$reflectionProvider = new CachedReflectionProvider();
|
||||
$this->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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user