Skip to content

Commit

Permalink
Issue #36 (#37)
Browse files Browse the repository at this point in the history
* work in progress: support json comparison

* Json object comparison and tests

* Fixes to make scrutinizer happy

* Fixed tests

* Added documentation. Lowered method complexity
  • Loading branch information
mcustiel committed Jun 10, 2018
1 parent 819a7b5 commit a16e0e1
Show file tree
Hide file tree
Showing 16 changed files with 451 additions and 78 deletions.
48 changes: 47 additions & 1 deletion README.md
Expand Up @@ -404,13 +404,16 @@ To reset all scenarios to the initial state (Scenario.START) use this simple met

$phiremock->resetScenarios();
```

#### API call:

```
DELETE /__phiremock/scenarios HTTP/1.1
Host: your.phiremock.host
```

To define a scenario state in any moment:

```php
use Mcustiel\Phiremock\Client\Phiremock;
use Mcustiel\Phiremock\Domain\ScenarioState;
Expand All @@ -419,8 +422,10 @@ To define a scenario state in any moment:

$phiremock->setScenarioState(new ScenarioState('saved', 'Scenario.START')));
```

#### API call:
```

```php
PUT /__phiremock/scenarios HTTP/1.1
Host: your.phiremock.host

Expand Down Expand Up @@ -468,6 +473,44 @@ It could be the case a mock is not needed for certain call. For this specific ca
```
In this case, Phiremock will POST `http://your.real.service/some/path/script.php` with the configured body and header and return it's response.

### Compare JSON objects
Phiremock supports comparing strict equality of json objects, in case it's used in the API.
The comparison is object-wise, so it does not matter that indentation or spacing is different.

#### Example:

```php
use Mcustiel\Phiremock\Client\Phiremock;

$phiremock = new Phiremock('phiremock.server', '8080');

$expectation = Phiremock::on(
A::posttRequest()->andUrl(Is::equalTo('/my/resource'))
->andBody(Is::sameJsonObjectAs('{"some": "json", "here":[1, 2, 3]}'))
->andHeader('Content-Type', Is::equalTo('application/json'))
)->then(
Respond::withStatusCode(201)->andBody('{"id": 1}')
);
$phiremock->createExpectation($expectation);
```

Also passing of arrays or \JsonSerializable objects is supported.

```php
use Mcustiel\Phiremock\Client\Phiremock;

$phiremock = new Phiremock('phiremock.server', '8080');

$expectation = Phiremock::on(
A::posttRequest()->andUrl(Is::equalTo('/my/resource'))
->andBody(Is::sameJsonObjectAs(['some' => 'json', 'here' => [1, 2, 3]]))
->andHeader('Content-Type', Is::equalTo('application/json'))
)->then(
Respond::withStatusCode(201)->andBody('{"id": 1}')
);
$phiremock->createExpectation($expectation);
```

### Generate response body based in request data
It could happen that you want to make your response dependent on data you receive in your request. For this cases
you can use regexp matching for request url and/or body, and access the subpatterns matches from your response body specification
Expand All @@ -489,7 +532,9 @@ using `${body.matchIndex}` or `${url.matchIndex}` notation.
);
$phiremock->createExpectation($expectation);
```

Also retrieving data from multiple matches is supported:

```php
use Mcustiel\Phiremock\Client\Phiremock;

Expand All @@ -511,6 +556,7 @@ Also retrieving data from multiple matches is supported:
### Shorthand syntax for common requests
Phiremock is a bit too much expressive to create requests and that is a bit annoying when writing simple stubs. For that,
there is a simpler syntax using `Phiremock::onRequest` method.

#### Example:

