Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 84 additions & 1 deletion src/Http/Faking/MockClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,18 @@

namespace Saloon\Http\Faking;

use Closure;
use ReflectionType;
use ReflectionClass;
use ReflectionFunction;
use ReflectionNamedType;
use ReflectionUnionType;
use Saloon\Http\Request;
use Saloon\Http\Response;
use Saloon\Http\Connector;
use Saloon\Helpers\Helpers;
use Saloon\Helpers\URLHelper;
use ReflectionIntersectionType;
use Saloon\Http\PendingRequest;
use PHPUnit\Framework\Assert as PHPUnit;
use Saloon\Exceptions\NoMockResponseFoundException;
Expand Down Expand Up @@ -443,10 +449,16 @@ private function checkClosureAgainstResponses(callable $closure, ?int $index = n
return false;
}

$firstParameterType = $this->getClosureFirstParameterType($closure);

if (! is_null($index)) {
$response = $this->getRecordedResponses()[$index];
$request = $response->getPendingRequest()->getRequest();

if ($this->requestMatchesType($request, $firstParameterType) === false) {
return false;
}

return $closure($request, $response);
}

Expand All @@ -456,7 +468,13 @@ private function checkClosureAgainstResponses(callable $closure, ?int $index = n
$lastResponse = $this->getLastResponse();

if ($lastResponse instanceof Response) {
$passed = $closure($lastResponse->getPendingRequest()->getRequest(), $lastResponse);
$lastRequest = $lastResponse->getPendingRequest()->getRequest();

$passed = false;

if ($this->requestMatchesType($lastRequest, $firstParameterType)) {
$passed = $closure($lastRequest, $lastResponse);
}

if ($passed === true) {
return true;
Expand All @@ -469,6 +487,10 @@ private function checkClosureAgainstResponses(callable $closure, ?int $index = n
foreach ($this->getRecordedResponses() as $response) {
$request = $response->getPendingRequest()->getRequest();

if ($this->requestMatchesType($request, $firstParameterType) === false) {
continue;
}

$passed = $closure($request, $response);

if ($passed === true) {
Expand All @@ -479,6 +501,67 @@ private function checkClosureAgainstResponses(callable $closure, ?int $index = n
return false;
}

/**
* Get the first closure parameter type if one exists.
*/
private function getClosureFirstParameterType(callable $closure): ?ReflectionType
{
$reflection = new ReflectionFunction(Closure::fromCallable($closure));
$parameters = $reflection->getParameters();

if ($parameters === []) {
return null;
}

return $parameters[0]->getType();
}

/**
* Determine if a request matches a closure first parameter type.
*/
private function requestMatchesType(Request $request, ?ReflectionType $type): bool
{
if ($type === null) {
return true;
}

if ($type instanceof ReflectionNamedType) {
$typeName = $type->getName();

if ($typeName === 'mixed') {
return true;
}

if ($type->isBuiltin()) {
return $typeName === 'object';
}

return $request instanceof $typeName;
}

if ($type instanceof ReflectionUnionType) {
foreach ($type->getTypes() as $subType) {
if ($this->requestMatchesType($request, $subType)) {
return true;
}
}

return false;
}

if ($type instanceof ReflectionIntersectionType) {
foreach ($type->getTypes() as $subType) {
if ($this->requestMatchesType($request, $subType) === false) {
return false;
}
}

return true;
}

return true;
}

/**
* Will return true if the history is empty.
*/
Expand Down
51 changes: 51 additions & 0 deletions tests/Unit/MockClientAssertionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Saloon\Tests\Fixtures\Requests\UserRequest;
use Saloon\Tests\Fixtures\Requests\ErrorRequest;
use PHPUnit\Framework\ExpectationFailedException;
use Saloon\Tests\Fixtures\Requests\ConfigRequest;
use Saloon\Tests\Fixtures\Connectors\TestConnector;

test('assertSent works with a request', function () {
Expand Down Expand Up @@ -209,6 +210,56 @@
});
});

test('assertSent with a typed closure skips non-matching requests and still finds a match', function () {
$mockClient = new MockClient([
MockResponse::make(['name' => 'Sam']),
MockResponse::make(['message' => 'Error'], 500),
]);

$connector = new TestConnector;

$connector->send(new UserRequest, $mockClient);
$connector->send(new ErrorRequest, $mockClient);

$mockClient->assertSent(function (UserRequest $request) {
return $request->userId === null;
});
});

test('assertSent with a union typed closure skips non-matching requests and still finds a match', function () {
$mockClient = new MockClient([
MockResponse::make(['name' => 'Sam']),
MockResponse::make(['message' => 'Error'], 500),
MockResponse::make(['config' => true]),
]);

$connector = new TestConnector;

$connector->send(new UserRequest, $mockClient);
$connector->send(new ErrorRequest, $mockClient);
$connector->send(new ConfigRequest, $mockClient);

$mockClient->assertSent(function (UserRequest|ErrorRequest $request) {
return $request instanceof UserRequest && $request->userId === null;
});

$mockClient->assertSent(function (UserRequest|ErrorRequest $request) {
return $request instanceof ErrorRequest;
});
});

test('assertSent with a typed closure fails assertion when no matching request type was sent', function () {
$mockClient = new MockClient([
ErrorRequest::class => MockResponse::make(['message' => 'Error'], 500),
]);

connector()->send(new ErrorRequest, $mockClient);

$mockClient->assertSent(function (UserRequest $request) {
return true;
});
})->expectException(ExpectationFailedException::class);

test('it can assert requests are sent in a specific order', function () {
$mockClient = new MockClient([
MockResponse::make(['name' => 'Sam']),
Expand Down
Loading