Compare commits

...

34 commits

Author SHA1 Message Date
a9a9c7a2b8
update deps 2023-10-03 12:45:02 +03:00
1de2133b12 fix phpstan 2023-10-03 12:03:27 +03:00
a5f7f280f2 update deps 2023-10-03 11:25:47 +03:00
eef38bf466
fix build status in readme 2023-02-07 14:02:12 +03:00
bbfe55bc94 fix for internal array comparison logic 2022-04-14 15:43:40 +03:00
f455ba52d5 fix form-data matcher logic 2022-04-14 13:59:59 +03:00
5a92b28b5f phpstan baseline & better tests for the port matcher 2022-04-04 16:24:30 +03:00
f38395734a port matcher & php 8.1 support 2022-04-04 16:16:24 +03:00
aec5a4c4a0 fix goal in readme 2021-09-23 20:07:27 +03:00
dcc642a0ea real networking for mocked requests 2021-09-23 20:06:42 +03:00
2170930dc5
Update README.md 2021-09-23 19:58:51 +03:00
f7832865a1 rename tests and namespace. no version bump since the change only applies to the tests 2021-09-23 19:54:54 +03:00
d892514e21 rename serializer decorators to serializer adapters 2021-09-23 19:53:08 +03:00
cea2f0a19e
Update README.md 2021-09-23 19:51:00 +03:00
d5b1044efa Merge branch 'master' of github.com:Neur0toxine/pock 2021-09-23 19:41:15 +03:00
dd441f54db regexp, form-data and multipart form request matchers 2021-09-23 19:41:02 +03:00
b7f47bb6b9
fix for the mistake in the readme 2021-09-23 16:14:24 +03:00
064b07940c
more details in the new roadmap entries 2021-09-23 16:13:30 +03:00
f09304f129
add two more matchers to the readme 2021-09-23 16:11:27 +03:00
66e6e3ab2a matcher for preserialized JSON 2021-09-23 12:07:15 +03:00
285d06a01d
Add goal for real networking to the readme 2021-09-16 15:36:17 +03:00
2a124cfdd8 RecursiveLtrArrayComparator improvement and test 2021-05-31 13:25:30 +03:00
b4142304ab matchOrigin matcher 2021-05-25 10:49:29 +03:00
321650388e sort README checks 2021-05-21 20:39:37 +03:00
e7a17599ad ugly hack for tests 2021-05-21 20:31:07 +03:00
b99fb4f255 fix for tests & json decoder trait 2021-05-21 19:54:17 +03:00
e352c310d7 better xml matcher, refactor comparators 2021-05-21 19:22:59 +03:00
b08ba6f6cd add new goal to the readme 2021-05-21 09:52:52 +03:00
60003ef9cc replyWithFactory and replyWithCallback methods 2021-05-21 09:38:15 +03:00
26d707000c Merge branch 'master' of github.com:Neur0toxine/pock 2021-05-20 20:57:57 +03:00
21021e5d70 at(N), always() methods, separate PSR-18 exceptions, throw methods for them, roadmap to stable 2021-05-20 20:56:45 +03:00
03fc4e4c2a add roadmap to stable 2021-05-20 19:30:31 +03:00
b0f0c48815
add direct link to PockBuilder and PockResponseBuilder to the readme 2021-05-15 21:46:21 +03:00
70eef2abfa
Update README.md 2021-05-15 21:42:34 +03:00
66 changed files with 3073 additions and 318 deletions

View file

@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
php-version: ['7.2', '7.3', '7.4', '8.0']
php-version: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2']
steps:
- name: Check out code into the workspace
uses: actions/checkout@v2

View file

