better xml matcher, refactor comparators

This commit is contained in:
Pavel 2021-05-21 18:05:43 +03:00
parent b08ba6f6cd
commit e352c310d7
21 changed files with 917 additions and 154 deletions

View File

@ -47,7 +47,8 @@
"jms/serializer": "^2 | ^3.12", "jms/serializer": "^2 | ^3.12",
"symfony/phpunit-bridge": "^5.2", "symfony/phpunit-bridge": "^5.2",
"symfony/serializer": "^5.2", "symfony/serializer": "^5.2",
"symfony/property-access": "^5.2" "symfony/property-access": "^5.2",
"php-mock/php-mock": "^2.3"
}, },
"provide": { "provide": {
"psr/http-client-implementation": "1.0", "psr/http-client-implementation": "1.0",

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,64 @@
<?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) && !self::recursiveCompareArrays($value, $second[$key])) {
return false;
}
if ($value !== $second[$key]) {
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,61 @@
<?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) && !self::recursiveCompareArrays($value, $haystack[$key])) {
return false;
}
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

@ -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

@ -9,6 +9,8 @@
namespace Pock\Matchers; namespace Pock\Matchers;
use Pock\Comparator\ComparatorLocator;
use Pock\Comparator\RecursiveArrayComparator;
use Pock\Traits\SeekableStreamDataExtractor; use Pock\Traits\SeekableStreamDataExtractor;
use Psr\Http\Message\RequestInterface; use Psr\Http\Message\RequestInterface;
@ -18,7 +20,7 @@ use Psr\Http\Message\RequestInterface;
* @category AbstractSerializedBodyMatcher * @category AbstractSerializedBodyMatcher
* @package Pock\Matchers * @package Pock\Matchers
*/ */
abstract class AbstractSerializedBodyMatcher extends AbstractArrayPoweredComponent implements RequestMatcherInterface abstract class AbstractSerializedBodyMatcher implements RequestMatcherInterface
{ {
use SeekableStreamDataExtractor; use SeekableStreamDataExtractor;
@ -54,7 +56,7 @@ abstract class AbstractSerializedBodyMatcher extends AbstractArrayPoweredCompone
return false; 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) public function __construct($contents)
{ {
if (is_string($contents)) { $this->contents = static::getEntryItemData($contents);
$this->contents = $contents;
}
if ($contents instanceof StreamInterface) {
$this->contents = static::getStreamData($contents);
}
if (is_resource($contents)) {
$this->contents = static::readAllResource($contents);
}
} }
/** /**
@ -70,4 +60,24 @@ class BodyMatcher implements RequestMatcherInterface
fseek($resource, 0); fseek($resource, 0);
return (string) stream_get_contents($resource); 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);
}
}
} }

View File

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

View File

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

View File

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

View File

@ -0,0 +1,194 @@
<?php
/**
* PHP 7.3
*
* @category XmlBodyMatcher
* @package Pock\Matchers
*/
namespace Pock\Matchers;
use DOMDocument;
use Pock\Exception\XmlException;
use Throwable;
use XSLTProcessor;
use Pock\Traits\SeekableStreamDataExtractor;
use Psr\Http\Message\RequestInterface;
use RuntimeException;
/**
* 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 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 (!extension_loaded('xsl') || !extension_loaded('dom')) {
$this->useFallback = true;
}
if (!extension_loaded('xsl')) {
$this->useFallback = true;
if (extension_loaded('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;
}
}

View File

@ -10,11 +10,13 @@
namespace Pock; namespace Pock;
use Diff\ArrayComparer\StrictArrayComparer; use Diff\ArrayComparer\StrictArrayComparer;
use DOMDocument;
use Pock\Enum\RequestMethod; use Pock\Enum\RequestMethod;
use Pock\Enum\RequestScheme; use Pock\Enum\RequestScheme;
use Pock\Exception\PockClientException; use Pock\Exception\PockClientException;
use Pock\Exception\PockNetworkException; use Pock\Exception\PockNetworkException;
use Pock\Exception\PockRequestException; use Pock\Exception\PockRequestException;
use Pock\Exception\XmlException;
use Pock\Factory\CallbackReplyFactory; use Pock\Factory\CallbackReplyFactory;
use Pock\Factory\ReplyFactoryInterface; use Pock\Factory\ReplyFactoryInterface;
use Pock\Matchers\AnyRequestMatcher; use Pock\Matchers\AnyRequestMatcher;
@ -36,6 +38,7 @@ use Pock\Matchers\QueryMatcher;
use Pock\Matchers\RequestMatcherInterface; use Pock\Matchers\RequestMatcherInterface;
use Pock\Matchers\SchemeMatcher; use Pock\Matchers\SchemeMatcher;
use Pock\Matchers\UriMatcher; use Pock\Matchers\UriMatcher;
use Pock\Matchers\XmlBodyMatcher;
use Pock\Traits\JsonDecoderTrait; use Pock\Traits\JsonDecoderTrait;
use Pock\Traits\JsonSerializerAwareTrait; use Pock\Traits\JsonSerializerAwareTrait;
use Pock\Traits\XmlSerializerAwareTrait; use Pock\Traits\XmlSerializerAwareTrait;
@ -286,21 +289,38 @@ class PockBuilder
)); ));
} }
/**
* 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.
*
* @see \Pock\PockBuilder::matchSerializedXmlBody()
*
* @param DOMDocument|\Psr\Http\Message\StreamInterface|resource|string $data
*
* @return self
*/
public function matchXmlBody($data): self
{
return $this->addMatcher(new XmlBodyMatcher($data));
}
/** /**
* Match XML request body. * Match XML request body.
* *
* **Note:** this method will use string comparison for now. It'll be improved in future. * This method will try to use available XML serializer before matching.
* *
* @todo Don't use simple string comparison. Match the entire body by its DOM. * @phpstan-ignore-next-line
* * @param string|array|object $data
* @param mixed $data
* *
* @return self * @return self
* @throws \Pock\Exception\XmlException * @throws \Pock\Exception\XmlException
*/ */
public function matchXmlBody($data): self public function matchSerializedXmlBody($data): self
{ {
return $this->matchBody(self::serializeXml($data) ?? ''); return $this->matchXmlBody(self::serializeXml($data) ?? '');
} }
/** /**

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,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,10 +9,11 @@
namespace Pock\Tests; namespace Pock\Tests;
use DOMDocument;
use phpmock\MockBuilder;
use Pock\Enum\RequestMethod; use Pock\Enum\RequestMethod;
use Pock\Enum\RequestScheme; use Pock\Enum\RequestScheme;
use Pock\Exception\UnsupportedRequestException; use Pock\Exception\UnsupportedRequestException;
use Pock\Factory\ReplyFactoryInterface;
use Pock\PockBuilder; use Pock\PockBuilder;
use Pock\PockResponseBuilder; use Pock\PockResponseBuilder;
use Pock\TestUtils\PockTestCase; use Pock\TestUtils\PockTestCase;
@ -22,7 +23,6 @@ use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Client\NetworkExceptionInterface; use Psr\Http\Client\NetworkExceptionInterface;
use Psr\Http\Client\RequestExceptionInterface; use Psr\Http\Client\RequestExceptionInterface;
use Psr\Http\Message\RequestInterface; use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use RuntimeException; use RuntimeException;
/** /**
@ -85,6 +85,23 @@ class PockBuilderTest extends PockTestCase
); );
} }
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 public function testMatchHeader(): void
{ {
$builder = new PockBuilder(); $builder = new PockBuilder();
@ -305,7 +322,124 @@ class PockBuilderTest extends PockTestCase
], json_decode($response->getBody()->getContents(), true)); ], 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 = <<<'EOF'
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
@ -315,11 +449,77 @@ class PockBuilderTest extends PockTestCase
EOF; EOF;
if ($expectException) {
$this->expectException(UnsupportedRequestException::class);
}
$mock = (new MockBuilder())->setNamespace('Pock\Matchers')
->setName('extension_loaded')
->setFunction(
static function (string $extension) {
if ('xsl' === $extension) {
return false;
}
return \extension_loaded($extension);
}
)->build();
$mock->enable();
$document = new DOMDocument();
$document->loadXML($simpleObject);
$builder = new PockBuilder(); $builder = new PockBuilder();
$builder->matchMethod(RequestMethod::GET) $builder->matchMethod(RequestMethod::GET)
->matchScheme(RequestScheme::HTTPS) ->matchScheme(RequestScheme::HTTPS)
->matchHost(self::TEST_HOST) ->matchHost(self::TEST_HOST)
->matchXmlBody(new SimpleObject()) ->matchXmlBody($document)
->repeat(2)
->reply(403)
->withHeader('Content-Type', 'text/xml')
->withXml(['error' => 'Forbidden']);
$mock->disable();
$response = $builder->getClient()->sendRequest(
self::getPsr17Factory()
->createRequest(RequestMethod::GET, self::TEST_URI)
->withBody(self::getPsr17Factory()->createStream($simpleObject))
);
self::assertEquals(403, $response->getStatusCode());
self::assertEquals(['Content-Type' => ['text/xml']], $response->getHeaders());
self::assertEquals($xml, $response->getBody()->getContents());
}
public function testSerializedXmlResponse(): void
{
$xml = <<<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<result>
<entry><![CDATA[Forbidden]]></entry>
</result>
EOF;
$simpleObjectFreeFormXml = <<<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<result>
<field>
<![CDATA[test]]>
</field>
</result>
EOF;
$builder = new PockBuilder();
$builder->matchMethod(RequestMethod::GET)
->matchScheme(RequestScheme::HTTPS)
->matchHost(self::TEST_HOST)
->matchSerializedXmlBody(new SimpleObject())
->repeat(2)
->reply(403) ->reply(403)
->withHeader('Content-Type', 'text/xml') ->withHeader('Content-Type', 'text/xml')
->withXml(['error' => 'Forbidden']); ->withXml(['error' => 'Forbidden']);
@ -328,13 +528,23 @@ EOF;
self::getPsr17Factory() self::getPsr17Factory()
->createRequest(RequestMethod::GET, self::TEST_URI) ->createRequest(RequestMethod::GET, self::TEST_URI)
->withBody(self::getPsr17Factory()->createStream( ->withBody(self::getPsr17Factory()->createStream(
self::getXmlSerializer()->serialize(new SimpleObject()) PHP_EOL . self::getXmlSerializer()->serialize(new SimpleObject()) . PHP_EOL
)) ))
); );
self::assertEquals(403, $response->getStatusCode()); self::assertEquals(403, $response->getStatusCode());
self::assertEquals(['Content-Type' => ['text/xml']], $response->getHeaders()); self::assertEquals(['Content-Type' => ['text/xml']], $response->getHeaders());
self::assertEquals($xml, $response->getBody()->getContents()); 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 testFirstExampleApiMock(): void public function testFirstExampleApiMock(): void
@ -649,4 +859,18 @@ EOF;
self::TEST_URI self::TEST_URI
)); ));
} }
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

@ -44,6 +44,19 @@ abstract class PockTestCase extends TestCase
return static::getPsr17Factory()->createRequest($method ?? static::TEST_METHOD, static::TEST_URI); 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 * @return \Nyholm\Psr7\Factory\Psr17Factory
*/ */