From 7bf29076a9c5f8bbd0fa4e159bf4718fdb990bae Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Wed, 17 Jun 2026 15:47:41 +0100 Subject: [PATCH 1/4] Add WAF conditions validator --- composer.json | 4 +- composer.lock | 138 ++++++++++++++++++++--------- src/Validator/Conditions.php | 77 ++++++++++++++++ tests/Validator/ConditionsTest.php | 59 ++++++++++++ 4 files changed, 234 insertions(+), 44 deletions(-) create mode 100644 src/Validator/Conditions.php create mode 100644 tests/Validator/ConditionsTest.php diff --git a/composer.json b/composer.json index 1a60556..87936f0 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,8 @@ "type": "library", "license": "MIT", "require": { - "php": ">=8.2" + "php": ">=8.2", + "utopia-php/validators": "0.2.*" }, "require-dev": { "laravel/pint": "^1.18", @@ -30,4 +31,3 @@ "minimum-stability": "stable", "prefer-stable": true } - diff --git a/composer.lock b/composer.lock index bb92df9..8663d44 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,49 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2ea83540769daf9a5da7ac560f807ae3", - "packages": [], + "content-hash": "a986dbfae76439382aef9763aa893dd0", + "packages": [ + { + "name": "utopia-php/validators", + "version": "0.2.6", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/validators.git", + "reference": "f1ac4f08d6876daf117a1d9af53a4c7b752c2c0b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/validators/zipball/f1ac4f08d6876daf117a1d9af53a4c7b752c2c0b", + "reference": "f1ac4f08d6876daf117a1d9af53a4c7b752c2c0b", + "shasum": "" + }, + "require": { + "php": ">=8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A lightweight collection of reusable validators for Utopia projects", + "keywords": [ + "php", + "utopia", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/utopia-php/validators/issues", + "source": "https://github.com/utopia-php/validators/tree/0.2.6" + }, + "time": "2026-06-10T14:45:13+00:00" + } + ], "packages-dev": [ { "name": "laravel/pint", @@ -136,16 +177,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.6.2", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "3a454ca033b9e06b63282ce19562e892747449bb" + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb", - "reference": "3a454ca033b9e06b63282ce19562e892747449bb", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { @@ -188,9 +229,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "time": "2025-10-21T19:32:17+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { "name": "phar-io/manifest", @@ -365,35 +406,35 @@ }, { "name": "phpunit/php-code-coverage", - "version": "11.0.11", + "version": "11.0.12", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4" + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4", - "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2c1ed04922802c15e1de5d7447b4856de949cf56", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^5.4.0", + "nikic/php-parser": "^5.7.0", "php": ">=8.2", "phpunit/php-file-iterator": "^5.1.0", "phpunit/php-text-template": "^4.0.1", "sebastian/code-unit-reverse-lookup": "^4.0.1", "sebastian/complexity": "^4.0.1", - "sebastian/environment": "^7.2.0", + "sebastian/environment": "^7.2.1", "sebastian/lines-of-code": "^3.0.1", "sebastian/version": "^5.0.2", - "theseer/tokenizer": "^1.2.3" + "theseer/tokenizer": "^1.3.1" }, "require-dev": { - "phpunit/phpunit": "^11.5.2" + "phpunit/phpunit": "^11.5.46" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -431,7 +472,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.11" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.12" }, "funding": [ { @@ -451,32 +492,32 @@ "type": "tidelift" } ], - "time": "2025-08-27T14:37:49+00:00" + "time": "2025-12-24T07:01:01+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "5.1.0", + "version": "5.1.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6" + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/118cfaaa8bc5aef3287bf315b6060b1174754af6", - "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/2f3a64888c814fc235386b7387dd5b5ed92ad903", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^11.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "5.1-dev" } }, "autoload": { @@ -504,15 +545,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.0" + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" } ], - "time": "2024-08-27T05:02:59+00:00" + "time": "2026-02-02T13:52:54+00:00" }, { "name": "phpunit/php-invoker", @@ -700,16 +753,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.44", + "version": "11.5.55", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "c346885c95423eda3f65d85a194aaa24873cda82" + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c346885c95423eda3f65d85a194aaa24873cda82", - "reference": "c346885c95423eda3f65d85a194aaa24873cda82", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/adc7262fccc12de2b30f12a8aa0b33775d814f00", + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00", "shasum": "" }, "require": { @@ -723,19 +776,20 @@ "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.2", - "phpunit/php-code-coverage": "^11.0.11", - "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-code-coverage": "^11.0.12", + "phpunit/php-file-iterator": "^5.1.1", "phpunit/php-invoker": "^5.0.1", "phpunit/php-text-template": "^4.0.1", "phpunit/php-timer": "^7.0.1", "sebastian/cli-parser": "^3.0.2", "sebastian/code-unit": "^3.0.3", - "sebastian/comparator": "^6.3.2", + "sebastian/comparator": "^6.3.3", "sebastian/diff": "^6.0.2", "sebastian/environment": "^7.2.1", "sebastian/exporter": "^6.3.2", "sebastian/global-state": "^7.0.2", "sebastian/object-enumerator": "^6.0.1", + "sebastian/recursion-context": "^6.0.3", "sebastian/type": "^5.1.3", "sebastian/version": "^5.0.2", "staabm/side-effects-detector": "^1.0.5" @@ -781,7 +835,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.44" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.55" }, "funding": [ { @@ -805,7 +859,7 @@ "type": "tidelift" } ], - "time": "2025-11-13T07:17:35+00:00" + "time": "2026-02-18T12:37:06+00:00" }, { "name": "sebastian/cli-parser", @@ -979,16 +1033,16 @@ }, { "name": "sebastian/comparator", - "version": "6.3.2", + "version": "6.3.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8" + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/85c77556683e6eee4323e4c5468641ca0237e2e8", - "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", "shasum": "" }, "require": { @@ -1047,7 +1101,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.2" + "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.3" }, "funding": [ { @@ -1067,7 +1121,7 @@ "type": "tidelift" } ], - "time": "2025-08-10T08:07:46+00:00" + "time": "2026-01-24T09:26:40+00:00" }, { "name": "sebastian/complexity", @@ -1905,5 +1959,5 @@ "php": ">=8.2" }, "platform-dev": {}, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/src/Validator/Conditions.php b/src/Validator/Conditions.php new file mode 100644 index 0000000..643cd40 --- /dev/null +++ b/src/Validator/Conditions.php @@ -0,0 +1,77 @@ +maxConditions > 0 && $count > $this->maxConditions) { + return false; + } + + foreach ($value as $condition) { + if (\is_string($condition)) { + if ($this->maxPayloadLength > 0 && \strlen($condition) > $this->maxPayloadLength) { + return false; + } + + try { + Condition::decode($condition); + } catch (\Throwable) { + return false; + } + + continue; + } + + if (\is_array($condition)) { + try { + Condition::fromArray($condition); + } catch (\Throwable) { + return false; + } + + continue; + } + + return false; + } + + return true; + } + + public function getType(): string + { + return self::TYPE_STRING; + } +} diff --git a/tests/Validator/ConditionsTest.php b/tests/Validator/ConditionsTest.php new file mode 100644 index 0000000..001f6ff --- /dev/null +++ b/tests/Validator/ConditionsTest.php @@ -0,0 +1,59 @@ +assertFalse($validator->isValid([])); + } + + public function testAcceptsConditionArraysAndStrings(): void + { + $validator = new Conditions(); + + $this->assertTrue($validator->isValid([ + Condition::equal('ip', ['198.51.100.5'])->toArray(), + Condition::startsWith('path', '/v1')->encode(), + ])); + } + + public function testRejectsInvalidConditionArray(): void + { + $validator = new Conditions(); + + $this->assertFalse($validator->isValid([ + [ + 'method' => 'unknown', + 'attribute' => 'ip', + 'values' => ['1.2.3.4'], + ], + ])); + } + + public function testRejectsTooManyConditions(): void + { + $validator = new Conditions(maxConditions: 1); + + $this->assertFalse($validator->isValid([ + Condition::equal('ip', ['198.51.100.5'])->toArray(), + Condition::startsWith('path', '/v1')->encode(), + ])); + } + + public function testRejectsLongConditionStrings(): void + { + $validator = new Conditions(maxPayloadLength: 5); + + $this->assertFalse($validator->isValid([ + Condition::startsWith('path', '/v1')->encode(), + ])); + } +} From 22a1026bbd796906569e4f9d5c6a329195d27192 Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Thu, 18 Jun 2026 14:48:28 +0100 Subject: [PATCH 2/4] Tighten WAF conditions validator --- src/Validator/Conditions.php | 73 ++++++++++++++++++++---------- tests/Validator/ConditionsTest.php | 51 +++++++++++++++++++-- 2 files changed, 96 insertions(+), 28 deletions(-) diff --git a/src/Validator/Conditions.php b/src/Validator/Conditions.php index 643cd40..375161f 100644 --- a/src/Validator/Conditions.php +++ b/src/Validator/Conditions.php @@ -15,7 +15,7 @@ public function __construct( public function getDescription(): string { - return 'Array of at least one WAF condition encoded as JSON strings.'; + return 'Array of at least one WAF condition definition.'; } public function isArray(): bool @@ -29,49 +29,76 @@ public function isValid($value): bool return false; } - $count = \count($value); - - if ($count === 0) { + if (\count($value) === 0) { return false; } + $count = 0; + + foreach ($value as $condition) { + if (!\is_array($condition) || !$this->isValidCondition($condition, $count)) { + return false; + } + } + + return true; + } + + public function getType(): string + { + return self::TYPE_ARRAY; + } + + /** + * @param array $payload + */ + private function isValidCondition(array $payload, int &$count): bool + { + $count++; + if ($this->maxConditions > 0 && $count > $this->maxConditions) { return false; } - foreach ($value as $condition) { - if (\is_string($condition)) { - if ($this->maxPayloadLength > 0 && \strlen($condition) > $this->maxPayloadLength) { - return false; - } + if ($this->maxPayloadLength > 0 && !$this->isWithinPayloadLimit($payload)) { + return false; + } - try { - Condition::decode($condition); - } catch (\Throwable) { - return false; - } + $method = $payload['method'] ?? ''; + $values = $payload['values'] ?? []; - continue; + if (\in_array($method, [Condition::TYPE_AND, Condition::TYPE_OR], true)) { + if (!\is_array($values) || \count($values) === 0) { + return false; } - if (\is_array($condition)) { - try { - Condition::fromArray($condition); - } catch (\Throwable) { + foreach ($values as $value) { + if (!\is_array($value) || !$this->isValidCondition($value, $count)) { return false; } - - continue; } + } + try { + Condition::fromArray($payload); + } catch (\Throwable) { return false; } return true; } - public function getType(): string + /** + * @param array $payload + */ + private function isWithinPayloadLimit(array $payload): bool { - return self::TYPE_STRING; + try { + $encoded = \json_encode($payload, JSON_THROW_ON_ERROR); + } catch (\Throwable) { + return false; + } + + return \strlen($encoded) <= $this->maxPayloadLength; } } diff --git a/tests/Validator/ConditionsTest.php b/tests/Validator/ConditionsTest.php index 001f6ff..a3344c4 100644 --- a/tests/Validator/ConditionsTest.php +++ b/tests/Validator/ConditionsTest.php @@ -8,6 +8,14 @@ class ConditionsTest extends TestCase { + public function testReturnsArrayType(): void + { + $validator = new Conditions(); + + $this->assertTrue($validator->isArray()); + $this->assertSame(Conditions::TYPE_ARRAY, $validator->getType()); + } + public function testRejectsEmptyConditionList(): void { $validator = new Conditions(); @@ -15,12 +23,21 @@ public function testRejectsEmptyConditionList(): void $this->assertFalse($validator->isValid([])); } - public function testAcceptsConditionArraysAndStrings(): void + public function testAcceptsConditionArrays(): void { $validator = new Conditions(); $this->assertTrue($validator->isValid([ Condition::equal('ip', ['198.51.100.5'])->toArray(), + Condition::startsWith('path', '/v1')->toArray(), + ])); + } + + public function testRejectsConditionStrings(): void + { + $validator = new Conditions(); + + $this->assertFalse($validator->isValid([ Condition::startsWith('path', '/v1')->encode(), ])); } @@ -44,16 +61,40 @@ public function testRejectsTooManyConditions(): void $this->assertFalse($validator->isValid([ Condition::equal('ip', ['198.51.100.5'])->toArray(), - Condition::startsWith('path', '/v1')->encode(), + Condition::startsWith('path', '/v1')->toArray(), ])); } - public function testRejectsLongConditionStrings(): void + public function testRejectsTooManyNestedConditions(): void { - $validator = new Conditions(maxPayloadLength: 5); + $validator = new Conditions(maxConditions: 2); $this->assertFalse($validator->isValid([ - Condition::startsWith('path', '/v1')->encode(), + Condition::and([ + Condition::equal('ip', ['198.51.100.5']), + Condition::startsWith('path', '/v1'), + ])->toArray(), + ])); + } + + public function testRejectsLongConditionArrays(): void + { + $validator = new Conditions(maxPayloadLength: 64); + + $this->assertFalse($validator->isValid([ + Condition::equal('ip', [\str_repeat('1', 128)])->toArray(), + ])); + } + + public function testRejectsEmptyLogicalConditions(): void + { + $validator = new Conditions(); + + $this->assertFalse($validator->isValid([ + [ + 'method' => Condition::TYPE_AND, + 'values' => [], + ], ])); } } From bced394be6221c0605fc441a87d08e58953bf36c Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Fri, 19 Jun 2026 18:43:43 +0100 Subject: [PATCH 3/4] Accept encoded condition strings --- src/Validator/Conditions.php | 18 +++++++++++++--- tests/Validator/ConditionsTest.php | 34 ++++++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/Validator/Conditions.php b/src/Validator/Conditions.php index 375161f..f4ff0fa 100644 --- a/src/Validator/Conditions.php +++ b/src/Validator/Conditions.php @@ -36,7 +36,7 @@ public function isValid($value): bool $count = 0; foreach ($value as $condition) { - if (!\is_array($condition) || !$this->isValidCondition($condition, $count)) { + if (!$this->isValidCondition($condition, $count)) { return false; } } @@ -50,10 +50,22 @@ public function getType(): string } /** - * @param array $payload + * @param array|string $payload */ - private function isValidCondition(array $payload, int &$count): bool + private function isValidCondition(array|string $payload, int &$count): bool { + if (\is_string($payload)) { + if ($this->maxPayloadLength > 0 && \strlen($payload) > $this->maxPayloadLength) { + return false; + } + + try { + $payload = Condition::decode($payload)->toArray(); + } catch (\Throwable) { + return false; + } + } + $count++; if ($this->maxConditions > 0 && $count > $this->maxConditions) { diff --git a/tests/Validator/ConditionsTest.php b/tests/Validator/ConditionsTest.php index a3344c4..7d5d00b 100644 --- a/tests/Validator/ConditionsTest.php +++ b/tests/Validator/ConditionsTest.php @@ -33,15 +33,24 @@ public function testAcceptsConditionArrays(): void ])); } - public function testRejectsConditionStrings(): void + public function testAcceptsEncodedConditionStrings(): void { $validator = new Conditions(); - $this->assertFalse($validator->isValid([ + $this->assertTrue($validator->isValid([ Condition::startsWith('path', '/v1')->encode(), ])); } + public function testRejectsInvalidConditionStrings(): void + { + $validator = new Conditions(); + + $this->assertFalse($validator->isValid([ + '{"method":', + ])); + } + public function testRejectsInvalidConditionArray(): void { $validator = new Conditions(); @@ -77,6 +86,18 @@ public function testRejectsTooManyNestedConditions(): void ])); } + public function testRejectsTooManyNestedEncodedConditions(): void + { + $validator = new Conditions(maxConditions: 2); + + $this->assertFalse($validator->isValid([ + Condition::and([ + Condition::equal('ip', ['198.51.100.5']), + Condition::startsWith('path', '/v1'), + ])->encode(), + ])); + } + public function testRejectsLongConditionArrays(): void { $validator = new Conditions(maxPayloadLength: 64); @@ -86,6 +107,15 @@ public function testRejectsLongConditionArrays(): void ])); } + public function testRejectsLongConditionStrings(): void + { + $validator = new Conditions(maxPayloadLength: 64); + + $this->assertFalse($validator->isValid([ + Condition::equal('ip', [\str_repeat('1', 128)])->encode(), + ])); + } + public function testRejectsEmptyLogicalConditions(): void { $validator = new Conditions(); From 740a5661d3cd83c652dde8174f2274298a62d354 Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Fri, 19 Jun 2026 19:20:45 +0100 Subject: [PATCH 4/4] Guard condition payload types --- src/Validator/Conditions.php | 4 ++++ tests/Validator/ConditionsTest.php | 14 ++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/Validator/Conditions.php b/src/Validator/Conditions.php index f4ff0fa..f27526b 100644 --- a/src/Validator/Conditions.php +++ b/src/Validator/Conditions.php @@ -36,6 +36,10 @@ public function isValid($value): bool $count = 0; foreach ($value as $condition) { + if (!\is_array($condition) && !\is_string($condition)) { + return false; + } + if (!$this->isValidCondition($condition, $count)) { return false; } diff --git a/tests/Validator/ConditionsTest.php b/tests/Validator/ConditionsTest.php index 7d5d00b..6acaeb6 100644 --- a/tests/Validator/ConditionsTest.php +++ b/tests/Validator/ConditionsTest.php @@ -64,6 +64,20 @@ public function testRejectsInvalidConditionArray(): void ])); } + public function testRejectsMixedConditionTypes(): void + { + $validator = new Conditions(); + + $this->assertFalse($validator->isValid([ + Condition::startsWith('path', '/v1')->toArray(), + null, + ])); + + $this->assertFalse($validator->isValid([ + 123, + ])); + } + public function testRejectsTooManyConditions(): void { $validator = new Conditions(maxConditions: 1);