Compare commits

...

38 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
7c62c096de remove useless dependency 2021-05-15 20:38:22 +03:00
b68c11976a fix for query matching, very basic XML body matching, fix grammar in the readme 2021-05-15 20:28:41 +03:00
285136a674 keywords 2021-05-15 19:53:38 +03:00
d8c69ad11f fix github pages link 2021-05-15 19:44:50 +03:00
66 changed files with 3113 additions and 313 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](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
@ -88,13 +91,13 @@ pock supports JMS serializer and Symfony serializer out of the box. Available se
It will be used to serialize requests and responses in mocks which means you actually can pass an entire DTO
into the corresponding methods (for example, `matchJsonBody` as an assertion or `withJsonBody` to generate a response body).
By default JMS serializer has more priority than the Symfony serializer. You can use methods below before running tests (`bootstrap.php`)
By default, JMS serializer has more priority than the Symfony serializer. You can use methods below before running tests (`bootstrap.php`)
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

@ -3,6 +3,16 @@
"description": "PSR-18 compatible HTTP mock library",
"type": "library",
"license": "MIT",
"homepage": "https://github.com/Neur0toxine/pock",
"keywords": [
"php-http",
"http",
"symfony",
"mock",
"psr-7",
"psr-18",
"mock"
],
"authors": [
{
"name": "Neur0toxine",
@ -24,21 +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/var-dumper": "^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",
@ -61,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::isNeedlePresentInHaystack($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,15 +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;
/**
@ -44,11 +62,15 @@ use Throwable;
*
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
* @SuppressWarnings(PHPMD.TooManyPublicMethods)
* @SuppressWarnings(PHPMD.TooManyMethods)
* @SuppressWarnings(PHPMD.ExcessivePublicCount)
* @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
*/
class PockBuilder
{
use JsonDecoderTrait;
use JsonSerializerAwareTrait;
use XmlSerializerAwareTrait;
/** @var \Pock\Matchers\MultipleMatcher */
private $matcher;
@ -56,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;
@ -112,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.
*
@ -124,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.
@ -217,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.
@ -230,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.
*
@ -242,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.
*
@ -254,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.
*
@ -272,11 +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.
*
* This method will try to use available XML serializer before matching.
*
* @phpstan-ignore-next-line
* @param string|array|object $data
*
* @return self
* @throws \Pock\Exception\XmlException
*/
public function matchSerializedXmlBody($data): self
{
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
*/
@ -310,6 +524,8 @@ class PockBuilder
*/
public function repeat(int $hits): self
{
$this->closePrevious();
if ($hits > 0) {
$this->maxHits = $hits;
}
@ -317,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.
*
@ -331,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
*
@ -347,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.
*
@ -355,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;
@ -387,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());
}
@ -400,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,17 +496,29 @@ 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($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)
self::getPsr17Factory()
->createRequest(RequestMethod::GET, self::TEST_URI)
->withBody(self::getPsr17Factory()->createStream($simpleObject))
);
self::assertEquals(403, $response->getStatusCode());
@ -298,6 +526,182 @@ EOF;
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']);
$response = $builder->getClient()->sendRequest(
self::getPsr17Factory()
->createRequest(RequestMethod::GET, self::TEST_URI)
->withBody(self::getPsr17Factory()->createStream(
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
{
$data = [
@ -430,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)
@ -498,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)
@ -507,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

@ -13,6 +13,9 @@ use Nyholm\Psr7\Factory\Psr17Factory;
use PHPUnit\Framework\TestCase;
use Pock\Enum\RequestMethod;
use Pock\Enum\RequestScheme;
use Pock\Factory\JsonSerializerFactory;
use Pock\Factory\XmlSerializerFactory;
use Pock\Serializer\SerializerInterface;
use Psr\Http\Message\RequestInterface;
/**
@ -41,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
*/
@ -52,4 +68,14 @@ abstract class PockTestCase extends TestCase
return static::$psr17Factory;
}
protected static function getJsonSerializer(): SerializerInterface
{
return JsonSerializerFactory::create();
}
protected static function getXmlSerializer(): SerializerInterface
{
return XmlSerializerFactory::create();
}
}

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();
}
}