@ -1,14 +1,17 @@
[![Build Status](https://github.com/Neur0toxine/pock/workflows/Tests/badge.svg)](https://github.com/Neur0toxine/pock/actions)
[![Coverage](https://img.shields.io/codecov/c/gh/Neur0toxine/pock/master.svg?logo=codecov&logoColor=white)](https://codecov.io/gh/Neur0toxine/pock)
[![Latest stable](https://img.shields.io/packagist/v/neur0toxine/pock.svg)](https://packagist.org/packages/neur0toxine/pock)
[![PHP from Packagist](https://img.shields.io/packagist/php-v/neur0toxine/pock.svg?logo=php&logoColor=white)](https://packagist.org/packages/neur0toxine/pock)
![Build Status](https://img.shields.io/github/actions/workflow/status/Neur0toxine/pock/tests.yml?branch=master&style=flat-square)
[![Coverage](https://img.shields.io/codecov/c/gh/Neur0toxine/pock/master.svg?logo=codecov&logoColor=white&style=flat-square)](https://codecov.io/gh/Neur0toxine/pock)
[![Latest stable](https://img.shields.io/packagist/v/neur0toxine/pock.svg?style=flat-square)](https://packagist.org/packages/neur0toxine/pock)
[![PHP from Packagist](https://img.shields.io/packagist/php-v/neur0toxine/pock.svg?logo=php&logoColor=white&style=flat-square)](https://packagist.org/packages/neur0toxine/pock)
![License](https://img.shields.io/github/license/Neur0toxine/pock?style=flat-square)
# pock
Easy to use HTTP mocking solution, compatible with PSR-18 and HTTPlug.
Project is still in its early development stage. API can change over time, but I'll try to not introduce breaking changes.
You can find autogenerated documentation [here](https://neur0toxine.github.io/pock/) or look at the examples.
You can find autogenerated documentation [here](https://neur0toxine.github.io/pock/) or look at the examples. API for the mock building can be found
[here](https://neur0toxine.github.io/pock/classes/Pock-PockBuilder.html) and API for the response building (returned from `PockBuilder::reply` call)
can be found [here](https://neur0toxine.github.io/pock/classes/Pock-PockResponseBuilder.html).
# Examples
@ -94,7 +97,7 @@ if you want to override default behavior.
```php
use Pock\Factory\JsonSerializerFactory;
use Pock\Factory\XmlSerializerFactory;
use Pock\Serializer\SymfonySerializerDecorator;
use Pock\Serializer\SymfonySerializerAdapter;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
@ -102,10 +105,27 @@ use Symfony\Component\Serializer\Serializer;
$encoders = [new XmlEncoder(), new JsonEncoder()];
$normalizers = [new ObjectNormalizer()];
$serializer = new SymfonySerializerDecorator(new Serializer($normalizers, $encoders));
$serializer = new SymfonySerializerAdapter(new Serializer($normalizers, $encoders));
JsonSerializerFactory::setSerializer($serializer);
XmlSerializerFactory::setSerializer($serializer);
```
In order to use unsupported serializer you should create a decorator which implements `Pock\Serializer\SerializerInterface`.
In order to use unsupported serializer you should create an adapter which implements `Pock\Serializer\SerializerInterface`.
# Roadmap to stable
- [x] `at(N)` - execute mock only at Nth call.
- [x] `always()` - always execute this mock (removes mock expiration).
- [x] Separate `UniversalMockException` into several exceptions (`PockClientException`, `PockNetworkException`, etc).
- [x] Add methods for easier throwing of exceptions listed in previous entry.
- [x] `replyWithCallback` - reply using specified callback.
- [x] `replyWithFactory` - reply using specified response factory (provide corresponding interface).
- [x] Compare XML bodies using `DOMDocument`, fallback to text comparison in case of problems.
- [x] Regexp matchers for body, query, URI and path.
- [x] Form Data body matcher (partial & exact)
- [x] Multipart form body matcher (just like callback matcher but parses the body as a multipart form data)
- [x] **BREAKING CHANGE:** Rename serializer decorators to serializer adapters.
- [x] Real network response for mocked requests.
- [ ] `symfony/http-client` support.
- [ ] Document everything (with examples if its feasible).

View file

@ -34,20 +34,23 @@
"php": ">=7.2.0",
"ext-json": "*",
"psr/http-client": "^1.0",
"psr/http-message": "^1.0",
"psr/http-message": "^1.0 || ^2.0",
"php-http/httplug": "^1.0 || ^2.0",
"nyholm/psr7": "^1.4"
"nyholm/psr7": "^1.4",
"riverline/multipart-parser": "^2.0"
},
"require-dev": {
"squizlabs/php_codesniffer": "^3.6",
"phpmd/phpmd": "^2.10",
"phpmd/phpmd": "^2.12",
"dealerdirect/phpcodesniffer-composer-installer": "^0.7.1",
"phpcompatibility/php-compatibility": "^9.3",
"phpstan/phpstan": "^0.12.87",
"jms/serializer": "^2 | ^3.12",
"phpstan/phpstan": "^1.5",
"jms/serializer": "^2 | ^3.17",
"symfony/phpunit-bridge": "^5.2",
"symfony/serializer": "^5.2",
"symfony/property-access": "^5.2"
"symfony/property-access": "^5.2",
"php-http/multipart-stream-builder": "^1.2",
"symfony/http-client": "^5.3"
},
"provide": {
"psr/http-client-implementation": "1.0",
@ -70,5 +73,11 @@
"@lint",
"@phpunit"
]
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true,
"php-http/discovery": true
}
}
}

View file

@ -10,7 +10,15 @@
<rule ref="rulesets/design.xml" />
<rule ref="rulesets/cleancode.xml" />
<rule ref="rulesets/codesize.xml" />
<rule ref="rulesets/naming.xml" />
<rule ref="rulesets/naming.xml">
<exclude name="ShortMethodName" />
</rule>
<rule ref="rulesets/naming.xml/ShortMethodName">
<properties>
<property name="minimum" value="2" />
</properties>
</rule>
<exclude-pattern>tests/*</exclude-pattern>
</ruleset>

111
phpstan-baseline.neon Normal file
View file

@ -0,0 +1,111 @@
parameters:
ignoreErrors:
-
message: "#^Method Pock\\\\Client\\:\\:doSendRequest\\(\\) should return Psr\\\\Http\\\\Message\\\\ResponseInterface but returns mixed\\.$#"
count: 1
path: src/Client.php
-
message: "#^Static property Pock\\\\Comparator\\\\ComparatorLocator\\:\\:\\$comparators \\(array\\<Pock\\\\Comparator\\\\ComparatorInterface\\>\\) does not accept array\\<object\\>\\.$#"
count: 1
path: src/Comparator/ComparatorLocator.php
-
message: "#^Unsafe access to private property Pock\\\\Comparator\\\\ComparatorLocator\\:\\:\\$comparators through static\\:\\:\\.$#"
count: 3
path: src/Comparator/ComparatorLocator.php
-
message: "#^Unsafe access to private property Pock\\\\Factory\\\\JsonSerializerFactory\\:\\:\\$mainSerializer through static\\:\\:\\.$#"
count: 2
path: src/Factory/JsonSerializerFactory.php
-
message: "#^Unsafe access to private property Pock\\\\Factory\\\\XmlSerializerFactory\\:\\:\\$mainSerializer through static\\:\\:\\.$#"
count: 2
path: src/Factory/XmlSerializerFactory.php
-
message: "#^Parameter \\#4 \\$flags of function preg_match expects TFlags of 0\\|256\\|512\\|768, int given\\.$#"
count: 1
path: src/Matchers/AbstractRegExpMatcher.php
-
message: "#^Unsafe call to private method Pock\\\\Matchers\\\\ExactHeadersMatcher\\:\\:headerValuesEqual\\(\\) through static\\:\\:\\.$#"
count: 2
path: src/Matchers/ExactHeadersMatcher.php
-
message: "#^Method Pock\\\\Matchers\\\\JsonBodyMatcher\\:\\:deserialize\\(\\) should return array\\|null but returns mixed\\.$#"
count: 1
path: src/Matchers/JsonBodyMatcher.php
-
message: "#^Parameter \\#3 \\$depth of function json_decode expects int\\<1, max\\>, int given\\.$#"
count: 1
path: src/Matchers/JsonBodyMatcher.php
-
message: "#^Method Pock\\\\Matchers\\\\QueryMatcher\\:\\:parseQuery\\(\\) should return array\\<string, mixed\\> but returns array\\<int\\|string, array\\|string\\>\\.$#"
count: 1
path: src/Matchers/QueryMatcher.php
-
message: "#^Method Pock\\\\Matchers\\\\XmlBodyMatcher\\:\\:sortXmlTags\\(\\) should return string but returns string\\|null\\.$#"
count: 1
path: src/Matchers/XmlBodyMatcher.php
-
message: "#^Unsafe access to private constant Pock\\\\Matchers\\\\XmlBodyMatcher\\:\\:TAG_SORT_XSLT through static\\:\\:\\.$#"
count: 1
path: src/Matchers/XmlBodyMatcher.php
-
message: "#^Unsafe access to private property Pock\\\\Matchers\\\\XmlBodyMatcher\\:\\:\\$sorter through static\\:\\:\\.$#"
count: 4
path: src/Matchers/XmlBodyMatcher.php
-
message: "#^Unsafe call to private method Pock\\\\Matchers\\\\XmlBodyMatcher\\:\\:createDOMDocument\\(\\) through static\\:\\:\\.$#"
count: 2
path: src/Matchers/XmlBodyMatcher.php
-
message: "#^Unsafe call to private method Pock\\\\Matchers\\\\XmlBodyMatcher\\:\\:getDOMString\\(\\) through static\\:\\:\\.$#"
count: 1
path: src/Matchers/XmlBodyMatcher.php
-
message: "#^Unsafe call to private method Pock\\\\Matchers\\\\XmlBodyMatcher\\:\\:getSorter\\(\\) through static\\:\\:\\.$#"
count: 1
path: src/Matchers/XmlBodyMatcher.php
-
message: "#^Unsafe call to private method Pock\\\\Matchers\\\\XmlBodyMatcher\\:\\:hasExtension\\(\\) through static\\:\\:\\.$#"
count: 4
path: src/Matchers/XmlBodyMatcher.php
-
message: "#^Unsafe call to private method Pock\\\\Matchers\\\\XmlBodyMatcher\\:\\:sortXmlTags\\(\\) through static\\:\\:\\.$#"
count: 2
path: src/Matchers/XmlBodyMatcher.php
-
message: "#^Parameter \\#1 \\$data of class Pock\\\\Matchers\\\\JsonBodyMatcher constructor expects array, mixed given\\.$#"
count: 2
path: src/PockBuilder.php
-
message: "#^Parameter \\#3 \\$depth of function json_decode expects int\\<1, max\\>, int given\\.$#"
count: 1
path: src/PockBuilder.php
-
message: "#^Parameter \\#3 \\$depth of function json_encode expects int\\<1, max\\>, int given\\.$#"
count: 1
path: src/PockBuilder.php
-
message: "#^Parameter \\#3 \\$depth of function json_encode expects int\\<1, max\\>, int given\\.$#"
count: 1
path: src/PockResponseBuilder.php

View file

@ -1,3 +1,6 @@
includes:
- phpstan-baseline.neon # TODO: Configure new phpstan
parameters:
level: max
paths:

View file

@ -74,17 +74,31 @@ class Client implements ClientInterface, HttpClient, HttpAsyncClient
continue;
}
if ($mock->getMatcher()->matches($request)) {
if ($mock->matches($request)) {
if (null !== $mock->getResponse()) {
$mock->registerHit();
return new HttpFulfilledPromise($mock->getResponse());
}
if (null !== $mock->getThrowable()) {
if (null !== $mock->getReplyFactory()) {
$mock->registerHit();
return new HttpRejectedPromise($mock->getThrowable());
try {
return new HttpFulfilledPromise(
$mock->getReplyFactory()->createReply($request, new PockResponseBuilder())
);
} catch (Throwable $throwable) {
return new HttpRejectedPromise($throwable);
}
}
$throwable = $mock->getThrowable($request);
if (null !== $throwable) {
$mock->registerHit();
return new HttpRejectedPromise($throwable);
}
throw new IncompleteMockException($mock);
@ -92,13 +106,23 @@ class Client implements ClientInterface, HttpClient, HttpAsyncClient
}
if (null !== $this->fallbackClient) {
try {
return new HttpFulfilledPromise($this->fallbackClient->sendRequest($request));
} catch (Throwable $throwable) {
return new HttpRejectedPromise($throwable);
}
return $this->replyWithFallbackClient($request);
}
throw new UnsupportedRequestException();
}
/**
* @param \Psr\Http\Message\RequestInterface $request
*
* @return \Http\Promise\Promise
*/
protected function replyWithFallbackClient(RequestInterface $request): Promise
{
try {
return new HttpFulfilledPromise($this->fallbackClient->sendRequest($request)); // @phpstan-ignore-line
} catch (Throwable $throwable) {
return new HttpRejectedPromise($throwable);
}
}
}

View file

@ -0,0 +1,29 @@
<?php
/**
* PHP 7.3
*
* @category ComparatorInterface
* @package Pock\Comparator
*/
namespace Pock\Comparator;
/**
* Interface ComparatorInterface
*
* @category ComparatorInterface
* @package Pock\Comparator
*/
interface ComparatorInterface
{
/**
* Compare two values.
*
* @param mixed $first
* @param mixed $second
*
* @return bool
*/
public function compare($first, $second): bool;
}

View file

@ -0,0 +1,44 @@
<?php
/**
* PHP 7.3
*
* @category ComparatorLocator
* @package Pock\Comparator
*/
namespace Pock\Comparator;
use RuntimeException;
/**
* Class ComparatorLocator
*
* @category ComparatorLocator
* @package Pock\Comparator
*/
class ComparatorLocator
{
/** @var \Pock\Comparator\ComparatorInterface[] */
private static $comparators = [];
/**
* Returns comparator.
*
* @param string $fqn
*
* @return \Pock\Comparator\ComparatorInterface
*/
public static function get(string $fqn): ComparatorInterface
{
if (!class_exists($fqn)) {
throw new RuntimeException('Comparator ' . $fqn . ' does not exist.');
}
if (!array_key_exists($fqn, static::$comparators)) {
static::$comparators[$fqn] = new $fqn();
}
return static::$comparators[$fqn];
}
}

View file

@ -0,0 +1,53 @@
<?php
/**
* PHP 7.3
*
* @category LtrScalarArrayComparator
* @package Pock\Comparator
*/
namespace Pock\Comparator;
/**
* Class LtrScalarArrayComparator
*
* @category LtrScalarArrayComparator
* @package Pock\Comparator
*/
class LtrScalarArrayComparator implements ComparatorInterface
{
/**
* @inheritDoc
*/
public function compare($first, $second): bool
{
if (!is_array($first) || !is_array($second)) {
return false;
}
return static::isNeedlePresentInHaystack($first, $second);
}
/**
* Returns true if all needle values is present in haystack.
* Doesn't work for multidimensional arrays.
*
* @phpstan-ignore-next-line
* @param array $needle
* @phpstan-ignore-next-line
* @param array $haystack
*
* @return bool
*/
protected static function isNeedlePresentInHaystack(array $needle, array $haystack): bool
{
foreach ($needle as $value) {
if (!in_array($value, $haystack, true)) {
return false;
}
}
return true;
}
}

View file

@ -0,0 +1,68 @@
<?php
/**
* PHP 7.3
*
* @category RecursiveArrayComparator
* @package Pock\Comparator
*/
namespace Pock\Comparator;
/**
* Class RecursiveArrayComparator
*
* @category RecursiveArrayComparator
* @package Pock\Comparator
*/
class RecursiveArrayComparator implements ComparatorInterface
{
/**
* @inheritDoc
*/
public function compare($first, $second): bool
{
if (!is_array($first) || !is_array($second)) {
return false;
}
return static::recursiveCompareArrays($first, $second);
}
/**
* Returns true if both arrays are equal recursively.
*
* @phpstan-ignore-next-line
* @param array $first
* @phpstan-ignore-next-line
* @param array $second
*
* @return bool
*/
protected static function recursiveCompareArrays(array $first, array $second): bool
{
if (count($first) !== count($second)) {
return false;
}
if (!empty(array_diff(array_keys($first), array_keys($second)))) {
return false;
}
foreach ($first as $key => $value) {
if (is_array($value)) {
if (!is_array($second[$key]) || !self::recursiveCompareArrays($value, $second[$key])) {
return false;
}
continue;
}
if ($value !== $second[$key]) {
return false;
}
}
return true;
}
}

View file

@ -0,0 +1,65 @@
<?php
/**
* PHP 7.3
*
* @category RecursiveLtrArrayComparator
* @package Pock\Comparator
*/
namespace Pock\Comparator;
/**
* Class RecursiveLtrArrayComparator
*
* @category RecursiveLtrArrayComparator
* @package Pock\Comparator
*/
class RecursiveLtrArrayComparator extends RecursiveArrayComparator
{
/**
* @inheritDoc
*/
public function compare($first, $second): bool
{
if (!is_array($first) || !is_array($second)) {
return false;
}
return static::recursiveNeedlePresentInHaystack($first, $second);
}
/**
* Returns true if all needle values is present in haystack.
* Works for multidimensional arrays. Internal arrays will be treated as values (e.g. will be compared recursively).
*
* @phpstan-ignore-next-line
* @param array $needle
* @phpstan-ignore-next-line
* @param array $haystack
*
* @return bool
*/
protected static function recursiveNeedlePresentInHaystack(array $needle, array $haystack): bool
{
if (!empty(array_diff(array_keys($needle), array_keys($haystack)))) {
return false;
}
foreach ($needle as $key => $value) {
if (is_array($value)) {
if (!is_array($haystack[$key]) || !self::recursiveCompareArrays($value, $haystack[$key])) {
return false;
}
continue;
}
if ($value !== $haystack[$key]) {
return false;
}
}
return true;
}
}

View file

@ -0,0 +1,47 @@
<?php
/**
* PHP 7.3
*
* @category ScalarFlatArrayComparator
* @package Pock\Comparator
*/
namespace Pock\Comparator;
/**
* Class ScalarFlatArrayComparator
*
* @category ScalarFlatArrayComparator
* @package Pock\Comparator
*/
class ScalarFlatArrayComparator implements ComparatorInterface
{
/**
* @inheritDoc
*/
public function compare($first, $second): bool
{
if (!is_array($first) || !is_array($second)) {
return false;
}
return static::compareScalarFlatArrays($first, $second);
}
/**
* Returns true if two one-dimensional string arrays are equal.
*
* @phpstan-ignore-next-line
* @param array $first
* @phpstan-ignore-next-line
* @param array $second
*
* @return bool
*/
protected static function compareScalarFlatArrays(array $first, array $second): bool
{
return count($first) === count($second) &&
array_diff($first, $second) === array_diff($second, $first);
}
}

View file

@ -10,7 +10,7 @@
namespace Pock\Creator;
use Throwable;
use Pock\Serializer\JmsSerializerDecorator;
use Pock\Serializer\JmsSerializerAdapter;
use Pock\Serializer\SerializerInterface;
/**
@ -30,13 +30,13 @@ abstract class AbstractJmsSerializerCreator implements SerializerCreatorInterfac
{
if (
class_exists(self::BUILDER_CLASS) &&
method_exists(self::BUILDER_CLASS, 'create')
method_exists(self::BUILDER_CLASS, 'create') // @phpstan-ignore-line
) {
try {
$builder = call_user_func([self::BUILDER_CLASS, 'create']);
$builder = call_user_func([self::BUILDER_CLASS, 'create']); // @phpstan-ignore-line
if (null !== $builder && method_exists($builder, 'build')) {
return new JmsSerializerDecorator($builder->build(), static::getFormat()); // @phpstan-ignore-line
if (null !== $builder && method_exists($builder, 'build')) { // @phpstan-ignore-line
return new JmsSerializerAdapter($builder->build(), static::getFormat()); // @phpstan-ignore-line
}
} catch (Throwable $throwable) {
return null;

View file

@ -10,7 +10,7 @@
namespace Pock\Creator;
use Pock\Serializer\SerializerInterface;
use Pock\Serializer\SymfonySerializerDecorator;
use Pock\Serializer\SymfonySerializerAdapter;
/**
* Class AbstractSymfonySerializerCreator
@ -33,8 +33,8 @@ abstract class AbstractSymfonySerializerCreator implements SerializerCreatorInte
$normalizer = self::OBJECT_NORMALIZER_CLASS;
$encoder = static::getEncoderClass();
return new SymfonySerializerDecorator(
new $serializer([new $normalizer()], [new $encoder()]),
return new SymfonySerializerAdapter(
new $serializer([new $normalizer()], [new $encoder()]), // @phpstan-ignore-line
static::getFormat()
);
}

View file

@ -0,0 +1,45 @@
<?php
/**
* PHP 7.3
*
* @category AbstractRequestAwareException
* @package Pock\Exception
*/
namespace Pock\Exception;
use Exception;
use Psr\Http\Message\RequestInterface;
/**
* Class AbstractRequestAwareException
*
* @category AbstractRequestAwareException
* @package Pock\Exception
*/
class AbstractRequestAwareException extends Exception
{
/** @var RequestInterface */
private $request;
/**
* @param \Psr\Http\Message\RequestInterface $request
*
* @return self
*/
public function setRequest(RequestInterface $request): self
{
$instance = new static($this->message, $this->code, $this->getPrevious()); // @phpstan-ignore-line
$instance->request = $request;
return $instance;
}
/**
* @return \Psr\Http\Message\RequestInterface
*/
public function getRequest(): RequestInterface
{
return $this->request;
}
}

View file

@ -0,0 +1,23 @@
<?php
/**
* PHP 7.3
*
* @category PockClientException
* @package Pock\Exception
*/
namespace Pock\Exception;
use Exception;
use Psr\Http\Client\ClientExceptionInterface;
/**
* Class PockClientException
*
* @category PockClientException
* @package Pock\Exception
*/
class PockClientException extends Exception implements ClientExceptionInterface
{
}

View file

@ -0,0 +1,22 @@
<?php
/**
* PHP 7.3
*
* @category PockNetworkException
* @package Pock\Exception
*/
namespace Pock\Exception;
use Psr\Http\Client\NetworkExceptionInterface;
/**
* Class PockNetworkException
*
* @category PockNetworkException
* @package Pock\Exception
*/
class PockNetworkException extends AbstractRequestAwareException implements NetworkExceptionInterface
{
}

View file

@ -0,0 +1,22 @@
<?php
/**
* PHP 7.3
*
* @category PockRequestException
* @package Pock\Exception
*/
namespace Pock\Exception;
use Psr\Http\Client\RequestExceptionInterface;
/**
* Class PockRequestException
*
* @category PockRequestException
* @package Pock\Exception
*/
class PockRequestException extends AbstractRequestAwareException implements RequestExceptionInterface
{
}

View file

@ -1,51 +0,0 @@
<?php
/**
* PHP 7.2
*
* @category UniversalMockException
* @package Pock\Exception
*/
namespace Pock\Exception;
use Exception;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Client\NetworkExceptionInterface;
use Psr\Http\Client\RequestExceptionInterface;
use Psr\Http\Message\RequestInterface;
/**
* Class UniversalMockException
*
* @category UniversalMockException
* @package Pock\Exception
*/
class UniversalMockException extends Exception implements
ClientExceptionInterface,
NetworkExceptionInterface,
RequestExceptionInterface
{
/** @var mixed */
private $request;
/**
* UniversalMockException constructor.
*
* @param mixed $request
*/
public function __construct($request)
{
parent::__construct('Default mock exception');
$this->request = $request;
}
/**
* @inheritDoc
*/
public function getRequest(): RequestInterface
{
return $this->request;
}
}

View file

@ -0,0 +1,44 @@
<?php
/**
* PHP 7.3
*
* @category CallbackReplyFactory
* @package Pock\Factory
*/
namespace Pock\Factory;
use Pock\PockResponseBuilder;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Class CallbackReplyFactory
*
* @category CallbackReplyFactory
* @package Pock\Factory
*/
class CallbackReplyFactory implements ReplyFactoryInterface
{
/** @var callable */
private $callback;
/**
* CallbackReplyFactory constructor.
*
* @param callable $callback
*/
public function __construct(callable $callback)
{
$this->callback = $callback;
}
/**
* @inheritDoc
*/
public function createReply(RequestInterface $request, PockResponseBuilder $responseBuilder): ResponseInterface
{
return call_user_func($this->callback, $request, $responseBuilder);
}
}

View file

@ -0,0 +1,38 @@
<?php
/**
* PHP 7.3
*
* @category ReplyFactoryInterface
* @package Pock\Factory
*/
namespace Pock\Factory;
use Pock\PockResponseBuilder;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Interface ReplyFactoryInterface
*
* @category ReplyFactoryInterface
* @package Pock\Factory
*/
interface ReplyFactoryInterface
{
/**
* Reply to the specified request.
*
* If this method throws any exception, it will be treated as with the `PockBuilder::throwException call`.
*
* @see \Pock\PockBuilder::throwException()
*
* @param \Psr\Http\Message\RequestInterface $request
* @param \Pock\PockResponseBuilder $responseBuilder
*
* @return \Psr\Http\Message\ResponseInterface
* @throws \Throwable
*/
public function createReply(RequestInterface $request, PockResponseBuilder $responseBuilder): ResponseInterface;
}

View file

@ -1,120 +0,0 @@
<?php
/**
* PHP 7.1
*
* @category AbstractArrayPoweredComponent
* @package Pock\Matchers
*/
namespace Pock\Matchers;
/**
* Class AbstractArrayPoweredComponent
*
* @category AbstractArrayPoweredComponent
* @package Pock\Matchers
*/
abstract class AbstractArrayPoweredComponent
{
/**
* Returns true if both arrays are equal recursively.
*
* @phpstan-ignore-next-line
* @param array $first
* @phpstan-ignore-next-line
* @param array $second
*
* @return bool
*/
protected static function recursiveCompareArrays(array $first, array $second): bool
{
if (count($first) !== count($second)) {
return false;
}
if (!empty(array_diff(array_keys($first), array_keys($second)))) {
return false;
}
foreach ($first as $key => $value) {
if (is_array($value) && !self::recursiveCompareArrays($value, $second[$key])) {
return false;
}
if ($value !== $second[$key]) {
return false;
}
}
return true;
}
/**
* Returns true if two one-dimensional string arrays are equal.
*
* @phpstan-ignore-next-line
* @param array $first
* @phpstan-ignore-next-line
* @param array $second
*
* @return bool
*/
protected static function compareStringArrays(array $first, array $second): bool
{
return count($first) === count($second) &&
array_diff($first, $second) === array_diff($second, $first);
}
/**
* Returns true if all needle values is present in haystack.
* Doesn't work for multidimensional arrays.
*
* @phpstan-ignore-next-line
* @param array $needle
* @phpstan-ignore-next-line
* @param array $haystack
*
* @return bool
*/
protected static function isNeedlePresentInHaystack(array $needle, array $haystack): bool
{
foreach ($needle as $value) {
if (!in_array($value, $haystack, true)) {
return false;
}
}
return true;
}
/**
* Returns true if all needle values is present in haystack.
* Works for multidimensional arrays. Internal arrays will be treated as values (e.g. will be compared recursively).
*
* @phpstan-ignore-next-line
* @param array $needle
* @phpstan-ignore-next-line
* @param array $haystack
*
* @return bool
*/
protected static function recursiveNeedlePresentInHaystack(array $needle, array $haystack): bool
{
if (!empty(array_diff(array_keys($needle), array_keys($haystack)))) {
return false;
}
foreach ($needle as $key => $value) {
if (is_array($value) && !self::recursiveCompareArrays($value, $haystack[$key])) {
return false;
}
if ($value !== $haystack[$key]) {
return false;
}
}
return true;
}
}

View file

@ -0,0 +1,41 @@
<?php
/**
* PHP version 7.3
*
* @category AbstractRegExpMatcher
* @package Pock\Matchers
*/
namespace Pock\Matchers;
/**
* Class AbstractRegExpMatcher
*
* @category AbstractRegExpMatcher
* @package Pock\Matchers
*/
abstract class AbstractRegExpMatcher implements RequestMatcherInterface
{
/** @var string */
protected $expression;
/** @var int */
protected $flags = 0;
/**
* @param string $expression
* @param int $flags
*/
public function __construct(string $expression, int $flags = 0)
{
$this->expression = $expression;
$this->flags = $flags;
}
protected function matchRegExp(string $content): bool
{
$matches = [];
return 1 === preg_match($this->expression, $content, $matches, $this->flags);
}
}

View file

@ -9,6 +9,8 @@
namespace Pock\Matchers;
use Pock\Comparator\ComparatorLocator;
use Pock\Comparator\RecursiveArrayComparator;
use Pock\Traits\SeekableStreamDataExtractor;
use Psr\Http\Message\RequestInterface;
@ -18,7 +20,7 @@ use Psr\Http\Message\RequestInterface;
* @category AbstractSerializedBodyMatcher
* @package Pock\Matchers
*/
abstract class AbstractSerializedBodyMatcher extends AbstractArrayPoweredComponent implements RequestMatcherInterface
abstract class AbstractSerializedBodyMatcher implements RequestMatcherInterface
{
use SeekableStreamDataExtractor;
@ -54,7 +56,7 @@ abstract class AbstractSerializedBodyMatcher extends AbstractArrayPoweredCompone
return false;
}
return self::recursiveCompareArrays($bodyData, $this->data);
return ComparatorLocator::get(RecursiveArrayComparator::class)->compare($bodyData, $this->data);
}
/**

View file

@ -33,17 +33,7 @@ class BodyMatcher implements RequestMatcherInterface
*/
public function __construct($contents)
{
if (is_string($contents)) {
$this->contents = $contents;
}
if ($contents instanceof StreamInterface) {
$this->contents = static::getStreamData($contents);
}
if (is_resource($contents)) {
$this->contents = static::readAllResource($contents);
}
$this->contents = static::getEntryItemData($contents);
}
/**
@ -70,4 +60,26 @@ class BodyMatcher implements RequestMatcherInterface
fseek($resource, 0);
return (string) stream_get_contents($resource);
}
/**
* @param StreamInterface|resource|string $contents
*
* @return string
*/
protected static function getEntryItemData($contents): string
{
if (is_string($contents)) {
return $contents;
}
if ($contents instanceof StreamInterface) {
return static::getStreamData($contents);
}
if (is_resource($contents)) {
return static::readAllResource($contents);
}
return '';
}
}

View file

@ -0,0 +1,40 @@
<?php
/**
* PHP version 7.3
*
* @category ExactFormDataMatcher
* @package Pock\Matchers
*/
namespace Pock\Matchers;
use Pock\Comparator\ComparatorLocator;
use Pock\Comparator\RecursiveArrayComparator;
use Pock\Traits\SeekableStreamDataExtractor;
use Psr\Http\Message\RequestInterface;
/**
* Class ExactFormDataMatcher
*
* @category ExactFormDataMatcher
* @package Pock\Matchers
*/
class ExactFormDataMatcher extends QueryMatcher
{
use SeekableStreamDataExtractor;
/**
* @inheritDoc
*/
public function matches(RequestInterface $request): bool
{
$query = static::parseQuery(static::getStreamData($request->getBody()));
if (empty($query)) {
return false;
}
return ComparatorLocator::get(RecursiveArrayComparator::class)->compare($this->query, $query);
}
}

View file

@ -9,6 +9,8 @@
namespace Pock\Matchers;
use Pock\Comparator\ComparatorLocator;
use Pock\Comparator\ScalarFlatArrayComparator;
use Psr\Http\Message\RequestInterface;
/**
@ -28,6 +30,7 @@ class ExactHeaderMatcher extends HeaderMatcher
return false;
}
return self::compareStringArrays($request->getHeader($this->header), $this->value);
return ComparatorLocator::get(ScalarFlatArrayComparator::class)
->compare($request->getHeader($this->header), $this->value);
}
}

View file

@ -9,6 +9,8 @@
namespace Pock\Matchers;
use Pock\Comparator\ComparatorLocator;
use Pock\Comparator\RecursiveArrayComparator;
use Psr\Http\Message\RequestInterface;
/**
@ -30,6 +32,6 @@ class ExactQueryMatcher extends QueryMatcher
return false;
}
return self::recursiveCompareArrays($this->query, $query);
return ComparatorLocator::get(RecursiveArrayComparator::class)->compare($this->query, $query);
}
}

View file

@ -0,0 +1,40 @@
<?php
/**
* PHP version 7.3
*
* @category FormDataMatcher
* @package Pock\Matchers
*/
namespace Pock\Matchers;
use Pock\Comparator\ComparatorLocator;
use Pock\Comparator\RecursiveArrayComparator;
use Pock\Traits\SeekableStreamDataExtractor;
use Psr\Http\Message\RequestInterface;
/**
* Class FormDataMatcher
*
* @category FormDataMatcher
* @package Pock\Matchers
*/
class FormDataMatcher extends QueryMatcher
{
use SeekableStreamDataExtractor;
/**
* @inheritDoc
*/
public function matches(RequestInterface $request): bool
{
$query = static::parseQuery(static::getStreamData($request->getBody()));
if (empty($query)) {
return false;
}
return ComparatorLocator::get(RecursiveArrayComparator::class)->compare($this->query, $query);
}
}

View file

@ -9,6 +9,8 @@
namespace Pock\Matchers;
use Pock\Comparator\ComparatorLocator;
use Pock\Comparator\LtrScalarArrayComparator;
use Psr\Http\Message\RequestInterface;
/**
@ -17,7 +19,7 @@ use Psr\Http\Message\RequestInterface;
* @category HeaderMatcher
* @package Pock\Matchers
*/
class HeaderMatcher extends AbstractArrayPoweredComponent implements RequestMatcherInterface
class HeaderMatcher implements RequestMatcherInterface
{
/** @var string */
protected $header;
@ -51,6 +53,7 @@ class HeaderMatcher extends AbstractArrayPoweredComponent implements RequestMatc
return false;
}
return self::isNeedlePresentInHaystack($this->value, $request->getHeader($this->header));
return ComparatorLocator::get(LtrScalarArrayComparator::class)
->compare($this->value, $request->getHeader($this->header));
}
}

View file

@ -9,6 +9,8 @@
namespace Pock\Matchers;
use Pock\Comparator\ComparatorLocator;
use Pock\Comparator\LtrScalarArrayComparator;
use Psr\Http\Message\RequestInterface;
/**
@ -17,7 +19,7 @@ use Psr\Http\Message\RequestInterface;
* @category HeadersMatcher
* @package Pock\Matchers
*/
class HeadersMatcher extends AbstractArrayPoweredComponent implements RequestMatcherInterface
class HeadersMatcher implements RequestMatcherInterface
{
/** @var array<string, string|string[]> */
protected $headers;
@ -48,7 +50,7 @@ class HeadersMatcher extends AbstractArrayPoweredComponent implements RequestMat
$value = [$value];
}
if (!static::isNeedlePresentInHaystack($value, $request->getHeader($name))) {
if (!ComparatorLocator::get(LtrScalarArrayComparator::class)->compare($value, $request->getHeader($name))) {
return false;
}
}

View file

@ -0,0 +1,57 @@
<?php
/**
* PHP version 7.1
*
* @category MultipartFormDataMatcher
* @package Pock\Matchers
*/
namespace Pock\Matchers;
use InvalidArgumentException;
use Pock\Traits\SeekableStreamDataExtractor;
use Psr\Http\Message\RequestInterface;
use Riverline\MultiPartParser\StreamedPart;
use Riverline\MultiPartParser\Converters\PSR7;
use RuntimeException;
/**
* Class MultipartFormDataMatcher
*
* @category MultipartFormDataMatcher
* @package Pock\Matchers
*/
class MultipartFormDataMatcher implements RequestMatcherInterface
{
use SeekableStreamDataExtractor;
/** @var callable */
private $callback;
/**
* MultipartFormDataMatcher constructor.
*
* @param callable $callback Accepts Riverline\MultiPartParser\StreamedPart as an argument, returns true if matched.
*/
public function __construct(callable $callback)
{
$this->callback = $callback;
}
/**
* @inheritDoc
* @SuppressWarnings(PHPMD.StaticAccess)
*/
public function matches(RequestInterface $request): bool
{
try {
$part = PSR7::convert($request);
$request->getBody()->rewind();
} catch (InvalidArgumentException $exception) {
return false;
}
return call_user_func($this->callback, $part);
}
}

View file

@ -0,0 +1,56 @@
<?php
/**
* PHP version 7.3
*
* @category PortMatcher
* @package Pock\Matchers
*/
namespace Pock\Matchers;
use Pock\Enum\RequestScheme;
use Psr\Http\Message\RequestInterface;
/**
* Class PortMatcher
*
* @category PortMatcher
* @package Pock\Matchers
*/
class PortMatcher implements RequestMatcherInterface
{
/** @var int */
protected $port;
/**
* PortMatcher constructor.
*
* @param int $port
*/
public function __construct(int $port)
{
$this->port = $port;
}
/**
* @inheritDoc
*/
public function matches(RequestInterface $request): bool
{
$port = $request->getUri()->getPort();
if (null === $port) {
switch ($request->getUri()->getScheme()) {
case RequestScheme::HTTP:
return 80 === $this->port;
case RequestScheme::HTTPS:
return 443 === $this->port;
default:
return false;
}
}
return $port === $this->port;
}
}

View file

@ -9,8 +9,9 @@
namespace Pock\Matchers;
use Pock\Comparator\ComparatorLocator;
use Pock\Comparator\RecursiveLtrArrayComparator;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\UriInterface;
/**
* Class QueryMatcher
@ -18,7 +19,7 @@ use Psr\Http\Message\UriInterface;
* @category QueryMatcher
* @package Pock\Matchers
*/
class QueryMatcher extends AbstractArrayPoweredComponent implements RequestMatcherInterface
class QueryMatcher implements RequestMatcherInterface
{
/** @var array<string, mixed> */
protected $query;
@ -44,7 +45,7 @@ class QueryMatcher extends AbstractArrayPoweredComponent implements RequestMatch
return false;
}
return self::recursiveNeedlePresentInHaystack($this->query, $query);
return ComparatorLocator::get(RecursiveLtrArrayComparator::class)->compare($this->query, $query);
}
/**

View file

@ -0,0 +1,32 @@
<?php
/**
* PHP version 7.3
*
* @category RegExpBodyMatcher
* @package Pock\Matchers
*/
namespace Pock\Matchers;
use Pock\Traits\SeekableStreamDataExtractor;
use Psr\Http\Message\RequestInterface;
/**
* Class RegExpBodyMatcher
*
* @category RegExpBodyMatcher
* @package Pock\Matchers
*/
class RegExpBodyMatcher extends AbstractRegExpMatcher
{
use SeekableStreamDataExtractor;
/**
* @inheritDoc
*/
public function matches(RequestInterface $request): bool
{
return $this->matchRegExp(static::getStreamData($request->getBody()));
}
}

View file

@ -0,0 +1,29 @@
<?php
/**
* PHP version 7.3
*
* @category RegExpPathMatcher
* @package Pock\Matchers
*/
namespace Pock\Matchers;
use Psr\Http\Message\RequestInterface;
/**
* Class RegExpPathMatcher
*
* @category RegExpPathMatcher
* @package Pock\Matchers
*/
class RegExpPathMatcher extends AbstractRegExpMatcher
{
/**
* @inheritDoc
*/
public function matches(RequestInterface $request): bool
{
return $this->matchRegExp($request->getUri()->getPath());
}
}

View file

@ -0,0 +1,29 @@
<?php
/**
* PHP version 7.3
*
* @category RegExpQueryMatcher
* @package Pock\Matchers
*/
namespace Pock\Matchers;
use Psr\Http\Message\RequestInterface;
/**
* Class RegExpQueryMatcher
*
* @category RegExpQueryMatcher
* @package Pock\Matchers
*/
class RegExpQueryMatcher extends AbstractRegExpMatcher
{
/**
* @inheritDoc
*/
public function matches(RequestInterface $request): bool
{
return $this->matchRegExp($request->getUri()->getQuery());
}
}

View file

@ -0,0 +1,29 @@
<?php
/**
* PHP version 7.3
*
* @category RegExpUriMatcher
* @package Pock\Matchers
*/
namespace Pock\Matchers;
use Psr\Http\Message\RequestInterface;
/**
* Class RegExpUriMatcher
*
* @category RegExpUriMatcher
* @package Pock\Matchers
*/
class RegExpUriMatcher extends AbstractRegExpMatcher
{
/**
* @inheritDoc
*/
public function matches(RequestInterface $request): bool
{
return $this->matchRegExp((string) $request->getUri());
}
}

View file

@ -0,0 +1,210 @@
<?php
/**
* PHP 7.3
*
* @category XmlBodyMatcher
* @package Pock\Matchers
*/
namespace Pock\Matchers;
use DOMDocument;
use Pock\Exception\XmlException;
use Pock\Traits\SeekableStreamDataExtractor;
use Psr\Http\Message\RequestInterface;
use RuntimeException;
use XSLTProcessor;
/**
* Class XmlBodyMatcher
*
* @category XmlBodyMatcher
* @package Pock\Matchers
*/
class XmlBodyMatcher extends BodyMatcher
{
use SeekableStreamDataExtractor;
private const TAG_SORT_XSLT = <<<EOT
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="@*">
<xsl:sort select="name()"/>
</xsl:apply-templates>
<xsl:apply-templates select="node()">
<xsl:sort select="name()"/>
</xsl:apply-templates>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
EOT;
/** @var bool */
public static $forceTextComparison = false;
/** @var XSLTProcessor|null */
private static $sorter;
/** @var bool */
private $useFallback;
/**
* XmlBodyMatcher constructor.
*
* @param DOMDocument|\Psr\Http\Message\StreamInterface|resource|string $referenceXml
*
* @throws \Pock\Exception\XmlException
*/
public function __construct($referenceXml)
{
if (!static::hasExtension('xsl') || !static::hasExtension('dom')) {
$this->useFallback = true;
}
if (!static::hasExtension('xsl')) {
$this->useFallback = true;
if (static::hasExtension('dom') && $referenceXml instanceof DOMDocument) {
$referenceXml = static::getDOMString($referenceXml);
}
parent::__construct($referenceXml); // @phpstan-ignore-line
return;
}
if ($referenceXml instanceof DOMDocument) {
parent::__construct(static::sortXmlTags($referenceXml));
return;
}
parent::__construct(static::sortXmlTags(
static::createDOMDocument(static::getEntryItemData($referenceXml))
));
}
/**
* @inheritDoc
*/
public function matches(RequestInterface $request): bool
{
if ($this->useFallback) {
return parent::matches($request);
}
if (0 === $request->getBody()->getSize()) {
return '' === $this->contents;
}
return self::sortXmlTags(self::createDOMDocument(self::getStreamData($request->getBody()))) === $this->contents;
}
/**
* Returns new document with tags sorted alphabetically.
*
* @param \DOMDocument $document
*
* @return string
* @throws \RuntimeException|\Pock\Exception\XmlException
*/
private static function sortXmlTags(DOMDocument $document): string
{
$xml = static::getSorter()->transformToXml($document);
if (false === $xml) {
throw new RuntimeException('Cannot sort XML nodes');
}
return $xml;
}
/**
* Returns XSLTProcessor with XSLT which sorts tags alphabetically.
*
* @return \XSLTProcessor
* @throws \Pock\Exception\XmlException
*/
private static function getSorter(): XSLTProcessor
{
if (null === static::$sorter) {
static::$sorter = new XSLTProcessor();
static::$sorter->importStylesheet(static::createDOMDocument(static::TAG_SORT_XSLT));
}
return static::$sorter;
}
/**
* Create DOMDocument with provided XML string.
*
* @param string $xml
* @param string $version
* @param string $encoding
*
* @return \DOMDocument
* @throws \Pock\Exception\XmlException
*/
private static function createDOMDocument(string $xml, string $version = '1.0', string $encoding = ''): DOMDocument
{
if ('' === $xml) {
throw new XmlException('XML must not be empty.');
}
$error = null;
$document = new DOMDocument($version, $encoding);
try {
set_error_handler(static function ($code, $message) {
throw new XmlException($message, $code);
});
$document->loadXML(trim($xml));
} catch (XmlException $exception) {
$error = $exception;
} finally {
restore_error_handler();
}
if (null !== $error) {
throw $error;
}
return $document;
}
/**
* @param \DOMDocument $document
*
* @return string
* @throws \Pock\Exception\XmlException
*/
private static function getDOMString(DOMDocument $document): string
{
$result = $document->saveXML();
if (false === $result) {
throw new XmlException('Cannot export XML.');
}
return $result;
}
/**
* @param string $extension
*
* @return bool
*/
private static function hasExtension(string $extension): bool
{
if (static::$forceTextComparison && 'xsl' === $extension) {
return false;
}
return extension_loaded($extension);
}
}

View file

@ -9,7 +9,13 @@
namespace Pock;
use Pock\Exception\PockNetworkException;
use Pock\Exception\PockRequestException;
use Pock\Factory\ReplyFactoryInterface;
use Pock\Matchers\RequestMatcherInterface;
use Psr\Http\Client\NetworkExceptionInterface;
use Psr\Http\Client\RequestExceptionInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Throwable;
@ -24,6 +30,9 @@ class Mock implements MockInterface
/** @var \Pock\Matchers\RequestMatcherInterface */
private $matcher;
/** @var \Pock\Factory\ReplyFactoryInterface|null */
private $replyFactory;
/** @var \Psr\Http\Message\ResponseInterface|null */
private $response;
@ -36,25 +45,38 @@ class Mock implements MockInterface
/** @var int */
private $maxHits;
/** @var int */
private $matchAt;
/**
* Mock constructor.
*
* @param \Pock\Matchers\RequestMatcherInterface $matcher
* @param \Pock\Factory\ReplyFactoryInterface|null $replyFactory
* @param \Psr\Http\Message\ResponseInterface|null $response
* @param \Throwable|null $throwable
* @param int $maxHits
* @param int $matchAt
*/
public function __construct(
RequestMatcherInterface $matcher,
?ReplyFactoryInterface $replyFactory,
?ResponseInterface $response,
?Throwable $throwable,
int $maxHits
int $maxHits,
int $matchAt
) {
$this->matcher = $matcher;
$this->replyFactory = $replyFactory;
$this->response = $response;
$this->throwable = $throwable;
$this->matchAt = $matchAt;
$this->maxHits = $maxHits;
$this->hits = 0;
if ($this->maxHits < ($matchAt + 1) && -1 !== $this->maxHits) {
$this->maxHits = $matchAt + 1;
}
}
/**
@ -62,7 +84,10 @@ class Mock implements MockInterface
*/
public function registerHit(): MockInterface
{
++$this->hits;
if (-1 !== $this->maxHits) {
++$this->hits;
}
return $this;
}
@ -71,15 +96,27 @@ class Mock implements MockInterface
*/
public function available(): bool
{
return $this->hits < $this->maxHits;
return -1 === $this->maxHits || $this->hits < $this->maxHits;
}
/**
* @inheritDoc
*/
public function getMatcher(): RequestMatcherInterface
public function matches(RequestInterface $request): bool
{
return $this->matcher;
if ($this->matcher->matches($request)) {
if ($this->matchAt <= 0) {
return true;
}
if ($this->matchAt === $this->hits) {
return true;
}
$this->registerHit();
}
return false;
}
/**
@ -101,8 +138,20 @@ class Mock implements MockInterface
/**
* @inheritDoc
*/
public function getThrowable(): ?Throwable
public function getReplyFactory(): ?ReplyFactoryInterface
{
return $this->replyFactory;
}
/**
* @inheritDoc
*/
public function getThrowable(RequestInterface $request): ?Throwable
{
if ($this->throwable instanceof PockRequestException || $this->throwable instanceof PockNetworkException) {
return $this->throwable->setRequest($request);
}
return $this->throwable;
}
}

View file

@ -9,7 +9,9 @@
namespace Pock;
use Pock\Factory\ReplyFactoryInterface;
use Pock\Matchers\RequestMatcherInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Throwable;
@ -36,11 +38,14 @@ interface MockInterface
public function available(): bool;
/**
* Returns matcher for the request.
* Returns true if underlying matcher has matched provided request.
* It also returns false if matcher has matched request but hits condition is not met yet.
*
* @return \Pock\Matchers\RequestMatcherInterface
* @param \Psr\Http\Message\RequestInterface $request
*
* @return bool
*/
public function getMatcher(): RequestMatcherInterface;
public function matches(RequestInterface $request): bool;
/**
* Returns response which should be used as mock data.
@ -49,10 +54,19 @@ interface MockInterface
*/
public function getResponse(): ?ResponseInterface;
/**
* Returns reply factory which should be used to form the mocked response.
*
* @return \Pock\Factory\ReplyFactoryInterface|null
*/
public function getReplyFactory(): ?ReplyFactoryInterface;
/**
* Returns the throwable which will be thrown as mock data.
*
* @param \Psr\Http\Message\RequestInterface $request This request may be set into exception if possible
*
* @return \Throwable|null
*/
public function getThrowable(): ?Throwable;
public function getThrowable(RequestInterface $request): ?Throwable;
}

View file

@ -9,15 +9,20 @@
namespace Pock;
use Diff\ArrayComparer\StrictArrayComparer;
use Pock\Enum\RequestMethod;
use Pock\Enum\RequestScheme;
use DOMDocument;
use Pock\Exception\PockClientException;
use Pock\Exception\PockNetworkException;
use Pock\Exception\PockRequestException;
use Pock\Factory\CallbackReplyFactory;
use Pock\Factory\ReplyFactoryInterface;
use Pock\Matchers\AnyRequestMatcher;
use Pock\Matchers\BodyMatcher;
use Pock\Matchers\CallbackRequestMatcher;
use Pock\Matchers\ExactFormDataMatcher;
use Pock\Matchers\ExactHeaderMatcher;
use Pock\Matchers\ExactHeadersMatcher;
use Pock\Matchers\ExactQueryMatcher;
use Pock\Matchers\FormDataMatcher;
use Pock\Matchers\HeaderLineMatcher;
use Pock\Matchers\HeaderLineRegexpMatcher;
use Pock\Matchers\HeaderMatcher;
@ -25,16 +30,28 @@ use Pock\Matchers\HeadersMatcher;
use Pock\Matchers\HostMatcher;
use Pock\Matchers\JsonBodyMatcher;
use Pock\Matchers\MethodMatcher;
use Pock\Matchers\MultipartFormDataMatcher;
use Pock\Matchers\MultipleMatcher;
use Pock\Matchers\PathMatcher;
use Pock\Matchers\PortMatcher;
use Pock\Matchers\QueryMatcher;
use Pock\Matchers\RegExpBodyMatcher;
use Pock\Matchers\RegExpPathMatcher;
use Pock\Matchers\RegExpQueryMatcher;
use Pock\Matchers\RegExpUriMatcher;
use Pock\Matchers\RequestMatcherInterface;
use Pock\Matchers\SchemeMatcher;
use Pock\Matchers\UriMatcher;
use Pock\Matchers\XmlBodyMatcher;
use Pock\Traits\JsonDecoderTrait;
use Pock\Traits\JsonSerializerAwareTrait;
use Pock\Traits\XmlSerializerAwareTrait;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use RuntimeException;
use Symfony\Component\HttpClient\Psr18Client;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Throwable;
/**
@ -45,6 +62,9 @@ use Throwable;
*
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
* @SuppressWarnings(PHPMD.TooManyPublicMethods)
* @SuppressWarnings(PHPMD.TooManyMethods)
* @SuppressWarnings(PHPMD.ExcessivePublicCount)
* @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
*/
class PockBuilder
{
@ -58,12 +78,18 @@ class PockBuilder
/** @var \Pock\PockResponseBuilder|null */
private $responseBuilder;
/** @var ReplyFactoryInterface|null */
private $replyFactory;
/** @var \Throwable|null */
private $throwable;
/** @var int */
private $maxHits;
/** @var int */
private $matchAt;
/** @var \Pock\MockInterface[] */
private $mocks;
@ -114,6 +140,49 @@ class PockBuilder
return $this->addMatcher(new HostMatcher($host));
}
/**
* Matches request by the port.
*
* @param int $port
*
* @return self
*/
public function matchPort(int $port): self
{
return $this->addMatcher(new PortMatcher($port));
}
/**
* Matches request by origin.
*
* @param string $origin
*
* @return self
* @throws \RuntimeException
*/
public function matchOrigin(string $origin): self
{
$parsed = parse_url($origin);
if (!is_array($parsed)) {
throw new RuntimeException('Malformed origin: ' . $origin);
}
if (array_key_exists('scheme', $parsed) && !empty($parsed['scheme'])) {
$this->matchScheme($parsed['scheme']);
}
if (array_key_exists('host', $parsed) && !empty($parsed['host'])) {
$this->matchHost($parsed['host']);
}
if (array_key_exists('port', $parsed) && is_int($parsed['port']) && $parsed['port'] > 0) {
$this->matchPort($parsed['port']);
}
return $this;
}
/**
* Matches request by the whole URI.
*
@ -126,6 +195,19 @@ class PockBuilder
return $this->addMatcher(new UriMatcher($uri));
}
/**
* Matches request by the whole URI using regular expression.
*
* @param string $expression
* @param int $flags
*
* @return self
*/
public function matchUriRegExp(string $expression, int $flags = 0): self
{
return $this->addMatcher(new RegExpUriMatcher($expression, $flags));
}
/**
* Matches request by header value or several values. Header can have other values which are not specified here.
* @see PockBuilder::matchExactHeader() if you want to match exact header values.
@ -219,6 +301,20 @@ class PockBuilder
return $this->addMatcher(new PathMatcher($path));
}
/**
* Match request by its path using regular expression. This matcher doesn't care about prefix slash
* since it's pretty easy to do it using regular expression.
*
* @param string $expression
* @param int $flags
*
* @return self
*/
public function matchPathRegExp(string $expression, int $flags = 0): self
{
return $this->addMatcher(new RegExpPathMatcher($expression, $flags));
}
/**
* Match request by its query. Request can contain other query variables.
* @see PockBuilder::matchExactQuery() if you want to match an entire query string.
@ -232,6 +328,19 @@ class PockBuilder
return $this->addMatcher(new QueryMatcher($query));
}
/**
* Match request by its query using regular expression.
*
* @param string $expression
* @param int $flags
*
* @return self
*/
public function matchQueryRegExp(string $expression, int $flags = 0): self
{
return $this->addMatcher(new RegExpQueryMatcher($expression, $flags));
}
/**
* Match request by its query. Additional query parameters aren't allowed.
*
@ -244,6 +353,44 @@ class PockBuilder
return $this->addMatcher(new ExactQueryMatcher($query));
}
/**
* Match request with form-data.
*
* @param array<string, mixed> $formFields
*
* @return self
*/
public function matchFormData(array $formFields): self
{
return $this->addMatcher(new FormDataMatcher($formFields));
}
/**
* Match request with form-data. Additional fields aren't allowed.
*
* @param array<string, mixed> $formFields
*
* @return self
*/
public function matchExactFormData(array $formFields): self
{
return $this->addMatcher(new ExactFormDataMatcher($formFields));
}
/**
* Match request multipart form data. Will not match the request if body is not multipart.
* Uses third-party library to parse the data.
*
* @param callable $callback Accepts Riverline\MultiPartParser\StreamedPart as an argument, returns true if matched.
*
* @return self
* @see https://github.com/Riverline/multipart-parser#usage
*/
public function matchMultipartFormData(callable $callback): self
{
return $this->addMatcher(new MultipartFormDataMatcher($callback));
}
/**
* Match entire request body.
*
@ -256,6 +403,19 @@ class PockBuilder
return $this->addMatcher(new BodyMatcher($data));
}
/**
* Match entire request body using provided regular expression.
*
* @param string $expression
* @param int $flags
*
* @return self
*/
public function matchBodyRegExp(string $expression, int $flags = 0): self
{
return $this->addMatcher(new RegExpBodyMatcher($expression, $flags));
}
/**
* Match JSON request body.
*
@ -274,28 +434,63 @@ class PockBuilder
));
}
/**
* Match JSON request body against JSON string or array with data.
*
* @param array<int|string, mixed>|string $data
*
* @return self
* @throws \Pock\Exception\JsonException
*/
public function matchSerializedJsonBody($data): self
{
if (is_string($data)) {
$data = self::jsonDecode($data, true);
}
return $this->addMatcher(new JsonBodyMatcher($data));
}
/**
* Match XML request body using raw XML data.
*
* **Note:** this method will fallback to the string comparison if ext-xsl is not available.
* It also doesn't serializer values with available XML serializer.
* Use PockBuilder::matchSerializedXmlBody if you want to execute available serializer.
*
* @param DOMDocument|\Psr\Http\Message\StreamInterface|resource|string $data
*
* @return self
* @throws \Pock\Exception\XmlException
* @see \Pock\PockBuilder::matchSerializedXmlBody()
*
*/
public function matchXmlBody($data): self
{
return $this->addMatcher(new XmlBodyMatcher($data));
}
/**
* Match XML request body.
*
* **Note:** this method will use string comparison for now. It'll be improved in future.
* This method will try to use available XML serializer before matching.
*
* @todo Don't use simple string comparison. Match the entire body by its DOM.
*
* @param mixed $data
* @phpstan-ignore-next-line
* @param string|array|object $data
*
* @return self
* @throws \Pock\Exception\XmlException
*/
public function matchXmlBody($data): self
public function matchSerializedXmlBody($data): self
{
return $this->matchBody(self::serializeXml($data) ?? '');
return $this->matchXmlBody(self::serializeXml($data) ?? '');
}
/**
* Match request using provided callback. Callback should receive RequestInterface and return boolean.
* If returned value is true then request is matched.
*
* @param callable $callback
* @param callable $callback Callable that accepts PSR-7 RequestInterface as it's first argument.
*
* @return self
*/
@ -329,6 +524,8 @@ class PockBuilder
*/
public function repeat(int $hits): self
{
$this->closePrevious();
if ($hits > 0) {
$this->maxHits = $hits;
}
@ -336,6 +533,49 @@ class PockBuilder
return $this;
}
/**
* Always execute this mock if matched. Mock with this call will not be expired ever.
*
* @return self
*/
public function always(): self
{
$this->closePrevious();
$this->maxHits = -1;
return $this;
}
/**
* Match request only at Nth hit. Previous matches will not be executed.
*
* **Note:** There IS a catch if you use this with the equal mocks. The test Client will not register hit
* for the second mock and the second mock will be executed at N+1 time.
*
* For example, if you try to send 5 requests with this mocks and log response codes:
* ```php
* $builder = new PockBuilder();
*
* $builder->matchHost('example.com')->at(2)->reply(200);
* $builder->matchHost('example.com')->at(4)->reply(201);
* $builder->always()->reply(400);
* ```
*
* You will get this: 400, 400, 200, 400, 400, 201
* Instead of this: 400, 400, 200, 400, 201, 400
*
* @param int $hit
*
* @return self
*/
public function at(int $hit): self
{
$this->closePrevious();
$this->matchAt = $hit - 1;
return $this;
}
/**
* Throw an exception when request is being sent.
*
@ -350,6 +590,42 @@ class PockBuilder
return $this;
}
/**
* Throw an ClientExceptionInterface instance with specified message
*
* @param string $message
*
* @return self
*/
public function throwClientException(string $message = 'Pock ClientException'): self
{
return $this->throwException(new PockClientException($message));
}
/**
* Throw an NetworkExceptionInterface instance with specified message
*
* @param string $message
*
* @return self
*/
public function throwNetworkException(string $message = 'Pock NetworkException'): self
{
return $this->throwException(new PockNetworkException($message));
}
/**
* Throw an RequestExceptionInterface instance with specified message
*
* @param string $message
*
* @return self
*/
public function throwRequestException(string $message = 'Pock RequestException'): self
{
return $this->throwException(new PockRequestException($message));
}
/**
* @param int $statusCode
*
@ -366,6 +642,47 @@ class PockBuilder
return $this->responseBuilder->withStatusCode($statusCode);
}
/**
* Construct the response during request execution using provided ReplytFactoryInterface implementation.
*
* @param \Pock\Factory\ReplyFactoryInterface $factory
* @see ReplyFactoryInterface
*/
public function replyWithFactory(ReplyFactoryInterface $factory): void
{
$this->replyFactory = $factory;
}
/**
* Construct the response during request execution using provided callback.
*
* Callback should receive the same parameters as in the `ReplyFactoryInterface::createReply` method.
*
* @see ReplyFactoryInterface::createReply()
*
* @param callable $callback
*/
public function replyWithCallback(callable $callback): void
{
$this->replyWithFactory(new CallbackReplyFactory($callback));
}
/**
* Reply to the request using provided client. Can be used to send real network request.
*
* @param \Psr\Http\Client\ClientInterface $client
* @SuppressWarnings(unused)
*/
public function replyWithClient(ClientInterface $client): void
{
$this->replyWithCallback(function (
RequestInterface $request,
PockResponseBuilder $responseBuilder
) use ($client): ResponseInterface {
return $client->sendRequest($request);
});
}
/**
* Resets the builder.
*
@ -374,9 +691,11 @@ class PockBuilder
public function reset(): self
{
$this->matcher = new MultipleMatcher();
$this->replyFactory = null;
$this->responseBuilder = null;
$this->throwable = null;
$this->maxHits = 1;
$this->matchAt = -1;
$this->mocks = [];
return $this;
@ -406,7 +725,7 @@ class PockBuilder
private function closePrevious(): void
{
if (null !== $this->responseBuilder || null !== $this->throwable) {
if (null !== $this->responseBuilder || null !== $this->replyFactory || null !== $this->throwable) {
if (0 === count($this->matcher)) {
$this->matcher->addMatcher(new AnyRequestMatcher());
}
@ -419,14 +738,18 @@ class PockBuilder
$this->mocks[] = new Mock(
$this->matcher,
$this->replyFactory,
$response,
$this->throwable,
$this->maxHits
$this->maxHits,
$this->matchAt
);
$this->matcher = new MultipleMatcher();
$this->replyFactory = null;
$this->responseBuilder = null;
$this->throwable = null;
$this->maxHits = 1;
$this->matchAt = -1;
}
}
}

View file

@ -3,7 +3,7 @@
/**
* PHP 7.1
*
* @category CallbackSerializerDecorator
* @category CallbackSerializerAdapter
* @package Pock\Serializer
*/
@ -12,18 +12,18 @@ namespace Pock\Serializer;
use RuntimeException;
/**
* Class CallbackSerializerDecorator
* Class CallbackSerializerAdapter
*
* @category CallbackSerializerDecorator
* @category CallbackSerializerAdapter
* @package Pock\Serializer
*/
class CallbackSerializerDecorator implements SerializerInterface
class CallbackSerializerAdapter implements SerializerInterface
{
/** @var callable */
private $callback;
/**
* CallbackSerializerDecorator constructor.
* CallbackSerializerAdapter constructor.
*
* @param callable $callback
*/

View file

@ -3,19 +3,19 @@
/**
* PHP 7.2
*
* @category JmsSerializerDecorator
* @category JmsSerializerAdapter
* @package Pock\Serializer
*/
namespace Pock\Serializer;
/**
* Class JmsSerializerDecorator
* Class JmsSerializerAdapter
*
* @category JmsSerializerDecorator
* @category JmsSerializerAdapter
* @package Pock\Serializer
*/
class JmsSerializerDecorator implements SerializerInterface
class JmsSerializerAdapter implements SerializerInterface
{
/** @var object */
private $serializer;
@ -24,7 +24,7 @@ class JmsSerializerDecorator implements SerializerInterface
private $format;
/**
* JmsSerializerDecorator constructor.
* JmsSerializerAdapter constructor.
*
* @param object $serializer
*/

View file

@ -0,0 +1,20 @@
<?php
/**
* PHP 7.1
*
* @category SymfonySerializerAdapter
* @package Pock\Serializer
*/
namespace Pock\Serializer;
/**
* Class SymfonySerializerAdapter
*
* @category SymfonySerializerAdapter
* @package Pock\Serializer
*/
class SymfonySerializerAdapter extends JmsSerializerAdapter
{
}

View file

@ -1,20 +0,0 @@
<?php
/**
* PHP 7.1
*
* @category SymfonySerializerDecorator
* @package Pock\Serializer
*/
namespace Pock\Serializer;
/**
* Class SymfonySerializerDecorator
*
* @category SymfonySerializerDecorator
* @package Pock\Serializer
*/
class SymfonySerializerDecorator extends JmsSerializerDecorator
{
}

View file

@ -22,11 +22,11 @@ trait JsonDecoderTrait
/**
* json_decode which throws exception on error.
*
* @param string $json
* @param bool|null $associative
* @param int $depth
* @param string $json
* @param bool $associative
* @param int $depth
*
* @param int $flags
* @param int $flags
*
* @return mixed
* @throws \Pock\Exception\JsonException
@ -35,7 +35,7 @@ trait JsonDecoderTrait
*/
public static function jsonDecode(
string $json,
?bool $associative = false,
bool $associative = false,
int $depth = 512,
int $flags = 0
) {

View file

@ -0,0 +1,48 @@
<?php
/**
* PHP 7.3
*
* @category ComparatorLocatorTest
* @package Pock\Tests\Comparator
*/
namespace Pock\Tests\Comparator;
use PHPUnit\Framework\TestCase;
use Pock\Comparator\ComparatorLocator;
use Pock\Comparator\LtrScalarArrayComparator;
use Pock\Comparator\RecursiveArrayComparator;
use Pock\Comparator\RecursiveLtrArrayComparator;
use Pock\Comparator\ScalarFlatArrayComparator;
use RuntimeException;
/**
* Class ComparatorLocatorTest
*
* @category ComparatorLocatorTest
* @package Pock\Tests\Comparator
*/
class ComparatorLocatorTest extends TestCase
{
public function testGetException(): void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Comparator random does not exist.');
ComparatorLocator::get('random');
}
public function testGet(): void
{
$comparator = ComparatorLocator::get(ScalarFlatArrayComparator::class);
self::assertInstanceOf(ScalarFlatArrayComparator::class, $comparator);
self::assertTrue($comparator->compare(['1'], ['1']));
self::assertFalse($comparator->compare(['1'], ['2']));
self::assertFalse($comparator->compare(null, null));
self::assertFalse(ComparatorLocator::get(LtrScalarArrayComparator::class)->compare(null, null));
self::assertFalse(ComparatorLocator::get(RecursiveArrayComparator::class)->compare(null, null));
self::assertFalse(ComparatorLocator::get(RecursiveLtrArrayComparator::class)->compare(null, null));
}
}

View file

@ -0,0 +1,75 @@
<?php
/**
* PHP version 7.3
*
* @category RecursiveArrayComparatorTest
* @package Pock\Tests\Comparator
*/
namespace Pock\Tests\Comparator;
use PHPUnit\Framework\TestCase;
use Pock\Comparator\ComparatorLocator;
use Pock\Comparator\RecursiveArrayComparator;
/**
* Class RecursiveArrayComparatorTest
*
* @category RecursiveArrayComparatorTest
* @package Pock\Tests\Comparator
*/
class RecursiveArrayComparatorTest extends TestCase
{
public function testMatches(): void
{
$needle = [
'filter' => [
'createdAtFrom' => '2020-01-01 00:00:00',
'createdAtTo' => '2021-08-01 00:00:00',
],
'test' => ''
];
$haystack = [
'filter' => [
'createdAtFrom' => '2020-01-01 00:00:00',
'createdAtTo' => '2021-08-01 00:00:00',
],
'test' => ''
];
self::assertTrue(ComparatorLocator::get(RecursiveArrayComparator::class)->compare($needle, $haystack));
}
public function testNotMatches(): void
{
$needle = [
'filter' => [
'createdAtFrom' => '2020-01-01 00:00:00',
'createdAtTo' => '2021-08-01 00:00:00',
],
'test2' => [1]
];
$haystack = [
'filter' => [
'createdAtFrom' => '2020-01-01 00:00:00',
'createdAtTo' => '2021-08-01 00:00:00',
],
'test2' => 1
];
self::assertFalse(ComparatorLocator::get(RecursiveArrayComparator::class)->compare($needle, $haystack));
}
public function testNotMatchesKeyPositions(): void
{
$needle = [
'source' => json_decode('{"medium":"tiktok","source":"Test Ad","campaign":"Test Campaign"}', true)
];
$haystack = [
'source' => json_decode('{"source":"Test Ad","medium":"tiktok","campaign":"Test Campaign"}', true)
];
self::assertTrue(ComparatorLocator::get(RecursiveArrayComparator::class)->compare($needle, $haystack));
}
}

View file

@ -0,0 +1,75 @@
<?php
/**
* PHP version 7.3
*
* @category RecursiveArrayLtrComparatorTest
* @package Pock\Tests\Comparator
*/
namespace Pock\Tests\Comparator;
use PHPUnit\Framework\TestCase;
use Pock\Comparator\ComparatorLocator;
use Pock\Comparator\RecursiveLtrArrayComparator;
/**
* Class RecursiveArrayLtrComparatorTest
*
* @category RecursiveArrayLtrComparatorTest
* @package Pock\Tests\Comparator
*/
class RecursiveArrayLtrComparatorTest extends TestCase
{
public function testMatches(): void
{
$needle = [
'filter' => [
'createdAtFrom' => '2020-01-01 00:00:00',
'createdAtTo' => '2021-08-01 00:00:00',
]
];
$haystack = [
'filter' => [
'createdAtFrom' => '2020-01-01 00:00:00',
'createdAtTo' => '2021-08-01 00:00:00',
],
'test' => ''
];
self::assertTrue(ComparatorLocator::get(RecursiveLtrArrayComparator::class)->compare($needle, $haystack));
}
public function testNotMatches(): void
{
$needle = [
'filter' => [
'createdAtFrom' => '2020-01-01 00:00:00',
'createdAtTo' => '2021-08-01 00:00:00',
],
'test2' => [1]
];
$haystack = [
'filter' => [
'createdAtFrom' => '2020-01-01 00:00:00',
'createdAtTo' => '2021-08-01 00:00:00',
],
'test2' => 1,
'test' => ''
];
self::assertFalse(ComparatorLocator::get(RecursiveLtrArrayComparator::class)->compare($needle, $haystack));
}
public function testNotMatchesKeyPositions(): void
{
$needle = [
'source' => json_decode('{"medium":"tiktok","source":"Test Ad","campaign":"Test Campaign"}', true)
];
$haystack = [
'source' => json_decode('{"source":"Test Ad","medium":"tiktok","campaign":"Test Campaign"}', true)
];
self::assertTrue(ComparatorLocator::get(RecursiveLtrArrayComparator::class)->compare($needle, $haystack));
}
}

View file

@ -1,29 +0,0 @@
<?php
/**
* PHP 7.1
*
* @category CallbackSerializerDecoratorTest
* @package Pock\Tests\Decorator
*/
namespace Pock\Tests\Decorator;
use PHPUnit\Framework\TestCase;
use Pock\Serializer\CallbackSerializerDecorator;
/**
* Class CallbackSerializerDecoratorTest
*
* @category CallbackSerializerDecoratorTest
* @package Pock\Tests\Decorator
*/
class CallbackSerializerDecoratorTest extends TestCase
{
public function testSerialize(): void
{
self::assertEquals('{}', (new CallbackSerializerDecorator(function ($data) {
return $data;
}))->serialize('{}'));
}
}

View file

@ -12,7 +12,7 @@ namespace Pock\Tests\Factory;
use PHPUnit\Framework\TestCase;
use Pock\Factory\JsonSerializerFactory;
use Pock\Factory\XmlSerializerFactory;
use Pock\Serializer\CallbackSerializerDecorator;
use Pock\Serializer\CallbackSerializerAdapter;
use Pock\Serializer\SerializerInterface;
use Pock\TestUtils\EmptyJsonSerializerDecorator;
use Pock\TestUtils\EmptyXmlSerializerDecorator;

View file

@ -0,0 +1,62 @@
<?php
/**
* PHP version 7.3
*
* @category ExactFormDataMatcherTest
* @package Pock\Tests\Matchers
*/
namespace Pock\Tests\Matchers;
use Pock\Matchers\ExactFormDataMatcher;
use Pock\TestUtils\PockTestCase;
/**
* Class ExactFormDataMatcherTest
*
* @category ExactFormDataMatcherTest
* @package Pock\Tests\Matchers
*/
class ExactFormDataMatcherTest extends PockTestCase
{
public function testInvalidData(): void
{
$matcher = new ExactFormDataMatcher(['field3' => 'value3']);
$request = self::getRequestWithBody('doesn\'t look like form-data at all');
self::assertFalse($matcher->matches($request));
}
public function testNoMatches(): void
{
$matcher = new ExactFormDataMatcher(['field3' => 'value3']);
$request = self::getRequestWithBody('field1=value1&field2=value2');
self::assertFalse($matcher->matches($request));
}
public function testNoMatchesByValue(): void
{
$matcher = new ExactFormDataMatcher(['field1' => 'value2']);
$request = self::getRequestWithBody('field1=value1&field2=value2');
self::assertFalse($matcher->matches($request));
}
public function testNoMatchesRedundantParam(): void
{
$matcher = new ExactFormDataMatcher(['field2' => 'value2']);
$request = self::getRequestWithBody('field1=value1&field2=value2');
self::assertFalse($matcher->matches($request));
}
public function testMatches(): void
{
$matcher = new ExactFormDataMatcher(['field1' => 'value1', 'field2' => 'value2']);
$request = self::getRequestWithBody('field1=value1&field2=value2');
self::assertTrue($matcher->matches($request));
}
}

View file

@ -0,0 +1,62 @@
<?php
/**
* PHP version 7.3
*
* @category FormDataMatcherTest
* @package Pock\Tests\Matchers
*/
namespace Pock\Tests\Matchers;
use Pock\Matchers\FormDataMatcher;
use Pock\TestUtils\PockTestCase;
/**
* Class FormDataMatcherTest
*
* @category FormDataMatcherTest
* @package Pock\Tests\Matchers
*/
class FormDataMatcherTest extends PockTestCase
{
public function testInvalidData(): void
{
$matcher = new FormDataMatcher(['field3' => 'value3']);
$request = self::getRequestWithBody('doesn\'t look like form-data at all');
self::assertFalse($matcher->matches($request));
}
public function testNoMatches(): void
{
$matcher = new FormDataMatcher(['field3' => 'value3']);
$request = self::getRequestWithBody('field1=value1&field2=value2');
self::assertFalse($matcher->matches($request));
}
public function testNoMatchesByValue(): void
{
$matcher = new FormDataMatcher(['field1' => 'value2']);
$request = self::getRequestWithBody('field1=value1&field2=value2');
self::assertFalse($matcher->matches($request));
}
public function testNoMatchesExtraValues(): void
{
$matcher = new FormDataMatcher(['field2' => 'value2']);
$request = self::getRequestWithBody('field1=value1&field2=value2');
self::assertFalse($matcher->matches($request));
}
public function testMatches(): void
{
$matcher = new FormDataMatcher(['field1' => 'value1', 'field2' => 'value2']);
$request = self::getRequestWithBody('field1=value1&field2=value2');
self::assertTrue($matcher->matches($request));
}
}

View file

@ -0,0 +1,59 @@
<?php
/**
* PHP version 7.3
*
* @category MultipartFormDataMatcherTest
* @package Pock\Tests\Matchers
*/
namespace Pock\Tests\Matchers;
use Http\Message\MultipartStream\MultipartStreamBuilder;
use Pock\Matchers\MultipartFormDataMatcher;
use Pock\TestUtils\PockTestCase;
use Psr\Http\Message\RequestInterface;
use Riverline\MultiPartParser\StreamedPart;
/**
* Class MultipartFormDataMatcherTest
*
* @category MultipartFormDataMatcherTest
* @package Pock\Tests\Matchers
*/
class MultipartFormDataMatcherTest extends PockTestCase
{
public function testNoMatchesNotMultipart(): void
{
$matcher = new MultipartFormDataMatcher(function (StreamedPart $part) {
return $part->isMultiPart();
});
self::assertFalse($matcher->matches(self::getTestRequest()));
self::assertFalse($matcher->matches(self::getRequestWithBody('param=value&param2=value')));
}
public function testMatches(): void
{
$matcher = new MultipartFormDataMatcher(function (StreamedPart $part) {
return $part->isMultiPart() &&
1 === count($part->getPartsByName('param1')) &&
1 === count($part->getPartsByName('param2')) &&
'value1' === $part->getPartsByName('param1')[0]->getBody() &&
'value2' === $part->getPartsByName('param2')[0]->getBody() &&
'text/plain' === $part->getPartsByName('param1')[0]->getHeader('Content-Type');
});
$builder = new MultipartStreamBuilder(self::getPsr17Factory());
$builder->addResource('param1', 'value1', ['headers' => ['Content-Type' => 'text/plain']])
->addResource('param2', 'value2');
self::assertTrue($matcher->matches(self::getMultipartRequest($builder)));
}
private static function getMultipartRequest(MultipartStreamBuilder $builder): RequestInterface
{
return self::getPsr17Factory()->createRequest('POST', 'https://example.com')
->withHeader('Content-Type', 'multipart/form-data; boundary="' . $builder->getBoundary() . '"')
->withBody($builder->build());
}
}

View file

@ -0,0 +1,60 @@
<?php
/**
* PHP version 7.3
*
* @category PortMatcherTest
* @package Pock\Tests\Matchers
*/
namespace Pock\Tests\Matchers;
use Nyholm\Psr7\Uri;
use Pock\Enum\RequestScheme;
use Pock\Matchers\PortMatcher;
use Pock\TestUtils\PockTestCase;
/**
* Class PortMatcherTest
*
* @category PortMatcherTest
* @package Pock\Tests\Matchers
*/
class PortMatcherTest extends PockTestCase
{
public function testNotMatches(): void
{
self::assertFalse((new PortMatcher(80))->matches(static::getTestRequest()));
}
public function testMatches(): void
{
self::assertTrue((new PortMatcher(443))->matches(static::getTestRequest()));
}
public function testMatchesWithoutProto(): void
{
self::assertTrue((new PortMatcher(80))->matches(static::getTestRequest()->withUri(new class extends Uri {
public function getScheme(): string
{
return RequestScheme::HTTP;
}
public function getPort(): ?int
{
return null;
}
})));
self::assertTrue((new PortMatcher(443))->matches(static::getTestRequest()->withUri(new class extends Uri {
public function getScheme(): string
{
return RequestScheme::HTTPS;
}
public function getPort(): ?int
{
return null;
}
})));
}
}

View file

@ -0,0 +1,38 @@
<?php
/**
* PHP version 7.3
*
* @category RegExpBodyMatcherTest
* @package Pock\Tests\Matchers
*/
namespace Pock\Tests\Matchers;
use Pock\Matchers\RegExpBodyMatcher;
use Pock\TestUtils\PockTestCase;
/**
* Class RegExpBodyMatcherTest
*
* @category RegExpBodyMatcherTest
* @package Pock\Tests\Matchers
*/
class RegExpBodyMatcherTest extends PockTestCase
{
public function testNoMatches(): void
{
$matcher = new RegExpBodyMatcher('/\d+-\d+/m');
$request = static::getRequestWithBody('test unmatchable request');
self::assertFalse($matcher->matches($request));
}
public function testMatches(): void
{
$matcher = new RegExpBodyMatcher('/\d+-\d+/m', PREG_UNMATCHED_AS_NULL);
$request = static::getRequestWithBody('23-900');
self::assertTrue($matcher->matches($request));
}
}

View file

@ -0,0 +1,38 @@
<?php
/**
* PHP version 7.3
*
* @category RegExpPathMatcherTest
* @package Pock\Tests\Matchers
*/
namespace Pock\Tests\Matchers;
use Pock\Matchers\RegExpPathMatcher;
use Pock\TestUtils\PockTestCase;
/**
* Class RegExpPathMatcherTest
*
* @category RegExpPathMatcherTest
* @package Pock\Tests\Matchers
*/
class RegExpPathMatcherTest extends PockTestCase
{
public function testNoMatches(): void
{
$matcher = new RegExpPathMatcher('/\/?\d+-\d+/m');
$request = static::getTestRequest()->withUri(static::getPsr17Factory()->createUri('https://test.com/test'));
self::assertFalse($matcher->matches($request));
}
public function testMatches(): void
{
$matcher = new RegExpPathMatcher('/\d+-\d+/m', PREG_UNMATCHED_AS_NULL);
$request = static::getTestRequest()->withUri(static::getPsr17Factory()->createUri('https://test.com/23-900'));
self::assertTrue($matcher->matches($request));
}
}

View file

@ -0,0 +1,44 @@
<?php
/**
* PHP version 7.3
*
* @category RegExpQueryMatcherTest
* @package Pock\Tests\Matchers
*/
namespace Pock\Tests\Matchers;
use Pock\Matchers\RegExpQueryMatcher;
use Pock\TestUtils\PockTestCase;
/**
* Class RegExpQueryMatcherTest
*
* @category RegExpQueryMatcherTest
* @package Pock\Tests\Matchers
*/
class RegExpQueryMatcherTest extends PockTestCase
{
public function testNoMatches(): void
{
$matcher = new RegExpQueryMatcher('/\d+-\d+/m');
$request = static::getTestRequest()->withUri(
static::getPsr17Factory()->createUri('https://test.com')
->withQuery('param=value')
);
self::assertFalse($matcher->matches($request));
}
public function testMatches(): void
{
$matcher = new RegExpQueryMatcher('/\d+-\d+/m', PREG_UNMATCHED_AS_NULL);
$request = static::getTestRequest()->withUri(
static::getPsr17Factory()->createUri('https://test.com')
->withQuery('param=23-900')
);
self::assertTrue($matcher->matches($request));
}
}

View file

@ -0,0 +1,41 @@
<?php
/**
* PHP version 7.3
*
* @category RegExpUriMatcherTest
* @package Pock\Tests\Matchers
*/
namespace Pock\Tests\Matchers;
use Pock\Matchers\RegExpUriMatcher;
use Pock\TestUtils\PockTestCase;
/**
* Class RegExpUriMatcherTest
*
* @category RegExpUriMatcher
* @package Pock\Tests\Matchers
*/
class RegExpUriMatcherTest extends PockTestCase
{
public function testNoMatches(): void
{
$matcher = new RegExpUriMatcher('/https\:\/\/\w+\.com\/\d+-\d+\?param=\d+-\d+/m');
$request = static::getTestRequest();
self::assertFalse($matcher->matches($request));
}
public function testMatches(): void
{
$matcher = new RegExpUriMatcher('/https\:\/\/\w+\.com\/\d+-\d+\?param=\d+-\d+/m', PREG_UNMATCHED_AS_NULL);
$request = static::getTestRequest()->withUri(
static::getPsr17Factory()->createUri('https://example.com/23-900')
->withQuery('param=23-900')
);
self::assertTrue($matcher->matches($request));
}
}

View file

@ -0,0 +1,62 @@
<?php
/**
* PHP 7.3
*
* @category XmlBodyMatcherTest
* @package Pock\Tests\Matchers
*/
namespace Pock\Tests\Matchers;
use Pock\Matchers\XmlBodyMatcher;
use Pock\TestUtils\PockTestCase;
/**
* Class XmlBodyMatcherTest
*
* @category XmlBodyMatcherTest
* @package Pock\Tests\Matchers
*/
class XmlBodyMatcherTest extends PockTestCase
{
public function testEmptyXml(): void
{
$this->expectExceptionMessage('XML must not be empty.');
new XmlBodyMatcher('');
}
public function testInvalidXml(): void
{
$brokenXml = <<<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<result>
<field><![CDATA[test]></field>
</result>
EOF;
$this->expectExceptionMessage('DOMDocument::loadXML(): CData section not finished');
new XmlBodyMatcher($brokenXml);
}
public function testMatchXml(): void
{
$expected = <<<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<result>
<field key="2" id="1"><![CDATA[test]]></field>
</result>
EOF;
$actual = <<<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<result>
<field id="1" key="2">
<![CDATA[test]]>
</field>
</result>
EOF;
self::assertTrue((new XmlBodyMatcher($expected))->matches(static::getRequestWithBody($actual)));
}
}

View file

@ -9,15 +9,22 @@
namespace Pock\Tests;
use DOMDocument;
use Http\Message\MultipartStream\MultipartStreamBuilder;
use Pock\Enum\RequestMethod;
use Pock\Enum\RequestScheme;
use Pock\Exception\UniversalMockException;
use Pock\Exception\UnsupportedRequestException;
use Pock\Matchers\XmlBodyMatcher;
use Pock\PockBuilder;
use Pock\PockResponseBuilder;
use Pock\TestUtils\PockTestCase;
use Pock\TestUtils\SimpleObject;
use Pock\TestUtils\TestReplyFactory;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Client\NetworkExceptionInterface;
use Psr\Http\Client\RequestExceptionInterface;
use Psr\Http\Message\RequestInterface;
use Riverline\MultiPartParser\StreamedPart;
use RuntimeException;
/**
@ -28,6 +35,11 @@ use RuntimeException;
*/
class PockBuilderTest extends PockTestCase
{
protected function setUp(): void
{
XmlBodyMatcher::$forceTextComparison = false;
}
public function testNoHit(): void
{
$this->expectException(UnsupportedRequestException::class);
@ -35,7 +47,7 @@ class PockBuilderTest extends PockTestCase
->createRequest(RequestMethod::GET, self::TEST_URI));
}
public function testThrowException(): void
public function testThrowClientException(): void
{
$this->expectException(ClientExceptionInterface::class);
@ -43,13 +55,60 @@ class PockBuilderTest extends PockTestCase
$builder->matchMethod(RequestMethod::GET)
->matchScheme(RequestScheme::HTTPS)
->matchHost(self::TEST_HOST)
->throwException(new UniversalMockException('Boom!'));
->throwClientException();
$builder->getClient()->sendRequest(
self::getPsr17Factory()->createRequest(RequestMethod::GET, self::TEST_URI)
);
}
public function testThrowNetworkException(): void
{
$this->expectException(NetworkExceptionInterface::class);
$builder = new PockBuilder();
$builder->matchMethod(RequestMethod::GET)
->matchScheme(RequestScheme::HTTPS)
->matchHost(self::TEST_HOST)
->throwNetworkException();
$builder->getClient()->sendRequest(
self::getPsr17Factory()->createRequest(RequestMethod::GET, self::TEST_URI)
);
}
public function testThrowRequestException(): void
{
$this->expectException(RequestExceptionInterface::class);
$builder = new PockBuilder();
$builder->matchMethod(RequestMethod::GET)
->matchScheme(RequestScheme::HTTPS)
->matchHost(self::TEST_HOST)
->throwRequestException();
$builder->getClient()->sendRequest(
self::getPsr17Factory()->createRequest(RequestMethod::GET, self::TEST_URI)
);
}
public function testThrowRequestExceptionGetRequest(): void
{
$builder = new PockBuilder();
$request = self::getPsr17Factory()->createRequest(RequestMethod::GET, self::TEST_URI);
$builder->matchMethod(RequestMethod::GET)
->matchScheme(RequestScheme::HTTPS)
->matchHost(self::TEST_HOST)
->throwRequestException();
try {
$builder->getClient()->sendRequest($request);
} catch (RequestExceptionInterface $exception) {
self::assertEquals($request, $exception->getRequest());
}
}
public function testMatchHeader(): void
{
$builder = new PockBuilder();
@ -94,6 +153,46 @@ class PockBuilderTest extends PockTestCase
self::assertEquals('Successful', $response->getBody()->getContents());
}
public function testMatchOriginFailure(): void
{
$incorrectOrigin = RequestScheme::HTTPS . ':///' . self::TEST_HOST;
$this->expectExceptionMessage($incorrectOrigin);
$builder = new PockBuilder();
$builder->matchMethod(RequestMethod::GET)
->matchOrigin($incorrectOrigin)
->reply(200)
->withHeader('Content-Type', 'text/plain')
->withBody('Successful');
$builder->getClient()->sendRequest(
self::getPsr17Factory()
->createRequest(RequestMethod::GET, 'https://another-example.com')
);
}
public function testMatchOrigin(): void
{
$origin = RequestScheme::HTTPS . '://' . self::TEST_HOST . ':443';
$builder = new PockBuilder();
$builder->matchMethod(RequestMethod::GET)
->matchOrigin($origin)
->reply(200)
->withHeader('Content-Type', 'text/plain')
->withBody('Successful');
$response = $builder->getClient()->sendRequest(
self::getPsr17Factory()
->createRequest(RequestMethod::GET, $origin)
);
self::assertEquals(200, $response->getStatusCode());
self::assertEquals(['Content-Type' => ['text/plain']], $response->getHeaders());
self::assertEquals('Successful', $response->getBody()->getContents());
}
public function testMatchExactHeader(): void
{
$builder = new PockBuilder();
@ -270,7 +369,124 @@ class PockBuilderTest extends PockTestCase
], json_decode($response->getBody()->getContents(), true));
}
public function testXmlResponse(): void
public function testMatchXmlString(): void
{
$xml = <<<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<result>
<entry><![CDATA[Forbidden]]></entry>
</result>
EOF;
$simpleObject = <<<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<result>
<field><![CDATA[test]]></field>
</result>
EOF;
$builder = new PockBuilder();
$builder->matchMethod(RequestMethod::GET)
->matchScheme(RequestScheme::HTTPS)
->matchHost(self::TEST_HOST)
->matchXmlBody($simpleObject)
->repeat(2)
->reply(403)
->withHeader('Content-Type', 'text/xml')
->withXml(['error' => 'Forbidden']);
$response = $builder->getClient()->sendRequest(
self::getPsr17Factory()
->createRequest(RequestMethod::GET, self::TEST_URI)
->withBody(self::getPsr17Factory()->createStream($simpleObject))
);
self::assertEquals(403, $response->getStatusCode());
self::assertEquals(['Content-Type' => ['text/xml']], $response->getHeaders());
self::assertEquals($xml, $response->getBody()->getContents());
}
public function testMatchXmlStream(): void
{
$xml = <<<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<result>
<entry><![CDATA[Forbidden]]></entry>
</result>
EOF;
$simpleObject = <<<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<result>
<field><![CDATA[test]]></field>
</result>
EOF;
$builder = new PockBuilder();
$builder->matchMethod(RequestMethod::GET)
->matchScheme(RequestScheme::HTTPS)
->matchHost(self::TEST_HOST)
->matchXmlBody(self::getPsr17Factory()->createStream($simpleObject))
->repeat(2)
->reply(403)
->withHeader('Content-Type', 'text/xml')
->withXml(['error' => 'Forbidden']);
$response = $builder->getClient()->sendRequest(
self::getPsr17Factory()
->createRequest(RequestMethod::GET, self::TEST_URI)
->withBody(self::getPsr17Factory()->createStream($simpleObject))
);
self::assertEquals(403, $response->getStatusCode());
self::assertEquals(['Content-Type' => ['text/xml']], $response->getHeaders());
self::assertEquals($xml, $response->getBody()->getContents());
}
public function testMatchXmlDOMDocument(): void
{
$xml = <<<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<result>
<entry><![CDATA[Forbidden]]></entry>
</result>
EOF;
$simpleObject = <<<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<result>
<field><![CDATA[test]]></field>
</result>
EOF;
$document = new DOMDocument();
$document->loadXML($simpleObject);
$builder = new PockBuilder();
$builder->matchMethod(RequestMethod::GET)
->matchScheme(RequestScheme::HTTPS)
->matchHost(self::TEST_HOST)
->matchXmlBody($document)
->repeat(2)
->reply(403)
->withHeader('Content-Type', 'text/xml')
->withXml(['error' => 'Forbidden']);
$response = $builder->getClient()->sendRequest(
self::getPsr17Factory()
->createRequest(RequestMethod::GET, self::TEST_URI)
->withBody(self::getPsr17Factory()->createStream($simpleObject))
);
self::assertEquals(403, $response->getStatusCode());
self::assertEquals(['Content-Type' => ['text/xml']], $response->getHeaders());
self::assertEquals($xml, $response->getBody()->getContents());
}
/**
* @dataProvider matchXmlNoXslProvider
*/
public function testMatchXmlNoXsl(string $simpleObject, bool $expectException): void
{
$xml = <<<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
@ -280,11 +496,64 @@ class PockBuilderTest extends PockTestCase
EOF;
if ($expectException) {
$this->expectException(UnsupportedRequestException::class);
}
$document = new DOMDocument();
$document->loadXML($simpleObject);
XmlBodyMatcher::$forceTextComparison = true;
$builder = new PockBuilder();
$builder->matchMethod(RequestMethod::GET)
->matchScheme(RequestScheme::HTTPS)
->matchHost(self::TEST_HOST)
->matchXmlBody(new SimpleObject())
->matchXmlBody($document)
->repeat(2)
->reply(403)
->withHeader('Content-Type', 'text/xml')
->withXml(['error' => 'Forbidden']);
$response = $builder->getClient()->sendRequest(
self::getPsr17Factory()
->createRequest(RequestMethod::GET, self::TEST_URI)
->withBody(self::getPsr17Factory()->createStream($simpleObject))
);
self::assertEquals(403, $response->getStatusCode());
self::assertEquals(['Content-Type' => ['text/xml']], $response->getHeaders());
self::assertEquals($xml, $response->getBody()->getContents());
}
public function testSerializedXmlResponse(): void
{
$xml = <<<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<result>
<entry><![CDATA[Forbidden]]></entry>
</result>
EOF;
$simpleObjectFreeFormXml = <<<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<result>
<field>
<![CDATA[test]]>
</field>
</result>
EOF;
$builder = new PockBuilder();
$builder->matchMethod(RequestMethod::GET)
->matchScheme(RequestScheme::HTTPS)
->matchHost(self::TEST_HOST)
->matchSerializedXmlBody(new SimpleObject())
->repeat(2)
->reply(403)
->withHeader('Content-Type', 'text/xml')
->withXml(['error' => 'Forbidden']);
@ -293,13 +562,144 @@ EOF;
self::getPsr17Factory()
->createRequest(RequestMethod::GET, self::TEST_URI)
->withBody(self::getPsr17Factory()->createStream(
self::getXmlSerializer()->serialize(new SimpleObject())
PHP_EOL . self::getXmlSerializer()->serialize(new SimpleObject()) . PHP_EOL
))
);
self::assertEquals(403, $response->getStatusCode());
self::assertEquals(['Content-Type' => ['text/xml']], $response->getHeaders());
self::assertEquals($xml, $response->getBody()->getContents());
$response = $builder->getClient()->sendRequest(
self::getPsr17Factory()
->createRequest(RequestMethod::GET, self::TEST_URI)
->withBody(self::getPsr17Factory()->createStream($simpleObjectFreeFormXml))
);
self::assertEquals(403, $response->getStatusCode());
self::assertEquals(['Content-Type' => ['text/xml']], $response->getHeaders());
self::assertEquals($xml, $response->getBody()->getContents());
}
public function testMultipartFormDataMock(): void
{
$builder = new PockBuilder();
$builder->matchMethod(RequestMethod::POST)
->matchScheme(RequestScheme::HTTPS)
->matchHost(self::TEST_HOST)
->matchMultipartFormData(function (StreamedPart $part) {
return $part->isMultiPart() &&
1 === count($part->getPartsByName('param1')) &&
1 === count($part->getPartsByName('param2')) &&
'value1' === $part->getPartsByName('param1')[0]->getBody() &&
'value2' === $part->getPartsByName('param2')[0]->getBody() &&
'text/plain' === $part->getPartsByName('param1')[0]->getHeader('Content-Type');
})->reply(200)
->withHeader('Content-Type', 'text/plain')
->withBody('ok');
$streamBuilder = (new MultipartStreamBuilder(self::getPsr17Factory()))
->addResource('param1', 'value1', ['headers' => ['Content-Type' => 'text/plain']])
->addResource('param2', 'value2');
$response = $builder->getClient()->sendRequest(
self::getPsr17Factory()
->createRequest(RequestMethod::POST, self::TEST_URI)
->withHeader('Content-Type', 'multipart/form-data; boundary="' . $streamBuilder->getBoundary() . '"')
->withBody($streamBuilder->build())
);
self::assertEquals(200, $response->getStatusCode());
self::assertEquals(['Content-Type' => ['text/plain']], $response->getHeaders());
self::assertEquals('ok', $response->getBody()->getContents());
}
public function testMatchBodyRegExp(): void
{
$builder = new PockBuilder();
$builder->matchMethod(RequestMethod::GET)
->matchUri(self::TEST_URI)
->matchBodyRegExp('/\d+-\d+/')
->reply(200);
$response = $builder->getClient()->sendRequest(static::getRequestWithBody('test matchable 23-900'));
self::assertEquals(200, $response->getStatusCode());
}
public function testMatchPathRegExp(): void
{
$builder = new PockBuilder();
$builder->matchMethod(RequestMethod::GET)
->matchOrigin(self::TEST_HOST)
->matchPathRegExp('/^\/?test$/')
->reply(200);
$response = $builder->getClient()->sendRequest(
static::getTestRequest()->withUri(static::getPsr17Factory()->createUri('https://test.com/test'))
);
self::assertEquals(200, $response->getStatusCode());
}
public function testMatchQueryRegExp(): void
{
$builder = new PockBuilder();
$builder->matchMethod(RequestMethod::GET)
->matchOrigin(self::TEST_HOST)
->matchQueryRegExp('/\d+-\d+/')
->reply(200);
$response = $builder->getClient()->sendRequest(
static::getTestRequest()->withUri(
static::getPsr17Factory()->createUri(self::TEST_URI)
->withQuery('param=23-900')
)
);
self::assertEquals(200, $response->getStatusCode());
}
public function testMatchUriRegExp(): void
{
$builder = new PockBuilder();
$builder->matchMethod(RequestMethod::GET)
->matchUriRegExp('/https\:\/\/\w+\.com\/\d+-\d+\?param=\d+-\d+/')
->reply(200);
$response = $builder->getClient()->sendRequest(
static::getTestRequest()->withUri(
static::getPsr17Factory()->createUri('https://example.com/23-900')
->withQuery('param=23-900')
)
);
self::assertEquals(200, $response->getStatusCode());
}
public function testMatchFormData(): void
{
$builder = new PockBuilder();
$builder->matchMethod(RequestMethod::GET)
->matchUri(self::TEST_URI)
->matchFormData(['field1' => 'value1', 'field2' => 'value2'])
->reply(200);
$response = $builder->getClient()->sendRequest(self::getRequestWithBody('field1=value1&field2=value2'));
self::assertEquals(200, $response->getStatusCode());
}
public function testMatchExactFormData(): void
{
$builder = new PockBuilder();
$builder->matchMethod(RequestMethod::GET)
->matchUri(self::TEST_URI)
->matchExactFormData(['field2' => 'value2'])
->reply(200);
$response = $builder->getClient()->sendRequest(self::getRequestWithBody('field2=value2'));
self::assertEquals(200, $response->getStatusCode());
}
public function testFirstExampleApiMock(): void
@ -434,6 +834,19 @@ EOF;
->withHeader('Content-Type', 'text/plain')
->withBody('Second token (post json)');
$builder->matchMethod(RequestMethod::POST)
->matchScheme(RequestScheme::HTTPS)
->matchHost(self::TEST_HOST)
->matchPath('/ping')
->matchHeaders([
'Authorization' => 'Token token_3',
'Content-Type' => 'application/json'
])
->matchSerializedJsonBody('{"field": "value3"}')
->reply(200)
->withHeader('Content-Type', 'text/plain')
->withBody('Third token (post json with match against serialized data)');
$builder->matchMethod(RequestMethod::POST)
->matchScheme(RequestScheme::HTTPS)
->matchHost(self::TEST_HOST)
@ -502,6 +915,19 @@ EOF;
);
self::assertEquals('Second token (post json)', $response->getBody()->getContents());
$response = $client->sendRequest(
self::getPsr17Factory()
->createRequest(RequestMethod::POST, self::TEST_URI)
->withHeader('Authorization', 'Token token_3')
->withHeader('Content-Type', 'application/json')
->withUri(self::getPsr17Factory()->createUri(self::TEST_URI . 'ping'))
->withBody(self::getPsr17Factory()->createStream('{"field": "value3"}'))
);
self::assertEquals(
'Third token (post json with match against serialized data)',
$response->getBody()->getContents()
);
$response = $client->sendRequest(
self::getPsr17Factory()
->createRequest(RequestMethod::POST, self::TEST_URI)
@ -511,4 +937,141 @@ EOF;
);
self::assertEquals('Second token (post)', $response->getBody()->getContents());
}
public function testAlways(): void
{
$builder = new PockBuilder();
$builder->matchMethod(RequestMethod::GET)
->matchUri(self::TEST_URI)
->always()
->reply(200)
->withHeader('Content-Type', 'text/plain')
->withBody('Successful');
for ($i = 0; $i < 10; $i++) {
$response = $builder->getClient()->sendRequest(
self::getPsr17Factory()->createRequest(RequestMethod::GET, self::TEST_URI)
);
self::assertEquals(200, $response->getStatusCode());
self::assertEquals(['Content-Type' => ['text/plain']], $response->getHeaders());
self::assertEquals('Successful', $response->getBody()->getContents());
}
}
public function testAt(): void
{
$builder = new PockBuilder();
$builder->matchMethod(RequestMethod::GET)
->matchUri(self::TEST_URI)
->at(2)
->reply(200);
$builder->matchMethod(RequestMethod::GET)
->matchUri(self::TEST_URI)
->at(4)
->reply(201);
$builder->always()->reply(400);
$builder->getClient();
for ($i = 0; $i < 5; $i++) {
$response = $builder->getClient()->sendRequest(
self::getPsr17Factory()->createRequest(RequestMethod::GET, self::TEST_URI)
);
self::assertEquals(1 === $i ? 200 : (4 === $i ? 201 : 400), $response->getStatusCode());
}
}
public function testReplyWithFactory(): void
{
$builder = new PockBuilder();
$builder->matchMethod(RequestMethod::GET)
->matchUri(self::TEST_URI)
->always()
->replyWithFactory(new TestReplyFactory());
for ($i = 0; $i < 5; $i++) {
$response = $builder->getClient()->sendRequest(
self::getPsr17Factory()->createRequest(RequestMethod::GET, self::TEST_URI)
);
self::assertEquals(200, $response->getStatusCode());
self::assertEquals('Request #' . ($i + 1), $response->getBody()->getContents());
}
}
public function testReplyWithCallback(): void
{
$builder = new PockBuilder();
$builder->matchMethod(RequestMethod::GET)
->matchUri(self::TEST_URI)
->always()
->replyWithCallback(static function (RequestInterface $request, PockResponseBuilder $responseBuilder) {
return $responseBuilder->withStatusCode(200)
->withBody(self::TEST_URI)
->getResponse();
});
$response = $builder->getClient()->sendRequest(
self::getPsr17Factory()->createRequest(RequestMethod::GET, self::TEST_URI)
);
self::assertEquals(200, $response->getStatusCode());
self::assertEquals(self::TEST_URI, $response->getBody()->getContents());
}
public function testReplyWithCallbackException(): void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Exception from the callback');
$builder = new PockBuilder();
$builder->matchMethod(RequestMethod::GET)
->matchUri(self::TEST_URI)
->always()
->replyWithCallback(static function (RequestInterface $request, PockResponseBuilder $responseBuilder) {
throw new RuntimeException('Exception from the callback');
});
$builder->getClient()->sendRequest(self::getPsr17Factory()->createRequest(
RequestMethod::GET,
self::TEST_URI
));
}
public function testReplyWithClient(): void
{
$inlined = new PockBuilder();
$inlined->reply(429);
$builder = new PockBuilder();
$builder->matchMethod(RequestMethod::GET)
->matchUri(self::TEST_URI)
->always()
->replyWithClient($inlined->getClient());
$response = $builder->getClient()->sendRequest(self::getPsr17Factory()->createRequest(
RequestMethod::GET,
self::TEST_URI
));
self::assertEquals(429, $response->getStatusCode());
}
public function matchXmlNoXslProvider(): array
{
$simpleObject = <<<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<result>
<field><![CDATA[test]]></field>
</result>
EOF;
return [
[$simpleObject, true],
[$simpleObject . "\n", false]
];
}
}

View file

@ -0,0 +1,29 @@
<?php
/**
* PHP 7.1
*
* @category CallbackSerializerAdapterTest
* @package Pock\Tests\Serializer
*/
namespace Pock\Tests\Serializer;
use PHPUnit\Framework\TestCase;
use Pock\Serializer\CallbackSerializerAdapter;
/**
* Class CallbackSerializerAdapterTest
*
* @category CallbackSerializerAdapterTest
* @package Pock\Tests\Serializer
*/
class CallbackSerializerAdapterTest extends TestCase
{
public function testSerialize(): void
{
self::assertEquals('{}', (new CallbackSerializerAdapter(function ($data) {
return $data;
}))->serialize('{}'));
}
}

View file

@ -44,6 +44,19 @@ abstract class PockTestCase extends TestCase
return static::getPsr17Factory()->createRequest($method ?? static::TEST_METHOD, static::TEST_URI);
}
/**
* @param string $body
*
* @return \Psr\Http\Message\RequestInterface
*/
protected static function getRequestWithBody(string $body): RequestInterface
{
return static::getPsr17Factory()->createRequest(
RequestMethod::GET,
static::TEST_URI
)->withBody(self::getPsr17Factory()->createStream($body));
}
/**
* @return \Nyholm\Psr7\Factory\Psr17Factory
*/

View file

@ -0,0 +1,37 @@
<?php
/**
* PHP 7.3
*
* @category TestReplyFactory
* @package Pock\TestUtils
*/
namespace Pock\TestUtils;
use Pock\Factory\ReplyFactoryInterface;
use Pock\PockResponseBuilder;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Class TestReplyFactory
*
* @category TestReplyFactory
* @package Pock\TestUtils
*/
class TestReplyFactory implements ReplyFactoryInterface
{
/** @var int */
private $requestNumber = 0;
/**
* @inheritDoc
*/
public function createReply(RequestInterface $request, PockResponseBuilder $responseBuilder): ResponseInterface
{
return $responseBuilder->withStatusCode(200)
->withBody('Request #' . ++$this->requestNumber)
->getResponse();
}
}