From e352c310d795cae6da8f8cdd200601c7e2049a57 Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Fri, 21 May 2021 18:05:43 +0300 Subject: [PATCH] better xml matcher, refactor comparators --- composer.json | 3 +- src/Comparator/ComparatorInterface.php | 29 +++ src/Comparator/ComparatorLocator.php | 44 ++++ src/Comparator/LtrScalarArrayComparator.php | 53 ++++ src/Comparator/RecursiveArrayComparator.php | 64 +++++ .../RecursiveLtrArrayComparator.php | 61 +++++ src/Comparator/ScalarFlatArrayComparator.php | 47 ++++ .../AbstractArrayPoweredComponent.php | 120 --------- .../AbstractSerializedBodyMatcher.php | 6 +- src/Matchers/BodyMatcher.php | 32 ++- src/Matchers/ExactHeaderMatcher.php | 5 +- src/Matchers/ExactQueryMatcher.php | 4 +- src/Matchers/HeaderMatcher.php | 7 +- src/Matchers/HeadersMatcher.php | 6 +- src/Matchers/QueryMatcher.php | 7 +- src/Matchers/XmlBodyMatcher.php | 194 +++++++++++++++ src/PockBuilder.php | 32 ++- .../src/Comparator/ComparatorLocatorTest.php | 48 ++++ tests/src/Matchers/XmlBodyMatcherTest.php | 62 +++++ tests/src/PockBuilderTest.php | 234 +++++++++++++++++- tests/utils/PockTestCase.php | 13 + 21 files changed, 917 insertions(+), 154 deletions(-) create mode 100644 src/Comparator/ComparatorInterface.php create mode 100644 src/Comparator/ComparatorLocator.php create mode 100644 src/Comparator/LtrScalarArrayComparator.php create mode 100644 src/Comparator/RecursiveArrayComparator.php create mode 100644 src/Comparator/RecursiveLtrArrayComparator.php create mode 100644 src/Comparator/ScalarFlatArrayComparator.php delete mode 100644 src/Matchers/AbstractArrayPoweredComponent.php create mode 100644 src/Matchers/XmlBodyMatcher.php create mode 100644 tests/src/Comparator/ComparatorLocatorTest.php create mode 100644 tests/src/Matchers/XmlBodyMatcherTest.php diff --git a/composer.json b/composer.json index 01028f6..5c60588 100644 --- a/composer.json +++ b/composer.json @@ -47,7 +47,8 @@ "jms/serializer": "^2 | ^3.12", "symfony/phpunit-bridge": "^5.2", "symfony/serializer": "^5.2", - "symfony/property-access": "^5.2" + "symfony/property-access": "^5.2", + "php-mock/php-mock": "^2.3" }, "provide": { "psr/http-client-implementation": "1.0", diff --git a/src/Comparator/ComparatorInterface.php b/src/Comparator/ComparatorInterface.php new file mode 100644 index 0000000..cff66c2 --- /dev/null +++ b/src/Comparator/ComparatorInterface.php @@ -0,0 +1,29 @@ + $value) { + if (is_array($value) && !self::recursiveCompareArrays($value, $second[$key])) { + return false; + } + + if ($value !== $second[$key]) { + return false; + } + } + + return true; + } +} diff --git a/src/Comparator/RecursiveLtrArrayComparator.php b/src/Comparator/RecursiveLtrArrayComparator.php new file mode 100644 index 0000000..94c8876 --- /dev/null +++ b/src/Comparator/RecursiveLtrArrayComparator.php @@ -0,0 +1,61 @@ + $value) { + if (is_array($value) && !self::recursiveCompareArrays($value, $haystack[$key])) { + return false; + } + + if ($value !== $haystack[$key]) { + return false; + } + } + + return true; + } +} diff --git a/src/Comparator/ScalarFlatArrayComparator.php b/src/Comparator/ScalarFlatArrayComparator.php new file mode 100644 index 0000000..cb1fe50 --- /dev/null +++ b/src/Comparator/ScalarFlatArrayComparator.php @@ -0,0 +1,47 @@ + $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; - } -} diff --git a/src/Matchers/AbstractSerializedBodyMatcher.php b/src/Matchers/AbstractSerializedBodyMatcher.php index 772a78f..64295a0 100644 --- a/src/Matchers/AbstractSerializedBodyMatcher.php +++ b/src/Matchers/AbstractSerializedBodyMatcher.php @@ -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); } /** diff --git a/src/Matchers/BodyMatcher.php b/src/Matchers/BodyMatcher.php index 5c86eb6..eb14a9c 100644 --- a/src/Matchers/BodyMatcher.php +++ b/src/Matchers/BodyMatcher.php @@ -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,24 @@ 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); + } + } } diff --git a/src/Matchers/ExactHeaderMatcher.php b/src/Matchers/ExactHeaderMatcher.php index cc5a8a8..bd21297 100644 --- a/src/Matchers/ExactHeaderMatcher.php +++ b/src/Matchers/ExactHeaderMatcher.php @@ -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); } } diff --git a/src/Matchers/ExactQueryMatcher.php b/src/Matchers/ExactQueryMatcher.php index 6625d5e..defa17d 100644 --- a/src/Matchers/ExactQueryMatcher.php +++ b/src/Matchers/ExactQueryMatcher.php @@ -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); } } diff --git a/src/Matchers/HeaderMatcher.php b/src/Matchers/HeaderMatcher.php index 8dbe2f0..8ccd1b3 100644 --- a/src/Matchers/HeaderMatcher.php +++ b/src/Matchers/HeaderMatcher.php @@ -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)); } } diff --git a/src/Matchers/HeadersMatcher.php b/src/Matchers/HeadersMatcher.php index 728d8f6..5de1d22 100644 --- a/src/Matchers/HeadersMatcher.php +++ b/src/Matchers/HeadersMatcher.php @@ -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 */ 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; } } diff --git a/src/Matchers/QueryMatcher.php b/src/Matchers/QueryMatcher.php index 26a5220..631a36b 100644 --- a/src/Matchers/QueryMatcher.php +++ b/src/Matchers/QueryMatcher.php @@ -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 */ protected $query; @@ -44,7 +45,7 @@ class QueryMatcher extends AbstractArrayPoweredComponent implements RequestMatch return false; } - return self::recursiveNeedlePresentInHaystack($this->query, $query); + return ComparatorLocator::get(RecursiveLtrArrayComparator::class)->compare($this->query, $query); } /** diff --git a/src/Matchers/XmlBodyMatcher.php b/src/Matchers/XmlBodyMatcher.php new file mode 100644 index 0000000..5c6fde3 --- /dev/null +++ b/src/Matchers/XmlBodyMatcher.php @@ -0,0 +1,194 @@ + + + + + + + + + + + + + + +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; + } +} diff --git a/src/PockBuilder.php b/src/PockBuilder.php index 647179d..1f47250 100644 --- a/src/PockBuilder.php +++ b/src/PockBuilder.php @@ -10,11 +10,13 @@ namespace Pock; use Diff\ArrayComparer\StrictArrayComparer; +use DOMDocument; use Pock\Enum\RequestMethod; use Pock\Enum\RequestScheme; use Pock\Exception\PockClientException; use Pock\Exception\PockNetworkException; use Pock\Exception\PockRequestException; +use Pock\Exception\XmlException; use Pock\Factory\CallbackReplyFactory; use Pock\Factory\ReplyFactoryInterface; use Pock\Matchers\AnyRequestMatcher; @@ -36,6 +38,7 @@ use Pock\Matchers\QueryMatcher; 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; @@ -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. * - * **Note:** this method will use string comparison for now. It'll be improved in future. + * This method will try to use available XML serializer before matching. * - * @todo Don't use simple string comparison. Match the entire body by its DOM. - * - * @param mixed $data + * @phpstan-ignore-next-line + * @param string|array|object $data * * @return self * @throws \Pock\Exception\XmlException */ - public function matchXmlBody($data): self + public function matchSerializedXmlBody($data): self { - return $this->matchBody(self::serializeXml($data) ?? ''); + return $this->matchXmlBody(self::serializeXml($data) ?? ''); } /** diff --git a/tests/src/Comparator/ComparatorLocatorTest.php b/tests/src/Comparator/ComparatorLocatorTest.php new file mode 100644 index 0000000..cc41166 --- /dev/null +++ b/tests/src/Comparator/ComparatorLocatorTest.php @@ -0,0 +1,48 @@ +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)); + } +} diff --git a/tests/src/Matchers/XmlBodyMatcherTest.php b/tests/src/Matchers/XmlBodyMatcherTest.php new file mode 100644 index 0000000..571fcfb --- /dev/null +++ b/tests/src/Matchers/XmlBodyMatcherTest.php @@ -0,0 +1,62 @@ +expectExceptionMessage('XML must not be empty.'); + new XmlBodyMatcher(''); + } + + public function testInvalidXml(): void + { + $brokenXml = <<<'EOF' + + + + +EOF; + + $this->expectExceptionMessage('DOMDocument::loadXML(): CData section not finished'); + new XmlBodyMatcher($brokenXml); + } + + public function testMatchXml(): void + { + $expected = <<<'EOF' + + + + +EOF; + $actual = <<<'EOF' + + + + + + + +EOF; + + self::assertTrue((new XmlBodyMatcher($expected))->matches(static::getRequestWithBody($actual))); + } +} diff --git a/tests/src/PockBuilderTest.php b/tests/src/PockBuilderTest.php index 23dd4df..5c2aab8 100644 --- a/tests/src/PockBuilderTest.php +++ b/tests/src/PockBuilderTest.php @@ -9,10 +9,11 @@ namespace Pock\Tests; +use DOMDocument; +use phpmock\MockBuilder; use Pock\Enum\RequestMethod; use Pock\Enum\RequestScheme; use Pock\Exception\UnsupportedRequestException; -use Pock\Factory\ReplyFactoryInterface; use Pock\PockBuilder; use Pock\PockResponseBuilder; use Pock\TestUtils\PockTestCase; @@ -22,7 +23,6 @@ use Psr\Http\Client\ClientExceptionInterface; use Psr\Http\Client\NetworkExceptionInterface; use Psr\Http\Client\RequestExceptionInterface; use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\ResponseInterface; 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 { $builder = new PockBuilder(); @@ -305,7 +322,124 @@ class PockBuilderTest extends PockTestCase ], json_decode($response->getBody()->getContents(), true)); } - public function testXmlResponse(): void + public function testMatchXmlString(): void + { + $xml = <<<'EOF' + + + + + +EOF; + $simpleObject = <<<'EOF' + + + + +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' + + + + + +EOF; + $simpleObject = <<<'EOF' + + + + +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' + + + + + +EOF; + $simpleObject = <<<'EOF' + + + + +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' @@ -315,11 +449,77 @@ class PockBuilderTest extends PockTestCase 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->matchMethod(RequestMethod::GET) ->matchScheme(RequestScheme::HTTPS) ->matchHost(self::TEST_HOST) - ->matchXmlBody(new SimpleObject()) + ->matchXmlBody($document) + ->repeat(2) + ->reply(403) + ->withHeader('Content-Type', 'text/xml') + ->withXml(['error' => 'Forbidden']); + + $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' + + + + + +EOF; + $simpleObjectFreeFormXml = <<<'EOF' + + + + + + + + + + +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']); @@ -328,13 +528,23 @@ EOF; self::getPsr17Factory() ->createRequest(RequestMethod::GET, self::TEST_URI) ->withBody(self::getPsr17Factory()->createStream( - self::getXmlSerializer()->serialize(new SimpleObject()) + PHP_EOL . self::getXmlSerializer()->serialize(new SimpleObject()) . PHP_EOL )) ); self::assertEquals(403, $response->getStatusCode()); self::assertEquals(['Content-Type' => ['text/xml']], $response->getHeaders()); self::assertEquals($xml, $response->getBody()->getContents()); + + $response = $builder->getClient()->sendRequest( + self::getPsr17Factory() + ->createRequest(RequestMethod::GET, self::TEST_URI) + ->withBody(self::getPsr17Factory()->createStream($simpleObjectFreeFormXml)) + ); + + self::assertEquals(403, $response->getStatusCode()); + self::assertEquals(['Content-Type' => ['text/xml']], $response->getHeaders()); + self::assertEquals($xml, $response->getBody()->getContents()); } public function testFirstExampleApiMock(): void @@ -649,4 +859,18 @@ EOF; self::TEST_URI )); } + + public function matchXmlNoXslProvider(): array + { + $simpleObject = <<<'EOF' + + + + +EOF; + return [ + [$simpleObject, true], + [$simpleObject . "\n", false] + ]; + } } diff --git a/tests/utils/PockTestCase.php b/tests/utils/PockTestCase.php index 04d1f3d..6b2df17 100644 --- a/tests/utils/PockTestCase.php +++ b/tests/utils/PockTestCase.php @@ -44,6 +44,19 @@ abstract class PockTestCase extends TestCase return static::getPsr17Factory()->createRequest($method ?? static::TEST_METHOD, static::TEST_URI); } + /** + * @param string $body + * + * @return \Psr\Http\Message\RequestInterface + */ + protected static function getRequestWithBody(string $body): RequestInterface + { + return static::getPsr17Factory()->createRequest( + RequestMethod::GET, + static::TEST_URI + )->withBody(self::getPsr17Factory()->createStream($body)); + } + /** * @return \Nyholm\Psr7\Factory\Psr17Factory */