diff --git a/phpstan.baseline.neon b/phpstan.baseline.neon index 23ea18cc..d52346f6 100644 --- a/phpstan.baseline.neon +++ b/phpstan.baseline.neon @@ -54,3 +54,18 @@ parameters: message: "#^Match arm is unreachable because previous comparison is always true.$#" count: 1 path: src/Http/Pool.php + + - + message: "#^Method Saloon\\\\Http\\\\Faking\\\\MockClient\\:\\:getRequestClass\\(\\) should return class-string|null but returns class-string|Saloon\\\\Http\\\\Request$#" + count: 1 + path: src/Http/Faking/MockClient.php + + - + message: "#^Call to an undefined method ReflectionType\\:\\:getName\\(\\)\\.$#" + count: 1 + path: src/Http/Faking/MockClient.php + + - + message: "#^Parameter \\#1 \\$function of class ReflectionFunction constructor expects Closure|string, callable\\(\\)\\: mixed given\\.$#" + count: 1 + path: src/Http/Faking/MockClient.php diff --git a/src/Http/Faking/MockClient.php b/src/Http/Faking/MockClient.php index 2e92a8b8..5ccbb41d 100644 --- a/src/Http/Faking/MockClient.php +++ b/src/Http/Faking/MockClient.php @@ -451,14 +451,29 @@ private function checkClosureAgainstResponses(callable $closure, ?int $index = n return false; } - if(! is_null($index)) { + if (! is_null($index)) { $response = $this->getRecordedResponses()[$index]; $request = $response->getPendingRequest()->getRequest(); return $closure($request, $response); } - // Let's first check if the latest response resolves the callable + // Let's first check if the callable type-hints the latest request class. + // If so, we try to find the corresponding request in the recorded responses + // and call the callable accordingly. We will only fail if it returns `false`. + + if ($fqcn = $this->getRequestClass($closure)) { + /** @var Response */ + foreach ($this->getRecordedResponses() as $response) { + if (get_class($request = $response->getPendingRequest()->getRequest()) !== $fqcn) { + continue; + } + + return $closure($request, $response) !== false; + } + } + + // Let's then check if the latest response resolves the callable // with a successful result. $lastResponse = $this->getLastResponse(); @@ -524,4 +539,25 @@ private function getRequestSentCount(): array return array_count_values($requests); } + + /** + * Get the FQCN of the request class if type-hinted. + * + * @return class-string + */ + private function getRequestClass(callable $closure): ?string + { + $reflection = new \ReflectionFunction($closure); + $parameters = $reflection->getParameters(); + + if (! ($fqcn = $parameters[0]->getType()?->getName())) { + return null; + } + + if (! is_a($fqcn, Request::class, allow_string: true)) { + return null; + } + + return $fqcn; + } } diff --git a/tests/Unit/MockClientTest.php b/tests/Unit/MockClientTest.php index f542bee0..11a88d28 100644 --- a/tests/Unit/MockClientTest.php +++ b/tests/Unit/MockClientTest.php @@ -2,10 +2,12 @@ declare(strict_types=1); +use Pest\Expectation; use Saloon\Http\Faking\MockClient; use Saloon\Http\Faking\MockResponse; use Saloon\Tests\Fixtures\Requests\UserRequest; use Saloon\Tests\Fixtures\Requests\ErrorRequest; +use PHPUnit\Framework\ExpectationFailedException; use Saloon\Exceptions\NoMockResponseFoundException; use Saloon\Tests\Fixtures\Connectors\TestConnector; use Saloon\Tests\Fixtures\Exceptions\TestResponseException; @@ -261,3 +263,36 @@ $response = connector()->send(new UserRequest, $mockClient); $response->throw(); }); + +test('`assertSent` accepts the request class as a type-hint', function () { + $mockClient = new MockClient([ + MockResponse::make(['name' => 'Sam']), + ]); + + $request = new UserRequest; + $request->headers()->add('X-Foo', 'bar'); + + connector()->send($request, $mockClient); + + $mockClient->assertSent(function (UserRequest $request) { + expect($request->headers()->all())->toMatchArray([ + 'X-Foo' => 'bar', + ]); + }); +}); + +test('`assertSent` fails or succeeds depending on the closure result when the closure is type-hinted', function (mixed $returns, bool $shouldThrow) { + $mockClient = new MockClient([ + MockResponse::make(['name' => 'Sam']), + ]); + + connector()->send(new UserRequest, $mockClient); + + expect(fn () => $mockClient->assertSent(fn (UserRequest $request) => $returns)) + ->when($shouldThrow, fn (Expectation $e) => $e->toThrow(ExpectationFailedException::class)) + ->when(! $shouldThrow, fn (Expectation $e) => $e->not->toThrow(ExpectationFailedException::class)); +})->with([ + [false, true], + [true, false], + [null, false], +]);