Skip to content

Commit

Permalink
Add TestOnlyExpectedExchangeHandler
Browse files Browse the repository at this point in the history
  • Loading branch information
jakubkulhanek committed Mar 25, 2024
1 parent 9d4a16d commit 257e3c9
Showing 1 changed file with 242 additions and 0 deletions.
242 changes: 242 additions & 0 deletions src/MockeryTools/Http/Exchange/TestOnlyExpectedExchangeHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
<?php declare(strict_types = 1);

namespace BrandEmbassy\MockeryTools\Http\Exchange;

use Exception;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Promise\FulfilledPromise;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Promise\RejectedPromise;
use GuzzleHttp\Psr7\MultipartStream;
use Nette\Utils\Json;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\StreamInterface;
use function assert;
use function is_array;
use function json_decode;
use function json_last_error;
use function ksort;
use function parse_str;
use function sprintf;
use function str_replace;
use const JSON_ERROR_NONE;

/**
* @phpstan-import-type TRequestOptions from TestOnlyExpectedExchangeFactory
*/
class TestOnlyExpectedExchangeHandler
{
/**
* @var TestOnlyExpectedExchange[]
*/
private array $expectedExchanges = [];


public function __invoke(): callable
{
return fn(RequestInterface $actualRequest): PromiseInterface => $this->handleRequest($actualRequest);
}


private function handleRequest(RequestInterface $actualRequest): PromiseInterface
{
foreach ($this->expectedExchanges as $key => $expectedExchange) {
$areRequestsSame = $this->areRequestsSame(
$expectedExchange->getRequest(),
$actualRequest,
);

if ($areRequestsSame) {
unset($this->expectedExchanges[$key]);

if ($expectedExchange->getResponse()->getStatusCode() >= 400) {
return $this->createRejectedPromise($expectedExchange);
}

return new FulfilledPromise($expectedExchange->getResponse());
}
}

throw new Exception(
sprintf(
'Unexpected request: %s %s %s %s',
$actualRequest->getUri(),
$actualRequest->getMethod(),
Json::encode($actualRequest->getHeaders()),
$actualRequest->getBody(),
),
);
}


public function expectExchange(TestOnlyExpectedExchange $expectedExchange): void
{
$this->expectedExchanges[] = $expectedExchange;
}


public function hasExpectedExchanges(): bool
{
return $this->expectedExchanges !== [];
}


/**
* @return TestOnlyExpectedExchange[]
*/
public function getExpectedExchanges(): array
{
return $this->expectedExchanges;
}


private function areRequestsHeadersSame(RequestInterface $expectedRequest, RequestInterface $actualRequest): bool
{
$actualRequestHeaders = $actualRequest->getHeaders();
$expectedRequestHeaders = $expectedRequest->getHeaders();

/**
* Content-Type is unset here because it's generated by Guzzle at run-time using random_bytes function.
* We are unable to create the expected request with the same value.
*
* @see GuzzleHttp\Psr7\MultipartStream contrustor.
*/
if ($actualRequest->getBody() instanceof MultipartStream) {
unset($actualRequestHeaders['Content-Type']);
unset($expectedRequestHeaders['Content-Type']);
}

ksort($actualRequestHeaders);
ksort($expectedRequestHeaders);

return $actualRequestHeaders === $expectedRequestHeaders;
}


private function areRequestsSame(RequestInterface $expectedRequest, RequestInterface $actualRequest): bool
{
$areHeadersSame = $this->areRequestsHeadersSame(
$expectedRequest,
$actualRequest,
);

if (!$areHeadersSame) {
return false;
}

$areBodiesSame = $this->areRequestsBodiesSame(
$expectedRequest,
$actualRequest,
);

if (!$areBodiesSame) {
return false;
}

return $expectedRequest->getMethod() === $actualRequest->getMethod()
&& (string)$expectedRequest->getUri() === (string)$actualRequest->getUri();
}


private function areRequestsBodiesSame(RequestInterface $expectedRequest, RequestInterface $actualRequest): bool
{
if ($actualRequest->getBody() instanceof MultipartStream
&& $expectedRequest->getBody() instanceof MultipartStream) {
return $this->areRequestsMultipartStreamsSame($expectedRequest->getBody(), $actualRequest->getBody());
}

$expectedRequestBody = (string)$expectedRequest->getBody();
$actualRequestBody = (string)$actualRequest->getBody();

if ($actualRequest->getHeaderLine('Content-Type') === 'application/x-www-form-urlencoded') {
return $this->areRequestsFormsBodySame($expectedRequestBody, $actualRequestBody);
}

if ($this->isRequestBodyJson($actualRequestBody)
&& $this->isRequestBodyJson($expectedRequestBody)
) {
return $this->areRequestsJsonBodiesSame($expectedRequestBody, $actualRequestBody);
}

return $expectedRequestBody === $actualRequestBody;
}


private function areRequestsJsonBodiesSame(string $expectedRequestBody, string $actualRequestBody): bool
{
$expectedRequestBody = Json::decode($expectedRequestBody, Json::FORCE_ARRAY);
$actualRequestBody = Json::decode($actualRequestBody, Json::FORCE_ARRAY);

$expectedRequestBody = $this->sortRequestBody($expectedRequestBody);
$actualRequestBody = $this->sortRequestBody($actualRequestBody);

return $expectedRequestBody === $actualRequestBody;
}


private function areRequestsMultipartStreamsSame(StreamInterface $expectedRequestStream, StreamInterface $actualRequestStream): bool
{
assert($expectedRequestStream instanceof MultipartStream);
assert($actualRequestStream instanceof MultipartStream);

if ($expectedRequestStream->getSize() !== $actualRequestStream->getSize()) {
return false;
}

$expectedRequestContentWithoutBoundary = str_replace($expectedRequestStream->getBoundary(), '', $expectedRequestStream->getContents());
$actualRequestContentWithoutBoundary = str_replace($actualRequestStream->getBoundary(), '', $actualRequestStream->getContents());

return $expectedRequestContentWithoutBoundary === $actualRequestContentWithoutBoundary;
}


private function areRequestsFormsBodySame(string $expectedRequestBody, string $actualRequestBody): bool
{
$parsedExpectedRequestBody = [];
parse_str($expectedRequestBody, $parsedExpectedRequestBody);

$parsedActualRequestBody = [];
parse_str($actualRequestBody, $parsedActualRequestBody);

return $this->sortRequestBody($parsedExpectedRequestBody) === $this->sortRequestBody($parsedActualRequestBody);
}


/**
* @param mixed[] $requestBody
*
* @return mixed[]
*/
private function sortRequestBody(array &$requestBody): array
{
ksort($requestBody);

foreach ($requestBody as &$value) {
if (is_array($value)) {
$this->sortRequestBody($value);
}
}
unset($value);

return $requestBody;
}


private function isRequestBodyJson(string $requestBody): bool
{
json_decode($requestBody);

return json_last_error() === JSON_ERROR_NONE;
}


private function createRejectedPromise(TestOnlyExpectedExchange $expectedExchange): RejectedPromise
{
$requestException = RequestException::create(
$expectedExchange->getRequest(),
$expectedExchange->getResponse(),
);

return new RejectedPromise($requestException);
}
}

0 comments on commit 257e3c9

Please sign in to comment.