Skip to content

Commit

Permalink
Merge pull request #31 from Sammyjo20/feature/mocking-assertions
Browse files Browse the repository at this point in the history
Added new mock assertions
  • Loading branch information
Sammyjo20 authored Feb 7, 2022
2 parents 69e2bff + cfbaa1d commit d81733b
Show file tree
Hide file tree
Showing 10 changed files with 649 additions and 20 deletions.
256 changes: 251 additions & 5 deletions src/Clients/BaseMockClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,56 @@
namespace Sammyjo20\Saloon\Clients;

use ReflectionClass;
use Illuminate\Support\Str;
use Sammyjo20\Saloon\Helpers\URLHelper;
use Sammyjo20\Saloon\Http\MockResponse;
use PHPUnit\Framework\Assert as PHPUnit;
use Sammyjo20\Saloon\Http\SaloonRequest;
use Sammyjo20\Saloon\Http\SaloonResponse;
use Sammyjo20\Saloon\Http\SaloonConnector;
use Sammyjo20\Saloon\Helpers\ReflectionHelper;
use Sammyjo20\Saloon\Exceptions\SaloonNoMockResponseFoundException;
use Sammyjo20\Saloon\Exceptions\SaloonNoMockResponsesProvidedException;
use Sammyjo20\Saloon\Exceptions\SaloonInvalidMockResponseCaptureMethodException;

class BaseMockClient
{
/**
* Collection of all the responses that will be sequenced.
*
* @var array
*/
protected array $sequenceResponses = [];

/**
* Collection of responses used only when a connector is called.
*
* @var array
*/
protected array $connectorResponses = [];

/**
* Collection of responses used only when a request is called.
*
* @var array
*/
protected array $requestResponses = [];

/**
* Collection of responses that will run when the request is matched.
*
* @var array
*/
protected array $urlResponses = [];

/**
* @param array $responses
* @throws SaloonNoMockResponsesProvidedException
* Collection of all the recorded responses.
*
* @var array
*/
protected array $recordedResponses = [];

/**
* @param array $mockData
* @throws SaloonInvalidMockResponseCaptureMethodException
*/
public function __construct(array $mockData = [])
{
Expand All @@ -48,6 +77,14 @@ public function addResponses(array $responses): void
}
}

