From 6b7684d052b99e23330fb0df0ebb2dd3e4e54c9e Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Tue, 7 Jan 2025 23:30:01 +0100 Subject: [PATCH] Refactor & move xsd-specific tests to xml-common --- src/Assert.php | 34 +--- src/Base64Trait.php | 64 ++++++++ src/CustomAssertionTrait.php | 292 --------------------------------- src/NotInArrayTrait.php | 47 ++++++ src/URITrait.php | 95 +++++++++++ tests/Assert/AssertTest.php | 10 +- tests/Assert/Base64Test.php | 4 +- tests/Assert/DurationTest.php | 66 -------- tests/Assert/HexBinaryTest.php | 51 ------ tests/Assert/NCNameTest.php | 56 ------- tests/Assert/NMTokenTest.php | 56 ------- tests/Assert/NMTokensTest.php | 57 ------- tests/Assert/QNameTest.php | 61 ------- 13 files changed, 219 insertions(+), 674 deletions(-) create mode 100644 src/Base64Trait.php delete mode 100644 src/CustomAssertionTrait.php create mode 100644 src/NotInArrayTrait.php create mode 100644 src/URITrait.php delete mode 100644 tests/Assert/DurationTest.php delete mode 100644 tests/Assert/HexBinaryTest.php delete mode 100644 tests/Assert/NCNameTest.php delete mode 100644 tests/Assert/NMTokenTest.php delete mode 100644 tests/Assert/NMTokensTest.php delete mode 100644 tests/Assert/QNameTest.php diff --git a/src/Assert.php b/src/Assert.php index 6577e74..1cfc5dc 100644 --- a/src/Assert.php +++ b/src/Assert.php @@ -311,46 +311,24 @@ * @method static void nullOrThrows(Closure|null $expression, string $class, string $message = '', string $exception = '') * @method static void allThrows(Closure[] $expression, string $class, string $message = '', string $exception = '') * - * @method static void validHexBinary(mixed $value, string $message = '', string $exception = '') - * @method static void validNMToken(mixed $value, string $message = '', string $exception = '') - * @method static void validNMTokens(mixed $value, string $message = '', string $exception = '') - * @method static void validDuration(mixed $value, string $message = '', string $exception = '') - * @method static void stringPlausibleBase64(mixed $value, string $message = '', string $exception = '') - * @method static void validDateTime(mixed $value, string $message = '', string $exception = '') + * @method static void validBase64(mixed $value, string $message = '', string $exception = '') * @method static void notInArray(mixed $value, array $values, string $message = '', string $exception = '') * @method static void validURN(mixed $value, string $message = '', string $exception = '') * @method static void validURI(mixed $value, string $message = '', string $exception = '') * @method static void validURL(mixed $value, string $message = '', string $exception = '') - * @method static void validNCName(mixed $value, string $message = '', string $exception = '') - * @method static void validQName(mixed $value, string $message = '', string $exception = '') - * @method static void nullOrValidHexBinary(mixed $value, string $message = '', string $exception = '') - * @method static void nullOrValidNMToken(mixed $value, string $message = '', string $exception = '') - * @method static void nullOrValidNMTokens(mixed $value, string $message = '', string $exception = '') - * @method static void nullOrValidDuration(mixed $value, string $message = '', string $exception = '') - * @method static void nullOrStringPlausibleBase64(mixed $value, string $message = '', string $exception = '') - * @method static void nullOrValidDateTime(mixed $value, string $message = '', string $exception = '') + * @method static void nullOrValidBase64(mixed $value, string $message = '', string $exception = '') * @method static void nullOrNotInArray(mixed $value, array $values, string $message = '', string $exception = '') * @method static void nullOrValidURN(mixed $value, string $message = '', string $exception = '') * @method static void nullOrValidURI(mixed $value, string $message = '', string $exception = '') * @method static void nullOrValidURL(mixed $value, string $message = '', string $exception = '') - * @method static void nullOrValidNCName(mixed $value, string $message = '', string $exception = '') - * @method static void nullOrValidQName(mixed $value, string $message = '', string $exception = '') - * @method static void allValidHexBinary(mixed $value, string $message = '', string $exception = '') - * @method static void allValidNMToken(mixed $value, string $message = '', string $exception = '') - * @method static void allValidNMTokens(mixed $value, string $message = '', string $exception = '') - * @method static void allValidDuration(mixed $value, string $message = '', string $exception = '') - * @method static void allStringPlausibleBase64(mixed $value, string $message = '', string $exception = '') - * @method static void allValidDateTime(mixed $value, string $message = '', string $exception = '') + * @method static void allValidBase64(mixed $value, string $message = '', string $exception = '') * @method static void allNotInArray(mixed $value, array $values, string $message = '', string $exception = '') - * @method static void allValidURN(mixed $value, string $message = '', string $exception = '') - * @method static void allValidURI(mixed $value, string $message = '', string $exception = '') - * @method static void allValidURL(mixed $value, string $message = '', string $exception = '') - * @method static void allValidNCName(mixed $value, string $message = '', string $exception = '') - * @method static void allValidQName(mixed $value, string $message = '', string $exception = '') */ final class Assert { - use CustomAssertionTrait; + use Base64Trait; + use NotInArrayTrait; + use URITrait; /** diff --git a/src/Base64Trait.php b/src/Base64Trait.php new file mode 100644 index 0000000..18177ef --- /dev/null +++ b/src/Base64Trait.php @@ -0,0 +1,64 @@ + ['regexp' => self::$base64_regex]]) === false) { + $result = false; + } elseif (strlen($value) % 4 !== 0) { + $result = false; + } else { + $decoded = base64_decode($value, true); + if (empty($decoded)) { // Invalid _or_ empty string + $result = false; + } elseif (base64_encode($decoded) !== $value) { + $result = false; + } + } + + if ($result === false) { + throw new InvalidArgumentException(sprintf( + $message ?: '\'%s\' is not a valid Base64 encoded string', + $value, + )); + } + } +} diff --git a/src/CustomAssertionTrait.php b/src/CustomAssertionTrait.php deleted file mode 100644 index b56b2d0..0000000 --- a/src/CustomAssertionTrait.php +++ /dev/null @@ -1,292 +0,0 @@ -\d+(?:[\.\,]\d+)?)Y)?(?:(?\d+(?:[\.\,]\d+)?)M)?(?:(?\d+(?:[\.\,]\d+)?)W)?(?:(?\d+(?:[\.\,]\d+)?)D)?(T(?=\d)(?:(?\d+(?:[\.\,]\d+)?)H)?(?:(?\d+(?:[\.\,]\d+)?)M)?(?:(?\d+(?:[\.\,]\d+)?)S)?)?$/D'; - - /** @var string */ - private static string $qname_regex = '/^[a-zA-Z_][\w.-]*:[a-zA-Z_][\w.-]*$/D'; - - /** @var string */ - private static string $ncname_regex = '/^[a-zA-Z_][\w.-]*$/D'; - - /** @var string */ - private static string $base64_regex = '/^(?:[a-z0-9+\/]{4})*(?:[a-z0-9+\/]{2}==|[a-z0-9+\/]{3}=)?$/i'; - - /*********************************************************************************** - * NOTE: Custom assertions may be added below this line. * - * They SHOULD be marked as `private` to ensure the call is forced * - * through __callStatic(). * - * Assertions marked `public` are called directly and will * - * not handle any custom exception passed to it. * - ***********************************************************************************/ - - - /** - * @param string $value - * @param string $message - */ - private static function validNMToken(string $value, string $message = ''): void - { - if (filter_var($value, FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => self::$nmtoken_regex]]) === false) { - throw new InvalidArgumentException(sprintf( - $message ?: '\'%s\' is not a valid xs:NMTOKEN', - $value, - )); - } - } - - - /** - * @param string $value - * @param string $message - */ - private static function validNMTokens(string $value, string $message = ''): void - { - if (filter_var($value, FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => self::$nmtokens_regex]]) === false) { - throw new InvalidArgumentException(sprintf( - $message ?: '\'%s\' is not a valid xs:NMTOKENS', - $value, - )); - } - } - - - /** - * @param string $value - * @param string $message - */ - private static function validDuration(string $value, string $message = ''): void - { - if (filter_var($value, FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => self::$duration_regex]]) === false) { - throw new InvalidArgumentException(sprintf( - $message ?: '\'%s\' is not a valid xs:duration', - $value, - )); - } - } - - - /** - * Note: This test is not bullet-proof but prevents a string containing illegal characters - * from being passed and ensures the string roughly follows the correct format for a Base64 encoded string - * - * @param string $value - * @param string $message - */ - private static function stringPlausibleBase64(string $value, string $message = ''): void - { - $result = true; - - if (filter_var($value, FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => self::$base64_regex]]) === false) { - $result = false; - } elseif (strlen($value) % 4 !== 0) { - $result = false; - } else { - $decoded = base64_decode($value, true); - if (empty($decoded)) { // Invalid _or_ empty string - $result = false; - } elseif (base64_encode($decoded) !== $value) { - $result = false; - } - } - - if ($result === false) { - throw new InvalidArgumentException(sprintf( - $message ?: '\'%s\' is not a valid Base64 encoded string', - $value, - )); - } - } - - - /** - * @param string $value - * @param string $message - */ - private static function validHexBinary(string $value, string $message = ''): void - { - $result = true; - - if (filter_var($value, FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => self::$hexbin_regex]]) === false) { - $result = false; - } - - if ($result === false) { - throw new InvalidArgumentException(sprintf( - $message ?: '\'%s\' is not a valid hexBinary string', - $value, - )); - } - } - - - /** - * @param string $value - * @param string $message - */ - private static function validDateTime(string $value, string $message = ''): void - { - if (filter_var($value, FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => self::$datetime_regex]]) === false) { - throw new InvalidArgumentException(sprintf( - $message ?: '\'%s\' is not a valid xs:dateTime', - $value, - )); - } - } - - - /** - * @param mixed $value - * @param array $values - * @param string $message - */ - private static function notInArray($value, array $values, string $message = ''): void - { - if (in_array($value, $values, true)) { - $callable = /** @param mixed $val */function ($val) { - return self::valueToString($val); - }; - - throw new InvalidArgumentException(sprintf( - $message ?: 'Expected none of: %2$s. Got: %s', - self::valueToString($value), - implode(', ', array_map($callable, $values)), - )); - } - } - - - /** - * @param string $value - * @param string $message - */ - private static function validURN(string $value, string $message = ''): void - { - try { - $uri = new Uri($value); - } catch (MalformedUriException $e) { - throw new InvalidArgumentException(sprintf( - $message ?: '\'%s\' is not a valid RFC3986 compliant URI', - $value, - )); - } - - if ( - $uri->getScheme() !== 'urn' - || (($uri->getScheme() !== null) && $uri->getPath() !== substr($value, strlen($uri->getScheme()) + 1)) - ) { - throw new InvalidArgumentException(sprintf( - $message ?: '\'%s\' is not a valid RFC8141 compliant URN', - $value, - )); - } - } - - - /** - * @param string $value - * @param string $message - */ - private static function validURL(string $value, string $message = ''): void - { - try { - $uri = new Uri($value); - } catch (MalformedUriException $e) { - throw new InvalidArgumentException(sprintf( - $message ?: '\'%s\' is not a valid RFC3986 compliant URI', - $value, - )); - } - - if ($uri->getScheme() !== 'http' && $uri->getScheme() !== 'https') { - throw new InvalidArgumentException(sprintf( - $message ?: '\'%s\' is not a valid RFC2396 compliant URL', - $value, - )); - } - } - - - /** - * @param string $value - * @param string $message - */ - private static function validURI(string $value, string $message = ''): void - { - try { - new Uri($value); - } catch (MalformedUriException $e) { - throw new InvalidArgumentException(sprintf( - $message ?: '\'%s\' is not a valid RFC3986 compliant URI', - $value, - )); - } - } - - - /** - * @param string $value - * @param string $message - */ - private static function validNCName(string $value, string $message = ''): void - { - if (filter_var($value, FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => self::$ncname_regex]]) === false) { - throw new InvalidArgumentException(sprintf( - $message ?: '\'%s\' is not a valid non-colonized name (NCName)', - $value, - )); - } - } - - - /** - * @param string $value - * @param string $message - */ - private static function validQName(string $value, string $message = ''): void - { - if ( - filter_var($value, FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => self::$qname_regex]]) === false && - filter_var($value, FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => self::$ncname_regex]]) === false - ) { - throw new InvalidArgumentException(sprintf( - $message ?: '\'%s\' is not a valid qualified name (QName)', - $value, - )); - } - } -} diff --git a/src/NotInArrayTrait.php b/src/NotInArrayTrait.php new file mode 100644 index 0000000..55d66f4 --- /dev/null +++ b/src/NotInArrayTrait.php @@ -0,0 +1,47 @@ + $values + * @param string $message + */ + protected static function notInArray($value, array $values, string $message = ''): void + { + if (in_array($value, $values, true)) { + $callable = function (mixed $val) { + return self::valueToString($val); + }; + + throw new InvalidArgumentException(sprintf( + $message ?: 'Expected none of: %2$s. Got: %s', + self::valueToString($value), + implode(', ', array_map($callable, $values)), + )); + } + } +} diff --git a/src/URITrait.php b/src/URITrait.php new file mode 100644 index 0000000..1edb4a5 --- /dev/null +++ b/src/URITrait.php @@ -0,0 +1,95 @@ +getScheme() !== 'urn' + || (($uri->getScheme() !== null) && $uri->getPath() !== substr($value, strlen($uri->getScheme()) + 1)) + ) { + throw new InvalidArgumentException(sprintf( + $message ?: '\'%s\' is not a valid RFC8141 compliant URN', + $value, + )); + } + } + + + /** + * @param string $value + * @param string $message + */ + protected static function validURL(string $value, string $message = ''): void + { + try { + $uri = new Uri($value); + } catch (MalformedUriException $e) { + throw new InvalidArgumentException(sprintf( + $message ?: '\'%s\' is not a valid RFC3986 compliant URI', + $value, + )); + } + + if ($uri->getScheme() !== 'http' && $uri->getScheme() !== 'https') { + throw new InvalidArgumentException(sprintf( + $message ?: '\'%s\' is not a valid RFC2396 compliant URL', + $value, + )); + } + } + + + /** + * @param string $value + * @param string $message + */ + protected static function validURI(string $value, string $message = ''): void + { + try { + new Uri($value); + } catch (MalformedUriException $e) { + throw new InvalidArgumentException(sprintf( + $message ?: '\'%s\' is not a valid RFC3986 compliant URI', + $value, + )); + } + } +} diff --git a/tests/Assert/AssertTest.php b/tests/Assert/AssertTest.php index 322d894..aebd747 100644 --- a/tests/Assert/AssertTest.php +++ b/tests/Assert/AssertTest.php @@ -88,8 +88,8 @@ public function testUnknownAllAssertionRaisesBadMethodCallException(): void */ public function testNullOrCustomAssertionWorks(): void { - Assert::nullOrStringPlausibleBase64('U2ltcGxlU0FNTHBocA=='); - Assert::nullOrStringPlausibleBase64(null); + Assert::nullOrValidBase64('U2ltcGxlU0FNTHBocA=='); + Assert::nullOrValidBase64(null); // Also make sure it keeps working for Webmozart's native assertions Assert::nullOrString(null); @@ -97,7 +97,7 @@ public function testNullOrCustomAssertionWorks(): void // Test a failure for coverage $this->expectException(AssertionFailedException::class); - Assert::nullOrStringPlausibleBase64('U2ltcGxlU0FNTHocA=='); + Assert::nullOrValidBase64('U2ltcGxlU0FNTHocA=='); } @@ -105,14 +105,14 @@ public function testNullOrCustomAssertionWorks(): void */ public function testAllCustomAssertionWorks(): void { - Assert::allStringPlausibleBase64(['U2ltcGxlU0FNTHBocA==', 'dGVzdA==']); + Assert::allValidBase64(['U2ltcGxlU0FNTHBocA==', 'dGVzdA==']); // Also make sure it keeps working for Webmozart's native assertions Assert::allString(['test', 'phpunit']); // Test a failure for coverage $this->expectException(AssertionFailedException::class); - Assert::allStringPlausibleBase64(['U2ltcGxlU0FNTHocA==', null]); + Assert::allValidBase64(['U2ltcGxlU0FNTHocA==', null]); } diff --git a/tests/Assert/Base64Test.php b/tests/Assert/Base64Test.php index 19dad32..fdbcd69 100644 --- a/tests/Assert/Base64Test.php +++ b/tests/Assert/Base64Test.php @@ -23,10 +23,10 @@ final class Base64Test extends TestCase * @param string $name */ #[DataProvider('provideBase64')] - public function testStringPlausibleBase64(bool $shouldPass, string $name): void + public function testvalidBase64(bool $shouldPass, string $name): void { try { - Assert::StringPlausibleBase64($name); + Assert::validBase64($name); $this->assertTrue($shouldPass); } catch (AssertionFailedException $e) { $this->assertFalse($shouldPass); diff --git a/tests/Assert/DurationTest.php b/tests/Assert/DurationTest.php deleted file mode 100644 index f624b5c..0000000 --- a/tests/Assert/DurationTest.php +++ /dev/null @@ -1,66 +0,0 @@ -assertTrue($shouldPass); - } catch (AssertionFailedException $e) { - $this->assertFalse($shouldPass); - } - } - - - /** - * @return array - */ - public static function provideDuration(): array - { - return [ - [true, 'P2Y6M5DT12H35M30S'], - [true, 'P1DT2H'], - [true, 'P1W'], - [true, 'P20M'], - [true, 'PT20M'], - [true, 'P0Y20M0D'], - [true, 'P0Y'], - [true, '-P60D'], - [true, 'PT1M30.5S'], - [true, 'P15.5Y'], - [true, 'P15,5Y'], - [false, 'P-20M'], - [false, 'P20MT'], - [false, 'P1YM5D'], - [false, 'P1D2H'], - [false, '1Y2M'], - [false, 'P2M1Y'], - [false, 'P'], - [false, 'PT15.S'], - // Trailing newlines are forbidden - [false, "P20M\n"], - ]; - } -} diff --git a/tests/Assert/HexBinaryTest.php b/tests/Assert/HexBinaryTest.php deleted file mode 100644 index 75cca9f..0000000 --- a/tests/Assert/HexBinaryTest.php +++ /dev/null @@ -1,51 +0,0 @@ -assertTrue($shouldPass); - } catch (AssertionFailedException $e) { - $this->assertFalse($shouldPass); - } - } - - - /** - * @return array - */ - public static function provideHexBinary(): array - { - return [ - 'empty' => [false, ''], - 'base64' => [false, 'U2ltcGxlU0FNTHBocA=='], - 'valid' => [true, '3f3c6d78206c657673726f693d6e3122302e20226e656f636964676e223d54552d4622383e3f'], - 'invalid' => [false, '3f3r'], - 'bogus' => [false, '&*$(#&^@!(^%$'], - 'length not dividable by 4' => [false, '3f3'], - ]; - } -} diff --git a/tests/Assert/NCNameTest.php b/tests/Assert/NCNameTest.php deleted file mode 100644 index 4dd4b82..0000000 --- a/tests/Assert/NCNameTest.php +++ /dev/null @@ -1,56 +0,0 @@ -assertTrue($shouldPass); - } catch (AssertionFailedException $e) { - $this->assertFalse($shouldPass); - } - } - - - /** - * @return array - */ - public static function provideNCName(): array - { - return [ - [true, 'Test'], - [true, '_Test'], - // Prefixed v4 UUID - [true, '_5425e58e-e799-4884-92cc-ca64ecede32f'], - // An empty value is not valid, unless xsi:nil is used - [false, ''], - [false, 'Te*st'], - [false, '1Test'], - [false, 'Te:st'], - // Trailing newlines are forbidden - [false, "Test\n"], - ]; - } -} diff --git a/tests/Assert/NMTokenTest.php b/tests/Assert/NMTokenTest.php deleted file mode 100644 index bfd39d4..0000000 --- a/tests/Assert/NMTokenTest.php +++ /dev/null @@ -1,56 +0,0 @@ -assertTrue($shouldPass); - } catch (AssertionFailedException $e) { - $this->assertFalse($shouldPass); - } - } - - - /** - * @return array - */ - public static function provideNMToken(): array - { - return [ - [true, 'Snoopy'], - [true, 'CMS'], - [true, 'fööbár'], - [true, '1950-10-04'], - [true, '0836217462'], - // Spaces are forbidden - [false, 'foo bar'], - // Commas are forbidden - [false, 'foo,bar'], - // Trailing newlines are forbidden - [false, "foobar\n"], - ]; - } -} diff --git a/tests/Assert/NMTokensTest.php b/tests/Assert/NMTokensTest.php deleted file mode 100644 index 845029c..0000000 --- a/tests/Assert/NMTokensTest.php +++ /dev/null @@ -1,57 +0,0 @@ -assertTrue($shouldPass); - } catch (AssertionFailedException $e) { - $this->assertFalse($shouldPass); - } - } - - - /** - * @return array - */ - public static function provideNMTokens(): array - { - return [ - [true, 'Snoopy'], - [true, 'CMS'], - [true, 'fööbár'], - [true, '1950-10-04'], - [true, '0836217462 0836217463'], - [true, 'foo bar'], - // Quotes are forbidden - [false, 'foo "bar" baz'], - // Commas are forbidden - [false, 'foo,bar'], - // Trailing newlines are forbidden - [false, "foobar\n"], - ]; - } -} diff --git a/tests/Assert/QNameTest.php b/tests/Assert/QNameTest.php deleted file mode 100644 index 3919151..0000000 --- a/tests/Assert/QNameTest.php +++ /dev/null @@ -1,61 +0,0 @@ -assertTrue($shouldPass); - } catch (AssertionFailedException $e) { - $this->assertFalse($shouldPass); - } - } - - - /** - * @return array - */ - public static function provideQName(): array - { - return [ - [true, 'some:Test'], - [true, 'some:_Test'], - [true, '_some:_Test'], - [true, 'Test'], - // Cannot start with a colon - [false, ':test'], - // Cannot contain multiple colons - [false, 'test:test:test'], - // Cannot start with a number - [false, '1Test'], - // Cannot contain a wildcard character - [false, 'Te*st'], - // Prefixed newlines are forbidden - [false, "\nsome:Test"], - // Trailing newlines are forbidden - [false, "some:Test\n"], - ]; - } -}