-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
9d4a16d
commit 257e3c9
Showing
1 changed file
with
242 additions
and
0 deletions.
There are no files selected for viewing
242 changes: 242 additions & 0 deletions
242
src/MockeryTools/Http/Exchange/TestOnlyExpectedExchangeHandler.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |