diff --git a/README.md b/README.md index f2c803432..ff7187938 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ The source of truth for the [utopia-php](https://github.com/utopia-php) libraries. Each `packages/` is an independent Composer library; development happens here, and every push is mirrored to its read-only repository (e.g. `utopia-php/http`), so Composer/Packagist distribution is unchanged — mechanics in [docs/distribution.md](docs/distribution.md). -[`utopia-php/cache`](https://github.com/appwrite/appwrite/tree/main/packages/cache), [`utopia-php/circuit-breaker`](https://github.com/appwrite/appwrite/tree/main/packages/circuit-breaker), [`utopia-php/client`](https://github.com/appwrite/appwrite/tree/main/packages/client), [`utopia-php/compression`](https://github.com/appwrite/appwrite/tree/main/packages/compression), [`utopia-php/di`](https://github.com/appwrite/appwrite/tree/main/packages/di), [`utopia-php/image`](https://github.com/appwrite/appwrite/tree/main/packages/image), [`utopia-php/messaging`](https://github.com/appwrite/appwrite/tree/main/packages/messaging), [`utopia-php/openapi`](https://github.com/appwrite/appwrite/tree/main/packages/openapi), [`utopia-php/pools`](https://github.com/appwrite/appwrite/tree/main/packages/pools), [`utopia-php/schedule`](https://github.com/appwrite/appwrite/tree/main/packages/schedule), [`utopia-php/smtp`](https://github.com/appwrite/appwrite/tree/main/packages/smtp), [`utopia-php/span`](https://github.com/appwrite/appwrite/tree/main/packages/span), [`utopia-php/system`](https://github.com/appwrite/appwrite/tree/main/packages/system), [`utopia-php/telemetry`](https://github.com/appwrite/appwrite/tree/main/packages/telemetry), [`utopia-php/user-agent`](https://github.com/appwrite/appwrite/tree/main/packages/user-agent) and [`utopia-php/websocket`](https://github.com/appwrite/appwrite/tree/main/packages/websocket) are maintained in Appwrite. Their distribution mirrors and Packagist packages remain available. +[`utopia-php/cache`](https://github.com/appwrite/appwrite/tree/main/packages/cache), [`utopia-php/circuit-breaker`](https://github.com/appwrite/appwrite/tree/main/packages/circuit-breaker), [`utopia-php/client`](https://github.com/appwrite/appwrite/tree/main/packages/client), [`utopia-php/compression`](https://github.com/appwrite/appwrite/tree/main/packages/compression), [`utopia-php/di`](https://github.com/appwrite/appwrite/tree/main/packages/di), [`utopia-php/image`](https://github.com/appwrite/appwrite/tree/main/packages/image), [`utopia-php/messaging`](https://github.com/appwrite/appwrite/tree/main/packages/messaging), [`utopia-php/openapi`](https://github.com/appwrite/appwrite/tree/main/packages/openapi), [`utopia-php/pools`](https://github.com/appwrite/appwrite/tree/main/packages/pools), [`utopia-php/schedule`](https://github.com/appwrite/appwrite/tree/main/packages/schedule), [`utopia-php/smtp`](https://github.com/appwrite/appwrite/tree/main/packages/smtp), [`utopia-php/span`](https://github.com/appwrite/appwrite/tree/main/packages/span), [`utopia-php/system`](https://github.com/appwrite/appwrite/tree/main/packages/system), [`utopia-php/telemetry`](https://github.com/appwrite/appwrite/tree/main/packages/telemetry), [`utopia-php/user-agent`](https://github.com/appwrite/appwrite/tree/main/packages/user-agent), [`utopia-php/validators`](https://github.com/appwrite/appwrite/tree/main/packages/validators) and [`utopia-php/websocket`](https://github.com/appwrite/appwrite/tree/main/packages/websocket) are maintained in Appwrite. Their distribution mirrors and Packagist packages remain available. ## Quickstart @@ -51,29 +51,24 @@ Local siblings only: arrows point at dependencies between the packages in this c ```mermaid graph TD - audit --> validators cli --> servers - config --> validators - console --> validators - dns --> validators dns --> domains - domains --> validators http --> servers - http --> validators platform --> cli platform --> http platform --> queue platform --> servers queue --> servers - queue --> validators - servers --> validators - storage --> validators + audit auth cdn + config + console fastly nats replication reputation + storage vcs ``` diff --git a/packages/validators/.github/workflows/mirror.yml b/packages/validators/.github/workflows/mirror.yml deleted file mode 100644 index 09ccf56dc..000000000 --- a/packages/validators/.github/workflows/mirror.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: Mirror - -on: - pull_request_target: - types: [opened] - issues: - types: [opened] - -permissions: - issues: write - pull-requests: write - -jobs: - redirect: - uses: utopia-php/monorepo/.github/workflows/mirror-redirect.yml@main - with: - package: validators diff --git a/packages/validators/.gitignore b/packages/validators/.gitignore deleted file mode 100755 index e5f7a865f..000000000 --- a/packages/validators/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -/vendor/ -/.idea/ -*.cache -.history -.phpactor.json -composer.lock diff --git a/packages/validators/CHANGELOG.md b/packages/validators/CHANGELOG.md deleted file mode 100644 index d289d0d66..000000000 --- a/packages/validators/CHANGELOG.md +++ /dev/null @@ -1,54 +0,0 @@ -# Changelog - -All notable changes to `utopia-php/validators` are documented in this file. - -## 0.4.2 - -### Added - -- New `Identifier` validator. Extends `Text` and additionally requires a C-style - identifier (`^[A-Za-z_][A-Za-z0-9_]*$`): letters, digits and underscores only, - not starting with a digit. This is the shape required of environment variable - names and shell identifiers, so a value that passes a plain length check but - contains a tab, a space or an accented letter is rejected. Accepts an optional - `int $length` (0 = unlimited); minimum length is 1. - -## 0.4.0 - -### Phone validator - -#### Added - -- New `Phone` validator for E.164 phone number syntax. It validates values with a - leading `+` followed by 7 to 15 digits, supports optional empty strings through - `allowEmpty`, and can opt in to URL/path recovery through `normalize`. - -## 0.3.2 - -### JSON shape validators - -#### Added - -- New `JSON\ObjectValidator` and `JSON\ArrayValidator` validators for JSON shape checks. - Both accept decoded values or encoded JSON strings while preserving the distinction between - objects and arrays. - -## 0.3.1 - -### URL validator — OAuth2 secure-redirect transport policy - -#### Added - -- New optional constructor parameter `bool $httpsOrLoopback = false` (kept last - in the signature). When enabled, a standard (authority-bearing, non-private-use) - URL is valid only if its scheme is `https` on any host, or `http` on a loopback - host (`localhost`, `127.0.0.1`, or `[::1]`); every other standard scheme and any - routable `http` host is rejected (RFC 8252 §7.3). Private-use scheme URIs - (governed by `allowPrivateUseSchemes`) are exempt. The flag is self-contained and - independent of `allowedSchemes` — when both are set, a value must satisfy both. - `getDescription()` reflects the restriction when the flag is on. - -The change is backward compatible: `httpsOrLoopback` defaults to `false`, so -existing callers are unaffected, and the behavior of `allowedSchemes` and -`allowPrivateUseSchemes` (including how `allowedSchemes` restricts which -private-use schemes are accepted) is unchanged. diff --git a/packages/validators/LICENSE.md b/packages/validators/LICENSE.md deleted file mode 100755 index 27feb0854..000000000 --- a/packages/validators/LICENSE.md +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2013 Eldad Fux - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/validators/README.md b/packages/validators/README.md deleted file mode 100644 index 737cb8bc2..000000000 --- a/packages/validators/README.md +++ /dev/null @@ -1,87 +0,0 @@ -# Utopia Validators - -> [!IMPORTANT] -> This repository is a read-only mirror of the [utopia-php monorepo](https://github.com/utopia-php/monorepo). Development happens in [`packages/validators`](https://github.com/utopia-php/monorepo/tree/main/packages/validators) — please open issues and pull requests there. - -Reusable validation building blocks for [Utopia](https://github.com/utopia-php) projects. -This package exposes a consistent API for common HTTP-oriented validation concerns such as input sanitization, URL checks, IP validation, hostname filtering, lists enforcement, and more. - -## Installation - -```bash -composer require utopia-php/validators -``` - -## Usage - -```php -use Utopia\Validator\Text; -use Utopia\Validator\Range; - -$username = new Text(20, min: 3); -$age = new Range(min: 13, max: 120); - -if (! $username->isValid($input['username'])) { - throw new InvalidArgumentException($username->getDescription()); -} - -if (! $age->isValid($input['age'])) { - throw new InvalidArgumentException($age->getDescription()); -} -``` - -Validators expose a predictable contract: - -- `isValid(mixed $value): bool` – core validation rule -- `getDescription(): string` – human readable rule summary -- `getType(): string` – expected PHP type (string, integer, array, ...) -- `isArray(): bool` – hint whether the validator expects an array input - -For advanced flows combine validators with `Multiple`, `AnyOf`, `AllOf`, `NoneOf`, or wrap checks with helpers such as `Nullable`. - -## Available Validators - -- `AllOf`, `AnyOf`, `NoneOf`, `Multiple` – composition helpers -- `ArrayList`, `Assoc`, `Nullable`, `WhiteList`, `Wildcard` -- `Boolean`, `Integer`, `FloatValidator`, `Numeric`, `Range` -- `Domain`, `Host`, `Hostname`, `IP`, `URL` -- `HexColor`, `Identifier`, `JSON`, `Phone`, `Text` -- `JSON\ObjectValidator`, `JSON\ArrayValidator` – JSON shape checks that accept encoded strings -- `JSON\FCM` – FCM service account JSON with required credential fields - -### Validating JSON shape - -`JSON` accepts any valid JSON, including scalars such as `"1"` and `"\"text\""`. When a parameter must be -an object or a list, reach for the shape-specific validators instead. Both accept a value that is already -decoded or still encoded as a string: - -```php -use Utopia\Validator\JSON; - -$data = new JSON\ObjectValidator(); - -$data->isValid('{"event": "login"}'); // true -$data->isValid(['event' => 'login']); // true -$data->isValid('"login"'); // false -$data->isValid('[]'); // false -``` - -Strings decode to objects rather than associative arrays, so `'{}'` and `'[]'` stay distinguishable. A value -that arrives already decoded as an empty array is ambiguous — PHP represents both `{}` and `[]` that way — so -both validators accept it. - -Use `JSON\FCM` for Google service account credentials used with the FCM HTTP v1 API. It accepts an encoded -JSON object, associative array, or `stdClass`. The validator checks the credential type, Firebase project, -service account email, PEM private key, and OAuth token endpoint. It also validates standard optional service -account fields when present. - -## Development - -Run the static analysis and test suites from the project root: - -```bash -composer check -composer test -``` - -This project is released under the [MIT License](LICENSE.md). diff --git a/packages/validators/composer.json b/packages/validators/composer.json deleted file mode 100644 index e7d5987f7..000000000 --- a/packages/validators/composer.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "utopia-php/validators", - "description": "A lightweight collection of reusable validators for Utopia projects", - "type": "library", - "keywords": [ - "php", - "validation", - "validator", - "utopia" - ], - "license": "MIT", - "autoload": { - "psr-4": { - "Utopia\\": "src/" - } - }, - "scripts": { - "test": "phpunit --configuration phpunit.xml" - }, - "require": { - "php": ">=8.5" - } -} diff --git a/packages/validators/phpstan.neon b/packages/validators/phpstan.neon deleted file mode 100644 index 589fbcfc2..000000000 --- a/packages/validators/phpstan.neon +++ /dev/null @@ -1,8 +0,0 @@ -includes: - - ../../phpstan.neon - -services: - - - class: Utopia\PHPStan\DisallowAssertEqualsExtension - tags: - - phpstan.restrictedMethodUsageExtension diff --git a/packages/validators/phpunit.xml b/packages/validators/phpunit.xml deleted file mode 100755 index 568a8b94b..000000000 --- a/packages/validators/phpunit.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - ./tests/Validator/ - - - diff --git a/packages/validators/src/PHPStan/DisallowAssertEqualsExtension.php b/packages/validators/src/PHPStan/DisallowAssertEqualsExtension.php deleted file mode 100644 index d75462ed1..000000000 --- a/packages/validators/src/PHPStan/DisallowAssertEqualsExtension.php +++ /dev/null @@ -1,32 +0,0 @@ -getName() !== 'assertEquals') { - return null; - } - - $declaringClass = $methodReflection->getDeclaringClass(); - if ($declaringClass->getName() !== \PHPUnit\Framework\Assert::class) { - return null; - } - - return RestrictedUsage::create( - errorMessage: 'Use assertSame() instead of assertEquals()', - identifier: 'method.disallowedAssertEquals', - ); - } -} diff --git a/packages/validators/src/Validator.php b/packages/validators/src/Validator.php deleted file mode 100644 index ac8a8e352..000000000 --- a/packages/validators/src/Validator.php +++ /dev/null @@ -1,52 +0,0 @@ - $validators - */ - public function __construct(protected array $validators, protected string $type = self::TYPE_MIXED) {} - - /** - * Get Description - * - * Returns validator description - */ - public function getDescription(): string - { - if (!(\is_null($this->failedRule))) { - return $this->failedRule->getDescription(); - } - - return $this->validators[0]->getDescription(); - } - - /** - * Is valid - * - * Validation will pass when all rules are valid if only one of the rules is invalid validation will fail. - */ - public function isValid(mixed $value): bool - { - foreach ($this->validators as $rule) { - $valid = $rule->isValid($value); - - if (!$valid) { - $this->failedRule = $rule; - return false; - } - } - - return true; - } - - /** - * Get Type - * - * Returns validator type. - */ - public function getType(): string - { - return $this->type; - } - - /** - * Is array - * - * Function will return true if object is array. - */ - public function isArray(): bool - { - return true; - } -} diff --git a/packages/validators/src/Validator/AnyOf.php b/packages/validators/src/Validator/AnyOf.php deleted file mode 100644 index d011670b7..000000000 --- a/packages/validators/src/Validator/AnyOf.php +++ /dev/null @@ -1,86 +0,0 @@ - $validators - */ - public function __construct(protected array $validators, protected string $type = self::TYPE_MIXED) {} - - /** - * Get Validators - * - * Returns validators array - * - * @return array - */ - public function getValidators(): array - { - return $this->validators; - } - - /** - * Get Description - * - * Returns validator description - */ - public function getDescription(): string - { - if (!(\is_null($this->failedRule))) { - return $this->failedRule->getDescription(); - } - - return $this->validators[0]->getDescription(); - } - - /** - * Is valid - * - * Validation will pass when all rules are valid if only one of the rules is invalid validation will fail. - */ - public function isValid(mixed $value): bool - { - foreach ($this->validators as $rule) { - $valid = $rule->isValid($value); - - $this->failedRule = $rule; - - if ($valid) { - return true; - } - } - - return false; - } - - /** - * Get Type - * - * Returns validator type. - */ - public function getType(): string - { - return $this->type; - } - - /** - * Is array - * - * Function will return true if object is array. - */ - public function isArray(): bool - { - return true; - } -} diff --git a/packages/validators/src/Validator/ArrayList.php b/packages/validators/src/Validator/ArrayList.php deleted file mode 100644 index 51f06e13c..000000000 --- a/packages/validators/src/Validator/ArrayList.php +++ /dev/null @@ -1,89 +0,0 @@ -length > 0) { - $msg .= ' no longer than ' . $this->length . ' items'; - } - - if (!\in_array($this->validator->getDescription(), ['', '0'], true)) { - $msg .= ' and ' . $this->validator->getDescription(); - } - - return $msg; - } - - /** - * Is array - * - * Function will return true if object is array. - */ - public function isArray(): bool - { - return true; - } - - /** - * Get Type - * - * Returns validator type. - */ - public function getType(): string - { - return $this->validator->getType(); - } - - /** - * Get Nested Validator - */ - public function getValidator(): Validator - { - return $this->validator; - } - - /** - * Is valid - * - * Validation will pass when $value is valid array and validator is valid. - */ - public function isValid(mixed $value): bool - { - if (!\is_array($value)) { - return false; - } - - foreach ($value as $element) { - if (!$this->validator->isValid($element)) { - return false; - } - } - return !$this->length || \count($value) <= $this->length; - } -} diff --git a/packages/validators/src/Validator/Assoc.php b/packages/validators/src/Validator/Assoc.php deleted file mode 100644 index 271136939..000000000 --- a/packages/validators/src/Validator/Assoc.php +++ /dev/null @@ -1,71 +0,0 @@ - $this->length) { - return false; - } - - return array_keys($value) !== range(0, \count($value) - 1); - } -} diff --git a/packages/validators/src/Validator/Boolean.php b/packages/validators/src/Validator/Boolean.php deleted file mode 100644 index 1091271f4..000000000 --- a/packages/validators/src/Validator/Boolean.php +++ /dev/null @@ -1,74 +0,0 @@ -loose && ($value === 'true' || $value === 'false')) { // Accept strings - return true; - } - - if ($this->loose && ($value === '1' || $value === '0')) { // Accept numeric strings - return true; - } - - if ($this->loose && ($value === 1 || $value === 0)) { // Accept integers - return true; - } - return \is_bool($value); - } -} diff --git a/packages/validators/src/Validator/Contains.php b/packages/validators/src/Validator/Contains.php deleted file mode 100644 index 1dc758106..000000000 --- a/packages/validators/src/Validator/Contains.php +++ /dev/null @@ -1,99 +0,0 @@ -patterns = $patterns; - } - - /** - * Get Description - * - * Returns validator description - */ - public function getDescription(): string - { - $message = 'Value must contain one of (' . implode(', ', $this->patterns) . ')'; - - if ($this->strict) { - $message .= ' (case-sensitive)'; - } else { - $message .= ' (case-insensitive)'; - } - - return $message; - } - - /** - * Is valid - * - * Validation will pass when $value contains at least one of the patterns. - * - * @param mixed $value - */ - public function isValid($value): bool - { - if (!\is_string($value)) { - return false; - } - - if (!$this->strict) { - $value = mb_strtolower($value, 'UTF-8'); - } - - foreach ($this->patterns as $pattern) { - $pattern = $this->strict ? $pattern : mb_strtolower((string) $pattern, 'UTF-8'); - - if (str_contains($value, (string) $pattern)) { - return true; - } - } - - return false; - } - - /** - * Is array - * - * Function will return true if object is array. - */ - public function isArray(): bool - { - return false; - } - - /** - * Get Type - * - * Returns validator type. - */ - public function getType(): string - { - return self::TYPE_STRING; - } -} diff --git a/packages/validators/src/Validator/Domain.php b/packages/validators/src/Validator/Domain.php deleted file mode 100644 index dc2a435ff..000000000 --- a/packages/validators/src/Validator/Domain.php +++ /dev/null @@ -1,143 +0,0 @@ - $restrictions Set of conditions that prevent validation from passing - */ - public function __construct( - protected array $restrictions = [], - protected bool $hostnames = true, - protected bool $allowEmpty = false, - ) {} - - /** - * Helper for creating domain restriction rule. - * Such rules prevent validation from passing, so this behaves as deny-list. - * - * @param string $hostname A domain base, such as top-level domain or subdomain. Restriction is only applied if domain matches this hostname - * @param int $levels Specify what level (top-level, subdomain, sub-subdomain, ..) domain must be. Example: "stage.appwrite.io" is level 3 - * @param array $prefixDenyList Disallowed beginning of domain, useful for reserved behaviours, such as prefixing "branch-" for preview domains - * - */ - public static function createRestriction(string $hostname, ?int $levels = null, array $prefixDenyList = []): array - { - return [ - 'hostname' => $hostname, - 'levels' => $levels, - 'prefixDenyList' => $prefixDenyList, - ]; - } - - /** - * Get Description - * - * Returns validator description - */ - public function getDescription(): string - { - return 'Value must be a valid domain'; - } - - /** - * Is valid - * - * Validation will pass when $value is valid domain. - * - * Validates domain names against RFC 1034, RFC 1035, RFC 952, RFC 1123, RFC 2732, RFC 2181, and RFC 1123. - * - * @param mixed $value - */ - public function isValid($value): bool - { - if ($this->allowEmpty && $value === '') { - return true; - } - - if (empty($value)) { - return false; - } - - if (!\is_string($value)) { - return false; - } - - if ( - filter_var( - $value, - FILTER_VALIDATE_DOMAIN, - $this->hostnames ? FILTER_FLAG_HOSTNAME : 0, - ) === false - ) { - return false; - } - - if (str_ends_with($value, '.') || str_ends_with($value, '-')) { - return false; - } - - foreach ($this->restrictions as $restriction) { - $hostname = $restriction['hostname']; - $levels = $restriction['levels']; - $prefixDenyList = $restriction['prefixDenyList']; - - // Only apply restriction rules to relevant domains - if (!str_ends_with($value, (string) $hostname)) { - continue; - } - - // Domain-level restriction - if (!\is_null($levels)) { - $expectedPartsCount = $levels; - $partsCount = \count(explode('.', $value, $expectedPartsCount + 1)); - if ($partsCount !== $expectedPartsCount) { - return false; - } - } - - // Domain prefix (beginning) restriction - if (!empty($prefixDenyList)) { - foreach ($prefixDenyList as $deniedPrefix) { - if (str_starts_with($value, (string) $deniedPrefix)) { - return false; - } - } - } - } - - return true; - } - - /** - * Is array - * - * Function will return true if object is array. - */ - public function isArray(): bool - { - return false; - } - - /** - * Get Type - * - * Returns validator type. - */ - public function getType(): string - { - return self::TYPE_STRING; - } -} diff --git a/packages/validators/src/Validator/FloatValidator.php b/packages/validators/src/Validator/FloatValidator.php deleted file mode 100755 index fc1abb608..000000000 --- a/packages/validators/src/Validator/FloatValidator.php +++ /dev/null @@ -1,67 +0,0 @@ -loose) { - if (!is_numeric($value)) { - return false; - } - $value += 0; - } - return \is_float($value) || \is_int($value); - } -} diff --git a/packages/validators/src/Validator/Globstar.php b/packages/validators/src/Validator/Globstar.php deleted file mode 100644 index 40fa31647..000000000 --- a/packages/validators/src/Validator/Globstar.php +++ /dev/null @@ -1,217 +0,0 @@ -patterns === []) { - return true; - } - - $hasInclusions = false; - foreach ($this->patterns as $p) { - if (!str_starts_with((string) $p, '!')) { - $hasInclusions = true; - break; - } - } - - // Pure-exclusion mode: default to valid; any matching exclusion invalidates. - if (!$hasInclusions) { - return array_all($this->patterns, fn($pattern): bool => !$this->match($value, substr((string) $pattern, 1))); - } - - // Inclusion mode. - // - // Step 1 — literal (no *, ?, [) inclusion patterns always win: - // if any specific inclusion matches, the value is valid regardless of later exclusions. - $isWildcard = fn($p): bool => str_contains((string) $p, '*') || str_contains((string) $p, '?') || str_contains((string) $p, '['); - - foreach ($this->patterns as $pattern) { - if (!str_starts_with((string) $pattern, '!') && !$isWildcard($pattern) && $this->match($value, $pattern)) { - return true; - } - } - - // Step 2 — last-match-wins over the remaining (non-literal) patterns: - // non-! wildcard match → valid (true) - // ! match → invalid (false) - // Literal inclusions already handled above; skip them here. - $state = false; - foreach ($this->patterns as $pattern) { - if (str_starts_with((string) $pattern, '!')) { - if ($this->match($value, substr((string) $pattern, 1))) { - $state = false; - } - } elseif ($isWildcard($pattern)) { - if ($this->match($value, $pattern)) { - $state = true; - } - } - // literal non-! patterns are skipped (handled in step 1) - } - - return $state; - } - - /** - * Is array - */ - public function isArray(): bool - { - return false; - } - - /** - * Get Type - */ - public function getType(): string - { - return self::TYPE_STRING; - } - - /** - * Match a subject against a single pattern. - */ - private function match(string $subject, string $pattern): bool - { - return $this->matchGlobstar($subject, $pattern); - } - - /** - * Match using a regex built from a pattern that contains **. - * Handles **, *, ?, [abc] character classes, and \ escape sequences. - */ - private function matchGlobstar(string $subject, string $pattern): bool - { - $regex = ''; - $len = \strlen($pattern); - $i = 0; - - while ($i < $len) { - $char = $pattern[$i]; - - if ($char === '\\' && $i + 1 < $len) { - $regex .= preg_quote($pattern[$i + 1], '~'); - $i += 2; - } elseif ($char === '[') { - $j = $i + 1; - $bracketContent = ''; - $isNegated = false; - - // Check for negation marker (! or ^) - if ($j < $len && ($pattern[$j] === '!' || $pattern[$j] === '^')) { - $negChar = $pattern[$j]; - $j++; - // If ] immediately follows the negation marker, treat ! or ^ as a - // literal class member (edge case: [!] or [^]) rather than as negation. - if ($j < $len && $pattern[$j] === ']') { - // Literal class containing only ! or ^ — do NOT treat as negation - $bracketContent .= $negChar; - } else { - $isNegated = true; - $bracketContent .= $negChar; - } - } - - // Allow ] as first char inside bracket class (POSIX rule) - if ($j < $len && $pattern[$j] === ']' && $bracketContent === '') { - $bracketContent .= ']'; - $j++; - } elseif ($j < $len && $pattern[$j] === ']' && $isNegated) { - // After a negation marker, ] as first member is literal - $bracketContent .= ']'; - $j++; - } - - while ($j < $len && $pattern[$j] !== ']') { - $bracketContent .= $pattern[$j]; - $j++; - } - - if ($j < $len) { - // Well-formed [...] — normalise ! negation to ^ - $inner = $bracketContent; - if ($isNegated && str_starts_with($inner, '!')) { - $inner = '^' . substr($inner, 1); - } elseif ($isNegated && str_starts_with($inner, '^')) { - // already ^-prefixed, keep as-is - } elseif (!$isNegated && str_starts_with($inner, '^')) { - // Literal ^ as first char — escape it so PCRE doesn't treat as negation - $inner = '\\^' . substr($inner, 1); - } - $regex .= '[' . $inner . ']'; - $i = $j + 1; - } else { - // Unclosed bracket — treat [ as a literal character - $regex .= preg_quote('[', '~'); - $i++; - } - } elseif ($char === '*' && isset($pattern[$i + 1]) && $pattern[$i + 1] === '*') { - $prevSlash = $i === 0 || $pattern[$i - 1] === '/'; - $nextSlash = isset($pattern[$i + 2]) && $pattern[$i + 2] === '/'; - - if ($prevSlash && $nextSlash) { - // a/**/b — zero or more intermediate directories - $regex .= '(?:.+/)?'; - $i += 3; - } else { - // foo/** or standalone ** — matches everything - $regex .= '.*'; - $i += 2; - } - } elseif ($char === '*') { - $regex .= '[^/]*'; - $i++; - } elseif ($char === '?') { - $regex .= '[^/]'; - $i++; - } else { - $regex .= preg_quote($char, '~'); - $i++; - } - } - - return (bool) preg_match('~^' . $regex . '$~', $subject); - } -} diff --git a/packages/validators/src/Validator/HexColor.php b/packages/validators/src/Validator/HexColor.php deleted file mode 100644 index 104a5e6be..000000000 --- a/packages/validators/src/Validator/HexColor.php +++ /dev/null @@ -1,40 +0,0 @@ -whitelist); - } - - /** - * Is valid - * - * Validation will pass when $value starts with one of the given hosts - * - * @param mixed $value - */ - public function isValid($value): bool - { - $urlValidator = new URL(); - - if (!$urlValidator->isValid($value)) { - return false; - } - - $hostnameValidator = new Hostname($this->whitelist); - - return $hostnameValidator->isValid(parse_url((string) $value, PHP_URL_HOST)); - } - - /** - * Is array - * - * Function will return true if object is array. - */ - public function isArray(): bool - { - return false; - } - - /** - * Get Type - * - * Returns validator type. - */ - public function getType(): string - { - return self::TYPE_STRING; - } -} diff --git a/packages/validators/src/Validator/Hostname.php b/packages/validators/src/Validator/Hostname.php deleted file mode 100644 index 6b4c16118..000000000 --- a/packages/validators/src/Validator/Hostname.php +++ /dev/null @@ -1,95 +0,0 @@ - 253) { - return false; - } - - // This tests: 'http://', 'https://', and 'myapp.com/route' - if (str_contains($value, '/')) { - return false; - } - - // This tests for: 'myapp.com:3000' - if (str_contains($value, ':')) { - return false; - } - - // Logic #1: Empty allowList means everything is allowed - if ($this->allowList === []) { - return true; - } - - // Logic #2: Allow List not empty, there are rules to check - // Loop through all allowed hostnames until match is found - foreach ($this->allowList as $allowedHostname) { - // If exact match; allow - // If *, allow everything - if ($value === $allowedHostname || $allowedHostname === '*') { - return true; - } - - // If wildcard symbol used - if (str_starts_with($allowedHostname, '*')) { - // Remove starting * symbol before comparing - $allowedHostname = substr($allowedHostname, 1); - - // If rest of hostname match; allow - // Notice allowedHostname still includes starting dot. Root domain is NOT allowed by wildcard. - if (str_ends_with($value, $allowedHostname)) { - return true; - } - } - } - - // If finished loop above without result, match is not found - return false; - } -} diff --git a/packages/validators/src/Validator/IP.php b/packages/validators/src/Validator/IP.php deleted file mode 100644 index 42dcb550a..000000000 --- a/packages/validators/src/Validator/IP.php +++ /dev/null @@ -1,103 +0,0 @@ -type = $type; - } - - /** - * Get Description - * - * Returns validator description - */ - public function getDescription(): string - { - return 'Value must be a valid IP address'; - } - - /** - * Is valid - * - * Validation will pass when $value is valid IP address. - * - * @param mixed $value - */ - public function isValid($value): bool - { - switch ($this->type) { - case self::ALL: - if (filter_var($value, FILTER_VALIDATE_IP)) { - return true; - } - break; - - case self::V4: - if (filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { - return true; - } - break; - - case self::V6: - if (filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { - return true; - } - break; - - default: - return false; - } - - return false; - } - - /** - * Is array - * - * Function will return true if object is array. - */ - public function isArray(): bool - { - return false; - } - - /** - * Get Type - * - * Returns validator type. - */ - public function getType(): string - { - return self::TYPE_STRING; - } -} diff --git a/packages/validators/src/Validator/Identifier.php b/packages/validators/src/Validator/Identifier.php deleted file mode 100644 index 6f10c256d..000000000 --- a/packages/validators/src/Validator/Identifier.php +++ /dev/null @@ -1,40 +0,0 @@ -length !== 0) { - $description .= ', and be at most ' . $this->length . ' chars'; - } - - return $description . '.'; - } - - #[\Override] - public function isValid(mixed $value): bool - { - return parent::isValid($value) && preg_match('/^[A-Za-z_]\w*$/D', (string) $value) === 1; - } -} diff --git a/packages/validators/src/Validator/Integer.php b/packages/validators/src/Validator/Integer.php deleted file mode 100755 index dce4e915e..000000000 --- a/packages/validators/src/Validator/Integer.php +++ /dev/null @@ -1,148 +0,0 @@ -bits = $bits; - $this->unsigned = $unsigned; - } - - /** - * Get Description - * - * Returns validator description - */ - public function getDescription(): string - { - $signedness = $this->unsigned ? 'unsigned' : 'signed'; - - // Calculate min and max values based on bit size and signed/unsigned - if ($this->unsigned) { - $min = 0; - $max = (2 ** $this->bits) - 1; - } else { - $min = -(2 ** ($this->bits - 1)); - $max = (2 ** ($this->bits - 1)) - 1; - } - - return \sprintf( - 'Value must be a valid %s %d-bit integer between %s and %s', - $signedness, - $this->bits, - number_format($min), - number_format($max), - ); - } - - /** - * Is array - * - * Function will return true if object is array. - */ - public function isArray(): bool - { - return false; - } - - /** - * Get Type - * - * Returns validator type. - */ - public function getType(): string - { - return self::TYPE_INTEGER; - } - - /** - * Get Bits - * - * Returns the bit size of the integer. - */ - public function getBits(): int - { - return $this->bits; - } - - /** - * Is Unsigned - * - * Returns whether the integer is unsigned. - */ - public function isUnsigned(): bool - { - return $this->unsigned; - } - - /** - * Get Format - * - * Returns the OpenAPI/JSON Schema format string for this integer configuration. - */ - public function getFormat(): string - { - $prefix = $this->isUnsigned() ? 'uint' : 'int'; - return $prefix . $this->bits; - } - - /** - * Is valid - * - * Validation will pass when $value is integer and within the specified bit range. - */ - public function isValid(mixed $value): bool - { - if ($this->loose) { - if (!is_numeric($value)) { - return false; - } - $value += 0; - } - if (!\is_int($value)) { - return false; - } - - // Calculate min and max values based on bit size and signed/unsigned - if ($this->unsigned) { - $min = 0; - $max = (2 ** $this->bits) - 1; - } else { - $min = -(2 ** ($this->bits - 1)); - $max = (2 ** ($this->bits - 1)) - 1; - } - // Check if value is within range - return $value >= $min && $value <= $max; - } -} diff --git a/packages/validators/src/Validator/JSON.php b/packages/validators/src/Validator/JSON.php deleted file mode 100644 index d16a8d670..000000000 --- a/packages/validators/src/Validator/JSON.php +++ /dev/null @@ -1,50 +0,0 @@ -length > 0) { - $message .= ' no longer than ' . $this->length . ' characters when encoded'; - } - - return $message; - } - - /** - * Is array - * - * Function will return true if object is array. - */ - public function isArray(): bool - { - return true; - } - - /** - * Get Type - * - * Returns validator type. - */ - public function getType(): string - { - return self::TYPE_ARRAY; - } - - /** - * Is valid - * - * Validation will pass when $value is a JSON array, or a string encoding one. - */ - public function isValid(mixed $value): bool - { - if (\is_string($value)) { - if (!$this->hasValidLength($value)) { - return false; - } - - // Decoding to objects rather than associative arrays keeps `[]` and `{}` - // distinguishable, which is impossible once both collapse to an empty array. - $decoded = json_decode($value); - - return json_last_error() === JSON_ERROR_NONE && \is_array($decoded); - } - - if (!\is_array($value)) { - return false; - } - - // An already decoded empty array is an ambiguous `[]` or `{}`, so it stays valid. - // Encoded objects are rejected above, while their shape is still observable. - if ($value !== [] && !array_is_list($value)) { - return false; - } - - return $this->hasValidLength(json_encode($value)); - } - - private function hasValidLength(string|false $encoded): bool - { - if ($this->length === 0) { - return true; - } - - return $encoded !== false && \strlen($encoded) <= $this->length; - } -} diff --git a/packages/validators/src/Validator/JSON/FCM.php b/packages/validators/src/Validator/JSON/FCM.php deleted file mode 100644 index 4292d5af4..000000000 --- a/packages/validators/src/Validator/JSON/FCM.php +++ /dev/null @@ -1,142 +0,0 @@ - - */ - private const array REQUIRED_FIELDS = [ - 'type' => 'identifies the credentials as a Google service account', - 'project_id' => 'identifies the Firebase project receiving messages', - 'private_key' => 'signs the OAuth access-token request', - 'client_email' => 'identifies the service account used for authentication', - 'token_uri' => 'identifies the OAuth token endpoint', - ]; - - /** - * Standard service account fields that are validated when present. - * - * @var string[] - */ - private const array OPTIONAL_FIELDS = [ - 'private_key_id', - 'client_id', - 'auth_uri', - 'auth_provider_x509_cert_url', - 'client_x509_cert_url', - 'universe_domain', - ]; - - /** - * @var string[] - */ - private const array URL_FIELDS = [ - 'auth_uri', - 'token_uri', - 'auth_provider_x509_cert_url', - 'client_x509_cert_url', - ]; - - private ?string $error = null; - - public function getDescription(): string - { - return $this->error ?? 'Value must be valid Google service account JSON for FCM'; - } - - public function isArray(): bool - { - return false; - } - - public function getType(): string - { - return self::TYPE_OBJECT; - } - - public function isValid(mixed $value): bool - { - $this->error = null; - - if (\is_string($value)) { - $value = json_decode($value); - - if (json_last_error() !== JSON_ERROR_NONE) { - return false; - } - } - - if ($value instanceof \stdClass) { - $value = get_object_vars($value); - } elseif (!\is_array($value) || ($value !== [] && array_is_list($value))) { - return false; - } - - foreach (self::REQUIRED_FIELDS as $field => $purpose) { - if (!$this->isNonEmptyString($value[$field] ?? null)) { - $this->error = "FCM service account JSON must include a non-empty '{$field}' field, which {$purpose}."; - - return false; - } - } - - foreach (self::OPTIONAL_FIELDS as $field) { - if (\array_key_exists($field, $value) && !$this->isNonEmptyString($value[$field])) { - $this->error = "FCM service account JSON field '{$field}' must be a non-empty string when provided."; - - return false; - } - } - - if ($value['type'] !== 'service_account') { - $this->error = "FCM service account JSON field 'type' must be 'service_account'."; - - return false; - } - - if (filter_var($value['client_email'], FILTER_VALIDATE_EMAIL) === false) { - $this->error = "FCM service account JSON field 'client_email' must contain a valid email address."; - - return false; - } - - if (!str_starts_with((string) $value['private_key'], '-----BEGIN PRIVATE KEY-----') - || !str_ends_with(trim((string) $value['private_key']), '-----END PRIVATE KEY-----')) { - $this->error = "FCM service account JSON field 'private_key' must contain a PEM-encoded private key."; - - return false; - } - - foreach (self::URL_FIELDS as $field) { - if (isset($value[$field]) && !$this->isHttpsUrl($value[$field])) { - $this->error = "FCM service account JSON field '{$field}' must contain a valid HTTPS URL."; - - return false; - } - } - - return true; - } - - private function isNonEmptyString(mixed $value): bool - { - return \is_string($value) && trim($value) !== ''; - } - - private function isHttpsUrl(string $value): bool - { - return filter_var($value, FILTER_VALIDATE_URL) !== false - && strtolower((string) parse_url($value, PHP_URL_SCHEME)) === 'https'; - } -} diff --git a/packages/validators/src/Validator/JSON/ObjectValidator.php b/packages/validators/src/Validator/JSON/ObjectValidator.php deleted file mode 100644 index 6162eae37..000000000 --- a/packages/validators/src/Validator/JSON/ObjectValidator.php +++ /dev/null @@ -1,103 +0,0 @@ -length > 0) { - $message .= ' no longer than ' . $this->length . ' characters when encoded'; - } - - return $message; - } - - /** - * Is array - * - * Function will return true if object is array. - */ - public function isArray(): bool - { - return false; - } - - /** - * Get Type - * - * Returns validator type. - */ - public function getType(): string - { - return self::TYPE_OBJECT; - } - - /** - * Is valid - * - * Validation will pass when $value is a JSON object, or a string encoding one. - */ - public function isValid(mixed $value): bool - { - if (\is_string($value)) { - if (!$this->hasValidLength($value)) { - return false; - } - - // Decoding to objects rather than associative arrays keeps `{}` and `[]` - // distinguishable, which is impossible once both collapse to an empty array. - $decoded = json_decode($value); - - return json_last_error() === JSON_ERROR_NONE && $decoded instanceof \stdClass; - } - - if ($value instanceof \stdClass) { - return $this->hasValidLength(json_encode($value)); - } - - if (!\is_array($value)) { - return false; - } - - // An already decoded empty array is an ambiguous `{}` or `[]`, so it stays valid. - // Encoded lists are rejected above, while their shape is still observable. - if ($value !== [] && array_is_list($value)) { - return false; - } - - return $this->hasValidLength(json_encode($value)); - } - - private function hasValidLength(string|false $encoded): bool - { - if ($this->length === 0) { - return true; - } - - return $encoded !== false && \strlen($encoded) <= $this->length; - } -} diff --git a/packages/validators/src/Validator/Multiple.php b/packages/validators/src/Validator/Multiple.php deleted file mode 100644 index 1c561d8d7..000000000 --- a/packages/validators/src/Validator/Multiple.php +++ /dev/null @@ -1,99 +0,0 @@ -addRule($rule); - } - } - /** - * Add rule - * - * Add a new rule to the end of the rules containing array - */ - public function addRule(Validator $rule): static - { - $this->rules[] = $rule; - - return $this; - } - - /** - * Get Description - * - * Returns validator description - */ - public function getDescription(): string - { - $description = ''; - foreach ($this->rules as $key => $rule) { - $description .= ++$key . '. ' . $rule->getDescription() . " \n"; - } - - return $description; - } - - /** - * Is valid - * - * Validation will pass when all rules are valid if only one of the rules is invalid validation will fail. - */ - public function isValid(mixed $value): bool - { - foreach ($this->rules as $rule) { /* @var $rule Validator */ - if (false === $rule->isValid($value)) { - return false; - } - } - - return true; - } - - /** - * Get Type - * - * Returns validator type. - */ - public function getType(): string - { - return $this->type; - } - - /** - * Is array - * - * Function will return true if object is array. - */ - public function isArray(): bool - { - return true; - } -} diff --git a/packages/validators/src/Validator/NoneOf.php b/packages/validators/src/Validator/NoneOf.php deleted file mode 100644 index 74c7b9958..000000000 --- a/packages/validators/src/Validator/NoneOf.php +++ /dev/null @@ -1,72 +0,0 @@ - $validators - */ - public function __construct(protected array $validators, protected string $type = self::TYPE_MIXED) {} - - /** - * Get Description - * - * Returns validator description - */ - public function getDescription(): string - { - if (!(\is_null($this->failedRule))) { - return $this->failedRule->getDescription(); - } - return $this->validators[0]->getDescription(); - } - - /** - * Is valid - * - * Validation will pass when all rules are valid if only one of the rules is invalid validation will fail. - */ - public function isValid(mixed $value): bool - { - foreach ($this->validators as $rule) { - $valid = $rule->isValid($value); - - if ($valid) { - $this->failedRule = $rule; - return false; - } - } - - return true; - } - - /** - * Get Type - * - * Returns validator type. - */ - public function getType(): string - { - return $this->type; - } - - /** - * Is array - * - * Function will return true if object is array. - */ - public function isArray(): bool - { - return true; - } -} diff --git a/packages/validators/src/Validator/Nullable.php b/packages/validators/src/Validator/Nullable.php deleted file mode 100644 index 4df260a57..000000000 --- a/packages/validators/src/Validator/Nullable.php +++ /dev/null @@ -1,61 +0,0 @@ -validator->getDescription() . ' or null'; - } - - /** - * Is array - * - * Function will return true if object is array. - */ - public function isArray(): bool - { - return false; - } - - /** - * Get Type - * - * Returns validator type. - */ - public function getType(): string - { - return $this->validator->getType(); - } - - public function getValidator(): Validator - { - return $this->validator; - } - - /** - * Is valid - * - * Validation will pass when $value is text with valid length. - */ - public function isValid(mixed $value): bool - { - if (\is_null($value)) { - return true; - } - - return $this->validator->isValid($value); - } -} diff --git a/packages/validators/src/Validator/Numeric.php b/packages/validators/src/Validator/Numeric.php deleted file mode 100755 index bb742ce1c..000000000 --- a/packages/validators/src/Validator/Numeric.php +++ /dev/null @@ -1,55 +0,0 @@ -allowEmpty && $value === '') { - return true; - } - - if ($this->normalize) { - $value = self::normalize($value); - } - - return preg_match('/^\+[1-9]\d{6,14}$/', $value) === 1; - } -} diff --git a/packages/validators/src/Validator/Range.php b/packages/validators/src/Validator/Range.php deleted file mode 100755 index 346d6e823..000000000 --- a/packages/validators/src/Validator/Range.php +++ /dev/null @@ -1,112 +0,0 @@ -min; - } - - /** - * Get Range Maximum Value - */ - public function getMax(): int|float - { - return $this->max; - } - - /** - * Get Range Format - */ - public function getFormat(): string - { - return $this->format; - } - - /** - * Get Description - * - * Returns validator description - */ - #[\Override] - public function getDescription(): string - { - return 'Value must be a valid range between ' . number_format($this->min) . ' and ' . number_format($this->max); - } - - /** - * Is array - * - * Function will return true if object is array. - */ - #[\Override] - public function isArray(): bool - { - return false; - } - - /** - * Get Type - * - * Returns validator type. - */ - #[\Override] - public function getType(): string - { - return $this->format; - } - - /** - * Is valid - * - * Validation will pass when $value number is bigger or equal than $min number and lower or equal than $max. - * Not strict, considers any valid integer to be a valid float - * Considers infinity to be a valid integer - */ - #[\Override] - public function isValid(mixed $value): bool - { - if (!parent::isValid($value)) { - return false; - } - - switch ($this->format) { - case self::TYPE_INTEGER: - // Accept infinity as an integer - // Since gettype(INF) === TYPE_FLOAT - if ($value === INF || $value === -INF) { - break; // move to check if value is within range - } - $value += 0; - if (!\is_int($value)) { - return false; - } - break; - case self::TYPE_FLOAT: - if (!is_numeric($value)) { - return false; - } - $value += 0.0; - break; - default: - return false; - } - return $this->min <= $value && $this->max >= $value; - } -} diff --git a/packages/validators/src/Validator/Text.php b/packages/validators/src/Validator/Text.php deleted file mode 100644 index c1a5abe0a..000000000 --- a/packages/validators/src/Validator/Text.php +++ /dev/null @@ -1,123 +0,0 @@ -min === $this->length) { - $message .= ' and exactly ' . $this->length . ' chars'; - } else { - if ($this->min !== 0) { - $message .= ' and at least ' . $this->min . ' chars'; - } - - if ($this->length !== 0) { - $message .= ' and no longer than ' . $this->length . ' chars'; - } - } - - if ($this->allowList) { - $message .= ' and only consist of \'' . implode(', ', $this->allowList) . '\' chars'; - } - - if ($this->requireNonBlank) { - $message .= ' and not be blank'; - } - - return $message; - } - - /** - * Is array - * - * Function will return true if object is array. - */ - public function isArray(): bool - { - return false; - } - - /** - * Get Type - * - * Returns validator type. - */ - public function getType(): string - { - return self::TYPE_STRING; - } - - /** - * Is valid - * - * Validation will pass when $value is text with valid length. - */ - public function isValid(mixed $value): bool - { - if (!\is_string($value)) { - return false; - } - - if ($this->requireNonBlank && preg_match('/[^\p{Z}\p{Cf}\s]/u', $value) !== 1) { - return false; - } - - if (mb_strlen($value) < $this->min) { - return false; - } - - if (mb_strlen($value) > $this->length && $this->length !== 0) { - return false; - } - - if (\count($this->allowList) > 0) { - foreach (str_split($value) as $char) { - if (!\in_array($char, $this->allowList)) { - return false; - } - } - } - - return true; - } -} diff --git a/packages/validators/src/Validator/URL.php b/packages/validators/src/Validator/URL.php deleted file mode 100644 index fed0204c0..000000000 --- a/packages/validators/src/Validator/URL.php +++ /dev/null @@ -1,136 +0,0 @@ -httpsOrLoopback ? ' restricted to https or http on a loopback host' : ''; - - if ($this->allowedSchemes !== []) { - $description = 'Value must be a valid URL with following schemes (' . implode(', ', $this->allowedSchemes) . ')'; - - if (!$this->allowFragments) { - $description .= ' and without a fragment component'; - } - - return $description . $transport; - } - - if (!$this->allowFragments) { - return 'Value must be a valid URL without a fragment component' . $transport; - } - - return 'Value must be a valid URL' . $transport; - } - - /** - * Is valid - * - * Validation will pass when $value is valid URL. - * - * @param mixed $value - */ - public function isValid($value): bool - { - if ($this->allowEmpty && $value === '') { - return true; - } - - $isPrivateUseSchemeURI = $this->allowPrivateUseSchemes && $this->isPrivateUseSchemeURI($value); - - // FILTER_VALIDATE_URL rejects authority-less private-use URI schemes - // (e.g. "com.example.app:/oauth", RFC 8252 §7.1). Optionally accept those. - if (filter_var($value, FILTER_VALIDATE_URL) === false && !$isPrivateUseSchemeURI) { - return false; - } - - if ($this->allowedSchemes !== [] && !\in_array(parse_url((string) $value, PHP_URL_SCHEME), $this->allowedSchemes)) { - return false; - } - - if (!$this->allowFragments && parse_url((string) $value, PHP_URL_FRAGMENT) !== null) { - return false; - } - - // Secure-redirect transport policy: a standard (authority-bearing) URL must be - // https on any host, or http on a loopback host (RFC 8252 §7.3). Private-use - // scheme URIs are governed solely by $allowPrivateUseSchemes and are exempt. - if ($this->httpsOrLoopback && !$isPrivateUseSchemeURI) { - $scheme = strtolower((string) parse_url((string) $value, PHP_URL_SCHEME)); - if ($scheme === 'http') { - $host = strtolower((string) parse_url((string) $value, PHP_URL_HOST)); - if (!\in_array($host, ['localhost', '127.0.0.1', '[::1]'], true)) { - return false; - } - } elseif ($scheme !== 'https') { - return false; - } - } - - return true; - } - - /** - * Is private-use URI scheme - * Returns true when $value is an authority-less private-use URI scheme - * redirect URI as defined by RFC 8252 §7.1, e.g. "com.example.app:/oauth" - * @param mixed $value - */ - private function isPrivateUseSchemeURI($value): bool - { - if (!\is_string($value)) { - return false; - } - - $uri = \Uri\Rfc3986\Uri::parse($value); - if (!$uri instanceof \Uri\Rfc3986\Uri) { - return false; - } - - $scheme = $uri->getScheme(); - - return $scheme !== null - && str_contains($scheme, '.') - && $uri->getHost() === null; - } - - /** - * Is array - * - * Function will return true if object is array. - */ - public function isArray(): bool - { - return false; - } - - /** - * Get Type - * - * Returns validator type. - */ - public function getType(): string - { - return self::TYPE_STRING; - } -} diff --git a/packages/validators/src/Validator/WhiteList.php b/packages/validators/src/Validator/WhiteList.php deleted file mode 100755 index 4333c609a..000000000 --- a/packages/validators/src/Validator/WhiteList.php +++ /dev/null @@ -1,85 +0,0 @@ -strict) { - foreach ($this->list as $key => &$value) { - $this->list[$key] = strtolower((string) $value); - } - } - } - - /** - * Get List of All Allowed Values - */ - public function getList(): array - { - return $this->list; - } - - /** - * Get Description - * - * Returns validator description - */ - public function getDescription(): string - { - return 'Value must be one of (' . implode(', ', $this->list) . ')'; - } - - /** - * Is array - * - * Function will return true if object is array. - */ - public function isArray(): bool - { - return false; - } - - /** - * Get Type - * - * Returns validator type. - */ - public function getType(): string - { - return $this->type; - } - - /** - * Is valid - * - * Validation will pass if $value is in the white list array. - */ - public function isValid(mixed $value): bool - { - if (\is_array($value)) { - return false; - } - - $value = ($this->strict) ? $value : strtolower((string) $value); - return \in_array($value, $this->list, $this->strict); - } -} diff --git a/packages/validators/src/Validator/Wildcard.php b/packages/validators/src/Validator/Wildcard.php deleted file mode 100644 index 87b575753..000000000 --- a/packages/validators/src/Validator/Wildcard.php +++ /dev/null @@ -1,57 +0,0 @@ -assertFalse($arrayList->isValid(['text'])); - $this->assertSame('Value must a valid array and Value must be a valid signed 32-bit integer between -2,147,483,648 and 2,147,483,647', $arrayList->getDescription()); - - $arrayList = new ArrayList(new Integer(), 3); - $this->assertFalse($arrayList->isValid(['a', 'b', 'c', 'd'])); - $this->assertSame('Value must a valid array no longer than 3 items and Value must be a valid signed 32-bit integer between -2,147,483,648 and 2,147,483,647', $arrayList->getDescription()); - } - - public function testCanValidateTextValues(): void - { - $arrayList = new ArrayList(new Text(100)); - $this->assertTrue($arrayList->isArray(), 'true'); - $this->assertTrue($arrayList->isValid([0 => 'string', 1 => 'string'])); - $this->assertTrue($arrayList->isValid(['string', 'string'])); - $this->assertFalse($arrayList->isValid(['string', 'string', 3])); - $this->assertFalse($arrayList->isValid('string')); - $this->assertFalse($arrayList->isValid('string')); - $this->assertSame(\Utopia\Validator::TYPE_STRING, $arrayList->getType()); - $this->assertInstanceOf(Text::class, $arrayList->getValidator()); - } - - public function testCanValidateNumericValues(): void - { - $arrayList = new ArrayList(new Numeric()); - $this->assertTrue($arrayList->isValid([1, 2, 3])); - $this->assertFalse($arrayList->isValid(1)); - $this->assertFalse($arrayList->isValid('string')); - $this->assertSame(\Utopia\Validator::TYPE_MIXED, $arrayList->getType()); - $this->assertInstanceOf(Numeric::class, $arrayList->getValidator()); - } - - public function testCanValidateNumericValuesWithBoundaries(): void - { - $arrayList = new ArrayList(new Numeric(), 2); - $this->assertTrue($arrayList->isValid([1])); - $this->assertTrue($arrayList->isValid([1, 2])); - $this->assertFalse($arrayList->isValid([1, 2, 3])); - $this->assertSame(\Utopia\Validator::TYPE_MIXED, $arrayList->getType()); - $this->assertInstanceOf(Numeric::class, $arrayList->getValidator()); - } -} diff --git a/packages/validators/tests/Validator/AssocTest.php b/packages/validators/tests/Validator/AssocTest.php deleted file mode 100755 index 3b24b2e23..000000000 --- a/packages/validators/tests/Validator/AssocTest.php +++ /dev/null @@ -1,45 +0,0 @@ -assoc = new Assoc(); - } - - public function tearDown(): void - { - $this->assoc = null; - } - - public function testCanValidateAssocArray(): void - { - $this->assertTrue($this->assoc->isValid(['1' => 'a', '0' => 'b', '2' => 'c'])); - $this->assertTrue($this->assoc->isValid(['a' => 'a', 'b' => 'b', 'c' => 'c'])); - $this->assertTrue($this->assoc->isValid([])); - $this->assertTrue($this->assoc->isValid(['value' => str_repeat('-', 62000)])); - $this->assertFalse($this->assoc->isArray()); - $this->assertSame(\Utopia\Validator::TYPE_OBJECT, $this->assoc->getType()); - } - - public function testCantValidateSequentialArray(): void - { - $this->assertFalse($this->assoc->isValid([0 => 'string', 1 => 'string'])); - $this->assertFalse($this->assoc->isValid(['a'])); - $this->assertFalse($this->assoc->isValid(['a', 'b', 'c'])); - $this->assertFalse($this->assoc->isValid(['0' => 'a', '1' => 'b', '2' => 'c'])); - } - - public function testCantValidateAssocArrayWithOver65kCharacters(): void - { - $this->assertFalse($this->assoc->isValid(['value' => str_repeat('-', 66000)])); - } -} diff --git a/packages/validators/tests/Validator/BooleanTest.php b/packages/validators/tests/Validator/BooleanTest.php deleted file mode 100755 index e2e133afb..000000000 --- a/packages/validators/tests/Validator/BooleanTest.php +++ /dev/null @@ -1,48 +0,0 @@ -assertTrue($boolean->isValid(true)); - $this->assertTrue($boolean->isValid(false)); - $this->assertFalse($boolean->isValid('false')); - $this->assertFalse($boolean->isValid('true')); - $this->assertFalse($boolean->isValid('0')); - $this->assertFalse($boolean->isValid('1')); - $this->assertFalse($boolean->isValid(0)); - $this->assertFalse($boolean->isValid(1)); - $this->assertFalse($boolean->isValid(['string', 'string'])); - $this->assertFalse($boolean->isValid('string')); - $this->assertFalse($boolean->isValid(1.2)); - $this->assertFalse($boolean->isArray()); - $this->assertSame(\Utopia\Validator::TYPE_BOOLEAN, $boolean->getType()); - } - - public function testCanValidateLoosely(): void - { - $boolean = new Boolean(true); - - $this->assertTrue($boolean->isValid(true)); - $this->assertTrue($boolean->isValid(false)); - $this->assertTrue($boolean->isValid('false')); - $this->assertTrue($boolean->isValid('true')); - $this->assertTrue($boolean->isValid('0')); - $this->assertTrue($boolean->isValid('1')); - $this->assertTrue($boolean->isValid(0)); - $this->assertTrue($boolean->isValid(1)); - $this->assertFalse($boolean->isValid(['string', 'string'])); - $this->assertFalse($boolean->isValid('string')); - $this->assertFalse($boolean->isValid(1.2)); - $this->assertFalse($boolean->isArray()); - $this->assertSame(\Utopia\Validator::TYPE_BOOLEAN, $boolean->getType()); - } -} diff --git a/packages/validators/tests/Validator/ContainsTest.php b/packages/validators/tests/Validator/ContainsTest.php deleted file mode 100644 index a3872906d..000000000 --- a/packages/validators/tests/Validator/ContainsTest.php +++ /dev/null @@ -1,131 +0,0 @@ -assertTrue($validator->isValid('[skip ci] update changelog')); - $this->assertTrue($validator->isValid('docs: update readme [skip ci]')); - $this->assertTrue($validator->isValid('prefix[skip ci]suffix')); - - $this->assertFalse($validator->isValid('fix: real bug fix')); - $this->assertFalse($validator->isValid('skip deploy without brackets')); - $this->assertFalse($validator->isValid('')); - } - - public function testCanValidateWithMultiplePatterns(): void - { - $validator = new Contains(['[skip ci]', '[no ci]', '[ci skip]']); - - $this->assertTrue($validator->isValid('[skip ci]')); - $this->assertTrue($validator->isValid('[no ci]')); - $this->assertTrue($validator->isValid('[ci skip]')); - $this->assertFalse($validator->isValid('[skip deploy]')); - } - - public function testCanValidateLoosely(): void - { - $validator = new Contains(['[skip ci]']); - - $this->assertTrue($validator->isValid('[skip ci]')); - $this->assertTrue($validator->isValid('[SKIP CI]')); - $this->assertTrue($validator->isValid('[Skip Ci]')); - $this->assertTrue($validator->isValid('Docs update [SKIP CI]')); - } - - public function testCanValidateStrictly(): void - { - $validator = new Contains(['[skip ci]'], true); - - $this->assertTrue($validator->isValid('[skip ci]')); - $this->assertTrue($validator->isValid('prefix[skip ci]suffix')); - - $this->assertFalse($validator->isValid('[SKIP CI]')); - $this->assertFalse($validator->isValid('[Skip Ci]')); - } - - public function testCanValidateMultilineStrings(): void - { - $validator = new Contains(['[skip ci]']); - - $message = "feat: add new stuff\n\nMore detail here.\n\n[skip ci]"; - $this->assertTrue($validator->isValid($message)); - } - - public function testThrowsExceptionForEmptyPatternsArray(): void - { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Patterns array cannot be empty'); - - new Contains([]); - } - - public function testCanValidateWithEmptyPatternString(): void - { - $validator = new Contains(['']); - - $this->assertTrue($validator->isValid('any string')); - $this->assertTrue($validator->isValid('')); - } - - public function testCanValidateWithNonStringValues(): void - { - $validator = new Contains(['[skip ci]']); - - $this->assertFalse($validator->isValid(null)); - $this->assertFalse($validator->isValid([])); - $this->assertFalse($validator->isValid(123)); - $this->assertFalse($validator->isValid(12.34)); - $this->assertFalse($validator->isValid(true)); - $this->assertFalse($validator->isValid(false)); - $this->assertFalse($validator->isValid(new stdClass())); - } - - public function testCanValidatePartialMatches(): void - { - $validator = new Contains(['skip']); - - $this->assertTrue($validator->isValid('skip')); - $this->assertTrue($validator->isValid('skip ci')); - $this->assertTrue($validator->isValid('please skip this')); - $this->assertTrue($validator->isValid('skipping')); - - $this->assertFalse($validator->isValid('ski')); - $this->assertFalse($validator->isValid('')); - } - - public function testCanValidateWithUnicodeCharacters(): void - { - $validator = new Contains(['café', 'naïve']); - - $this->assertTrue($validator->isValid('I love café')); - $this->assertTrue($validator->isValid('Naïve approach')); - $this->assertTrue($validator->isValid('CAFÉ')); - - $this->assertFalse($validator->isValid('I love coffee')); - } - - public function testReturnsCorrectMetadata(): void - { - $validator = new Contains(['foo', 'bar']); - - $this->assertFalse($validator->isArray()); - $this->assertSame(\Utopia\Validator::TYPE_STRING, $validator->getType()); - $this->assertStringContainsString('foo', $validator->getDescription()); - $this->assertStringContainsString('bar', $validator->getDescription()); - $this->assertStringContainsString('case-insensitive', $validator->getDescription()); - - $validatorStrict = new Contains(['foo', 'bar'], true); - - $this->assertStringContainsString('case-sensitive', $validatorStrict->getDescription()); - } -} diff --git a/packages/validators/tests/Validator/DomainTest.php b/packages/validators/tests/Validator/DomainTest.php deleted file mode 100644 index a1c2de9fa..000000000 --- a/packages/validators/tests/Validator/DomainTest.php +++ /dev/null @@ -1,204 +0,0 @@ - - * @version 1.0 RC4 - * @license The MIT License (MIT) - */ - -namespace Utopia\Validator; - -use PHPUnit\Framework\TestCase; - -final class DomainTest extends TestCase -{ - protected Domain $domain; - - public function setUp(): void - { - $this->domain = new Domain(); - } - - public function testIsValid(): void - { - // Assertions - $this->assertTrue($this->domain->isValid('example.com')); - $this->assertTrue($this->domain->isValid('subdomain.example.com')); - $this->assertTrue($this->domain->isValid('subdomain.example-app.com')); - $this->assertFalse($this->domain->isValid('subdomain.example_app.com')); - $this->assertTrue($this->domain->isValid('subdomain-new.example.com')); - $this->assertFalse($this->domain->isValid('subdomain_new.example.com')); - $this->assertTrue($this->domain->isValid('localhost')); - $this->assertTrue($this->domain->isValid('example.io')); - $this->assertTrue($this->domain->isValid('example.org')); - $this->assertTrue($this->domain->isValid('example.org')); - $this->assertFalse($this->domain->isValid(false)); - $this->assertFalse($this->domain->isValid('api.appwrite.io.')); - $this->assertFalse($this->domain->isValid('.api.appwrite.io')); - $this->assertFalse($this->domain->isValid('.api.appwrite.io')); - $this->assertFalse($this->domain->isValid('api..appwrite.io')); - $this->assertFalse($this->domain->isValid('api-.appwrite.io')); - $this->assertFalse($this->domain->isValid('api.-appwrite.io')); - $this->assertFalse($this->domain->isValid('app write.io')); - $this->assertFalse($this->domain->isValid(' appwrite.io')); - $this->assertFalse($this->domain->isValid('appwrite.io ')); - $this->assertFalse($this->domain->isValid('-appwrite.io')); - $this->assertFalse($this->domain->isValid('appwrite.io-')); - $this->assertFalse($this->domain->isValid('.')); - $this->assertFalse($this->domain->isValid('..')); - $this->assertFalse($this->domain->isValid('')); - $this->assertFalse($this->domain->isValid(['string', 'string'])); - $this->assertFalse($this->domain->isValid(1)); - $this->assertFalse($this->domain->isValid(1.2)); - } - - /** - * Test domain validation with hostnames flag set to false (permissive mode) - */ - public function testDomainValidationWithHostnamesFalse(): void - { - // Create validator with hostnames=false for permissive validation - $permissiveValidator = new Domain([], false); - - $this->assertTrue($permissiveValidator->isValid('xn--e1afmkfd.xn--p1ai')); // пример.рф in punycode - $this->assertTrue($permissiveValidator->isValid('xn--fsq.com')); // 中.com in punycode - $this->assertTrue($permissiveValidator->isValid('123.com')); - $this->assertTrue($permissiveValidator->isValid('test123.example.com')); - $this->assertTrue($permissiveValidator->isValid('localhost')); - $this->assertTrue($permissiveValidator->isValid('intranet')); - $this->assertTrue($permissiveValidator->isValid('subdomain_new.example.com')); - $this->assertTrue($permissiveValidator->isValid('subdomain.example_app.com')); - $longLabel = str_repeat('a', 63); - $this->assertTrue($permissiveValidator->isValid($longLabel . '.com')); - $this->assertTrue($permissiveValidator->isValid('a.b.c.d.example.com')); - $this->assertTrue($permissiveValidator->isValid('sub1.sub2.sub3.example.org')); - $this->assertTrue($permissiveValidator->isValid('api-.appwrite.io')); // Dash at end of label - $this->assertTrue($permissiveValidator->isValid('api.-appwrite.io')); // Dash at start of label - $this->assertTrue($permissiveValidator->isValid('app write.io')); // Space in domain - $this->assertTrue($permissiveValidator->isValid(' appwrite.io')); // Leading space - $this->assertTrue($permissiveValidator->isValid('appwrite.io ')); // Trailing space - $this->assertTrue($permissiveValidator->isValid('-appwrite.io')); // Leading dash - } - - /** - * Test domains that are invalid even with hostnames=false - */ - public function testInvalidDomainsWithHostnamesFalse(): void - { - // Create validator with hostnames=false for permissive validation - $permissiveValidator = new Domain([], false); - - // These should still be invalid even in permissive mode - $this->assertFalse($permissiveValidator->isValid('example..com')); // Double dot - $this->assertFalse($permissiveValidator->isValid('.example.com')); // Leading dot - $this->assertFalse($permissiveValidator->isValid('example.com.')); // Trailing dot (caught by Domain validator) - $this->assertFalse($permissiveValidator->isValid('appwrite.io-')); // Trailing dash (caught by Domain validator) - - // Test label too long (more than 63 characters) - $tooLongLabel = str_repeat('a', 64); - $this->assertFalse($permissiveValidator->isValid($tooLongLabel . '.com')); - - // Test total domain length too long (more than 253 characters) - $longDomain = str_repeat('a', 50) . '.' . str_repeat('b', 50) . '.' - . str_repeat('c', 50) . '.' . str_repeat('d', 50) . '.' - . str_repeat('e', 50) . '.com'; - $this->assertFalse($permissiveValidator->isValid($longDomain)); - - // Note: These are actually allowed by FILTER_VALIDATE_DOMAIN without FILTER_FLAG_HOSTNAME - // but might be unexpected: - $this->assertTrue($permissiveValidator->isValid('exam ple.com')); // Space in domain - $this->assertTrue($permissiveValidator->isValid('example@.com')); // @ character - $this->assertTrue($permissiveValidator->isValid('example#.com')); // # character - $this->assertTrue($permissiveValidator->isValid('http://example.com')); // Protocol - $this->assertTrue($permissiveValidator->isValid('example.com:8080')); // Port - $this->assertTrue($permissiveValidator->isValid('example.com/path')); // Path - } - - /** - * Test allowEmpty parameter - */ - public function testAllowEmpty(): void - { - // By default, empty string is invalid - $this->assertFalse($this->domain->isValid('')); - - // With allowEmpty=true, empty string is valid - $domainAllowEmpty = new Domain([], true, true); - $this->assertTrue($domainAllowEmpty->isValid('')); - - // null is still invalid even with allowEmpty=true - $this->assertFalse($domainAllowEmpty->isValid(null)); - - // Valid domains still pass with allowEmpty=true - $this->assertTrue($domainAllowEmpty->isValid('example.com')); - $this->assertTrue($domainAllowEmpty->isValid('subdomain.example.com')); - - // Invalid domains still fail with allowEmpty=true - $this->assertFalse($domainAllowEmpty->isValid('invalid..domain')); - $this->assertFalse($domainAllowEmpty->isValid(1)); - } - - public function testRestrictions(): void - { - $validator = new Domain([ - Domain::createRestriction('appwrite.network', 3, ['preview-', 'branch-']), - Domain::createRestriction('fra.appwrite.run', 4), - ]); - - $this->assertTrue($validator->isValid('google.com')); - $this->assertTrue($validator->isValid('stage.google.com')); - $this->assertTrue($validator->isValid('shard4.stage.google.com')); - - $this->assertFalse($validator->isValid('appwrite.network')); - $this->assertFalse($validator->isValid('preview-a.appwrite.network')); - $this->assertFalse($validator->isValid('branch-a.appwrite.network')); - $this->assertTrue($validator->isValid('google.appwrite.network')); - $this->assertFalse($validator->isValid('stage.google.appwrite.network')); - $this->assertFalse($validator->isValid('shard4.stage.google.appwrite.network')); - - $this->assertFalse($validator->isValid('fra.appwrite.run')); - $this->assertTrue($validator->isValid('appwrite.run')); - $this->assertTrue($validator->isValid('google.fra.appwrite.run')); - $this->assertFalse($validator->isValid('shard4.google.fra.appwrite.run')); - $this->assertTrue($validator->isValid('branch-google.fra.appwrite.run')); - $this->assertTrue($validator->isValid('preview-google.fra.appwrite.run')); - } - - /** - * Test the hostnames parameter functionality - */ - public function testHostnamesParameter(): void - { - // Test with hostnames=true (default, strict mode with FILTER_FLAG_HOSTNAME) - $strictValidator = new Domain([], true); - $this->assertFalse($strictValidator->isValid('subdomain_new.example.com')); - $this->assertFalse($strictValidator->isValid('subdomain.example_app.com')); - $this->assertFalse($strictValidator->isValid('sub_domain.example.com')); - $this->assertFalse($strictValidator->isValid('app write.io')); - $this->assertFalse($strictValidator->isValid('api-.appwrite.io')); - $this->assertFalse($strictValidator->isValid('api.-appwrite.io')); - - // Test with hostnames=false (permissive mode without FILTER_FLAG_HOSTNAME) - $permissiveValidator = new Domain([], false); - $this->assertTrue($permissiveValidator->isValid('subdomain_new.example.com')); - $this->assertTrue($permissiveValidator->isValid('subdomain.example_app.com')); - $this->assertTrue($permissiveValidator->isValid('sub_domain.example.com')); - $this->assertTrue($permissiveValidator->isValid('app write.io')); - $this->assertTrue($permissiveValidator->isValid('api-.appwrite.io')); - $this->assertTrue($permissiveValidator->isValid('api.-appwrite.io')); - - // Domains without underscores should be valid in both modes - $this->assertTrue($strictValidator->isValid('subdomain.example.com')); - $this->assertTrue($strictValidator->isValid('subdomain-new.example.com')); - $this->assertTrue($permissiveValidator->isValid('subdomain.example.com')); - $this->assertTrue($permissiveValidator->isValid('subdomain-new.example.com')); - } -} diff --git a/packages/validators/tests/Validator/FloatValidatorTest.php b/packages/validators/tests/Validator/FloatValidatorTest.php deleted file mode 100755 index 40b380958..000000000 --- a/packages/validators/tests/Validator/FloatValidatorTest.php +++ /dev/null @@ -1,41 +0,0 @@ -assertTrue($validator->isValid(27.25)); - $this->assertTrue($validator->isValid(23)); - $this->assertTrue($validator->isValid(23.5)); - $this->assertTrue($validator->isValid(1e7)); - $this->assertFalse($validator->isValid('abc')); - $this->assertFalse($validator->isValid(true)); - $this->assertFalse($validator->isValid('23.5')); - $this->assertFalse($validator->isValid('23')); - $this->assertFalse($validator->isArray()); - $this->assertSame(\Utopia\Validator::TYPE_FLOAT, $validator->getType()); - } - - public function testCanValidateLoosely(): void - { - $validator = new FloatValidator(true); - - $this->assertTrue($validator->isValid(27.25)); - $this->assertTrue($validator->isValid(23)); - $this->assertTrue($validator->isValid(23.5)); - $this->assertTrue($validator->isValid(1e7)); - $this->assertTrue($validator->isValid('23.5')); - $this->assertTrue($validator->isValid('23')); - $this->assertFalse($validator->isValid('abc')); - $this->assertFalse($validator->isValid(true)); - $this->assertFalse($validator->isArray()); - $this->assertSame(\Utopia\Validator::TYPE_FLOAT, $validator->getType()); - } -} diff --git a/packages/validators/tests/Validator/GlobstarTest.php b/packages/validators/tests/Validator/GlobstarTest.php deleted file mode 100644 index ef39e9b5a..000000000 --- a/packages/validators/tests/Validator/GlobstarTest.php +++ /dev/null @@ -1,727 +0,0 @@ -assertTrue($validator->isValid('main')); - $this->assertTrue($validator->isValid('feature/anything')); - $this->assertTrue($validator->isValid('src/deep/nested/file.php')); - } - - // ------------------------------------------------------------------------- - // Pure inclusion — OR semantics (any one match is enough) - // ------------------------------------------------------------------------- - - public function testSingleExactInclusion(): void - { - $validator = new Globstar(['main']); - $this->assertTrue($validator->isValid('main')); - $this->assertFalse($validator->isValid('develop')); - $this->assertFalse($validator->isValid('main-extra')); - } - - public function testMultipleExactInclusionsOr(): void - { - $validator = new Globstar(['main', 'develop', 'staging']); - $this->assertTrue($validator->isValid('main')); - $this->assertTrue($validator->isValid('develop')); - $this->assertTrue($validator->isValid('staging')); - $this->assertFalse($validator->isValid('feature/foo')); - $this->assertFalse($validator->isValid('production')); - } - - public function testSingleWildcardInclusion(): void - { - $validator = new Globstar(['feature/*']); - $this->assertTrue($validator->isValid('feature/foo')); - $this->assertTrue($validator->isValid('feature/bar')); - $this->assertFalse($validator->isValid('feature/foo/bar')); // * does not cross / - $this->assertFalse($validator->isValid('main')); - } - - public function testSingleWildcardWithDirectoryPrefix(): void - { - // baz/*.txt — * matches within a single directory segment - $this->assertTrue(new Globstar(['baz/*.txt'])->isValid('baz/file.txt')); - $this->assertFalse(new Globstar(['baz/*.txt'])->isValid('a/baz/file.txt')); // prefix must match literally - $this->assertFalse(new Globstar(['baz/*.txt'])->isValid('baz/file.log')); - } - - public function testWildcardWithDash(): void - { - $validator = new Globstar(['feature/test-*']); - $this->assertTrue($validator->isValid('feature/test-1')); - $this->assertTrue($validator->isValid('feature/test-abc')); - $this->assertFalse($validator->isValid('feature/other')); - $this->assertFalse($validator->isValid('feature/test')); - } - - public function testQuestionMarkWildcard(): void - { - $validator = new Globstar(['v?.?']); - $this->assertTrue($validator->isValid('v1.0')); - $this->assertTrue($validator->isValid('v2.5')); - $this->assertFalse($validator->isValid('v10.0')); // ? matches exactly one char, not two - $this->assertFalse($validator->isValid('v1/0')); // ? does not cross / - } - - public function testQuestionMarkDoesNotCrossSlash(): void - { - $validator = new Globstar(['feature/?']); - $this->assertTrue($validator->isValid('feature/a')); - $this->assertTrue($validator->isValid('feature/z')); - $this->assertFalse($validator->isValid('feature/ab')); // ? matches only one char - $this->assertFalse($validator->isValid('feature/a/b')); // ? does not cross / - $this->assertFalse($validator->isValid('feature/')); - } - - public function testQuestionMarkMixedWithStar(): void - { - $validator = new Globstar(['fix-?.*']); - $this->assertTrue($validator->isValid('fix-1.php')); - $this->assertTrue($validator->isValid('fix-a.js')); - $this->assertFalse($validator->isValid('fix-12.php')); // ? matches only one char - $this->assertFalse($validator->isValid('fix-.php')); // ? requires exactly one char - } - - public function testQuestionMarkSuffix(): void - { - // qux? — question mark matches exactly one character as suffix - $this->assertTrue(new Globstar(['qux?'])->isValid('qux1')); - $this->assertTrue(new Globstar(['qux?'])->isValid('quxa')); - $this->assertFalse(new Globstar(['qux?'])->isValid('qux')); // requires exactly one char - $this->assertFalse(new Globstar(['qux?'])->isValid('qux12')); // does not match two chars - } - - public function testDoubleWildcardAtEnd(): void - { - $validator = new Globstar(['src/**']); - $this->assertTrue($validator->isValid('src/foo.js')); - $this->assertTrue($validator->isValid('src/a/b/c.js')); - $this->assertTrue($validator->isValid('src/deep/nested/file.php')); - $this->assertFalse($validator->isValid('lib/foo.js')); - } - - public function testDoubleWildcardInMiddle(): void - { - $validator = new Globstar(['a/**/b']); - $this->assertTrue($validator->isValid('a/b')); // zero intermediate dirs - $this->assertTrue($validator->isValid('a/x/b')); // one - $this->assertTrue($validator->isValid('a/x/y/b')); // two - $this->assertFalse($validator->isValid('a/b/c')); - $this->assertFalse($validator->isValid('x/a/b')); - } - - public function testDoubleWildcardAtStart(): void - { - $validator = new Globstar(['**/foo']); - $this->assertTrue($validator->isValid('foo')); // zero leading dirs - $this->assertTrue($validator->isValid('a/foo')); // one - $this->assertTrue($validator->isValid('a/b/foo')); // two - $this->assertFalse($validator->isValid('foobar')); - $this->assertFalse($validator->isValid('a/foobar')); - } - - public function testMixedExactAndWildcardInclusions(): void - { - $validator = new Globstar(['main', 'feature/*']); - $this->assertTrue($validator->isValid('main')); - $this->assertTrue($validator->isValid('feature/foo')); - $this->assertFalse($validator->isValid('develop')); - $this->assertFalse($validator->isValid('feature/foo/bar')); - } - - // ------------------------------------------------------------------------- - // Pure exclusion — AND semantics (must not match any exclusion) - // ------------------------------------------------------------------------- - - public function testSingleExactExclusion(): void - { - $validator = new Globstar(['!main']); - $this->assertFalse($validator->isValid('main')); - $this->assertTrue($validator->isValid('develop')); - $this->assertTrue($validator->isValid('feature/foo')); - } - - public function testMultipleExactExclusionsAnd(): void - { - $validator = new Globstar(['!main', '!develop']); - $this->assertFalse($validator->isValid('main')); - $this->assertFalse($validator->isValid('develop')); - $this->assertTrue($validator->isValid('staging')); - $this->assertTrue($validator->isValid('feature/foo')); - } - - public function testWildcardExclusion(): void - { - $validator = new Globstar(['!feature/*']); - $this->assertFalse($validator->isValid('feature/foo')); - $this->assertFalse($validator->isValid('feature/bar')); - $this->assertTrue($validator->isValid('main')); - $this->assertTrue($validator->isValid('hotfix/urgent')); - } - - public function testDoubleWildcardExclusion(): void - { - $validator = new Globstar(['!src/**']); - $this->assertFalse($validator->isValid('src/foo.js')); - $this->assertFalse($validator->isValid('src/a/b/c.js')); - $this->assertTrue($validator->isValid('lib/foo.js')); - $this->assertTrue($validator->isValid('main')); - } - - // ------------------------------------------------------------------------- - // Mixed inclusion + exclusion - // ------------------------------------------------------------------------- - - public function testInclusionTakesPrecedenceWhenBothMatch(): void - { - $validator = new Globstar(['!feature/*', 'feature/abc']); - $this->assertTrue($validator->isValid('feature/abc')); // inclusion wins - $this->assertFalse($validator->isValid('feature/xyz')); // only exclusion matches - $this->assertFalse($validator->isValid('main')); // no inclusion matches - } - - public function testInclusionWithNoMatchFails(): void - { - $validator = new Globstar(['main', '!develop']); - $this->assertTrue($validator->isValid('main')); - $this->assertFalse($validator->isValid('develop')); // excluded even if inclusion didn't match - $this->assertFalse($validator->isValid('staging')); // no inclusion match - } - - public function testExclusionBlocksWhenInclusionDoesNotMatch(): void - { - $validator = new Globstar(['feature/*', '!hotfix/*']); - $this->assertTrue($validator->isValid('feature/foo')); - $this->assertFalse($validator->isValid('hotfix/urgent')); // no inclusion match, also excluded - $this->assertFalse($validator->isValid('main')); // no inclusion match - } - - public function testMultipleInclusionsWithSingleExclusion(): void - { - $validator = new Globstar(['main', 'develop', 'feature/*', '!feature/wip']); - $this->assertTrue($validator->isValid('main')); - $this->assertTrue($validator->isValid('develop')); - $this->assertTrue($validator->isValid('feature/foo')); - $this->assertFalse($validator->isValid('feature/wip')); // specific exclusion overrides wildcard inclusion - $this->assertFalse($validator->isValid('hotfix/urgent')); // no inclusion match - } - - public function testSingleInclusionWithMultipleExclusions(): void - { - $validator = new Globstar(['feature/**', '!feature/wip', '!feature/experimental']); - $this->assertTrue($validator->isValid('feature/foo')); - $this->assertTrue($validator->isValid('feature/a/b')); - $this->assertFalse($validator->isValid('feature/wip')); - $this->assertFalse($validator->isValid('feature/experimental')); - $this->assertFalse($validator->isValid('main')); - } - - public function testMultipleInclusionsWithMultipleExclusions(): void - { - $validator = new Globstar(['main', 'feature/**', '!feature/wip', '!feature/experimental']); - $this->assertTrue($validator->isValid('main')); - $this->assertTrue($validator->isValid('feature/foo')); - $this->assertTrue($validator->isValid('feature/a/b')); - $this->assertFalse($validator->isValid('feature/wip')); - $this->assertFalse($validator->isValid('feature/experimental')); - $this->assertFalse($validator->isValid('develop')); - } - - public function testWildcardExclusionOverridesWildcardInclusion(): void - { - $validator = new Globstar(['src/**', '!src/generated/**']); - $this->assertTrue($validator->isValid('src/components/Button.php')); - $this->assertTrue($validator->isValid('src/utils/helper.js')); - $this->assertFalse($validator->isValid('src/generated/Foo.php')); - $this->assertFalse($validator->isValid('src/generated/bar/Baz.php')); - $this->assertFalse($validator->isValid('lib/other.php')); - } - - public function testSpecificInclusionOverridesWildcardExclusion(): void - { - $validator = new Globstar(['feature/hotfix/critical', '!feature/**']); - $this->assertTrue($validator->isValid('feature/hotfix/critical')); // inclusion wins - $this->assertFalse($validator->isValid('feature/foo')); - $this->assertFalse($validator->isValid('feature/hotfix/other')); - $this->assertFalse($validator->isValid('main')); - } - - public function testOnlyExclusionsDefaultToTrueUnlessExcluded(): void - { - $validator = new Globstar(['!main', '!develop']); - $this->assertFalse($validator->isValid('main')); - $this->assertFalse($validator->isValid('develop')); - $this->assertTrue($validator->isValid('staging')); - $this->assertTrue($validator->isValid('feature/foo')); - } - - // ------------------------------------------------------------------------- - // Standalone wildcards - // ------------------------------------------------------------------------- - - public function testStarAloneMatchesSingleSegmentOnly(): void - { - $validator = new Globstar(['*']); - $this->assertTrue($validator->isValid('main')); - $this->assertTrue($validator->isValid('develop')); - $this->assertFalse($validator->isValid('feature/foo')); // * cannot cross / - $this->assertFalse($validator->isValid('a/b/c')); - } - - public function testDoubleStarAloneMatchesEverything(): void - { - $validator = new Globstar(['**']); - $this->assertTrue($validator->isValid('main')); - $this->assertTrue($validator->isValid('feature/foo')); - $this->assertTrue($validator->isValid('src/a/b/c/d/file.php')); - } - - // ------------------------------------------------------------------------- - // Extension patterns — * scope vs. ** scope - // ------------------------------------------------------------------------- - - public function testStarDotExtMatchesRootLevelOnly(): void - { - $validator = new Globstar(['*.php']); - $this->assertTrue($validator->isValid('Foo.php')); - $this->assertTrue($validator->isValid('index.php')); - $this->assertFalse($validator->isValid('src/Foo.php')); // * does not cross / - $this->assertFalse($validator->isValid('a/b/Foo.php')); - $this->assertFalse($validator->isValid('Foo.js')); - } - - public function testDoubleStarSlashExtMatchesAnyDepth(): void - { - $validator = new Globstar(['**/*.php']); - $this->assertTrue($validator->isValid('Foo.php')); - $this->assertTrue($validator->isValid('src/Foo.php')); - $this->assertTrue($validator->isValid('src/components/Foo.php')); - $this->assertTrue($validator->isValid('a/b/c/d/Foo.php')); - $this->assertFalse($validator->isValid('Foo.js')); - $this->assertFalse($validator->isValid('src/Foo.js')); - } - - public function testDirPrefixDoubleStarExtPattern(): void - { - $validator = new Globstar(['src/**/*.php']); - $this->assertTrue($validator->isValid('src/Foo.php')); - $this->assertTrue($validator->isValid('src/components/Foo.php')); - $this->assertTrue($validator->isValid('src/a/b/c/Foo.php')); - $this->assertFalse($validator->isValid('Foo.php')); - $this->assertFalse($validator->isValid('lib/Foo.php')); - $this->assertFalse($validator->isValid('src/Foo.js')); - } - - public function testDoubleWildcardAtStartAlternateFilename(): void - { - // **/temp.txt — different filename from existing **/file.txt tests - $this->assertTrue(new Globstar(['**/temp.txt'])->isValid('temp.txt')); - $this->assertTrue(new Globstar(['**/temp.txt'])->isValid('a/temp.txt')); - $this->assertTrue(new Globstar(['**/temp.txt'])->isValid('a/b/temp.txt')); - } - - public function testDoubleWildcardAtEndNoExtension(): void - { - // src/** without extension filter - $this->assertTrue(new Globstar(['src/**'])->isValid('src/file.txt')); - $this->assertTrue(new Globstar(['src/**'])->isValid('src/a/file.txt')); - } - - public function testDoubleWildcardLogExtension(): void - { - // src/**/*.log - $this->assertTrue(new Globstar(['src/**/*.log'])->isValid('src/error.log')); - $this->assertTrue(new Globstar(['src/**/*.log'])->isValid('src/a/debug.log')); - } - - public function testDoubleWildcardMiddleWithTwoSegmentTail(): void - { - // a/**/b/c — tail is two segments (b/c) - $this->assertTrue(new Globstar(['a/**/b/c'])->isValid('a/b/c')); // zero intermediate - $this->assertTrue(new Globstar(['a/**/b/c'])->isValid('a/x/b/c')); // one intermediate - $this->assertTrue(new Globstar(['a/**/b/c'])->isValid('a/x/y/b/c')); // two intermediate - $this->assertFalse(new Globstar(['a/**/b/c'])->isValid('a/b/d')); // wrong tail - } - - public function testDoubleWildcardBothSides(): void - { - // **/d/e/** — globstar on both sides of a literal segment pair - $this->assertTrue(new Globstar(['**/d/e/**'])->isValid('d/e/file.txt')); - $this->assertTrue(new Globstar(['**/d/e/**'])->isValid('x/d/e/file.txt')); - $this->assertTrue(new Globstar(['**/d/e/**'])->isValid('x/y/d/e/z/file.txt')); - $this->assertFalse(new Globstar(['**/d/e/**'])->isValid('d/f/file.txt')); - } - - public function testDoubleWildcardWithJsExtension(): void - { - // src/foo/**/*.js - $this->assertTrue(new Globstar(['src/foo/**/*.js'])->isValid('src/foo/app/file.js')); - $this->assertTrue(new Globstar(['src/foo/**/*.js'])->isValid('src/foo/file.js')); - $this->assertFalse(new Globstar(['src/foo/**/*.js'])->isValid('src/bar/app/file.js')); - } - - public function testDoubleWildcardDeepNesting(): void - { - // Very deep path with ** in the middle - $this->assertTrue(new Globstar(['deep/**/logs/*.log'])->isValid('deep/level1/level2/level3/level4/level5/level6/level7/logs/app.log')); - } - - // ------------------------------------------------------------------------- - // Dots as literal characters - // ------------------------------------------------------------------------- - - public function testDotsInPatternAreLiteral(): void - { - $validator = new Globstar(['release-1.0.0']); - $this->assertTrue($validator->isValid('release-1.0.0')); - $this->assertFalse($validator->isValid('release-1X0Y0')); - $this->assertFalse($validator->isValid('release-1.0.0-hotfix')); - } - - public function testVersionWildcardBranchPattern(): void - { - $validator = new Globstar(['v*.*.*']); - $this->assertTrue($validator->isValid('v1.2.3')); - $this->assertTrue($validator->isValid('v10.20.30')); - $this->assertTrue($validator->isValid('v1.2.3.4')); - $this->assertFalse($validator->isValid('v1.2')); - $this->assertFalse($validator->isValid('1.2.3')); - $this->assertFalse($validator->isValid('v1/2/3')); - } - - public function testDottedFilenamePattern(): void - { - $validator = new Globstar(['*.test.js']); - $this->assertTrue($validator->isValid('Button.test.js')); - $this->assertTrue($validator->isValid('App.test.js')); - $this->assertFalse($validator->isValid('ButtonXtestYjs')); - $this->assertFalse($validator->isValid('src/Button.test.js')); - $this->assertFalse($validator->isValid('Button.test.ts')); - } - - // ------------------------------------------------------------------------- - // Prefix wildcard - // ------------------------------------------------------------------------- - - public function testPrefixWildcardBranchPattern(): void - { - $validator = new Globstar(['main*']); - $this->assertTrue($validator->isValid('main')); - $this->assertTrue($validator->isValid('main-extra')); - $this->assertTrue($validator->isValid('mainline')); - $this->assertFalse($validator->isValid('main/branch')); - $this->assertFalse($validator->isValid('develop')); - } - - // ------------------------------------------------------------------------- - // Deep nesting - // ------------------------------------------------------------------------- - - public function testDoubleWildcardInMiddleDeepNesting(): void - { - $validator = new Globstar(['a/**/b']); - $this->assertTrue($validator->isValid('a/x/y/z/b')); - $this->assertTrue($validator->isValid('a/p/q/r/s/b')); - $this->assertTrue($validator->isValid('a/1/2/3/4/5/b')); - $this->assertFalse($validator->isValid('a/x/y/z/b/extra')); - $this->assertFalse($validator->isValid('prefix/a/x/b')); - } - - public function testDoubleWildcardAtStartDeepNesting(): void - { - $validator = new Globstar(['**/README.md']); - $this->assertTrue($validator->isValid('README.md')); - $this->assertTrue($validator->isValid('docs/README.md')); - $this->assertTrue($validator->isValid('a/b/c/d/README.md')); - $this->assertTrue($validator->isValid('x/y/z/w/v/README.md')); - $this->assertFalse($validator->isValid('a/b/c/README.md.bak')); - $this->assertFalse($validator->isValid('a/b/c/README.md/extra')); - } - - // ------------------------------------------------------------------------- - // Real-world path patterns - // ------------------------------------------------------------------------- - - public function testGeneratedFilesAnywhereExclusion(): void - { - $validator = new Globstar(['!**/generated/**']); - $this->assertFalse($validator->isValid('generated/Foo.php')); - $this->assertFalse($validator->isValid('src/generated/Foo.php')); - $this->assertFalse($validator->isValid('src/api/generated/Bar.php')); - $this->assertFalse($validator->isValid('generated/sub/deep/File.php')); - $this->assertTrue($validator->isValid('src/components/Button.php')); - $this->assertTrue($validator->isValid('main')); - } - - public function testMultipleExtensionInclusions(): void - { - $validator = new Globstar(['**/*.php', '**/*.js']); - $this->assertTrue($validator->isValid('index.php')); - $this->assertTrue($validator->isValid('src/App.php')); - $this->assertTrue($validator->isValid('index.js')); - $this->assertTrue($validator->isValid('src/components/App.js')); - $this->assertFalse($validator->isValid('styles.css')); - $this->assertFalse($validator->isValid('src/styles.css')); - $this->assertFalse($validator->isValid('README.md')); - } - - // ------------------------------------------------------------------------- - // Named-prefix single-level branch - // ------------------------------------------------------------------------- - - public function testReleaseBranchPattern(): void - { - $validator = new Globstar(['release/*']); - $this->assertTrue($validator->isValid('release/1.0')); - $this->assertTrue($validator->isValid('release/hotfix')); - $this->assertTrue($validator->isValid('release/2024-01-15')); - $this->assertFalse($validator->isValid('release/1.0/patch')); - $this->assertFalse($validator->isValid('release')); - $this->assertFalse($validator->isValid('main')); - } - - // ------------------------------------------------------------------------- - // Case sensitivity - // ------------------------------------------------------------------------- - - public function testPatternMatchingIsCaseSensitive(): void - { - $branchValidator = new Globstar(['main']); - $this->assertTrue($branchValidator->isValid('main')); - $this->assertFalse($branchValidator->isValid('Main')); - $this->assertFalse($branchValidator->isValid('MAIN')); - - $wildcardValidator = new Globstar(['feature/*']); - $this->assertTrue($wildcardValidator->isValid('feature/foo')); - $this->assertFalse($wildcardValidator->isValid('Feature/foo')); - $this->assertFalse($wildcardValidator->isValid('FEATURE/foo')); - } - - // ------------------------------------------------------------------------- - // Character class patterns - // ------------------------------------------------------------------------- - - public function testCharacterClassInclusion(): void - { - $validator = new Globstar(['[Mm]ain']); - $this->assertTrue($validator->isValid('main')); - $this->assertTrue($validator->isValid('Main')); - $this->assertFalse($validator->isValid('MAIN')); - $this->assertFalse($validator->isValid('develop')); - } - - public function testCharacterClassInclusionWithWildcardExclusion(): void - { - // [Mm]ain is a character-class pattern (not a literal), so it must not - // short-circuit before exclusions are evaluated. - $validator = new Globstar(['[Mm]ain', '!**']); - $this->assertFalse($validator->isValid('main')); - $this->assertFalse($validator->isValid('Main')); - } - - public function testCharacterClassExclusion(): void - { - $validator = new Globstar(['!feature/[0-9]*']); - $this->assertFalse($validator->isValid('feature/123')); - $this->assertFalse($validator->isValid('feature/9fix')); - $this->assertTrue($validator->isValid('feature/abc')); - $this->assertTrue($validator->isValid('main')); - } - - // ------------------------------------------------------------------------- - // Metadata - // ------------------------------------------------------------------------- - - public function testValidatorMetadata(): void - { - $validator = new Globstar([]); - $this->assertFalse($validator->isArray()); - $this->assertSame(\Utopia\Validator::TYPE_STRING, $validator->getType()); - $this->assertNotEmpty($validator->getDescription()); - } - - public function testRejectsNonStringValues(): void - { - $validator = new Globstar(['main']); - $this->assertFalse($validator->isValid(123)); - $this->assertFalse($validator->isValid(null)); - $this->assertFalse($validator->isValid(['main'])); - $this->assertFalse($validator->isValid(true)); - } - - // ------------------------------------------------------------------------- - // Character class and escaped character coverage (gregpriday/gitignore-php parity) - // ------------------------------------------------------------------------- - - public function testLiteralsExact(): void - { - $this->assertTrue(new Globstar(['file.txt'])->isValid('file.txt')); - $this->assertFalse(new Globstar(['file.txt'])->isValid('file.txt.bak')); - } - - public function testSingleAsteriskDoesNotCrossSlash(): void - { - $this->assertTrue(new Globstar(['*.txt'])->isValid('file.txt')); - $this->assertTrue(new Globstar(['*.txt'])->isValid('another.txt')); - $this->assertFalse(new Globstar(['*.txt'])->isValid('file.txt.bak')); - $this->assertFalse(new Globstar(['*.txt'])->isValid('dir/file.txt')); // * does not cross / - } - - public function testQuestionMarkSingleChar(): void - { - $this->assertTrue(new Globstar(['file.?xt'])->isValid('file.txt')); - $this->assertTrue(new Globstar(['file.?xt'])->isValid('file.dxt')); - $this->assertFalse(new Globstar(['file.?xt'])->isValid('file.xtt')); - } - - public function testDoubleStarPrefixFileMatch(): void - { - $this->assertTrue(new Globstar(['**/file.txt'])->isValid('file.txt')); - $this->assertTrue(new Globstar(['**/file.txt'])->isValid('dir/file.txt')); - $this->assertTrue(new Globstar(['**/file.txt'])->isValid('dir/subdir/file.txt')); - $this->assertFalse(new Globstar(['**/file.txt'])->isValid('file.txt.bak')); - } - - public function testDoubleStarMiddleFileMatch(): void - { - $this->assertTrue(new Globstar(['src/**/file.txt'])->isValid('src/file.txt')); - $this->assertTrue(new Globstar(['src/**/file.txt'])->isValid('src/dir/file.txt')); - $this->assertTrue(new Globstar(['src/**/file.txt'])->isValid('src/dir/subdir/file.txt')); - $this->assertFalse(new Globstar(['src/**/file.txt'])->isValid('other/file.txt')); - } - - public function testEscapedAsteriskIsLiteral(): void - { - $this->assertTrue(new Globstar(['file\*.txt'])->isValid('file*.txt')); - $this->assertFalse(new Globstar(['file\*.txt'])->isValid('fileX.txt')); - } - - public function testEscapedHashIsNotComment(): void - { - // \# must be treated as a literal # character, not a comment marker - $this->assertTrue(new Globstar(['\#not_a_comment.txt'])->isValid('#not_a_comment.txt')); - } - - public function testEscapedQuestionMarkIsLiteral(): void - { - // \? must match literal ? rather than any single character - $this->assertTrue(new Globstar(['file\?.txt'])->isValid('file?.txt')); - } - - public function testBasicCharacterClasses(): void - { - $this->assertTrue(new Globstar(['[a]bc.txt'])->isValid('abc.txt')); - $this->assertFalse(new Globstar(['[a]bc.txt'])->isValid('bbc.txt')); - $this->assertTrue(new Globstar(['[a-z]est.txt'])->isValid('test.txt')); - $this->assertFalse(new Globstar(['[a-z]est.txt'])->isValid('Test.txt')); - $this->assertTrue(new Globstar(['[A-Z]est.txt'])->isValid('Test.txt')); - $this->assertFalse(new Globstar(['[A-Z]est.txt'])->isValid('test.txt')); - $this->assertTrue(new Globstar(['file[0-9].log'])->isValid('file5.log')); - $this->assertFalse(new Globstar(['file[0-9].log'])->isValid('fileA.log')); - $this->assertTrue(new Globstar(['[a-zA-Z]file.txt'])->isValid('afile.txt')); - $this->assertTrue(new Globstar(['[a-zA-Z]file.txt'])->isValid('Afile.txt')); - $this->assertFalse(new Globstar(['[a-zA-Z]file.txt'])->isValid('1file.txt')); - } - - public function testNegatedCharacterClasses(): void - { - $this->assertTrue(new Globstar(['[!a-z]file.txt'])->isValid('Afile.txt')); - $this->assertFalse(new Globstar(['[!a-z]file.txt'])->isValid('afile.txt')); - $this->assertTrue(new Globstar(['^[^a-z]file.txt'])->isValid('^1file.txt')); - $this->assertFalse(new Globstar(['[^a-z]file.txt'])->isValid('afile.txt')); - $this->assertTrue(new Globstar(['[!a-z0-9]file.txt'])->isValid('#file.txt')); - $this->assertFalse(new Globstar(['[!a-z0-9]file.txt'])->isValid('afile.txt')); - $this->assertFalse(new Globstar(['[!a-z0-9]file.txt'])->isValid('5file.txt')); - } - - public function testCaretNegatedCharacterClass(): void - { - $this->assertTrue(new Globstar(['[^a-z]file.txt'])->isValid('1file.txt')); - $this->assertFalse(new Globstar(['[^a-z]file.txt'])->isValid('afile.txt')); - } - - public function testSpecialCharsInsideCharacterClasses(): void - { - $this->assertTrue(new Globstar(['file[.+]name.txt'])->isValid('file.name.txt')); - $this->assertTrue(new Globstar(['file[.+]name.txt'])->isValid('file+name.txt')); - $this->assertFalse(new Globstar(['file[.+]name.txt'])->isValid('filename.txt')); - $this->assertTrue(new Globstar(['[_!@#]special.txt'])->isValid('_special.txt')); - $this->assertTrue(new Globstar(['[_!@#]special.txt'])->isValid('@special.txt')); - $this->assertFalse(new Globstar(['[_!@#]special.txt'])->isValid('xspecial.txt')); - $this->assertTrue(new Globstar(['[-abc]dash.txt'])->isValid('-dash.txt')); - $this->assertTrue(new Globstar(['[-abc]dash.txt'])->isValid('adash.txt')); - $this->assertTrue(new Globstar(['[abc-]dash.txt'])->isValid('-dash.txt')); - } - - public function testCharacterClassCombinedWithGlobstar(): void - { - $this->assertTrue(new Globstar(['[a-z]*.txt'])->isValid('abc.txt')); - $this->assertFalse(new Globstar(['[a-z]*.txt'])->isValid('Abc.txt')); - $this->assertTrue(new Globstar(['**/[a-z]*.txt'])->isValid('dir/abc.txt')); - $this->assertTrue(new Globstar(['**/[a-z]*.txt'])->isValid('dir/subdir/abc.txt')); - $this->assertFalse(new Globstar(['**/[a-z]*.txt'])->isValid('dir/Abc.txt')); - $this->assertTrue(new Globstar(['[a-z][0-9]*.txt'])->isValid('a1file.txt')); - $this->assertFalse(new Globstar(['[a-z][0-9]*.txt'])->isValid('ab.txt')); - $this->assertFalse(new Globstar(['[a-z][0-9]*.txt'])->isValid('A1file.txt')); - } - - public function testEdgeCaseEmptyCharacterClass(): void - { - // Empty character class [] matches nothing - $this->assertFalse(new Globstar(['[]file.txt'])->isValid('file.txt')); - } - - public function testEdgeCaseUnclosedBracket(): void - { - // Unclosed bracket treated as literal - $this->assertTrue(new Globstar(['[abc'])->isValid('[abc')); - $this->assertFalse(new Globstar(['[abc'])->isValid('abc')); - } - - public function testEdgeCaseExclamationOnlyClass(): void - { - // [!] — exclamation as sole content; behaviour is implementation-defined - // but the implementation treats it as a literal class containing '!' - $this->assertTrue(new Globstar(['[!]file.txt'])->isValid('!file.txt')); - } - - public function testEdgeCaseCaretOnlyClass(): void - { - // [^] — caret as sole content; behaviour is implementation-defined - // but the implementation treats it as a literal class containing '^' - $this->assertTrue(new Globstar(['[^]file.txt'])->isValid('^file.txt')); - } - - // ------------------------------------------------------------------------- - // Complex negation chain - // ------------------------------------------------------------------------- - - public function testComplexNegationChainPattern(): void - { - // Patterns: *.md, !README*.md, README-private*.md - // - inclusion exists (*.md), so non-matching paths fail - // - README*.md is excluded by the second pattern - // - README-private*.md is re-included by the third pattern - $patterns = ['*.md', '!README*.md', 'README-private*.md']; - - $this->assertTrue(new Globstar($patterns)->isValid('documentation.md')); // included, not excluded - $this->assertFalse(new Globstar($patterns)->isValid('README.md')); // excluded by !README*.md - $this->assertFalse(new Globstar($patterns)->isValid('README-public.md')); // excluded by !README*.md - $this->assertTrue(new Globstar($patterns)->isValid('README-private.md')); // re-included by README-private*.md - $this->assertTrue(new Globstar($patterns)->isValid('README-private-draft.md')); // re-included by README-private*.md - } -} diff --git a/packages/validators/tests/Validator/HexColorTest.php b/packages/validators/tests/Validator/HexColorTest.php deleted file mode 100755 index d67276e8f..000000000 --- a/packages/validators/tests/Validator/HexColorTest.php +++ /dev/null @@ -1,28 +0,0 @@ -assertTrue($hexColor->isValid('000')); - $this->assertTrue($hexColor->isValid('ffffff')); - $this->assertTrue($hexColor->isValid('fff')); - $this->assertTrue($hexColor->isValid('000000')); - - $this->assertFalse($hexColor->isValid('AB10BC99')); - $this->assertFalse($hexColor->isValid('AR1012')); - $this->assertFalse($hexColor->isValid('ab12bc99')); - $this->assertFalse($hexColor->isValid('00')); - $this->assertFalse($hexColor->isValid('ffff')); - $this->assertFalse($hexColor->isArray()); - - $this->assertSame(\Utopia\Validator::TYPE_STRING, $hexColor->getType()); - } -} diff --git a/packages/validators/tests/Validator/HostTest.php b/packages/validators/tests/Validator/HostTest.php deleted file mode 100644 index bed69b717..000000000 --- a/packages/validators/tests/Validator/HostTest.php +++ /dev/null @@ -1,44 +0,0 @@ - - * @version 1.0 RC4 - * @license The MIT License (MIT) - */ - -namespace Utopia\Validator; - -use PHPUnit\Framework\TestCase; - -final class HostTest extends TestCase -{ - protected Host $host; - - public function setUp(): void - { - $this->host = new Host(['example.io', 'subdomain.example.test', 'localhost', '*.appwrite.io']); - } - - public function testIsValid(): void - { - // Assertions - $this->assertTrue($this->host->isValid('https://example.io/link')); - $this->assertTrue($this->host->isValid('https://localhost')); - $this->assertFalse($this->host->isValid('localhost')); - $this->assertTrue($this->host->isValid('http://subdomain.example.test/path')); - $this->assertFalse($this->host->isValid('http://test.subdomain.example.test/path')); - $this->assertFalse($this->host->isValid('http://appwrite.io/path')); - $this->assertTrue($this->host->isValid('http://me.appwrite.io/path')); - $this->assertTrue($this->host->isValid('http://you.appwrite.io/path')); - $this->assertTrue($this->host->isValid('http://us.together.appwrite.io/path')); - $this->assertSame('string', $this->host->getType()); - } -} diff --git a/packages/validators/tests/Validator/HostnameTest.php b/packages/validators/tests/Validator/HostnameTest.php deleted file mode 100755 index 866ce9e93..000000000 --- a/packages/validators/tests/Validator/HostnameTest.php +++ /dev/null @@ -1,108 +0,0 @@ -assertSame(\Utopia\Validator::TYPE_STRING, $validator->getType()); - $this->assertFalse($validator->isArray()); - - $this->assertTrue($validator->isValid('myweb.com')); - $this->assertTrue($validator->isValid('httpmyweb.com')); - $this->assertTrue($validator->isValid('httpsmyweb.com')); - $this->assertTrue($validator->isValid('wsmyweb.com')); - $this->assertTrue($validator->isValid('wssmyweb.com')); - $this->assertTrue($validator->isValid('vercel.app')); - $this->assertTrue($validator->isValid('web.vercel.app')); - $this->assertTrue($validator->isValid('my-web.vercel.app')); - $this->assertTrue($validator->isValid('my-project.my-web.vercel.app')); - $this->assertTrue($validator->isValid('my-commit.my-project.my-web.vercel.app')); - $this->assertTrue($validator->isValid('myapp.co.uk')); - $this->assertTrue($validator->isValid('*.myapp.com')); - $this->assertTrue($validator->isValid('myapp.*')); - - $this->assertFalse($validator->isValid('https://myweb.com')); - $this->assertFalse($validator->isValid('ws://myweb.com')); - $this->assertFalse($validator->isValid('wss://myweb.com')); - $this->assertFalse($validator->isValid('http://myweb.com')); - $this->assertFalse($validator->isValid('http://myweb.com:3000')); - $this->assertFalse($validator->isValid('http://myweb.com/blog')); - $this->assertFalse($validator->isValid('myweb.com:80')); - $this->assertFalse($validator->isValid('myweb.com:3000')); - $this->assertFalse($validator->isValid('myweb.com/blog')); - $this->assertFalse($validator->isValid('myweb.com/blog/article1')); - - // Max length test - $domain = str_repeat('bestdomain', 25); // 250 chars total - - $domain .= '.sk'; // Exactly at the limit - $this->assertTrue($validator->isValid($domain)); - - $domain .= 'a'; // Exactly over the limit - $this->assertFalse($validator->isValid($domain)); - } - - public function testCanValidateHostnamesWithAllowList(): void - { - // allowList tests - $validator = new Hostname([ - 'myweb.vercel.app', - 'myweb.com', - '*.myapp.com', - ]); - - $this->assertTrue($validator->isValid('myweb.vercel.app')); - $this->assertFalse($validator->isValid('myweb.vercel.com')); - $this->assertFalse($validator->isValid('myweb2.vercel.app')); - $this->assertFalse($validator->isValid('vercel.app')); - $this->assertFalse($validator->isValid('mycommit.myweb.vercel.app')); - - $this->assertTrue($validator->isValid('myweb.com')); - $this->assertFalse($validator->isValid('myweb.eu')); - $this->assertFalse($validator->isValid('project.myweb.eu')); - $this->assertFalse($validator->isValid('commit.project.myweb.eu')); - - $this->assertTrue($validator->isValid('project1.myapp.com')); - $this->assertTrue($validator->isValid('project2.myapp.com')); - $this->assertTrue($validator->isValid('project-with-dash.myapp.com')); - $this->assertTrue($validator->isValid('anything.myapp.com')); - $this->assertTrue($validator->isValid('commit.anything.myapp.com')); - $this->assertFalse($validator->isValid('anything.myapp.eu')); - $this->assertFalse($validator->isValid('myapp.com')); - - $validator = new Hostname(['localhost']); - $this->assertTrue($validator->isValid('localhost')); - } - - public function testCanValidateHostnamesWithWildcard(): void - { - $validator = new Hostname(); - $this->assertTrue($validator->isValid('*')); - - $validator = new Hostname(['netlify.*']); - $this->assertFalse($validator->isValid('netlify.com')); - $this->assertFalse($validator->isValid('netlify.eu')); - $this->assertFalse($validator->isValid('netlify.app')); - - $validator = new Hostname(['*.*.app.io']); - $this->assertFalse($validator->isValid('app.io')); - $this->assertFalse($validator->isValid('project.app.io')); - $this->assertFalse($validator->isValid('commit.project.app.io')); - $this->assertFalse($validator->isValid('api.commit.project.app.io')); - - $validator = new Hostname(['*']); - $this->assertTrue($validator->isValid('*')); - $this->assertTrue($validator->isValid('localhost')); - $this->assertTrue($validator->isValid('anything')); // Like localhost - $this->assertTrue($validator->isValid('anything.com')); - $this->assertTrue($validator->isValid('anything.with.subdomains.eu')); - } -} diff --git a/packages/validators/tests/Validator/IPTest.php b/packages/validators/tests/Validator/IPTest.php deleted file mode 100644 index 7a05a8760..000000000 --- a/packages/validators/tests/Validator/IPTest.php +++ /dev/null @@ -1,80 +0,0 @@ - - * @version 1.0 RC4 - * @license The MIT License (MIT) - */ - -namespace Utopia\Validator; - -use PHPUnit\Framework\TestCase; - -final class IPTest extends TestCase -{ - protected IP $validator; - - public function testIsValidIP(): void - { - $validator = new IP(); - - // Assertions - $this->assertTrue($validator->isValid('2001:0db8:85a3:08d3:1319:8a2e:0370:7334')); - $this->assertTrue($validator->isValid('109.67.204.101')); - $this->assertFalse($validator->isValid(23.5)); - $this->assertFalse($validator->isValid('23.5')); - $this->assertFalse($validator->isValid(null)); - $this->assertFalse($validator->isValid(true)); - $this->assertFalse($validator->isValid(false)); - $this->assertSame('string', $validator->getType()); - } - - public function testIsValidIPALL(): void - { - $validator = new IP(IP::ALL); - - // Assertions - $this->assertTrue($validator->isValid('2001:0db8:85a3:08d3:1319:8a2e:0370:7334')); - $this->assertTrue($validator->isValid('109.67.204.101')); - $this->assertFalse($validator->isValid(23.5)); - $this->assertFalse($validator->isValid('23.5')); - $this->assertFalse($validator->isValid(null)); - $this->assertFalse($validator->isValid(true)); - $this->assertFalse($validator->isValid(false)); - } - - public function testIsValidIPV4(): void - { - $validator = new IP(IP::V4); - - // Assertions - $this->assertFalse($validator->isValid('2001:0db8:85a3:08d3:1319:8a2e:0370:7334')); - $this->assertTrue($validator->isValid('109.67.204.101')); - $this->assertFalse($validator->isValid(23.5)); - $this->assertFalse($validator->isValid('23.5')); - $this->assertFalse($validator->isValid(null)); - $this->assertFalse($validator->isValid(true)); - $this->assertFalse($validator->isValid(false)); - } - - public function testIsValidIPV6(): void - { - $validator = new IP(IP::V6); - - // Assertions - $this->assertTrue($validator->isValid('2001:0db8:85a3:08d3:1319:8a2e:0370:7334')); - $this->assertFalse($validator->isValid('109.67.204.101')); - $this->assertFalse($validator->isValid(23.5)); - $this->assertFalse($validator->isValid('23.5')); - $this->assertFalse($validator->isValid(null)); - $this->assertFalse($validator->isValid(true)); - $this->assertFalse($validator->isValid(false)); - } -} diff --git a/packages/validators/tests/Validator/IdentifierTest.php b/packages/validators/tests/Validator/IdentifierTest.php deleted file mode 100644 index 5d7473c1a..000000000 --- a/packages/validators/tests/Validator/IdentifierTest.php +++ /dev/null @@ -1,46 +0,0 @@ -assertTrue($identifier->isValid('APPWRITE_APP_SECRET')); - $this->assertTrue($identifier->isValid('_private')); - $this->assertTrue($identifier->isValid('lowercase_ok')); - $this->assertTrue($identifier->isValid('A1')); - - $this->assertFalse($identifier->isArray()); - $this->assertSame(\Utopia\Validator::TYPE_STRING, $identifier->getType()); - } - - public function testRejectsNonIdentifiers(): void - { - $identifier = new Identifier(); - - $this->assertFalse($identifier->isValid("APPWRITE_APP_SECRET\t"), 'trailing tab'); - $this->assertFalse($identifier->isValid("APPWRITE_APP_SECRET\n"), 'trailing newline'); - $this->assertFalse($identifier->isValid('GRAVAÇÃO'), 'accented letters'); - $this->assertFalse($identifier->isValid('9FOO'), 'leading digit'); - $this->assertFalse($identifier->isValid('MY-VAR'), 'hyphen'); - $this->assertFalse($identifier->isValid('MY VAR'), 'space'); - $this->assertFalse($identifier->isValid('FOO=BAR'), 'equals'); - $this->assertFalse($identifier->isValid(''), 'empty'); - $this->assertFalse($identifier->isValid(123), 'non-string'); - } - - public function testEnforcesLength(): void - { - $identifier = new Identifier(4); - - $this->assertTrue($identifier->isValid('ABCD')); - $this->assertFalse($identifier->isValid('ABCDE'), 'over-length'); - } -} diff --git a/packages/validators/tests/Validator/IntegerTest.php b/packages/validators/tests/Validator/IntegerTest.php deleted file mode 100755 index 89335b94c..000000000 --- a/packages/validators/tests/Validator/IntegerTest.php +++ /dev/null @@ -1,91 +0,0 @@ -assertTrue($validator->isValid(23)); - $this->assertFalse($validator->isValid('23')); - $this->assertFalse($validator->isValid(23.5)); - $this->assertFalse($validator->isValid('23.5')); - $this->assertFalse($validator->isValid(null)); - $this->assertFalse($validator->isValid(true)); - $this->assertFalse($validator->isValid(false)); - $this->assertFalse($validator->isArray()); - $this->assertSame(\Utopia\Validator::TYPE_INTEGER, $validator->getType()); - } - - public function testCanValidateLoosely(): void - { - $validator = new Integer(true); - $this->assertTrue($validator->isValid(23)); - $this->assertTrue($validator->isValid('23')); - $this->assertFalse($validator->isValid(23.5)); - $this->assertFalse($validator->isValid('23.5')); - $this->assertFalse($validator->isValid(null)); - $this->assertFalse($validator->isValid(true)); - $this->assertFalse($validator->isValid(false)); - $this->assertFalse($validator->isArray()); - $this->assertSame(\Utopia\Validator::TYPE_INTEGER, $validator->getType()); - } - - public function testBitSizeAndSignedness(): void - { - // Default: 32-bit signed - $validator = new Integer(); - $this->assertSame(32, $validator->getBits()); - $this->assertFalse($validator->isUnsigned()); - $this->assertSame('int32', $validator->getFormat()); - - // 8-bit signed: -128 to 127 - $validator8 = new Integer(false, 8); - $this->assertSame('int8', $validator8->getFormat()); - $this->assertTrue($validator8->isValid(-128)); - $this->assertTrue($validator8->isValid(127)); - $this->assertFalse($validator8->isValid(-129)); - $this->assertFalse($validator8->isValid(128)); - - // 8-bit unsigned: 0 to 255 - $validator8u = new Integer(false, 8, true); - $this->assertSame('uint8', $validator8u->getFormat()); - $this->assertTrue($validator8u->isValid(0)); - $this->assertTrue($validator8u->isValid(255)); - $this->assertFalse($validator8u->isValid(-1)); - $this->assertFalse($validator8u->isValid(256)); - - // 16-bit unsigned: 0 to 65535 - $validator16u = new Integer(false, 16, true); - $this->assertSame('uint16', $validator16u->getFormat()); - $this->assertTrue($validator16u->isValid(65535)); - $this->assertFalse($validator16u->isValid(65536)); - - // 32-bit unsigned - $validator32u = new Integer(false, 32, true); - $this->assertSame('uint32', $validator32u->getFormat()); - - // 64-bit signed - $validator64 = new Integer(false, 64); - $this->assertSame('int64', $validator64->getFormat()); - } - - public function testInvalidBitSize(): void - { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Bits must be 8, 16, 32, or 64'); - new Integer(false, 128); - } - - public function test64BitUnsignedNotSupported(): void - { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('64-bit unsigned integers are not supported due to PHP integer limitations'); - new Integer(false, 64, true); - } -} diff --git a/packages/validators/tests/Validator/JSON/ArrayValidatorTest.php b/packages/validators/tests/Validator/JSON/ArrayValidatorTest.php deleted file mode 100644 index a23a1480d..000000000 --- a/packages/validators/tests/Validator/JSON/ArrayValidatorTest.php +++ /dev/null @@ -1,110 +0,0 @@ -assertTrue($validator->isValid(['test'])); - $this->assertTrue($validator->isValid([1, 2])); - $this->assertTrue($validator->isValid([['test' => 'demo']])); - $this->assertTrue($validator->isValid([[1, 2], [3, 4]])); - } - - public function testAcceptsEncodedArrays(): void - { - $validator = new ArrayValidator(); - - $this->assertTrue($validator->isValid('[]')); - $this->assertTrue($validator->isValid('[1, 2]')); - $this->assertTrue($validator->isValid('["test"]')); - $this->assertTrue($validator->isValid('[{"test": "demo"}]')); - } - - public function testAcceptsAmbiguousEmptyArray(): void - { - // PHP cannot tell a decoded `[]` from a decoded `{}`, so the empty array stays valid. - $this->assertTrue(new ArrayValidator()->isValid([])); - } - - public function testRejectsObjects(): void - { - $validator = new ArrayValidator(); - - $this->assertFalse($validator->isValid('{}')); - $this->assertFalse($validator->isValid('{"test": "demo"}')); - $this->assertFalse($validator->isValid(['test' => 'demo'])); - $this->assertFalse($validator->isValid((object) ['test' => 'demo'])); - $this->assertFalse($validator->isValid(new \stdClass())); - } - - public function testRejectsEncodedScalars(): void - { - $validator = new ArrayValidator(); - - $this->assertFalse($validator->isValid('"not-an-array"')); - $this->assertFalse($validator->isValid('1')); - $this->assertFalse($validator->isValid('1.2')); - $this->assertFalse($validator->isValid('true')); - $this->assertFalse($validator->isValid('null')); - } - - public function testRejectsNonJson(): void - { - $validator = new ArrayValidator(); - - $this->assertFalse($validator->isValid('')); - $this->assertFalse($validator->isValid('string')); - $this->assertFalse($validator->isValid('[1, 2')); - $this->assertFalse($validator->isValid("['test']")); - } - - public function testRejectsScalars(): void - { - $validator = new ArrayValidator(); - - $this->assertFalse($validator->isValid(null)); - $this->assertFalse($validator->isValid(false)); - $this->assertFalse($validator->isValid(true)); - $this->assertFalse($validator->isValid(1)); - $this->assertFalse($validator->isValid(1.2)); - } - - public function testRejectsArraysNestedBeyondDecodingDepth(): void - { - $encoded = str_repeat('[', 600) . str_repeat(']', 600); - - $this->assertFalse(new ArrayValidator()->isValid($encoded)); - } - - public function testEnforcesLength(): void - { - $encoded = '["test"]'; - - $this->assertTrue(new ArrayValidator(\strlen($encoded))->isValid($encoded)); - $this->assertFalse(new ArrayValidator(\strlen($encoded) - 1)->isValid($encoded)); - $this->assertTrue(new ArrayValidator(\strlen($encoded))->isValid(['test'])); - $this->assertFalse(new ArrayValidator(\strlen($encoded) - 1)->isValid(['test'])); - } - - public function testContract(): void - { - $validator = new ArrayValidator(); - - $this->assertTrue($validator->isArray()); - $this->assertSame(Validator::TYPE_ARRAY, $validator->getType()); - $this->assertSame('Value must be a valid JSON array', $validator->getDescription()); - $this->assertSame( - 'Value must be a valid JSON array no longer than 16 characters when encoded', - new ArrayValidator(16)->getDescription(), - ); - } -} diff --git a/packages/validators/tests/Validator/JSON/FCMTest.php b/packages/validators/tests/Validator/JSON/FCMTest.php deleted file mode 100644 index 8ad2296d9..000000000 --- a/packages/validators/tests/Validator/JSON/FCMTest.php +++ /dev/null @@ -1,172 +0,0 @@ - 'service_account', - 'project_id' => 'test-project', - 'private_key_id' => 'test-private-key-id', - 'private_key' => "-----BEGIN PRIVATE KEY-----\ntest\n-----END PRIVATE KEY-----\n", - 'client_email' => 'test@appwrite.iam.gserviceaccount.com', - 'client_id' => '1234567890', - 'auth_uri' => 'https://accounts.google.com/o/oauth2/auth', - 'token_uri' => 'https://oauth2.googleapis.com/token', - 'auth_provider_x509_cert_url' => 'https://www.googleapis.com/oauth2/v1/certs', - 'client_x509_cert_url' => 'https://www.googleapis.com/robot/v1/metadata/x509/test%40appwrite.iam.gserviceaccount.com', - 'universe_domain' => 'googleapis.com', - ]; - - public function testAcceptsEncodedServiceAccount(): void - { - $this->assertTrue(new FCM()->isValid(json_encode(self::CREDENTIALS))); - } - - public function testAcceptsDecodedServiceAccounts(): void - { - $validator = new FCM(); - - $this->assertTrue($validator->isValid(self::CREDENTIALS)); - $this->assertTrue($validator->isValid((object) self::CREDENTIALS)); - } - - public function testAcceptsOnlyOperationalFieldsAndUnknownFields(): void - { - $credentials = array_intersect_key(self::CREDENTIALS, array_flip([ - 'type', - 'project_id', - 'private_key', - 'client_email', - 'token_uri', - ])); - $credentials['custom_field'] = 'allowed'; - - $this->assertTrue(new FCM()->isValid($credentials)); - } - - /** - * @param array $credentials - */ - #[\PHPUnit\Framework\Attributes\DataProvider('missingRequiredFieldProvider')] - public function testRejectsMissingOrEmptyRequiredFields(array $credentials, string $field, string $purpose): void - { - $validator = new FCM(); - - $this->assertFalse($validator->isValid($credentials)); - $this->assertSame( - "FCM service account JSON must include a non-empty '{$field}' field, which {$purpose}.", - $validator->getDescription(), - ); - } - - /** - * @return \Iterator, string, string}> - */ - public static function missingRequiredFieldProvider(): \Iterator - { - yield 'type' => [self::without('type'), 'type', 'identifies the credentials as a Google service account']; - yield 'project ID' => [self::without('project_id'), 'project_id', 'identifies the Firebase project receiving messages']; - yield 'private key' => [self::without('private_key'), 'private_key', 'signs the OAuth access-token request']; - yield 'client email' => [self::without('client_email'), 'client_email', 'identifies the service account used for authentication']; - yield 'token URI' => [self::without('token_uri'), 'token_uri', 'identifies the OAuth token endpoint']; - yield 'blank project ID' => [array_replace(self::CREDENTIALS, ['project_id' => " \t\n"]), 'project_id', 'identifies the Firebase project receiving messages']; - yield 'non-string client email' => [array_replace(self::CREDENTIALS, ['client_email' => 123]), 'client_email', 'identifies the service account used for authentication']; - } - - /** - * @param array $credentials - */ - #[\PHPUnit\Framework\Attributes\DataProvider('invalidFieldProvider')] - public function testRejectsInvalidFieldValues(array $credentials, string $description): void - { - $validator = new FCM(); - - $this->assertFalse($validator->isValid($credentials)); - $this->assertSame($description, $validator->getDescription()); - } - - /** - * @return \Iterator, string}> - */ - public static function invalidFieldProvider(): \Iterator - { - yield 'credential type' => [ - array_replace(self::CREDENTIALS, ['type' => 'authorized_user']), - "FCM service account JSON field 'type' must be 'service_account'.", - ]; - yield 'client email' => [ - array_replace(self::CREDENTIALS, ['client_email' => 'not-an-email']), - "FCM service account JSON field 'client_email' must contain a valid email address.", - ]; - yield 'private key' => [ - array_replace(self::CREDENTIALS, ['private_key' => 'not-a-private-key']), - "FCM service account JSON field 'private_key' must contain a PEM-encoded private key.", - ]; - yield 'HTTP token URI' => [ - array_replace(self::CREDENTIALS, ['token_uri' => 'http://oauth2.googleapis.com/token']), - "FCM service account JSON field 'token_uri' must contain a valid HTTPS URL.", - ]; - yield 'invalid auth URI' => [ - array_replace(self::CREDENTIALS, ['auth_uri' => 'not-a-url']), - "FCM service account JSON field 'auth_uri' must contain a valid HTTPS URL.", - ]; - yield 'empty optional field' => [ - array_replace(self::CREDENTIALS, ['private_key_id' => '']), - "FCM service account JSON field 'private_key_id' must be a non-empty string when provided.", - ]; - yield 'non-string optional field' => [ - array_replace(self::CREDENTIALS, ['client_id' => 123]), - "FCM service account JSON field 'client_id' must be a non-empty string when provided.", - ]; - } - - public function testRejectsInvalidJSONShapes(): void - { - $validator = new FCM(); - - $this->assertFalse($validator->isValid('not-json')); - $this->assertFalse($validator->isValid('[]')); - $this->assertFalse($validator->isValid('null')); - $this->assertFalse($validator->isValid('["credentials"]')); - $this->assertFalse($validator->isValid(['credentials'])); - $this->assertFalse($validator->isValid(null)); - $this->assertFalse($validator->isValid(true)); - } - - public function testDescriptionResetsBetweenValidations(): void - { - $validator = new FCM(); - - $this->assertFalse($validator->isValid([])); - $this->assertNotSame('Value must be valid Google service account JSON for FCM', $validator->getDescription()); - - $this->assertTrue($validator->isValid(self::CREDENTIALS)); - $this->assertSame('Value must be valid Google service account JSON for FCM', $validator->getDescription()); - } - - public function testContract(): void - { - $validator = new FCM(); - - $this->assertFalse($validator->isArray()); - $this->assertSame(Validator::TYPE_OBJECT, $validator->getType()); - $this->assertSame('Value must be valid Google service account JSON for FCM', $validator->getDescription()); - } - - /** - * @return array - */ - private static function without(string $field): array - { - $credentials = self::CREDENTIALS; - unset($credentials[$field]); - - return $credentials; - } -} diff --git a/packages/validators/tests/Validator/JSON/ObjectValidatorTest.php b/packages/validators/tests/Validator/JSON/ObjectValidatorTest.php deleted file mode 100644 index 02c6a650c..000000000 --- a/packages/validators/tests/Validator/JSON/ObjectValidatorTest.php +++ /dev/null @@ -1,113 +0,0 @@ -assertTrue($validator->isValid(['test' => 'demo'])); - $this->assertTrue($validator->isValid(['nested' => ['test' => 'demo']])); - $this->assertTrue($validator->isValid(['list' => ['one', 'two']])); - $this->assertTrue($validator->isValid((object) ['test' => 'demo'])); - $this->assertTrue($validator->isValid(new \stdClass())); - } - - public function testAcceptsEncodedObjects(): void - { - $validator = new ObjectValidator(); - - $this->assertTrue($validator->isValid('{}')); - $this->assertTrue($validator->isValid('{"test": "demo"}')); - $this->assertTrue($validator->isValid('{"nested": {"test": "demo"}}')); - $this->assertTrue($validator->isValid('{"list": ["one", "two"]}')); - } - - public function testAcceptsAmbiguousEmptyArray(): void - { - // PHP cannot tell a decoded `{}` from a decoded `[]`, so the empty array stays valid. - $this->assertTrue(new ObjectValidator()->isValid([])); - } - - public function testRejectsLists(): void - { - $validator = new ObjectValidator(); - - $this->assertFalse($validator->isValid('[]')); - $this->assertFalse($validator->isValid('[1, 2]')); - $this->assertFalse($validator->isValid('[{"test": "demo"}]')); - $this->assertFalse($validator->isValid(['test'])); - $this->assertFalse($validator->isValid([['test' => 'demo']])); - } - - public function testRejectsEncodedScalars(): void - { - $validator = new ObjectValidator(); - - $this->assertFalse($validator->isValid('"not-an-object"')); - $this->assertFalse($validator->isValid('1')); - $this->assertFalse($validator->isValid('1.2')); - $this->assertFalse($validator->isValid('true')); - $this->assertFalse($validator->isValid('null')); - } - - public function testRejectsNonJson(): void - { - $validator = new ObjectValidator(); - - $this->assertFalse($validator->isValid('')); - $this->assertFalse($validator->isValid('string')); - $this->assertFalse($validator->isValid('{"test": "demo"')); - $this->assertFalse($validator->isValid("{'test': 'demo'}")); - } - - public function testRejectsScalars(): void - { - $validator = new ObjectValidator(); - - $this->assertFalse($validator->isValid(null)); - $this->assertFalse($validator->isValid(false)); - $this->assertFalse($validator->isValid(true)); - $this->assertFalse($validator->isValid(1)); - $this->assertFalse($validator->isValid(1.2)); - } - - public function testRejectsObjectsNestedBeyondDecodingDepth(): void - { - $encoded = str_repeat('{"a":', 600) . '{}' . str_repeat('}', 600); - - $this->assertFalse(new ObjectValidator()->isValid($encoded)); - } - - public function testEnforcesLength(): void - { - $encoded = '{"test":"demo"}'; - - $this->assertTrue(new ObjectValidator(\strlen($encoded))->isValid($encoded)); - $this->assertFalse(new ObjectValidator(\strlen($encoded) - 1)->isValid($encoded)); - $this->assertTrue(new ObjectValidator(\strlen($encoded))->isValid(['test' => 'demo'])); - $this->assertFalse(new ObjectValidator(\strlen($encoded) - 1)->isValid(['test' => 'demo'])); - $this->assertTrue(new ObjectValidator(\strlen($encoded))->isValid((object) ['test' => 'demo'])); - $this->assertFalse(new ObjectValidator(\strlen($encoded) - 1)->isValid((object) ['test' => 'demo'])); - } - - public function testContract(): void - { - $validator = new ObjectValidator(); - - $this->assertFalse($validator->isArray()); - $this->assertSame(Validator::TYPE_OBJECT, $validator->getType()); - $this->assertSame('Value must be a valid JSON object', $validator->getDescription()); - $this->assertSame( - 'Value must be a valid JSON object no longer than 16 characters when encoded', - new ObjectValidator(16)->getDescription(), - ); - } -} diff --git a/packages/validators/tests/Validator/JSONTest.php b/packages/validators/tests/Validator/JSONTest.php deleted file mode 100755 index d93dfc350..000000000 --- a/packages/validators/tests/Validator/JSONTest.php +++ /dev/null @@ -1,30 +0,0 @@ -assertTrue($json->isValid('{}')); - $this->assertTrue($json->isValid([])); - $this->assertTrue($json->isValid(['test'])); - $this->assertTrue($json->isValid(['test' => 'demo'])); - $this->assertTrue($json->isValid('{"test": "demo"}')); - - $this->assertFalse($json->isValid('')); - $this->assertFalse($json->isValid(false)); - $this->assertFalse($json->isValid(null)); - $this->assertFalse($json->isValid('string')); - $this->assertFalse($json->isValid(1)); - $this->assertFalse($json->isValid(1.2)); - $this->assertFalse($json->isValid("{'test': 'demo'}")); - $this->assertFalse($json->isArray()); - $this->assertSame(\Utopia\Validator::TYPE_OBJECT, $json->getType()); - } -} diff --git a/packages/validators/tests/Validator/MultipleOfTest.php b/packages/validators/tests/Validator/MultipleOfTest.php deleted file mode 100644 index c2b89f1fb..000000000 --- a/packages/validators/tests/Validator/MultipleOfTest.php +++ /dev/null @@ -1,65 +0,0 @@ -assertSame('string', $validator->getType()); - $this->assertSame('Value must be a valid string and at least 1 chars and no longer than 20 chars', $validator->getDescription()); - - // Valid URL but invalid text length - $this->assertFalse($validator->isValid('http://example.com/very-long-url')); - - // Valid text within length, but invalid URL - $this->assertFalse($validator->isValid('hello world')); - - // Both conditions satisfied - $this->assertTrue($validator->isValid('http://example.com')); - $this->assertTrue($validator->isValid('https://google.com')); - - // Neither condition satisfied - $this->assertFalse($validator->isValid('example.com/hello-world')); - $this->assertFalse($validator->isValid('')); - } - - public function testRules(): void - { - $validTextValidUrl = 'http://example.com'; - $validTextInvalidUrl = 'hello world'; - $invalidTextValidUrl = 'http://example.com/very-long-url'; - $invalidTextInvalidUrl = 'Some very long text that is also not an URL'; - - $vaidator = new AllOf([new Text(20), new URL()], Validator::TYPE_STRING); - $this->assertTrue($vaidator->isValid($validTextValidUrl)); - $this->assertFalse($vaidator->isValid($validTextInvalidUrl)); - $this->assertFalse($vaidator->isValid($invalidTextValidUrl)); - $this->assertFalse($vaidator->isValid($invalidTextInvalidUrl)); - - $vaidator = new AnyOf([new Text(20), new URL()], Validator::TYPE_STRING); - $this->assertTrue($vaidator->isValid($validTextValidUrl)); - $this->assertTrue($vaidator->isValid($validTextInvalidUrl)); - $this->assertTrue($vaidator->isValid($invalidTextValidUrl)); - $this->assertFalse($vaidator->isValid($invalidTextInvalidUrl)); - - $this->assertCount(2, $vaidator->getValidators()); - $this->assertInstanceOf(\Utopia\Validator\Text::class, $vaidator->getValidators()[0]); - $this->assertInstanceOf(\Utopia\Validator\URL::class, $vaidator->getValidators()[1]); - - $vaidator = new NoneOf([new Text(20), new URL()], Validator::TYPE_STRING); - $this->assertFalse($vaidator->isValid($validTextValidUrl)); - $this->assertFalse($vaidator->isValid($validTextInvalidUrl)); - $this->assertFalse($vaidator->isValid($invalidTextValidUrl)); - $this->assertTrue($vaidator->isValid($invalidTextInvalidUrl)); - } -} diff --git a/packages/validators/tests/Validator/NullableTest.php b/packages/validators/tests/Validator/NullableTest.php deleted file mode 100755 index 5e7f55da4..000000000 --- a/packages/validators/tests/Validator/NullableTest.php +++ /dev/null @@ -1,24 +0,0 @@ -assertTrue($validator->isValid('text')); - $this->assertTrue($validator->isValid(null)); - $this->assertFalse($validator->isValid(123)); - } - - public function testCanReturnValidator(): void - { - $validator = new Nullable(new Text(0)); - $this->assertInstanceOf(\Utopia\Validator\Text::class, $validator->getValidator()); - } -} diff --git a/packages/validators/tests/Validator/NumericTest.php b/packages/validators/tests/Validator/NumericTest.php deleted file mode 100755 index 5fa14f1b6..000000000 --- a/packages/validators/tests/Validator/NumericTest.php +++ /dev/null @@ -1,26 +0,0 @@ -assertTrue($numeric->isValid('42')); - $this->assertTrue($numeric->isValid(1337)); - $this->assertTrue($numeric->isValid(0x539)); - $this->assertTrue($numeric->isValid(02471)); - $this->assertTrue($numeric->isValid(1337e0)); - $this->assertTrue($numeric->isValid(9.1)); - $this->assertFalse($numeric->isValid('not numeric')); - $this->assertFalse($numeric->isValid([])); - $this->assertFalse($numeric->isArray()); - $this->assertSame(\Utopia\Validator::TYPE_MIXED, $numeric->getType()); - } -} diff --git a/packages/validators/tests/Validator/PhoneTest.php b/packages/validators/tests/Validator/PhoneTest.php deleted file mode 100644 index 1a883d446..000000000 --- a/packages/validators/tests/Validator/PhoneTest.php +++ /dev/null @@ -1,71 +0,0 @@ -assertTrue($validator->isValid('+15550102680')); - $this->assertTrue($validator->isValid('+447911123456')); - $this->assertTrue($validator->isValid('+919876543210')); - $this->assertTrue($validator->isValid('+1234567')); - $this->assertTrue($validator->isValid('+123456789012345')); - - $this->assertFalse($validator->isValid('15550102680')); - $this->assertFalse($validator->isValid(' 15550102680')); - $this->assertFalse($validator->isValid('%2B15550102680')); - $this->assertFalse($validator->isValid('+0123456789')); - $this->assertFalse($validator->isValid('+123456')); - $this->assertFalse($validator->isValid('+1234567890123456')); - $this->assertFalse($validator->isValid('+1 555 010 2680')); - $this->assertFalse($validator->isValid('+1-555-010-2680')); - $this->assertFalse($validator->isValid('')); - $this->assertFalse($validator->isValid(null)); - $this->assertFalse($validator->isValid(['+15550102680'])); - $this->assertFalse($validator->isArray()); - $this->assertSame(\Utopia\Validator::TYPE_STRING, $validator->getType()); - } - - public function testCanAllowEmptyValue(): void - { - $validator = new Phone(allowEmpty: true); - - $this->assertTrue($validator->isValid('')); - $this->assertTrue($validator->isValid('+15550102680')); - $this->assertFalse($validator->isValid(null)); - } - - public function testCanNormalizePathEncoding(): void - { - $this->assertSame('+15550102680', Phone::normalize('%2B15550102680')); - $this->assertSame('+15550102680', Phone::normalize(' 15550102680')); - $this->assertSame(' 15550102680', Phone::normalize(' 15550102680')); - $this->assertSame('+15550102680', Phone::normalize('15550102680')); - $this->assertSame('+15550102680', Phone::normalize('+15550102680')); - $this->assertSame('+0123456789', Phone::normalize('+0123456789')); - $this->assertSame('0123456789', Phone::normalize('0123456789')); - } - - public function testCanValidateNormalizedPhone(): void - { - $validator = new Phone(normalize: true); - - $this->assertTrue($validator->isValid('%2B15550102680')); - $this->assertTrue($validator->isValid(' 15550102680')); - $this->assertTrue($validator->isValid('15550102680')); - - $this->assertFalse($validator->isValid(' 15550102680')); - $this->assertTrue($validator->isValid('+15550102680')); - - $this->assertFalse($validator->isValid('0123456789')); - $this->assertFalse($validator->isValid('%2B0123456789')); - $this->assertFalse($validator->isValid('+1 555 010 2680')); - } -} diff --git a/packages/validators/tests/Validator/RangeTest.php b/packages/validators/tests/Validator/RangeTest.php deleted file mode 100755 index 4dee61b4d..000000000 --- a/packages/validators/tests/Validator/RangeTest.php +++ /dev/null @@ -1,62 +0,0 @@ -assertTrue($range->isValid(0)); - $this->assertTrue($range->isValid(1)); - $this->assertTrue($range->isValid(4)); - $this->assertTrue($range->isValid(5)); - $this->assertTrue($range->isValid('5')); - $this->assertFalse($range->isValid('1.5')); - $this->assertFalse($range->isValid(6)); - $this->assertFalse($range->isValid(-1)); - $this->assertSame(0, $range->getMin()); - $this->assertSame(5, $range->getMax()); - $this->assertFalse($range->isArray()); - $this->assertSame(\Utopia\Validator::TYPE_INTEGER, $range->getFormat()); - $this->assertSame(\Utopia\Validator::TYPE_INTEGER, $range->getType()); - } - - public function testCanValidateFloatRange(): void - { - $range = new Range(0, 1, \Utopia\Validator::TYPE_FLOAT); - - $this->assertTrue($range->isValid(0.0)); - $this->assertTrue($range->isValid(1.0)); - $this->assertTrue($range->isValid(0.5)); - $this->assertTrue($range->isValid('0.5')); - $this->assertTrue($range->isValid('0.6')); - $this->assertFalse($range->isValid(4)); - $this->assertFalse($range->isValid(1.5)); - $this->assertFalse($range->isValid(-1)); - $this->assertSame(0, $range->getMin()); - $this->assertSame(1, $range->getMax()); - $this->assertFalse($range->isArray()); - $this->assertSame(\Utopia\Validator::TYPE_FLOAT, $range->getFormat()); - $this->assertSame(\Utopia\Validator::TYPE_FLOAT, $range->getType(), \Utopia\Validator::TYPE_FLOAT); - } - - public function canValidateInfinityRange(): void - { - $integer = new Range(5, INF, \Utopia\Validator::TYPE_INTEGER); - $float = new Range(-INF, 45.6, \Utopia\Validator::TYPE_FLOAT); - - $this->assertTrue($integer->isValid(25)); - $this->assertFalse($integer->isValid(3)); - $this->assertTrue($integer->isValid(INF)); - $this->assertTrue($float->isValid(32.1)); - $this->assertFalse($float->isValid(97.6)); - $this->assertTrue($float->isValid(-INF)); - } -} diff --git a/packages/validators/tests/Validator/TextTest.php b/packages/validators/tests/Validator/TextTest.php deleted file mode 100755 index ec828ef43..000000000 --- a/packages/validators/tests/Validator/TextTest.php +++ /dev/null @@ -1,111 +0,0 @@ -assertTrue($validator->isValid('text')); - $this->assertTrue($validator->isValid('7')); - $this->assertTrue($validator->isValid('7.9')); - $this->assertTrue($validator->isValid('["seven"]')); - $this->assertFalse($validator->isValid(['seven'])); - $this->assertFalse($validator->isValid(['seven', 8, 9.0])); - $this->assertFalse($validator->isValid(false)); - $this->assertFalse($validator->isArray()); - $this->assertSame(\Utopia\Validator::TYPE_STRING, $validator->getType()); - } - - public function testCanValidateBoundaries(): void - { - $validator = new Text(5); - $this->assertTrue($validator->isValid('hell')); - $this->assertTrue($validator->isValid('hello')); - $this->assertFalse($validator->isValid('hellow')); - $this->assertFalse($validator->isValid('')); - - $validator = new Text(5, 3); - $this->assertTrue($validator->isValid('hel')); - $this->assertTrue($validator->isValid('hell')); - $this->assertTrue($validator->isValid('hello')); - $this->assertFalse($validator->isValid('hellow')); - $this->assertFalse($validator->isValid('he')); - $this->assertFalse($validator->isValid('h')); - } - - public function testCanRequireNonBlankText(): void - { - $validator = new Text(20, min: 0); - $this->assertTrue($validator->isValid('')); - $this->assertTrue($validator->isValid(" \t\n")); - $this->assertTrue($validator->isValid("\u{00A0}")); - $this->assertTrue($validator->isValid("\u{200D}")); - - $validator = new Text(20, min: 0, requireNonBlank: true); - $this->assertFalse($validator->isValid('')); - $this->assertFalse($validator->isValid(" \t\n")); - $this->assertFalse($validator->isValid("\u{00A0}")); - $this->assertFalse($validator->isValid("\u{200D}")); - $this->assertTrue($validator->isValid('My App')); - $this->assertTrue($validator->isValid(' My App ')); - $this->assertSame( - 'Value must be a valid string and no longer than 20 chars and not be blank', - $validator->getDescription(), - ); - } - - public function testCanValidateTextWithAllowList(): void - { - // Test lowercase alphabet - $validator = new Text(100, allowList: Text::ALPHABET_LOWER); - $this->assertFalse($validator->isArray()); - $this->assertTrue($validator->isValid('qwertzuiopasdfghjklyxcvbnm')); - $this->assertTrue($validator->isValid('hello')); - $this->assertTrue($validator->isValid('world')); - $this->assertFalse($validator->isValid('hello world')); - $this->assertFalse($validator->isValid('Hello')); - $this->assertFalse($validator->isValid('worlD')); - $this->assertFalse($validator->isValid('hello123')); - - // Test uppercase alphabet - $validator = new Text(100, allowList: Text::ALPHABET_UPPER); - $this->assertFalse($validator->isArray()); - $this->assertTrue($validator->isValid('QWERTZUIOPASDFGHJKLYXCVBNM')); - $this->assertTrue($validator->isValid('HELLO')); - $this->assertTrue($validator->isValid('WORLD')); - $this->assertFalse($validator->isValid('HELLO WORLD')); - $this->assertFalse($validator->isValid('hELLO')); - $this->assertFalse($validator->isValid('WORLd')); - $this->assertFalse($validator->isValid('HELLO123')); - - // Test numbers - $validator = new Text(100, allowList: Text::NUMBERS); - $this->assertFalse($validator->isArray()); - $this->assertTrue($validator->isValid('1234567890')); - $this->assertTrue($validator->isValid('123')); - $this->assertFalse($validator->isValid('123 456')); - $this->assertFalse($validator->isValid('hello123')); - - // Test combination of allowLists - $validator = new Text(100, allowList: [ - ...Text::ALPHABET_LOWER, - ...Text::ALPHABET_UPPER, - ...Text::NUMBERS, - ]); - - $this->assertFalse($validator->isArray()); - $this->assertTrue($validator->isValid('1234567890')); - $this->assertTrue($validator->isValid('qwertzuiopasdfghjklyxcvbnm')); - $this->assertTrue($validator->isValid('QWERTZUIOPASDFGHJKLYXCVBNM')); - $this->assertTrue($validator->isValid('QWERTZUIOPASDFGHJKLYXCVBNMqwertzuiopasdfghjklyxcvbnm1234567890')); - $this->assertFalse($validator->isValid('hello-world')); - $this->assertFalse($validator->isValid('hello_world')); - $this->assertFalse($validator->isValid('hello/world')); - } -} diff --git a/packages/validators/tests/Validator/URLTest.php b/packages/validators/tests/Validator/URLTest.php deleted file mode 100644 index f2ede9d94..000000000 --- a/packages/validators/tests/Validator/URLTest.php +++ /dev/null @@ -1,180 +0,0 @@ - - * @version 1.0 RC4 - * @license The MIT License (MIT) - */ - -namespace Utopia\Validator; - -use PHPUnit\Framework\TestCase; - -final class URLTest extends TestCase -{ - protected ?URL $url; - - public function setUp(): void - { - $this->url = new URL(); - } - - public function tearDown(): void - { - $this->url = null; - } - - public function testIsValid(): void - { - $this->assertSame('Value must be a valid URL', $this->url->getDescription()); - $this->assertTrue($this->url->isValid('http://example.com')); - $this->assertTrue($this->url->isValid('https://example.com')); - $this->assertTrue($this->url->isValid('htts://example.com')); // does not validate protocol - $this->assertFalse($this->url->isValid('example.com')); // though, requires some kind of protocol - $this->assertFalse($this->url->isValid('http:/example.com')); - $this->assertTrue($this->url->isValid('http://exa-mple.com')); - $this->assertFalse($this->url->isValid('htt@s://example.com')); - $this->assertTrue($this->url->isValid('http://www.example.com/foo%2\u00c2\u00a9zbar')); - $this->assertTrue($this->url->isValid('http://www.example.com/?q=%3Casdf%3E')); - $this->assertTrue($this->url->isValid('https://example.com/callback#fragment')); - } - - public function testIsValidAllowedSchemes(): void - { - $this->url = new URL(['http', 'https']); - $this->assertSame('Value must be a valid URL with following schemes (http, https)', $this->url->getDescription()); - $this->assertTrue($this->url->isValid('http://example.com')); - $this->assertTrue($this->url->isValid('https://example.com')); - $this->assertFalse($this->url->isValid('gopher://www.example.com')); - } - - public function testAllowEmpty(): void - { - $urlAllowEmpty = new URL([], true); - $this->assertTrue($urlAllowEmpty->isValid('')); - $this->assertFalse($urlAllowEmpty->isValid(null)); - $this->assertTrue($urlAllowEmpty->isValid('https://example.com')); - $this->assertFalse($urlAllowEmpty->isValid('not-a-url')); - - $this->assertFalse($this->url->isValid('')); - $this->assertFalse($this->url->isValid(null)); - } - - public function testDisallowFragments(): void - { - $urlWithoutFragments = new URL(allowFragments: false); - - $this->assertSame('Value must be a valid URL without a fragment component', $urlWithoutFragments->getDescription()); - $this->assertTrue($urlWithoutFragments->isValid('https://example.com/callback')); - $this->assertFalse($urlWithoutFragments->isValid('https://example.com/callback#fragment')); - $this->assertFalse($urlWithoutFragments->isValid('https://example.com/callback#')); - } - - public function testDisallowFragmentsAllowedSchemes(): void - { - $urlWithoutFragments = new URL(['http', 'https'], allowFragments: false); - - $this->assertSame('Value must be a valid URL with following schemes (http, https) and without a fragment component', $urlWithoutFragments->getDescription()); - $this->assertTrue($urlWithoutFragments->isValid('https://example.com/callback')); - $this->assertFalse($urlWithoutFragments->isValid('https://example.com/callback#fragment')); - $this->assertFalse($urlWithoutFragments->isValid('gopher://www.example.com')); - } - - public function testAllowPrivateUseSchemes(): void - { - // Default: private-use schemes are rejected (backward compatibility). - $default = new URL(); - $this->assertFalse($default->isValid('com.raycast-x:/oauth')); - $this->assertFalse($default->isValid('com.example.app:/oauth2redirect/example-provider')); - - $url = new URL(allowPrivateUseSchemes: true); - - // Happy path — RFC 8252 §7.1 private-use URI scheme redirect URIs. - $this->assertTrue($url->isValid('com.raycast-x:/oauth')); - $this->assertTrue($url->isValid('com.example.app:/oauth2redirect/example-provider')); - $this->assertTrue($url->isValid('com.raycast-x:/oauth?state=abc')); // query allowed - $this->assertTrue($url->isValid('com.raycast-x:oauth')); // path-rootless (opaque) form - - // Standard hierarchical URLs still validate through the normal path. - $this->assertTrue($url->isValid('https://example.com/callback')); - $this->assertTrue($url->isValid('http://127.0.0.1:8080/callback')); // loopback redirect - - // Edge-case failures. - $this->assertFalse($url->isValid('http:/example.com')); // dotless standard scheme, no authority — still invalid - $this->assertFalse($url->isValid('1com.raycast:/oauth')); // scheme must not start with a digit (RFC 3986) - $this->assertFalse($url->isValid('com raycast:/oauth')); // space in scheme - $this->assertFalse($url->isValid(':/oauth')); // missing scheme - $this->assertFalse($url->isValid('/oauth')); // no scheme at all - $this->assertFalse($url->isValid('comraycast:/oauth')); // no dot -> not treated as reverse-DNS private-use scheme - $this->assertFalse($url->isValid('not a url')); - $this->assertFalse($url->isValid('')); // allowEmpty not set - } - - public function testAllowPrivateUseSchemesWithConstraints(): void - { - // Fragments forbidden must also apply to private-use schemes (RFC 6749 §3.1.2). - $noFragments = new URL(allowFragments: false, allowPrivateUseSchemes: true); - $this->assertTrue($noFragments->isValid('com.raycast-x:/oauth')); - $this->assertFalse($noFragments->isValid('com.raycast-x:/oauth#frag')); - - // allowEmpty composes as before. - $allowEmpty = new URL(allowEmpty: true, allowPrivateUseSchemes: true); - $this->assertTrue($allowEmpty->isValid('')); - $this->assertTrue($allowEmpty->isValid('com.raycast-x:/oauth')); - - // allowedSchemes still gates private-use schemes. - $scoped = new URL(['com.raycast-x'], allowPrivateUseSchemes: true); - $this->assertTrue($scoped->isValid('com.raycast-x:/oauth')); - $this->assertFalse($scoped->isValid('com.evil-app:/oauth')); - } - - public function testHttpsOrLoopback(): void - { - $url = new URL(allowFragments: false, allowPrivateUseSchemes: true, httpsOrLoopback: true); - - $this->assertSame( - 'Value must be a valid URL without a fragment component restricted to https or http on a loopback host', - $url->getDescription(), - ); - - // Accepted. - $this->assertTrue($url->isValid('https://app.example.com/callback')); - $this->assertTrue($url->isValid('https://app.example.com/callback?foo=bar')); - $this->assertTrue($url->isValid('http://localhost:6000/callback')); - $this->assertTrue($url->isValid('http://127.0.0.1:6000/callback')); - $this->assertTrue($url->isValid('http://[::1]:6000/callback')); - $this->assertTrue($url->isValid('http://localhost/callback')); - $this->assertTrue($url->isValid('com.example.app:/oauth')); - - // Rejected. - $this->assertFalse($url->isValid('http://app.example.com/callback')); // routable http - $this->assertFalse($url->isValid('http://localhost.evil.com/callback')); // loopback lookalike - $this->assertFalse($url->isValid('ftp://app.example.com/callback')); // non-http(s) standard scheme - $this->assertFalse($url->isValid('https://app.example.com/callback#frag')); // fragment - $this->assertFalse($url->isValid('com.example.app:/oauth#frag')); // private-use with fragment - $this->assertFalse($url->isValid('not a valid uri')); - $this->assertFalse($url->isValid('')); - } - - public function testHttpsOrLoopbackDefaultsToFalse(): void - { - // Flag off (default): routable http is still valid, and the transport clause - // is absent from the description. - $default = new URL(); - $this->assertSame('Value must be a valid URL', $default->getDescription()); - $this->assertTrue($default->isValid('http://app.example.com/callback')); - - // The flag is independent of and additive to allowedSchemes: both must hold. - $both = new URL(['https'], httpsOrLoopback: true); - $this->assertTrue($both->isValid('https://app.example.com/callback')); - $this->assertFalse($both->isValid('http://localhost/callback')); // http not in allowedSchemes - } -} diff --git a/packages/validators/tests/Validator/WhiteListTest.php b/packages/validators/tests/Validator/WhiteListTest.php deleted file mode 100755 index 02d1e1109..000000000 --- a/packages/validators/tests/Validator/WhiteListTest.php +++ /dev/null @@ -1,60 +0,0 @@ -assertTrue($whiteList->isValid(3)); - $this->assertTrue($whiteList->isValid(4)); - $this->assertTrue($whiteList->isValid('string1')); - $this->assertTrue($whiteList->isValid('string2')); - - $this->assertFalse($whiteList->isValid('string3')); - $this->assertFalse($whiteList->isValid('STRING1')); - $this->assertFalse($whiteList->isValid('strIng1')); - $this->assertFalse($whiteList->isValid('3')); - $this->assertFalse($whiteList->isValid(5)); - $this->assertFalse($whiteList->isArray()); - $this->assertSame($whiteList->getList(), ['string1', 'string2', 3, 4]); - $this->assertSame(\Utopia\Validator::TYPE_STRING, $whiteList->getType()); - } - - public function testCanValidateLoosely(): void - { - $whiteList = new WhiteList(['string1', 'string2', 3, 4]); - - $this->assertTrue($whiteList->isValid(3)); - $this->assertTrue($whiteList->isValid(4)); - $this->assertTrue($whiteList->isValid('string1')); - $this->assertTrue($whiteList->isValid('string2')); - $this->assertTrue($whiteList->isValid('STRING1')); - $this->assertTrue($whiteList->isValid('strIng1')); - $this->assertTrue($whiteList->isValid('3')); - $this->assertTrue($whiteList->isValid('4')); - $this->assertFalse($whiteList->isValid('string3')); - $this->assertFalse($whiteList->isValid(5)); - $this->assertSame($whiteList->getList(), ['string1', 'string2', '3', '4']); - - $whiteList = new WhiteList(['STRING1', 'STRING2', 3, 4]); - - $this->assertTrue($whiteList->isValid(3)); - $this->assertTrue($whiteList->isValid(4)); - $this->assertTrue($whiteList->isValid('string1')); - $this->assertTrue($whiteList->isValid('string2')); - $this->assertTrue($whiteList->isValid('STRING1')); - $this->assertTrue($whiteList->isValid('strIng1')); - $this->assertTrue($whiteList->isValid('3')); - $this->assertTrue($whiteList->isValid('4')); - $this->assertFalse($whiteList->isValid('string3')); - $this->assertFalse($whiteList->isValid(5)); - $this->assertSame($whiteList->getList(), ['string1', 'string2', '3', '4']); - } -} diff --git a/packages/validators/tests/Validator/WildcardTest.php b/packages/validators/tests/Validator/WildcardTest.php deleted file mode 100644 index 3f7f0404c..000000000 --- a/packages/validators/tests/Validator/WildcardTest.php +++ /dev/null @@ -1,23 +0,0 @@ -assertTrue($validator->isValid([0 => 'string', 1 => 'string'])); - $this->assertTrue($validator->isValid('')); - $this->assertTrue($validator->isValid([])); - $this->assertTrue($validator->isValid(1)); - $this->assertTrue($validator->isValid(true)); - $this->assertTrue($validator->isValid(false)); - $this->assertFalse($validator->isArray()); - $this->assertSame(\Utopia\Validator::TYPE_STRING, $validator->getType()); - } -}