From 60cba4531731d74e98ff0395b2f1a9601b55e237 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Sat, 29 Mar 2025 18:18:47 +0100 Subject: [PATCH] [JsonPath] Add `JsonPathAssertionsTrait` and related constraints --- .../JsonPath/Test/JsonPathAssertionsTrait.php | 80 ++++++++ .../JsonPath/Test/JsonPathContains.php | 43 ++++ .../Component/JsonPath/Test/JsonPathCount.php | 40 ++++ .../JsonPath/Test/JsonPathEquals.php | 40 ++++ .../JsonPath/Test/JsonPathNotContains.php | 43 ++++ .../JsonPath/Test/JsonPathNotEquals.php | 40 ++++ .../JsonPath/Test/JsonPathNotSame.php | 40 ++++ .../Component/JsonPath/Test/JsonPathSame.php | 40 ++++ .../Test/JsonPathAssertionsTraitTest.php | 191 ++++++++++++++++++ 9 files changed, 557 insertions(+) create mode 100644 src/Symfony/Component/JsonPath/Test/JsonPathAssertionsTrait.php create mode 100644 src/Symfony/Component/JsonPath/Test/JsonPathContains.php create mode 100644 src/Symfony/Component/JsonPath/Test/JsonPathCount.php create mode 100644 src/Symfony/Component/JsonPath/Test/JsonPathEquals.php create mode 100644 src/Symfony/Component/JsonPath/Test/JsonPathNotContains.php create mode 100644 src/Symfony/Component/JsonPath/Test/JsonPathNotEquals.php create mode 100644 src/Symfony/Component/JsonPath/Test/JsonPathNotSame.php create mode 100644 src/Symfony/Component/JsonPath/Test/JsonPathSame.php create mode 100644 src/Symfony/Component/JsonPath/Tests/Test/JsonPathAssertionsTraitTest.php diff --git a/src/Symfony/Component/JsonPath/Test/JsonPathAssertionsTrait.php b/src/Symfony/Component/JsonPath/Test/JsonPathAssertionsTrait.php new file mode 100644 index 0000000000000..42d35339a5760 --- /dev/null +++ b/src/Symfony/Component/JsonPath/Test/JsonPathAssertionsTrait.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\JsonPath\Test; + +use PHPUnit\Framework\Assert; +use PHPUnit\Framework\ExpectationFailedException; +use Symfony\Component\JsonPath\JsonPath; + +/** + * @author Alexandre Daubois + * + * @experimental + */ +trait JsonPathAssertionsTrait +{ + /** + * @throws ExpectationFailedException + */ + final public static function assertJsonPathEquals(mixed $expectedValue, JsonPath|string $jsonPath, string $json, string $message = ''): void + { + Assert::assertThat($expectedValue, new JsonPathEquals($jsonPath, $json), $message); + } + + /** + * @throws ExpectationFailedException + */ + final public static function assertJsonPathNotEquals(mixed $expectedValue, JsonPath|string $jsonPath, string $json, string $message = ''): void + { + Assert::assertThat($expectedValue, new JsonPathNotEquals($jsonPath, $json), $message); + } + + /** + * @throws ExpectationFailedException + */ + final public static function assertJsonPathCount(int $expectedCount, JsonPath|string $jsonPath, string $json, string $message = ''): void + { + Assert::assertThat($expectedCount, new JsonPathCount($jsonPath, $json), $message); + } + + /** + * @throws ExpectationFailedException + */ + final public static function assertJsonPathSame(mixed $expectedValue, JsonPath|string $jsonPath, string $json, string $message = ''): void + { + Assert::assertThat($expectedValue, new JsonPathSame($jsonPath, $json), $message); + } + + /** + * @throws ExpectationFailedException + */ + final public static function assertJsonPathNotSame(mixed $expectedValue, JsonPath|string $jsonPath, string $json, string $message = ''): void + { + Assert::assertThat($expectedValue, new JsonPathNotSame($jsonPath, $json), $message); + } + + /** + * @throws ExpectationFailedException + */ + final public static function assertJsonPathContains(mixed $expectedValue, JsonPath|string $jsonPath, string $json, bool $strict = true, string $message = ''): void + { + Assert::assertThat($expectedValue, new JsonPathContains($jsonPath, $json, $strict), $message); + } + + /** + * @throws ExpectationFailedException + */ + final public static function assertJsonPathNotContains(mixed $expectedValue, JsonPath|string $jsonPath, string $json, bool $strict = true, string $message = ''): void + { + Assert::assertThat($expectedValue, new JsonPathNotContains($jsonPath, $json, $strict), $message); + } +} diff --git a/src/Symfony/Component/JsonPath/Test/JsonPathContains.php b/src/Symfony/Component/JsonPath/Test/JsonPathContains.php new file mode 100644 index 0000000000000..e043b90a40637 --- /dev/null +++ b/src/Symfony/Component/JsonPath/Test/JsonPathContains.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\JsonPath\Test; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\JsonPath\JsonCrawler; +use Symfony\Component\JsonPath\JsonPath; + +/** + * @author Alexandre Daubois + * + * @experimental + */ +class JsonPathContains extends Constraint +{ + public function __construct( + private JsonPath|string $jsonPath, + private string $json, + private bool $strict = true, + ) { + } + + public function toString(): string + { + return \sprintf('is found in elements at JSON path "%s"', $this->jsonPath); + } + + protected function matches(mixed $other): bool + { + $result = (new JsonCrawler($this->json))->find($this->jsonPath); + + return \in_array($other, $result, $this->strict); + } +} diff --git a/src/Symfony/Component/JsonPath/Test/JsonPathCount.php b/src/Symfony/Component/JsonPath/Test/JsonPathCount.php new file mode 100644 index 0000000000000..8c973a8309345 --- /dev/null +++ b/src/Symfony/Component/JsonPath/Test/JsonPathCount.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\JsonPath\Test; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\JsonPath\JsonCrawler; +use Symfony\Component\JsonPath\JsonPath; + +/** + * @author Alexandre Daubois + * + * @experimental + */ +class JsonPathCount extends Constraint +{ + public function __construct( + private JsonPath|string $jsonPath, + private string $json, + ) { + } + + public function toString(): string + { + return \sprintf('matches expected count of JSON path "%s"', $this->jsonPath); + } + + protected function matches(mixed $other): bool + { + return $other === \count((new JsonCrawler($this->json))->find($this->jsonPath)); + } +} diff --git a/src/Symfony/Component/JsonPath/Test/JsonPathEquals.php b/src/Symfony/Component/JsonPath/Test/JsonPathEquals.php new file mode 100644 index 0000000000000..56825434b5faa --- /dev/null +++ b/src/Symfony/Component/JsonPath/Test/JsonPathEquals.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\JsonPath\Test; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\JsonPath\JsonCrawler; +use Symfony\Component\JsonPath\JsonPath; + +/** + * @author Alexandre Daubois + * + * @experimental + */ +class JsonPathEquals extends Constraint +{ + public function __construct( + private JsonPath|string $jsonPath, + private string $json, + ) { + } + + public function toString(): string + { + return \sprintf('equals JSON path "%s" result', $this->jsonPath); + } + + protected function matches(mixed $other): bool + { + return (new JsonCrawler($this->json))->find($this->jsonPath) == $other; + } +} diff --git a/src/Symfony/Component/JsonPath/Test/JsonPathNotContains.php b/src/Symfony/Component/JsonPath/Test/JsonPathNotContains.php new file mode 100644 index 0000000000000..721d60fa29984 --- /dev/null +++ b/src/Symfony/Component/JsonPath/Test/JsonPathNotContains.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\JsonPath\Test; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\JsonPath\JsonCrawler; +use Symfony\Component\JsonPath\JsonPath; + +/** + * @author Alexandre Daubois + * + * @experimental + */ +class JsonPathNotContains extends Constraint +{ + public function __construct( + private JsonPath|string $jsonPath, + private string $json, + private bool $strict = true, + ) { + } + + public function toString(): string + { + return \sprintf('is not found in elements at JSON path "%s"', $this->jsonPath); + } + + protected function matches(mixed $other): bool + { + $result = (new JsonCrawler($this->json))->find($this->jsonPath); + + return !\in_array($other, $result, $this->strict); + } +} diff --git a/src/Symfony/Component/JsonPath/Test/JsonPathNotEquals.php b/src/Symfony/Component/JsonPath/Test/JsonPathNotEquals.php new file mode 100644 index 0000000000000..d149dbb59c441 --- /dev/null +++ b/src/Symfony/Component/JsonPath/Test/JsonPathNotEquals.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\JsonPath\Test; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\JsonPath\JsonCrawler; +use Symfony\Component\JsonPath\JsonPath; + +/** + * @author Alexandre Daubois + * + * @experimental + */ +class JsonPathNotEquals extends Constraint +{ + public function __construct( + private JsonPath|string $jsonPath, + private string $json, + ) { + } + + public function toString(): string + { + return \sprintf('does not equal JSON path "%s" result', $this->jsonPath); + } + + protected function matches(mixed $other): bool + { + return (new JsonCrawler($this->json))->find($this->jsonPath) != $other; + } +} diff --git a/src/Symfony/Component/JsonPath/Test/JsonPathNotSame.php b/src/Symfony/Component/JsonPath/Test/JsonPathNotSame.php new file mode 100644 index 0000000000000..248ac456fcbef --- /dev/null +++ b/src/Symfony/Component/JsonPath/Test/JsonPathNotSame.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\JsonPath\Test; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\JsonPath\JsonCrawler; +use Symfony\Component\JsonPath\JsonPath; + +/** + * @author Alexandre Daubois + * + * @experimental + */ +class JsonPathNotSame extends Constraint +{ + public function __construct( + private JsonPath|string $jsonPath, + private string $json, + ) { + } + + public function toString(): string + { + return \sprintf('is not identical to JSON path "%s" result', $this->jsonPath); + } + + protected function matches(mixed $other): bool + { + return (new JsonCrawler($this->json))->find($this->jsonPath) !== $other; + } +} diff --git a/src/Symfony/Component/JsonPath/Test/JsonPathSame.php b/src/Symfony/Component/JsonPath/Test/JsonPathSame.php new file mode 100644 index 0000000000000..469922d8a0b90 --- /dev/null +++ b/src/Symfony/Component/JsonPath/Test/JsonPathSame.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\JsonPath\Test; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\JsonPath\JsonCrawler; +use Symfony\Component\JsonPath\JsonPath; + +/** + * @author Alexandre Daubois + * + * @experimental + */ +class JsonPathSame extends Constraint +{ + public function __construct( + private JsonPath|string $jsonPath, + private string $json, + ) { + } + + public function toString(): string + { + return \sprintf('is identical to JSON path "%s" result', $this->jsonPath); + } + + protected function matches(mixed $other): bool + { + return (new JsonCrawler($this->json))->find($this->jsonPath) === $other; + } +} diff --git a/src/Symfony/Component/JsonPath/Tests/Test/JsonPathAssertionsTraitTest.php b/src/Symfony/Component/JsonPath/Tests/Test/JsonPathAssertionsTraitTest.php new file mode 100644 index 0000000000000..62d64b53e1e8d --- /dev/null +++ b/src/Symfony/Component/JsonPath/Tests/Test/JsonPathAssertionsTraitTest.php @@ -0,0 +1,191 @@ +getMessage()); + + $thrown = true; + } + + self::assertTrue($thrown); + } + + public function testAssertJsonPathNotEqualsOk() + { + self::assertJsonPathNotEquals([2], '$.a[2]', self::getSimpleCollectionCrawlerData()); + } + + public function testAssertJsonPathNotEqualsKo() + { + $thrown = false; + try { + self::assertJsonPathNotEquals([1], '$.a[2]', self::getSimpleCollectionCrawlerData()); + } catch (AssertionFailedError $exception) { + self::assertMatchesRegularExpression('/Failed asserting that .+ does not equal JSON path "\$\.a\[2]" result./s', $exception->getMessage()); + + $thrown = true; + } + + self::assertTrue($thrown); + } + + public function testAssertJsonPathCountOk() + { + self::assertJsonPathCount(6, '$.a[*]', self::getSimpleCollectionCrawlerData()); + } + + public function testAssertJsonPathCountOkWithFilter() + { + self::assertJsonPathCount(2, '$.book[?(@.price > 25)]', <<getMessage()); + + $thrown = true; + } + + self::assertTrue($thrown); + } + + public function testAssertJsonPathSameOk() + { + self::assertJsonPathSame([1], '$.a[2]', self::getSimpleCollectionCrawlerData()); + } + + public function testAssertJsonPathSameKo() + { + $thrown = false; + try { + self::assertJsonPathSame([2], '$.a[2]', self::getSimpleCollectionCrawlerData()); + } catch (AssertionFailedError $exception) { + self::assertMatchesRegularExpression('/Failed asserting that .+ is identical to JSON path "\$\.a\[2]" result\./s', $exception->getMessage()); + + $thrown = true; + } + + self::assertTrue($thrown); + } + + public function testAssertJsonPathHasNoTypeCoercion() + { + $thrown = false; + try { + self::assertJsonPathSame(['1'], '$.a[2]', self::getSimpleCollectionCrawlerData()); + } catch (AssertionFailedError $exception) { + self::assertMatchesRegularExpression('/Failed asserting that .+ is identical to JSON path "\$\.a\[2]" result\./s', $exception->getMessage()); + + $thrown = true; + } + + self::assertTrue($thrown); + } + + public function testAssertJsonPathNotSameOk() + { + self::assertJsonPathNotSame([2], '$.a[2]', self::getSimpleCollectionCrawlerData()); + } + + public function testAssertJsonPathNotSameKo() + { + $thrown = false; + try { + self::assertJsonPathNotSame([1], '$.a[2]', self::getSimpleCollectionCrawlerData()); + } catch (AssertionFailedError $exception) { + self::assertMatchesRegularExpression('/Failed asserting that .+ is not identical to JSON path "\$\.a\[2]" result\./s', $exception->getMessage()); + + $thrown = true; + } + + self::assertTrue($thrown); + } + + public function testAssertJsonPathNotSameHasNoTypeCoercion() + { + self::assertJsonPathNotSame(['1'], '$.a[2]', self::getSimpleCollectionCrawlerData()); + } + + public function testAssertJsonPathContainsOk() + { + self::assertJsonPathContains(1, '$.a[*]', self::getSimpleCollectionCrawlerData()); + } + + public function testAssertJsonPathContainsKo() + { + $thrown = false; + try { + self::assertJsonPathContains(0, '$.a[*]', self::getSimpleCollectionCrawlerData()); + } catch (AssertionFailedError $exception) { + self::assertSame('Failed asserting that 0 is found in elements at JSON path "$.a[*]".', $exception->getMessage()); + + $thrown = true; + } + + self::assertTrue($thrown); + } + + public function testAssertJsonPathNotContainsOk() + { + self::assertJsonPathNotContains(0, '$.a[*]', self::getSimpleCollectionCrawlerData()); + } + + public function testAssertJsonPathNotContainsKo() + { + $thrown = false; + try { + self::assertJsonPathNotContains(1, '$.a[*]', self::getSimpleCollectionCrawlerData()); + } catch (AssertionFailedError $exception) { + self::assertSame('Failed asserting that 1 is not found in elements at JSON path "$.a[*]".', $exception->getMessage()); + + $thrown = true; + } + + self::assertTrue($thrown); + } + + private static function getSimpleCollectionCrawlerData(): string + { + return <<