```php
Expand Down
2 changes: 1 addition & 1 deletion bin/phiremock-cache-clear.php
Expand Up @@ -17,7 +17,7 @@
*/
use Mcustiel\Phiremock\Server\Config\Dependencies;

if (PHP_SAPI !== 'cli') {
if (\PHP_SAPI !== 'cli') {
throw new \Exception('This is a standalone CLI application');
}

Expand Down
4 changes: 2 additions & 2 deletions bin/phiremock.php
Expand Up @@ -17,7 +17,7 @@
*/
declare(ticks=1);

if (PHP_SAPI !== 'cli') {
if (\PHP_SAPI !== 'cli') {
throw new \Exception('This is a standalone CLI application');
}

Expand Down Expand Up @@ -51,7 +51,7 @@
: (isset($options['e']) ? $options['e'] : null);
$expectationsDir = $expectationsDirParam
? (new FileSystem())->getRealPath($expectationsDirParam)
: $di->get('homePathService')->getHomePath() . DIRECTORY_SEPARATOR . '.phiremock/expectations';
: $di->get('homePathService')->getHomePath() . \DIRECTORY_SEPARATOR . '.phiremock/expectations';

$logger->debug("Phiremock's expectation dir: $expectationsDir");

Expand Down
14 changes: 14 additions & 0 deletions src/Client/Utils/Is.php
Expand Up @@ -61,4 +61,18 @@ public static function containing($value)
{
return new Condition('contains', $value);
}

/**
* @param string|array|\JsonSerializable $value
*
* @return \Mcustiel\Phiremock\Domain\Condition
*/
public static function sameJsonObjectAs($value)
{
if (is_string($value)) {
return new Condition('isSameJsonObject', $value);
}

return new Condition('isSameJsonObject', json_encode($value));
}
}
6 changes: 3 additions & 3 deletions src/Common/Utils/FileSystem.php
Expand Up @@ -37,9 +37,9 @@ public function getRealPath($path)
}

return str_replace(
DIRECTORY_SEPARATOR,
\DIRECTORY_SEPARATOR,
'/',
$existentPath . '/' . implode(DIRECTORY_SEPARATOR, $tail)
$existentPath . '/' . implode(\DIRECTORY_SEPARATOR, $tail)
);
}

Expand All @@ -50,7 +50,7 @@ public function getRealPath($path)
*/
private function normalizePath($path)
{
$path = str_replace(DIRECTORY_SEPARATOR, '/', $path);
$path = str_replace(\DIRECTORY_SEPARATOR, '/', $path);
if ('/' !== $path[0]) {
$path = getcwd() . '/' . $path;
}
Expand Down
19 changes: 19 additions & 0 deletions src/Server/Config/Matchers.php
Expand Up @@ -24,4 +24,23 @@ class Matchers
const EQUAL_TO = 'isEqualTo';
const SAME_STRING = 'isSameString';
const CONTAINS = 'contains';
const SAME_JSON = 'isSameJsonObject';

const VALID_MATCHERS = [
self::CONTAINS,
self::EQUAL_TO,
self::MATCHES,
self::SAME_JSON,
self::SAME_STRING,
];

/**
* @param string $matcherName
*
* @return bool
*/
public static function isValidMatcher($matcherName)
{
return in_array($matcherName, self::VALID_MATCHERS, true);
}
}
4 changes: 3 additions & 1 deletion src/Server/Config/dependencies-setup.php
Expand Up @@ -21,6 +21,7 @@
use Mcustiel\Phiremock\Server\Config\RouterConfig;
use Mcustiel\Phiremock\Server\Http\Implementation\ReactPhpServer;
use Mcustiel\Phiremock\Server\Http\InputSources\UrlFromPath;
use Mcustiel\Phiremock\Server\Http\Matchers\JsonObjectsEquals;
use Mcustiel\Phiremock\Server\Model\Implementation\ExpectationAutoStorage;
use Mcustiel\Phiremock\Server\Model\Implementation\RequestAutoStorage;
use Mcustiel\Phiremock\Server\Model\Implementation\ScenarioAutoStorage;
Expand Down Expand Up @@ -161,12 +162,13 @@
);
});

$di->register('matcherFactory', function () {
$di->register('matcherFactory', function () use ($di) {
return new MatcherFactory([
Matchers::EQUAL_TO => new SingletonLazyCreator(Equals::class),
Matchers::MATCHES => new SingletonLazyCreator(RegExpMatcher::class),
Matchers::SAME_STRING => new SingletonLazyCreator(CaseInsensitiveEquals::class),
Matchers::CONTAINS => new SingletonLazyCreator(ContainsMatcher::class),
Matchers::SAME_JSON => new SingletonLazyCreator(JsonObjectsEquals::class, [$di->get('logger')]),
]);
});

Expand Down
137 changes: 137 additions & 0 deletions src/Server/Http/Matchers/JsonObjectsEquals.php
@@ -0,0 +1,137 @@
<?php
/**
* This file is part of Phiremock.
*
* Phiremock is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Phiremock is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Phiremock. If not, see <http://www.gnu.org/licenses/>.
*/

namespace Mcustiel\Phiremock\Server\Http\Matchers;

use Mcustiel\PowerRoute\Matchers\MatcherInterface;
use Psr\Log\LoggerInterface;

class JsonObjectsEquals implements MatcherInterface
{
/**
* @var LoggerInterface
*/
private $logger;

public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}

/**
* {@inheritdoc}
*
* @see \Mcustiel\PowerRoute\Matchers\MatcherInterface::match()
*/
public function match($value, $argument = null)
{
if (is_string($value)) {
$requestValue = $this->getParsedValue($value);
} else {
$requestValue = $value;
}
$configValue = $this->decodeJson($argument);

if (!is_array($requestValue) || !is_array($configValue)) {
return false;
}

return $this->areRecursivelyEquals($requestValue, $configValue);
}

/**
* @param string $value
*
* @return mixed
*/
private function decodeJson($value)
{
$decodedValue = json_decode($value, true);
if (JSON_ERROR_NONE !== json_last_error()) {
throw new \InvalidArgumentException('JSON parsing error: ' . json_last_error_msg());
}

return $decodedValue;
}

private function areRecursivelyEquals(array $array1, array $array2)
{
foreach ($array1 as $key => $value1) {
if (!array_key_exists($key, $array2)) {
return false;
}
if (!$this->haveTheSameTypeAndValue($value1, $array2[$key])) {
return false;
}
}

return true;
}

/**
* @param mixed $value1
* @param mixed $value2
*
* @return bool
*/
private function haveTheSameTypeAndValue($value1, $value2)
{
if (gettype($value1) !== gettype($value2)) {
return false;
}

return $this->haveTheSameValue($value1, $value2);
}

/**
* @param mixed $value1
* @param mixed $value2
*
* @return bool
*/
private function haveTheSameValue($value1, $value2)
{
if (is_array($value1)) {
if (!$this->areRecursivelyEquals($value1, $value2)) {
return false;
}
} else {
if ($value1 !== $value2) {
return false;
}
}
return true;
}

/**
* @param string $value
*
* @return mixed
*/
private function getParsedValue($value)
{
try {
$requestValue = $this->decodeJson($value);
} catch (\InvalidArgumentException $e) {
$requestValue = $value;
$this->logger->warning('Invalid json received in request: ' . $value);
}

return $requestValue;
}
}

0 comments on commit a16e0e1

Please sign in to comment.