/**
* Add a mock response to the client
*
* @param MockResponse $response
* @param string|null $captureMethod
* @return void
* @throws SaloonInvalidMockResponseCaptureMethodException
*/
public function addResponse(MockResponse $response, ?string $captureMethod = null): void
{
if (is_null($captureMethod)) {
Expand Down Expand Up @@ -84,6 +121,11 @@ public function addResponse(MockResponse $response, ?string $captureMethod = nul
$this->urlResponses[$captureMethod] = $response;
}

/**
* Get the next response in the sequence
*
* @return mixed
*/
public function getNextFromSequence(): mixed
{
return array_shift($this->sequenceResponses);
Expand Down Expand Up @@ -134,7 +176,7 @@ public function guessNextResponse(SaloonRequest $request): MockResponse
private function guessResponseFromUrl(SaloonRequest $request): ?MockResponse
{
foreach ($this->urlResponses as $url => $response) {
if (! Str::is(Str::start($url, '*'), $request->getFullRequestUrl())) {
if (! URLHelper::matches($url, $request->getFullRequestUrl())) {
continue;
}

Expand All @@ -153,4 +195,208 @@ public function isEmpty(): bool
{
return empty($this->sequenceResponses) && empty($this->connectorResponses) && empty($this->requestResponses) && empty($this->urlResponses);
}

/**
* Record a response.
*
* @param SaloonResponse $response
* @return void
*/
public function recordResponse(SaloonResponse $response): void
{
$this->recordedResponses[] = $response;
}

/**
* Get all the recorded responses
*
* @return array
*/
public function getRecordedResponses(): array
{
return $this->recordedResponses;
}

/**
* Get the last request that the mock manager sent.
*
* @return SaloonRequest|null
*/
public function getLastRequest(): ?SaloonRequest
{
return $this->getLastResponse()?->getOriginalRequest();
}

/**
* Get the last response that the mock manager sent.
*
* @return SaloonResponse|null
*/
public function getLastResponse(): ?SaloonResponse
{
if (empty($this->recordedResponses)) {
return null;
}

$lastResponse = end($this->recordedResponses);

reset($this->recordedResponses);

return $lastResponse;
}

/**
* Assert that a given request was sent.
*
* @param string|callable $value
* @return void
* @throws \ReflectionException
*/
public function assertSent(string|callable $value): void
{
$result = $this->checkRequestWasSent($value);

PHPUnit::assertTrue($result, 'An expected request was not sent.');
}

/**
* Assert that a given request was not sent.
*
* @param string|callable $request
* @return void
* @throws \ReflectionException
*/
public function assertNotSent(string|callable $request): void
{
$result = $this->checkRequestWasNotSent($request);

PHPUnit::assertTrue($result, 'An unexpected request was sent.');
}

/**
* Assert JSON data was sent
*
* @param string $request
* @param array $data
* @return void
* @throws \ReflectionException
*/
public function assertSentJson(string $request, array $data): void
{
$this->assertSent($request);

$response = $this->findResponseByRequest($request);

PHPUnit::assertEquals($response->json(), $data, 'Expected request data was not sent.');
}

/**
* Assert that nothing was sent.
*
* @return void
*/
public function assertNothingSent(): void
{
PHPUnit::assertEmpty($this->getRecordedResponses(), 'Requests were sent.');
}

/**
* Assert a request count has been met.
*
* @param int $count
* @return void
*/
public function assertSentCount(int $count): void
{
PHPUnit::assertCount($count, $this->getRecordedResponses());
}

/**
* Check if a given request was sent
*
* @param string|callable $request
* @return bool
* @throws \ReflectionException
* @throws \Sammyjo20\Saloon\Exceptions\SaloonInvalidConnectorException
*/
protected function checkRequestWasSent(string|callable $request): bool
{
$result = false;

if (is_callable($request)) {
$result = $request($this->getLastRequest(), $this->getLastResponse());
}

if (is_string($request)) {
if (class_exists($request) && ReflectionHelper::isSubclassOf($request, SaloonRequest::class)) {
$result = $this->findResponseByRequest($request) instanceof SaloonResponse;
} else {
$result = $this->findResponseByRequestUrl($request) instanceof SaloonResponse;
}
}

return $result;
}

/**
* Check if a request has not been sent.
*
* @param string|callable $request
* @return bool
* @throws \ReflectionException
* @throws \Sammyjo20\Saloon\Exceptions\SaloonInvalidConnectorException
*/
protected function checkRequestWasNotSent(string|callable $request): bool
{
return ! $this->checkRequestWasSent($request);
}

/**
* Assert a given request was sent.
*
* @param string $request
* @return SaloonResponse|null
*/
public function findResponseByRequest(string $request): ?SaloonResponse
{
$lastRequest = $this->getLastRequest();

if ($lastRequest instanceof $request) {
return $this->getLastResponse();
}

foreach ($this->getRecordedResponses() as $recordedResponse) {
if ($recordedResponse->getOriginalRequest() instanceof $request) {
return $recordedResponse;
}
}

return null;
}

/**
* Find a request that matches a given url pattern
*
* @param string $url
* @return SaloonResponse|null
* @throws \Sammyjo20\Saloon\Exceptions\SaloonInvalidConnectorException
*/
public function findResponseByRequestUrl(string $url): ?SaloonResponse
{
$lastRequest = $this->getLastRequest();

if ($lastRequest instanceof SaloonRequest && URLHelper::matches($url, $lastRequest->getFullRequestUrl())) {
return $this->getLastResponse();
}

foreach ($this->getRecordedResponses() as $recordedResponse) {
$request = $recordedResponse->getOriginalRequest();

if (URLHelper::matches($url, $request->getFullRequestUrl())) {
return $recordedResponse;
}
}

return null;
}
}
20 changes: 20 additions & 0 deletions src/Helpers/URLHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace Sammyjo20\Saloon\Helpers;

use Illuminate\Support\Str;

class URLHelper
{
/**
* Check if a URL matches a given pattern
*
* @param string $pattern
* @param string $value
* @return bool
*/
public static function matches(string $pattern, string $value): bool
{
return Str::is(Str::start($pattern, '*'), $value);
}
}
8 changes: 7 additions & 1 deletion src/Managers/RequestManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,13 @@ private function createResponse(array $requestOptions, Response $response, Reque
/** @var SaloonResponse $response */
$response = new $responseClass($requestOptions, $request, $response, $exception);

$response->setMocked($this->isMocking());
// If we are mocking, we should record the request and response on the mock manager,
// so we can run assertions on the responses.

if ($this->isMocking()) {
$response->setMocked(true);
$this->mockClient->recordResponse($response);
}

if (property_exists($this->connector, 'shouldGuessStatusFromBody') || property_exists($this->request, 'shouldGuessStatusFromBody')) {
$response->guessesStatusFromBody();
Expand Down
3 changes: 1 addition & 2 deletions src/Traits/CollectsConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,11 @@ public function addConfig(string $item, $value): self

/**
* Get all headers or filter with a key.
* Todo: Throw an error if it doesn't exist.
*
* @param string|null $key
* @return array
*/
public function getConfig(string $key = null): array
public function getConfig(string $key = null): mixed
{
if ($this->includeDefaultConfig === true) {
$configBag = array_merge($this->defaultConfig(), $this->customConfig);
Expand Down
11 changes: 0 additions & 11 deletions src/Traits/CollectsData.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,17 +108,6 @@ public function getData(string $key = null): mixed
return $dataBag;
}

/**
* Get an individual data
*
* @param string $key
* @return string
*/
public function getDataByKey(string $key): string
{
return $this->getData($key);
}

/**
* Should we ignore the default data when calling `->getData()`?
*
Expand Down
1 change: 0 additions & 1 deletion src/Traits/CollectsHeaders.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ public function addHeader(string $header, $value): self

/**
* Get all headers or filter with a key.
* Todo: Throw an error if it doesn't exist.
*
* @param string|null $key
* @return array
Expand Down
Loading

0 comments on commit d81733b

Please sign in to comment.