Skip to content

Commit c7f9210

Browse files
authored
Merge pull request #77 from Codeception/add-json-constraints
Add Json constraints
2 parents a67031a + 24e262d commit c7f9210

File tree

5 files changed

+568
-1
lines changed

5 files changed

+568
-1
lines changed

composer.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"ext-dom": "*",
1919
"ext-json": "*",
2020
"codeception/codeception": "^5.0.0-alpha2",
21+
"codeception/lib-xml": "^1.0",
2122
"justinrainbow/json-schema": "~5.2.9",
2223
"softcreatr/jsonpath": "^0.8"
2324
},
@@ -29,7 +30,7 @@
2930
"codeception/util-universalframework": "^1.0"
3031
},
3132
"conflict": {
32-
"codeception/codeception": "<5.0"
33+
"codeception/codeception": "<5.0.0-alpha3"
3334
},
3435
"suggest": {
3536
"aws/aws-sdk-php": "For using AWS Auth"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Codeception\PHPUnit\Constraint;
6+
7+
use Codeception\Util\JsonArray;
8+
use PHPUnit\Framework\AssertionFailedError;
9+
use PHPUnit\Framework\Constraint\Constraint;
10+
use PHPUnit\Framework\ExpectationFailedException;
11+
use SebastianBergmann\Comparator\ArrayComparator;
12+
use SebastianBergmann\Comparator\ComparisonFailure;
13+
use SebastianBergmann\Comparator\Factory;
14+
15+
use function is_array;
16+
17+
class JsonContains extends Constraint
18+
{
19+
/**
20+
* @var array
21+
*/
22+
protected $expected;
23+
24+
public function __construct(array $expected)
25+
{
26+
$this->expected = $expected;
27+
}
28+
29+
/**
30+
* Evaluates the constraint for parameter $other. Returns true if the
31+
* constraint is met, false otherwise.
32+
*
33+
* @param mixed $other Value or object to evaluate.
34+
*/
35+
protected function matches($other): bool
36+
{
37+
$jsonResponseArray = new JsonArray($other);
38+
if (!is_array($jsonResponseArray->toArray())) {
39+
throw new AssertionFailedError('JSON response is not an array: ' . $other);
40+
}
41+
$jsonArrayContainsArray = $jsonResponseArray->containsArray($this->expected);
42+
43+
if ($jsonArrayContainsArray) {
44+
return true;
45+
}
46+
47+
$comparator = new ArrayComparator();
48+
$comparator->setFactory(new Factory());
49+
try {
50+
$comparator->assertEquals($this->expected, $jsonResponseArray->toArray());
51+
} catch (ComparisonFailure $failure) {
52+
throw new ExpectationFailedException(
53+
"Response JSON does not contain the provided JSON\n",
54+
$failure
55+
);
56+
}
57+
58+
return false;
59+
}
60+
61+
/**
62+
* Returns a string representation of the constraint.
63+
*/
64+
public function toString(): string
65+
{
66+
//unused
67+
return '';
68+
}
69+
70+
protected function failureDescription($other): string
71+
{
72+
//unused
73+
return '';
74+
}
75+
}
+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Codeception\PHPUnit\Constraint;
6+
7+
use Codeception\Util\JsonArray;
8+
use Codeception\Util\JsonType as JsonTypeUtil;
9+
use PHPUnit\Framework\Constraint\Constraint;
10+
use PHPUnit\Framework\ExpectationFailedException;
11+
12+
use function json_encode;
13+
14+
class JsonType extends Constraint
15+
{
16+
/**
17+
* @var array
18+
*/
19+
protected $jsonType;
20+
/**
21+
* @var bool
22+
*/
23+
private $match;
24+
25+
public function __construct(array $jsonType, bool $match = true)
26+
{
27+
$this->jsonType = $jsonType;
28+
$this->match = $match;
29+
}
30+
31+
/**
32+
* Evaluates the constraint for parameter $other. Returns true if the
33+
* constraint is met, false otherwise.
34+
*
35+
* @param mixed $jsonArray Value or object to evaluate.
36+
*/
37+
protected function matches($jsonArray): bool
38+
{
39+
if ($jsonArray instanceof JsonArray) {
40+
$jsonArray = $jsonArray->toArray();
41+
}
42+
43+
$matched = (new JsonTypeUtil($jsonArray))->matches($this->jsonType);
44+
45+
if ($this->match) {
46+
if ($matched !== true) {
47+
throw new ExpectationFailedException($matched);
48+
}
49+
} elseif ($matched === true) {
50+
$jsonArray = json_encode($jsonArray, JSON_THROW_ON_ERROR);
51+
throw new ExpectationFailedException('Unexpectedly response matched: ' . $jsonArray);
52+
}
53+
return true;
54+
}
55+
56+
/**
57+
* Returns a string representation of the constraint.
58+
*/
59+
public function toString(): string
60+
{
61+
//unused
62+
return '';
63+
}
64+
65+
protected function failureDescription($other): string
66+
{
67+
//unused
68+
return '';
69+
}
70+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Codeception\Util;
6+
7+
use function array_intersect;
8+
use function array_keys;
9+
use function count;
10+
use function is_array;
11+
use function is_numeric;
12+
use function min;
13+
use function range;
14+
15+
class ArrayContainsComparator
16+
{
17+
protected array $haystack;
18+
19+
public function __construct(array $haystack)
20+
{
21+
$this->haystack = $haystack;
22+
}
23+
24+
public function getHaystack(): array
25+
{
26+
return $this->haystack;
27+
}
28+
29+
public function containsArray(array $needle): bool
30+
{
31+
return $needle == $this->arrayIntersectRecursive($needle, $this->haystack);
32+
}
33+
34+
/**
35+
* @return array|bool
36+
37+
* @link https://www.php.net/manual/en/function.array-intersect-assoc.php#39822
38+
*
39+
40+
*/
41+
private function arrayIntersectRecursive(mixed $arr1, mixed $arr2): bool|array|null
42+
{
43+
if (!is_array($arr1) || !is_array($arr2)) {
44+
return false;
45+
}
46+
// if it is not an associative array we do not compare keys
47+
if ($this->arrayIsSequential($arr1) && $this->arrayIsSequential($arr2)) {
48+
return $this->sequentialArrayIntersect($arr1, $arr2);
49+
}
50+
return $this->associativeArrayIntersect($arr1, $arr2);
51+
}
52+
53+
/**
54+
* This array has sequential keys?
55+
*/
56+
private function arrayIsSequential(array $array): bool
57+
{
58+
return array_keys($array) === range(0, count($array) - 1);
59+
}
60+
61+
private function sequentialArrayIntersect(array $arr1, array $arr2): array
62+
{
63+
$ret = [];
64+
65+
// Do not match the same item of $arr2 against multiple items of $arr1
66+
$matchedKeys = [];
67+
foreach ($arr1 as $key1 => $value1) {
68+
foreach ($arr2 as $key2 => $value2) {
69+
if (isset($matchedKeys[$key2])) {
70+
continue;
71+
}
72+
73+
$return = $this->arrayIntersectRecursive($value1, $value2);
74+
if ($return !== false && $return == $value1) {
75+
$ret[$key1] = $return;
76+
$matchedKeys[$key2] = true;
77+
break;
78+
}
79+
80+
if ($this->isEqualValue($value1, $value2)) {
81+
$ret[$key1] = $value1;
82+
$matchedKeys[$key2] = true;
83+
break;
84+
}
85+
}
86+
}
87+
88+
return $ret;
89+
}
90+
91+
/**
92+
* @return array|bool|null
93+
*/
94+
private function associativeArrayIntersect(array $arr1, array $arr2): bool|array|null
95+
{
96+
$commonKeys = array_intersect(array_keys($arr1), array_keys($arr2));
97+
98+
$ret = [];
99+
foreach ($commonKeys as $key) {
100+
$return = $this->arrayIntersectRecursive($arr1[$key], $arr2[$key]);
101+
if ($return !== false) {
102+
$ret[$key] = $return;
103+
continue;
104+
}
105+
if ($this->isEqualValue($arr1[$key], $arr2[$key])) {
106+
$ret[$key] = $arr1[$key];
107+
}
108+
}
109+
110+
if (empty($commonKeys)) {
111+
foreach ($arr2 as $arr) {
112+
$return = $this->arrayIntersectRecursive($arr1, $arr);
113+
if ($return && $return == $arr1) {
114+
return $return;
115+
}
116+
}
117+
}
118+
119+
if (count($ret) < min(count($arr1), count($arr2))) {
120+
return null;
121+
}
122+
123+
return $ret;
124+
}
125+
126+
private function isEqualValue($val1, $val2): bool
127+
{
128+
if (is_numeric($val1)) {
129+
$val1 = (string)$val1;
130+
}
131+
132+
if (is_numeric($val2)) {
133+
$val2 = (string)$val2;
134+
}
135+
136+
return $val1 === $val2;
137+
}
138+
}

0 commit comments

Comments
 (0)