Skip to content

Commit

Permalink
Merge pull request #77 from Codeception/add-json-constraints
Browse files Browse the repository at this point in the history
Add Json constraints
  • Loading branch information
Naktibalda committed Mar 11, 2022
2 parents a67031a + 24e262d commit c7f9210
Show file tree
Hide file tree
Showing 5 changed files with 568 additions and 1 deletion.
3 changes: 2 additions & 1 deletion composer.json
Expand Up @@ -18,6 +18,7 @@
"ext-dom": "*",
"ext-json": "*",
"codeception/codeception": "^5.0.0-alpha2",
"codeception/lib-xml": "^1.0",
"justinrainbow/json-schema": "~5.2.9",
"softcreatr/jsonpath": "^0.8"
},
Expand All @@ -29,7 +30,7 @@
"codeception/util-universalframework": "^1.0"
},
"conflict": {
"codeception/codeception": "<5.0"
"codeception/codeception": "<5.0.0-alpha3"
},
"suggest": {
"aws/aws-sdk-php": "For using AWS Auth"
Expand Down
75 changes: 75 additions & 0 deletions src/Codeception/Constraint/JsonContains.php
@@ -0,0 +1,75 @@
<?php

declare(strict_types=1);

namespace Codeception\PHPUnit\Constraint;

use Codeception\Util\JsonArray;
use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\Constraint\Constraint;
use PHPUnit\Framework\ExpectationFailedException;
use SebastianBergmann\Comparator\ArrayComparator;
use SebastianBergmann\Comparator\ComparisonFailure;
use SebastianBergmann\Comparator\Factory;

use function is_array;

class JsonContains extends Constraint
{
/**
* @var array
*/
protected $expected;

public function __construct(array $expected)
{
$this->expected = $expected;
}

/**
* Evaluates the constraint for parameter $other. Returns true if the
* constraint is met, false otherwise.
*
* @param mixed $other Value or object to evaluate.
*/
protected function matches($other): bool
{
$jsonResponseArray = new JsonArray($other);
if (!is_array($jsonResponseArray->toArray())) {
throw new AssertionFailedError('JSON response is not an array: ' . $other);
}
$jsonArrayContainsArray = $jsonResponseArray->containsArray($this->expected);

if ($jsonArrayContainsArray) {
return true;
}

$comparator = new ArrayComparator();
$comparator->setFactory(new Factory());
try {
$comparator->assertEquals($this->expected, $jsonResponseArray->toArray());
} catch (ComparisonFailure $failure) {
throw new ExpectationFailedException(
"Response JSON does not contain the provided JSON\n",
$failure
);
}

return false;
}

/**
* Returns a string representation of the constraint.
*/
public function toString(): string
{
//unused
return '';
}

protected function failureDescription($other): string
{
//unused
return '';
}
}
70 changes: 70 additions & 0 deletions src/Codeception/Constraint/JsonType.php
@@ -0,0 +1,70 @@
<?php

declare(strict_types=1);

namespace Codeception\PHPUnit\Constraint;

use Codeception\Util\JsonArray;
use Codeception\Util\JsonType as JsonTypeUtil;
use PHPUnit\Framework\Constraint\Constraint;
use PHPUnit\Framework\ExpectationFailedException;

use function json_encode;

class JsonType extends Constraint
{
/**
* @var array
*/
protected $jsonType;
/**
* @var bool
*/
private $match;

public function __construct(array $jsonType, bool $match = true)
{
$this->jsonType = $jsonType;
$this->match = $match;
}

/**
* Evaluates the constraint for parameter $other. Returns true if the
* constraint is met, false otherwise.
*
* @param mixed $jsonArray Value or object to evaluate.
*/
protected function matches($jsonArray): bool
{
if ($jsonArray instanceof JsonArray) {
$jsonArray = $jsonArray->toArray();
}

$matched = (new JsonTypeUtil($jsonArray))->matches($this->jsonType);

if ($this->match) {
if ($matched !== true) {
throw new ExpectationFailedException($matched);
}
} elseif ($matched === true) {
$jsonArray = json_encode($jsonArray, JSON_THROW_ON_ERROR);
throw new ExpectationFailedException('Unexpectedly response matched: ' . $jsonArray);
}
return true;
}

/**
* Returns a string representation of the constraint.
*/
public function toString(): string
{
//unused
return '';
}

protected function failureDescription($other): string
{
//unused
return '';
}
}
138 changes: 138 additions & 0 deletions src/Codeception/Util/ArrayContainsComparator.php
@@ -0,0 +1,138 @@
<?php

declare(strict_types=1);

namespace Codeception\Util;

use function array_intersect;
use function array_keys;
use function count;
use function is_array;
use function is_numeric;
use function min;
use function range;

class ArrayContainsComparator
{
protected array $haystack;

public function __construct(array $haystack)
{
$this->haystack = $haystack;
}

public function getHaystack(): array
{
return $this->haystack;
}

public function containsArray(array $needle): bool
{
return $needle == $this->arrayIntersectRecursive($needle, $this->haystack);
}

/**
* @return array|bool
* @author tiger.seo@gmail.com
* @link https://www.php.net/manual/en/function.array-intersect-assoc.php#39822
*
* @author nleippe@integr8ted.com
*/
private function arrayIntersectRecursive(mixed $arr1, mixed $arr2): bool|array|null
{
if (!is_array($arr1) || !is_array($arr2)) {
return false;
}
// if it is not an associative array we do not compare keys
if ($this->arrayIsSequential($arr1) && $this->arrayIsSequential($arr2)) {
return $this->sequentialArrayIntersect($arr1, $arr2);
}
return $this->associativeArrayIntersect($arr1, $arr2);
}

/**
* This array has sequential keys?
*/
private function arrayIsSequential(array $array): bool
{
return array_keys($array) === range(0, count($array) - 1);
}

private function sequentialArrayIntersect(array $arr1, array $arr2): array
{
$ret = [];

// Do not match the same item of $arr2 against multiple items of $arr1
$matchedKeys = [];
foreach ($arr1 as $key1 => $value1) {
foreach ($arr2 as $key2 => $value2) {
if (isset($matchedKeys[$key2])) {
continue;
}

$return = $this->arrayIntersectRecursive($value1, $value2);
if ($return !== false && $return == $value1) {
$ret[$key1] = $return;
$matchedKeys[$key2] = true;
break;
}

if ($this->isEqualValue($value1, $value2)) {
$ret[$key1] = $value1;
$matchedKeys[$key2] = true;
break;
}
}
}

return $ret;
}

/**
* @return array|bool|null
*/
private function associativeArrayIntersect(array $arr1, array $arr2): bool|array|null
{
$commonKeys = array_intersect(array_keys($arr1), array_keys($arr2));

$ret = [];
foreach ($commonKeys as $key) {
$return = $this->arrayIntersectRecursive($arr1[$key], $arr2[$key]);
if ($return !== false) {
$ret[$key] = $return;
continue;
}
if ($this->isEqualValue($arr1[$key], $arr2[$key])) {
$ret[$key] = $arr1[$key];
}
}

if (empty($commonKeys)) {
foreach ($arr2 as $arr) {
$return = $this->arrayIntersectRecursive($arr1, $arr);
if ($return && $return == $arr1) {
return $return;
}
}
}

if (count($ret) < min(count($arr1), count($arr2))) {
return null;
}

return $ret;
}

private function isEqualValue($val1, $val2): bool
{
if (is_numeric($val1)) {
$val1 = (string)$val1;
}

if (is_numeric($val2)) {
$val2 = (string)$val2;
}

return $val1 === $val2;
}
}

0 comments on commit c7f9210

Please sign in to comment.