/
githubmirror
/
framework
Обзор
Документация
Войти
/
githubmirror
/
framework
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
13.x
tests/Http/HttpClientTest.php
5 050 строк
167 KB
Jason McCreary
[13.x] Mockery cleanup (#61117)
10 авг 2026, 20:23
Не верифицирован
10 авг 2026, 20:23
2ee40ee
Код
Авторство
О чём код?
<?php namespace Illuminate\Tests\Http; use Exception; use GuzzleHttp\Client as GuzzleClient; use GuzzleHttp\Exception\ConnectException; use GuzzleHttp\Exception\RequestException as GuzzleRequestException; use GuzzleHttp\Exception\TooManyRedirectsException; use GuzzleHttp\Middleware; use GuzzleHttp\Promise\Create; use GuzzleHttp\Promise\PromiseInterface; use GuzzleHttp\Promise\RejectedPromise; use GuzzleHttp\Psr7\NoSeekStream; use GuzzleHttp\Psr7\Request as GuzzleRequest; use GuzzleHttp\Psr7\Response as Psr7Response; use GuzzleHttp\Psr7\Utils; use GuzzleHttp\TransferStats; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Support\Arrayable; use Illuminate\Http\Client\Batch; use Illuminate\Http\Client\BatchInProgressException; use Illuminate\Http\Client\ConnectionException; use Illuminate\Http\Client\Events\RequestSending; use Illuminate\Http\Client\Events\ResponseReceived; use Illuminate\Http\Client\Factory; use Illuminate\Http\Client\PendingRequest; use Illuminate\Http\Client\Pool; use Illuminate\Http\Client\Request; use Illuminate\Http\Client\RequestException; use Illuminate\Http\Client\Response; use Illuminate\Http\Client\ResponseSequence; use Illuminate\Http\Client\StrayRequestException; use Illuminate\Http\Response as HttpResponse; use Illuminate\Support\Arr; use Illuminate\Support\Carbon; use Illuminate\Support\Collection; use Illuminate\Support\Defer\DeferredCallback; use Illuminate\Support\Fluent; use Illuminate\Support\Sleep; use Illuminate\Support\Str; use Illuminate\Support\Stringable; use Illuminate\Support\Uri; use InvalidArgumentException; use JsonSerializable; use Mockery; use OutOfBoundsException; use PHPUnit\Framework\AssertionFailedError; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\TestCase; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use RuntimeException; use stdClass; use Symfony\Component\VarDumper\VarDumper; use Throwable; class HttpClientTest extends TestCase { /** * @var \Illuminate\Http\Client\Factory */ protected $factory; protected function setUp(): void { $this->factory = new Factory; RequestException::truncate(); } protected function tearDown(): void { Response::flushState(); } public function testStubbedResponsesAreReturnedAfterFaking() { $this->factory->fake(); $response = $this->factory->post('http://laravel.com/test-missing-page'); $this->assertTrue($response->ok()); } public function testCreatedRequest() { $this->factory->fake([ 'vapor.laravel.com' => $this->factory::response('', HttpResponse::HTTP_CREATED), 'forge.laravel.com' => $this->factory::response('', HttpResponse::HTTP_OK), ]); $response = $this->factory->post('http://vapor.laravel.com'); $this->assertTrue($response->created()); $response = $this->factory->post('http://forge.laravel.com'); $this->assertFalse($response->created()); } public function testFakeResponseHeaderValuesAreSerialized() { $response = $this->factory::response('OK', 200, [ 'X-Int' => 123, 'X-Null' => null, 'X-False' => false, 'X-Empty' => [], 'X-Laravel-Stringable' => new Stringable('laravel stringable'), 'X-Multiple' => ['first', 123, true, false, null], ])->wait(); $this->assertSame(['123'], $response->getHeader('X-Int')); $this->assertSame([''], $response->getHeader('X-Null')); $this->assertSame([''], $response->getHeader('X-False')); $this->assertSame([''], $response->getHeader('X-Empty')); $this->assertSame(['laravel stringable'], $response->getHeader('X-Laravel-Stringable')); $this->assertSame(['first', '123', '1', '', ''], $response->getHeader('X-Multiple')); } public function testFakeResponseHeaderValuesNormalizeNonFiniteFloats() { $response = $this->factory::response('OK', 200, [ 'X-Nan' => NAN, 'X-Inf' => INF, 'X-Negative-Inf' => -INF, 'X-Multiple' => [NAN, INF, -INF], ])->wait(); $this->assertSame(['NAN'], $response->getHeader('X-Nan')); $this->assertSame(['INF'], $response->getHeader('X-Inf')); $this->assertSame(['-INF'], $response->getHeader('X-Negative-Inf')); $this->assertSame(['NAN', 'INF', '-INF'], $response->getHeader('X-Multiple')); } #[DataProvider('invalidFakeResponseHeaderValuesProvider')] public function testInvalidFakeResponseHeaderValuesAreRejected($value) { $this->expectExceptionObject(new InvalidArgumentException('HTTP fake response header values must be scalar, null, Laravel Stringable, or arrays of scalar, null, or Laravel Stringable values.')); $this->factory::response('OK', 200, ['X-Test' => $value]); } public function testStatusCodeShorthand() { $this->factory->fake([ 'forge.laravel.com' => 204, 'vapor.laravel.com' => HttpResponse::HTTP_CREATED, ]); $response = $this->factory->post('http://forge.laravel.com'); $this->assertTrue($response->noContent()); $response = $this->factory->post('http://vapor.laravel.com'); $this->assertTrue($response->created()); } public function testBodyShorthands() { $this->factory->fake([ 'google.com' => 'Hello World', 'github.com' => ['foo' => 'bar'], ]); $response = $this->factory->get('http://google.com'); $this->assertTrue($response->ok()); $this->assertSame('Hello World', $response->body()); $response = $this->factory->post('http://github.com'); $this->assertTrue($response->ok()); $this->assertSame('{"foo":"bar"}', $response->body()); $this->assertSame(['foo' => 'bar'], $response->json()); } public function testFakeResponseSupportsPsr7StreamBody() { $stream = Utils::streamFor('Hello World'); $response = $this->factory::response($stream)->wait(); $this->assertSame($stream, $response->getBody()); $this->assertSame('Hello World', (string) $response->getBody()); } public function testFakeResponseSupportsResourceBody() { $resource = fopen('php://temp', 'w+'); fwrite($resource, 'Hello World'); rewind($resource); $response = $this->factory::response($resource)->wait(); $this->assertSame('Hello World', (string) $response->getBody()); } public function testFakeResponseRejectsUnsupportedBody() { $this->expectExceptionObject(new InvalidArgumentException('HTTP fake response body must be a string, array, stream resource, Psr\Http\Message\StreamInterface, or null.')); $this->factory::response(new stdClass); } public function testFakeResponseRejectsNonStreamResourceBody() { $this->expectExceptionObject(new InvalidArgumentException('HTTP fake response body must be a string, array, stream resource, Psr\Http\Message\StreamInterface, or null.')); $this->factory::response(stream_context_create()); } public function testAcceptedRequest() { $this->factory->fake([ 'vapor.laravel.com' => $this->factory::response('', HttpResponse::HTTP_ACCEPTED), 'forge.laravel.com' => $this->factory::response('', HttpResponse::HTTP_OK), ]); $response = $this->factory->post('http://vapor.laravel.com'); $this->assertTrue($response->accepted()); $response = $this->factory->post('http://forge.laravel.com'); $this->assertFalse($response->accepted()); } public function testMovedPermanentlyRequest() { $this->factory->fake([ 'vapor.laravel.com' => $this->factory::response('', HttpResponse::HTTP_MOVED_PERMANENTLY), 'forge.laravel.com' => $this->factory::response('', HttpResponse::HTTP_OK), ]); $response = $this->factory->post('http://vapor.laravel.com'); $this->assertTrue($response->movedPermanently()); $response = $this->factory->post('http://forge.laravel.com'); $this->assertFalse($response->movedPermanently()); } public function testNoContentRequest() { $this->factory->fake([ 'vapor.laravel.com' => $this->factory::response('', HttpResponse::HTTP_NO_CONTENT), 'forge.laravel.com' => $this->factory::response('', HttpResponse::HTTP_OK), ]); $response = $this->factory->post('http://vapor.laravel.com'); $this->assertTrue($response->noContent()); $response = $this->factory->post('http://forge.laravel.com'); $this->assertFalse($response->noContent()); } public function testFoundRequest() { $this->factory->fake([ 'vapor.laravel.com' => $this->factory::response('', HttpResponse::HTTP_FOUND), 'forge.laravel.com' => $this->factory::response('', HttpResponse::HTTP_OK), ]); $response = $this->factory->post('http://vapor.laravel.com'); $this->assertTrue($response->found()); $response = $this->factory->post('http://forge.laravel.com'); $this->assertFalse($response->found()); } public function testNotModifiedRequest(): void { $this->factory->fake([ 'vapor.laravel.com' => $this->factory::response('', HttpResponse::HTTP_NOT_MODIFIED), 'forge.laravel.com' => $this->factory::response('', HttpResponse::HTTP_OK), ]); $response = $this->factory->post('https://vapor.laravel.com'); $this->assertTrue($response->notModified()); $response = $this->factory->post('https://forge.laravel.com'); $this->assertFalse($response->notModified()); } public function testBadRequestRequest() { $this->factory->fake([ 'vapor.laravel.com' => $this->factory::response('', HttpResponse::HTTP_BAD_REQUEST), 'forge.laravel.com' => $this->factory::response('', HttpResponse::HTTP_OK), ]); $response = $this->factory->post('http://vapor.laravel.com'); $this->assertTrue($response->badRequest()); $response = $this->factory->post('http://forge.laravel.com'); $this->assertFalse($response->badRequest()); } public function testPaymentRequiredRequest() { $this->factory->fake([ 'vapor.laravel.com' => $this->factory::response('', HttpResponse::HTTP_PAYMENT_REQUIRED), 'forge.laravel.com' => $this->factory::response('', HttpResponse::HTTP_OK), ]); $response = $this->factory->post('http://vapor.laravel.com'); $this->assertTrue($response->paymentRequired()); $response = $this->factory->post('http://forge.laravel.com'); $this->assertFalse($response->paymentRequired()); } public function testRequestTimeoutRequest() { $this->factory->fake([ 'vapor.laravel.com' => $this->factory::response('', HttpResponse::HTTP_REQUEST_TIMEOUT), 'forge.laravel.com' => $this->factory::response('', HttpResponse::HTTP_OK), ]); $response = $this->factory->post('http://vapor.laravel.com'); $this->assertTrue($response->requestTimeout()); $response = $this->factory->post('http://forge.laravel.com'); $this->assertFalse($response->requestTimeout()); } public function testConflictResponseRequest() { $this->factory->fake([ 'vapor.laravel.com' => $this->factory::response('', HttpResponse::HTTP_CONFLICT), 'forge.laravel.com' => $this->factory::response('', HttpResponse::HTTP_OK), ]); $response = $this->factory->post('http://vapor.laravel.com'); $this->assertTrue($response->conflict()); $response = $this->factory->post('http://forge.laravel.com'); $this->assertFalse($response->conflict()); } public function testUnprocessableContentRequest() { $this->factory->fake([ 'vapor.laravel.com' => $this->factory::response('', HttpResponse::HTTP_UNPROCESSABLE_ENTITY), 'forge.laravel.com' => $this->factory::response('', HttpResponse::HTTP_OK), ]); $response = $this->factory->post('http://vapor.laravel.com'); $this->assertTrue($response->unprocessableContent()); $response = $this->factory->post('http://forge.laravel.com'); $this->assertFalse($response->unprocessableContent()); } public function testUnprocessableEntityRequest() { $this->factory->fake([ 'vapor.laravel.com' => $this->factory::response('', HttpResponse::HTTP_UNPROCESSABLE_ENTITY), 'forge.laravel.com' => $this->factory::response('', HttpResponse::HTTP_OK), ]); $response = $this->factory->post('http://vapor.laravel.com'); $this->assertTrue($response->unprocessableEntity()); $response = $this->factory->post('http://forge.laravel.com'); $this->assertFalse($response->unprocessableEntity()); } public function testTooManyRequestsRequest() { $this->factory->fake([ 'vapor.laravel.com' => $this->factory::response('', HttpResponse::HTTP_TOO_MANY_REQUESTS), 'forge.laravel.com' => $this->factory::response('', HttpResponse::HTTP_OK), ]); $response = $this->factory->post('http://vapor.laravel.com'); $this->assertTrue($response->tooManyRequests()); $response = $this->factory->post('http://forge.laravel.com'); $this->assertFalse($response->tooManyRequests()); } public function testUnauthorizedRequest() { $this->factory->fake([ 'laravel.com' => $this->factory::response('', 401), ]); $response = $this->factory->post('http://laravel.com'); $this->assertTrue($response->unauthorized()); } public function testForbiddenRequest() { $this->factory->fake([ 'laravel.com' => $this->factory::response('', 403), ]); $response = $this->factory->post('http://laravel.com'); $this->assertTrue($response->forbidden()); } public function testNotFoundResponse() { $this->factory->fake([ 'laravel.com' => $this->factory::response('', 404), ]); $response = $this->factory->post('http://laravel.com'); $this->assertTrue($response->notFound()); } public function testResponseBodyCasting() { $this->factory->fake([ '*' => ['result' => ['foo' => 'bar']], ]); $response = $this->factory->get('http://foo.com/api'); $this->assertSame('{"result":{"foo":"bar"}}', $response->body()); $this->assertSame('{"result":{"foo":"bar"}}', (string) $response); $this->assertIsArray($response->json()); $this->assertSame(['foo' => 'bar'], $response->json()['result']); $this->assertSame(['foo' => 'bar'], $response->json('result')); $this->assertSame('bar', $response->json('result.foo')); $this->assertSame('default', $response->json('missing_key', 'default')); $this->assertSame(['foo' => 'bar'], $response['result']); } public function testResponseObjectAsArray() { $this->factory->fake([ '*' => [['foo' => 'bar'], ['bar' => 'foo']], ]); $response = $this->factory->get('http://foo.com/api'); $this->assertSame('[{"foo":"bar"},{"bar":"foo"}]', $response->body()); $this->assertSame('[{"foo":"bar"},{"bar":"foo"}]', (string) $response); $this->assertIsArray($response->object()); $this->assertSame('bar', $response->object()[0]->foo); } public function testResponseObjectAsObject() { $this->factory->fake([ '*' => ['result' => ['foo' => 'bar']], ]); $response = $this->factory->get('http://foo.com/api'); $this->assertIsObject($response->object()); $this->assertSame('bar', $response->object()->result->foo); } public function testResponseObjectIsTappable() { $bar = null; $this->factory->fake([ '*' => ['result' => ['foo' => 'bar']], ]); $this->factory->get('http://foo.com/api') ->tap(function (Response $response) use (&$bar) { $bar = $response['result']['foo']; }); $this->assertSame('bar', $bar); } public function testResponseObjectIsMacroable() { Response::macro('movieFields', function () { return $this->collect() ->mapWithKeys(fn ($field, $key) => [strtolower($key) => $field]) ->toArray(); }); $response = $this->factory->fake([ '*' => [ 'Title' => 'The Godfather', 'Year' => 1972, 'Rated' => 'R', 'Runtime' => '175 min', 'Director' => 'Francis Ford Coppola', ], ]); $response = $this->factory->get('http://www.omdbapi.com/?apikey=test_api_key&i=test_imdb_id'); $this->assertIsArray($response->movieFields()); $this->assertSame([ 'title' => 'The Godfather', 'year' => 1972, 'rated' => 'R', 'runtime' => '175 min', 'director' => 'Francis Ford Coppola', ], $response->movieFields()); } public function testResponseCanBeReturnedAsResource() { $this->factory->fake([ '*' => ['result' => ['foo' => 'bar']], ]); $response = $this->factory->get('http://foo.com/api'); $this->assertIsResource($response->resource()); $this->assertSame('{"result":{"foo":"bar"}}', stream_get_contents($response->resource())); } public function testResponseCanBeReturnedAsCollection() { $this->factory->fake([ '*' => ['result' => ['foo' => 'bar']], ]); $response = $this->factory->get('http://foo.com/api'); $this->assertInstanceOf(Collection::class, $response->collect()); $this->assertEquals(collect(['result' => ['foo' => 'bar']]), $response->collect()); $this->assertEquals(collect(['foo' => 'bar']), $response->collect('result')); $this->assertEquals(collect(['bar']), $response->collect('result.foo')); $this->assertEquals(collect(), $response->collect('missing_key')); } public function testResponseCanBeReturnedAsFluent() { $this->factory->fake([ '*' => ['result' => ['foo' => 'bar']], ]); $response = $this->factory->get('http://foo.com/api'); $this->assertInstanceOf(Fluent::class, $response->fluent()); $this->assertEquals(new Fluent(['result' => ['foo' => 'bar']]), $response->fluent()); $this->assertEquals(new Fluent(['foo' => 'bar']), $response->fluent('result')); $this->assertEquals(new Fluent(['bar']), $response->fluent('result.foo')); $this->assertEquals(new Fluent([]), $response->fluent('missing_key')); } public function testSendRequestBodyAsJsonByDefault() { $body = '{"test":"phpunit"}'; $fakeRequest = function (Request $request) use ($body) { $this->assertSame($body, $request->body()); $this->assertContains('application/json', $request->header('Content-Type')); return ['my' => 'response']; }; $this->factory->fake($fakeRequest); $this->factory->withBody($body)->send('get', 'http://foo.com/api'); } public function testSendRequestBodyWithManyAmpersands() { $body = str_repeat('A thousand &. ', 1000); $fakeRequest = function (Request $request) use ($body) { $this->assertSame($body, $request->body()); $this->assertContains('text/plain', $request->header('Content-Type')); return ['my' => 'response']; }; $this->factory->fake($fakeRequest); $this->factory->withBody($body, 'text/plain')->send('post', 'http://foo.com/api'); } public function testSendStreamRequestBody() { $string = 'Look at me, i am a stream!!'; $resource = fopen('php://temp', 'w'); fwrite($resource, $string); rewind($resource); $body = Utils::streamFor($resource); $fakeRequest = function (Request $request) use ($string) { $this->assertSame($string, $request->body()); $this->assertContains('text/plain', $request->header('Content-Type')); return ['my' => 'response']; }; $this->factory->fake($fakeRequest); $this->factory->withBody($body, 'text/plain')->send('post', 'http://foo.com/api'); } public function testUrlsCanBeStubbedByPath() { $this->factory->fake([ 'foo.com/*' => ['page' => 'foo'], 'bar.com/*' => ['page' => 'bar'], '*' => ['page' => 'fallback'], ]); $fooResponse = $this->factory->post('http://foo.com/test'); $barResponse = $this->factory->post('http://bar.com/test'); $fallbackResponse = $this->factory->post('http://fallback.com/test'); $this->assertSame('foo', $fooResponse['page']); $this->assertSame('bar', $barResponse['page']); $this->assertSame('fallback', $fallbackResponse['page']); $this->factory->assertSent(function (Request $request) { return $request->url() === 'http://foo.com/test' && $request->hasHeader('Content-Type', 'application/json'); }); } public function testCanSendJsonData() { $this->factory->fake(); $this->factory->withHeaders([ 'X-Test-Header' => 'foo', 'X-Test-ArrayHeader' => ['bar', 'baz'], ])->post('http://foo.com/json', [ 'name' => 'Taylor', ]); $this->factory->assertSent(function (Request $request) { return $request->url() === 'http://foo.com/json' && $request->hasHeader('Content-Type', 'application/json') && $request->hasHeader('X-Test-Header', 'foo') && $request->hasHeader('X-Test-ArrayHeader', ['bar', 'baz']) && $request['name'] === 'Taylor'; }); } public function testCanSendFormData() { $this->factory->fake(); $this->factory->asForm()->post('http://foo.com/form', [ 'name' => 'Taylor', 'title' => 'Laravel Developer', ]); $this->factory->assertSent(function (Request $request) { return $request->url() === 'http://foo.com/form' && $request->hasHeader('Content-Type', 'application/x-www-form-urlencoded') && $request['name'] === 'Taylor'; }); } public function testCanSendJsonDataWithQueryMethod() { $this->factory->fake(); $this->factory->query('http://foo.com/search', [ 'filter' => 'active', ]); $this->factory->assertSent(function (Request $request) { return $request->url() === 'http://foo.com/search' && $request->method() === 'QUERY' && $request->hasHeader('Content-Type', 'application/json') && $request['filter'] === 'active'; }); } public function testCanSendFormDataWithQueryMethod() { $this->factory->fake(); $this->factory->asForm()->query('http://foo.com/search', [ 'filter' => 'active', ]); $this->factory->assertSent(function (Request $request) { return $request->url() === 'http://foo.com/search' && $request->method() === 'QUERY' && $request->hasHeader('Content-Type', 'application/x-www-form-urlencoded') && $request['filter'] === 'active'; }); } public function testQueryParametersNormalizeNonFiniteFloats() { $this->factory->fake(); $this->factory->get('https://laravel.com/search', [ 'nan' => NAN, 'inf' => INF, 'negative_inf' => -INF, 'nested' => ['value' => NAN], ]); $this->factory->assertSent(function (Request $request) { return $request->url() === 'https://laravel.com/search?nan=NAN&inf=INF&negative_inf=-INF&nested%5Bvalue%5D=NAN'; }); } public function testFormParamsNormalizeNonFiniteFloats() { $this->factory->fake(); $this->factory->asForm()->post('http://foo.com/form', [ 'nan' => NAN, 'inf' => INF, 'negative_inf' => -INF, 'nested' => ['value' => NAN], ]); $this->factory->assertSent(function (Request $request) { return $request->url() === 'http://foo.com/form' && $request->body() === 'nan=NAN&inf=INF&negative_inf=-INF&nested%5Bvalue%5D=NAN'; }); } #[DataProvider('methodsReceivingArrayableDataProvider')] public function testCanSendArrayableFormData(string $method) { $this->factory->fake(); $this->factory->asForm()->{$method}('http://foo.com/form', new Fluent([ 'name' => 'Taylor', 'title' => 'Laravel Developer', ])); $this->factory->assertSent(function (Request $request) { return $request->url() === 'http://foo.com/form' && $request->hasHeader('Content-Type', 'application/x-www-form-urlencoded') && $request['name'] === 'Taylor'; }); } #[DataProvider('methodsReceivingArrayableDataProvider')] public function testCanSendJsonSerializableData(string $method) { $this->factory->fake(); $this->factory->asJson()->{$method}('http://foo.com/form', new class implements JsonSerializable { public function jsonSerialize(): mixed { return [ 'name' => 'Taylor', 'title' => 'Laravel Developer', ]; } }); $this->factory->assertSent(function (Request $request) { return $request->url() === 'http://foo.com/form' && $request->hasHeader('Content-Type', 'application/json') && $request['name'] === 'Taylor'; }); } #[DataProvider('methodsReceivingArrayableDataProvider')] public function testPrefersJsonSerializableOverArrayableData(string $method) { $this->factory->fake(); $this->factory->asJson()->{$method}('http://foo.com/form', new class implements JsonSerializable, Arrayable { public function jsonSerialize(): mixed { return [ 'attributes' => (object) [], ]; } public function toArray(): array { return [ 'attributes' => [], ]; } }); $this->factory->assertSent(function (Request $request) { return $request->url() === 'http://foo.com/form' && $request->hasHeader('Content-Type', 'application/json') && $request->body() === '{"attributes":{}}'; }); } public function testCanSendJsonDataWithStringable() { $this->factory->fake(); $this->factory->withHeaders([ 'X-Test-Header' => 'foo', 'X-Test-ArrayHeader' => ['bar', 'baz'], ])->post('http://foo.com/json', [ 'name' => new Stringable('Taylor'), ]); $this->factory->assertSent(function (Request $request) { return $request->url() === 'http://foo.com/json' && $request->hasHeader('Content-Type', 'application/json') && $request->hasHeader('X-Test-Header', 'foo') && $request->hasHeader('X-Test-ArrayHeader', ['bar', 'baz']) && $request['name'] === 'Taylor'; }); } public function testHeaderValuesAreSerialized() { $this->factory->fake(); $this->factory->withHeaders([ 'X-Int' => 123, 'X-Float' => 1.5, 'X-Null' => null, 'X-True' => true, 'X-False' => false, 'X-Nan' => NAN, 'X-Inf' => INF, 'X-Negative-Inf' => -INF, 'X-Laravel-Stringable' => new Stringable('laravel stringable'), 'X-Multiple' => ['first', 123, true, false, null, NAN, INF, -INF], 'X-Empty' => [], ])->post('http://foo.com/json'); $this->factory->assertSent(function (Request $request) { return $request->hasHeader('X-Int', '123') && $request->hasHeader('X-Float', '1.5') && $request->hasHeader('X-Null', '') && $request->hasHeader('X-True', '1') && $request->hasHeader('X-False', '') && $request->hasHeader('X-Nan', 'NAN') && $request->hasHeader('X-Inf', 'INF') && $request->hasHeader('X-Negative-Inf', '-INF') && $request->hasHeader('X-Laravel-Stringable', 'laravel stringable') && $request->hasHeader('X-Multiple', ['first', '123', '1', '', '', 'NAN', 'INF', '-INF']) && $request->hasHeader('X-Empty', ''); }); } #[DataProvider('invalidHeaderValuesProvider')] public function testInvalidHeaderValuesAreRejected($value) { $this->factory->fake(); $this->expectExceptionObject(new InvalidArgumentException('HTTP header values must be scalar, null, Laravel Stringable, or arrays of scalar, null, or Laravel Stringable values.')); $this->factory->withHeaders(['X-Test' => $value])->post('http://foo.com/json'); } public function testHeaderValuesProvidedThroughOptionsAreSerialized() { $this->factory->fake(); $this->factory->withOptions([ 'headers' => ['X-Test' => 123, 'X-Null' => null, 'X-Nan' => NAN], ])->post('http://foo.com/json'); $this->factory->assertSent(function (Request $request) { return $request->hasHeader('X-Test', '123') && $request->hasHeader('X-Null', '') && $request->hasHeader('X-Nan', 'NAN'); }); } public function testRequestHeadersAreCheckedCaseInsensitively() { $this->factory->fake(); $this->factory->withHeaders([ 'foo' => 'Bar', ])->post('http://foo.com/json'); $this->factory->assertSent(function (Request $request) { return $request->hasHeader('Foo') && $request->hasHeader('Foo', 'Bar') && $request->header('Foo') === ['Bar']; }); } public function testContentTypeHeadersAreCheckedCaseInsensitively() { $this->factory->fake(); $this->factory->send('POST', 'http://foo.com/json', [ 'headers' => ['content-type' => 'application/json'], 'body' => '{"name":"Taylor"}', ]); $this->factory->assertSent(function (Request $request) { return $request->hasHeader('Content-Type') && $request->header('Content-Type') === ['application/json'] && $request->isJson(); }); } public function testCanSendFormDataWithStringable() { $this->factory->fake(); $this->factory->asForm()->post('http://foo.com/form', [ 'name' => new Stringable('Taylor'), 'title' => 'Laravel Developer', ]); $this->factory->assertSent(function (Request $request) { return $request->url() === 'http://foo.com/form' && $request->hasHeader('Content-Type', 'application/x-www-form-urlencoded') && $request['name'] === 'Taylor'; }); } public function testCanSendFormDataWithStringableInArrays() { $this->factory->fake(); $this->factory->asForm()->post('http://foo.com/form', [ 'posts' => [['title' => new Stringable('Taylor')]], ]); $this->factory->assertSent(function (Request $request) { return $request->url() === 'http://foo.com/form' && $request->hasHeader('Content-Type', 'application/x-www-form-urlencoded') && $request['posts'][0]['title'] === 'Taylor'; }); } public function testRecordedCallsAreEmptiedWhenFakeIsCalled() { $this->factory->fake([ 'http://foo.com/*' => ['page' => 'foo'], ]); $this->factory->get('http://foo.com/test'); $this->factory->assertSent(function (Request $request) { return $request->url() === 'http://foo.com/test'; }); $this->factory->fake(); $this->factory->assertNothingSent(); } public function testSpecificRequestIsNotBeingSent() { $this->factory->fake(); $this->factory->post('http://foo.com/form', [ 'name' => 'Taylor', ]); $this->factory->assertNotSent(function (Request $request) { return $request->url() === 'http://foo.com/form' && $request['name'] === 'Peter'; }); } public function testNoRequestIsNotBeingSent() { $this->factory->fake(); $this->factory->assertNothingSent(); } public function testRequestCount() { $this->factory->fake(); $this->factory->assertSentCount(0); $this->factory->post('http://foo.com/form', [ 'name' => 'Taylor', ]); $this->factory->assertSentCount(1); $this->factory->post('http://foo.com/form', [ 'name' => 'Jim', ]); $this->factory->assertSentCount(2); } public function testCanSendMultipartData() { $this->factory->fake(); $this->factory->asMultipart()->post('http://foo.com/multipart', [ [ 'name' => 'foo', 'contents' => 'data', 'headers' => ['X-Test-Header' => 'foo'], ], ]); $this->factory->assertSent(function (Request $request) { return $request->url() === 'http://foo.com/multipart' && Str::startsWith($request->header('Content-Type')[0], 'multipart') && $request[0]['name'] === 'foo'; }); } public function testFilesCanBeAttached() { $this->factory->fake(); $this->factory->attach('foo', 'data', 'file.txt', ['X-Test-Header' => 'foo']) ->post('http://foo.com/file'); $this->factory->assertSent(function (Request $request) { return $request->url() === 'http://foo.com/file' && Str::startsWith($request->header('Content-Type')[0], 'multipart') && $request[0]['name'] === 'foo' && $request->hasFile('foo', 'data', 'file.txt'); }); } public function testAttachPreservesEmptyContents() { $this->factory->fake(); $this->factory->attach('file')->post('http://foo.com/file'); $this->factory->assertSent(function (Request $request) { return $request->url() === 'http://foo.com/file' && $request->isMultipart() && $request[0]['name'] === 'file' && array_key_exists('contents', $request[0]) && $request[0]['contents'] === ''; }); } public function testAttachPreservesFalseyStringContentsAndName() { $this->factory->fake(); $this->factory->attach('0', '0')->post('http://foo.com/file'); $this->factory->assertSent(function (Request $request) { return $request->url() === 'http://foo.com/file' && $request->isMultipart() && $request[0]['name'] === '0' && $request[0]['contents'] === '0'; }); } public function testAttachHeaderValuesAreSerialized() { $this->factory->fake(); $this->factory->attach('file', 'data', 'file.txt', ['X-Part' => 123, 'X-Null' => null, 'X-Nan' => NAN])->post('http://foo.com/file'); $this->factory->assertSent(function (Request $request) { return $request[0]['headers']['X-Part'] === '123' && $request[0]['headers']['X-Null'] === '' && $request[0]['headers']['X-Nan'] === 'NAN'; }); } public function testMultipartHeaderValuesAreSerialized() { $this->factory->fake(); $this->factory->asMultipart()->post('http://foo.com/multipart', [ [ 'name' => 'file', 'contents' => 'data', 'headers' => [ 'X-Part' => 123, 'X-Null' => null, 'X-Nan' => NAN, 'X-Inf' => INF, 'X-Negative-Inf' => -INF, 'X-Empty' => [], 'X-Laravel-Stringable' => new Stringable('laravel stringable'), ], ], ]); $this->factory->assertSent(function (Request $request) { return $request[0]['headers']['X-Part'] === '123' && $request[0]['headers']['X-Null'] === '' && $request[0]['headers']['X-Nan'] === 'NAN' && $request[0]['headers']['X-Inf'] === 'INF' && $request[0]['headers']['X-Negative-Inf'] === '-INF' && $request[0]['headers']['X-Empty'] === '' && $request[0]['headers']['X-Laravel-Stringable'] === 'laravel stringable'; }); } #[DataProvider('invalidMultipartHeaderValuesProvider')] public function testInvalidMultipartHeaderValuesAreRejected($value) { $this->factory->fake(); $this->expectExceptionObject(new InvalidArgumentException('Multipart header values must be scalar, null, or Laravel Stringable.')); $this->factory->asMultipart()->post('http://foo.com/multipart', [ [ 'name' => 'file', 'contents' => 'data', 'headers' => ['X-Part' => $value], ], ]); } public function testCanSendMultipartDataWithSimplifiedParameters() { $this->factory->fake(); $this->factory->asMultipart()->post('http://foo.com/multipart', [ 'foo' => 'bar', ]); $this->factory->assertSent(function (Request $request) { return $request->url() === 'http://foo.com/multipart' && Str::startsWith($request->header('Content-Type')[0], 'multipart') && $request[0]['name'] === 'foo' && $request[0]['contents'] === 'bar'; }); } public function testCanSendMultipartDataWithBothSimplifiedAndExtendedParameters() { $this->factory->fake(); $this->factory->asMultipart()->post('http://foo.com/multipart', [ 'foo' => 'bar', [ 'name' => 'foobar', 'contents' => 'data', 'headers' => ['X-Test-Header' => 'foo'], ], ]); $this->factory->assertSent(function (Request $request) { return $request->url() === 'http://foo.com/multipart' && Str::startsWith($request->header('Content-Type')[0], 'multipart') && $request[0]['name'] === 'foo' && $request[0]['contents'] === 'bar' && $request[1]['name'] === 'foobar' && $request[1]['contents'] === 'data' && $request[1]['headers']['X-Test-Header'] === 'foo'; }); } public function testCanSendMultipartDataWithArrayValues() { $this->factory->fake(); $this->factory->asMultipart()->post('http://foo.com/multipart', [ 'name' => 'Steve', 'roles' => ['Network Administrator', 'Janitor'], ]); $this->factory->assertSent(function (Request $request) { return $request->url() === 'http://foo.com/multipart' && Str::startsWith($request->header('Content-Type')[0], 'multipart') && $request[0]['name'] === 'name' && $request[0]['contents'] === 'Steve' && $request[1]['name'] === 'roles' && $request[1]['contents'] === ['Network Administrator', 'Janitor']; }); } public function testMultipartContentsNormalizeNonFiniteFloats() { $this->factory->fake(); $this->factory->asMultipart()->post('http://foo.com/multipart', [ 'nan' => NAN, 'inf' => INF, 'negative_inf' => -INF, 'nested' => ['value' => NAN], ]); $this->factory->assertSent(function (Request $request) { return $request->url() === 'http://foo.com/multipart' && $request->isMultipart() && $request[0]['contents'] === 'NAN' && $request[1]['contents'] === 'INF' && $request[2]['contents'] === '-INF' && $request[3]['contents'] === ['value' => 'NAN']; }); } public function testCanSendMultipartDataWithFileAndArrayValues() { $this->factory->fake(); $this->factory ->attach('attachment', 'photo_content', 'photo.jpg', ['Content-Type' => 'image/jpeg']) ->post('http://foo.com/multipart', [ 'name' => 'Steve', 'roles' => ['Network Administrator', 'Janitor'], ]); $this->factory->assertSent(function (Request $request) { return $request->url() === 'http://foo.com/multipart' && Str::startsWith($request->header('Content-Type')[0], 'multipart') && $request[0]['name'] === 'name' && $request[0]['contents'] === 'Steve' && $request[1]['name'] === 'roles' && $request[1]['contents'] === ['Network Administrator', 'Janitor'] && $request[2]['name'] === 'attachment' && $request[2]['contents'] === 'photo_content' && $request[2]['filename'] === 'photo.jpg'; }); } public function testItCanSendToken() { $this->factory->fake(); $this->factory->withToken('token')->post('http://foo.com/json'); $this->factory->assertSent(function (Request $request) { return $request->url() === 'http://foo.com/json' && $request->hasHeader('Authorization', 'Bearer token'); }); } public function testItCanSendUserAgent() { $this->factory->fake(); $this->factory->withUserAgent('Laravel')->post('http://foo.com/json'); $this->factory->assertSent(function (Request $request) { return $request->url() === 'http://foo.com/json' && $request->hasHeader('User-Agent', 'Laravel'); }); } public function testItOnlySendsOneUserAgentHeader() { $this->factory->fake(); $this->factory->withUserAgent('Laravel') ->withUserAgent('FooBar') ->post('http://foo.com/json'); $this->factory->assertSent(function (Request $request) { $userAgent = $request->header('User-Agent'); return $request->url() === 'http://foo.com/json' && count($userAgent) === 1 && $userAgent[0] === 'FooBar'; }); } public function testSequenceBuilder() { $this->factory->fake([ '*' => $this->factory->sequence() ->push('Ok', 201) ->push(['fact' => 'Cats are great!']) ->pushFile(__DIR__.'/fixtures/test.txt') ->pushStatus(403), ]); $response = $this->factory->get('https://example.com'); $this->assertSame('Ok', $response->body()); $this->assertSame(201, $response->status()); $response = $this->factory->get('https://example.com'); $this->assertSame(['fact' => 'Cats are great!'], $response->json()); $this->assertSame('application/json', $response->header('Content-Type')); $this->assertSame(200, $response->status()); $response = $this->factory->get('https://example.com'); $this->assertSame( "This is a story about something that happened long ago when your grandfather was a child.\n", str_replace("\r\n", "\n", $response->body()) ); $this->assertSame(200, $response->status()); $response = $this->factory->get('https://example.com'); $this->assertSame('', $response->body()); $this->assertSame(403, $response->status()); $this->expectException(OutOfBoundsException::class); // The sequence is empty, it should throw an exception. $this->factory->get('https://example.com'); } public function testSequenceBuilderCanKeepGoingWhenEmpty() { $this->factory->fake([ '*' => $this->factory->sequence() ->dontFailWhenEmpty() ->push('Ok'), ]); $response = $this->factory->get('https://laravel.com'); $this->assertSame('Ok', $response->body()); // The sequence is empty, but it should not fail. $this->factory->get('https://laravel.com'); } public function testAssertSequencesAreEmpty() { $this->factory->fake([ '*' => $this->factory->sequence() ->push('1') ->push('2'), ]); $this->factory->get('https://example.com'); $this->factory->get('https://example.com'); $this->factory->assertSequencesAreEmpty(); } public function testFakeSequence() { $this->factory->fakeSequence() ->pushStatus(201) ->pushStatus(301); $this->assertSame(201, $this->factory->get('https://example.com')->status()); $this->assertSame(301, $this->factory->get('https://example.com')->status()); } public function testWithCookies() { $this->factory->fakeSequence()->pushStatus(200); $response = $this->factory->withCookies( ['foo' => 'bar'], 'https://laravel.com' )->get('https://laravel.com'); $this->assertCount(1, $response->cookies()->toArray()); /** @var \GuzzleHttp\Cookie\CookieJarInterface $responseCookies */ $responseCookie = $response->cookies()->toArray()[0]; $this->assertSame('foo', $responseCookie['Name']); $this->assertSame('bar', $responseCookie['Value']); $this->assertSame('https://laravel.com', $responseCookie['Domain']); } public function testWithQueryParameters() { $this->factory->fake(); $this->factory->withQueryParameters( ['foo' => 'bar'] )->get('https://laravel.com'); $this->factory->assertSent(function (Request $request) { return $request->url() === 'https://laravel.com?foo=bar'; }); } public function testWithArrayQueryParameters() { $this->factory->fake(); $this->factory->withQueryParameters( ['foo' => ['bar', 'baz']], )->get('https://laravel.com'); $this->factory->assertSent(function (Request $request) { return $request->url() === 'https://laravel.com?foo%5B0%5D=bar&foo%5B1%5D=baz'; }); } public function testWithQueryParametersAllowsAddingMoreOnRequest() { $this->factory->fake(); $this->factory->withQueryParameters( ['foo' => 'bar'] )->get('https://laravel.com', [ 'baz' => 'qux', ]); $this->factory->assertSent(function (Request $request) { return $request->url() === 'https://laravel.com?foo=bar&baz=qux'; }); } public function testWithQueryParametersAllowsOverridingParameterOnRequest() { $this->factory->fake(); $this->factory->withQueryParameters([ 'foo' => 'bar', 'baz' => 'baz', ])->get('https://laravel.com', [ // Override the previously set value 'baz' => 'qux', ]); $this->factory->assertSent(function (Request $request) { return $request->url() === 'https://laravel.com?foo=bar&baz=qux'; }); } public function testWithStringableQueryParameters() { $this->factory->fake(); $this->factory->withQueryParameters( ['foo' => new Stringable('bar')] )->get('https://laravel.com'); $this->factory->assertSent(function (Request $request) { return $request->url() === 'https://laravel.com?foo=bar'; }); } public function testWithArrayStringableQueryParameters() { $this->factory->fake(); $this->factory->withQueryParameters( ['foo' => ['bar', new Stringable('baz')]], )->get('https://laravel.com'); $this->factory->assertSent(function (Request $request) { return $request->url() === 'https://laravel.com?foo%5B0%5D=bar&foo%5B1%5D=baz'; }); } public function testGetWithArrayQueryParam() { $this->factory->fake(); $this->factory->get('http://foo.com/get', ['foo' => 'bar']); $this->factory->assertSent(function (Request $request) { return $request->url() === 'http://foo.com/get?foo=bar' && $request['foo'] === 'bar'; }); } public function testGetWithArrayableQueryParam() { $this->factory->fake(); $this->factory->get('http://foo.com/get', new Fluent(['foo' => 'bar'])); $this->factory->assertSent(function (Request $request) { return $request->url() === 'http://foo.com/get?foo=bar' && $request['foo'] === 'bar'; }); } public function testGetWithStringQueryParam() { $this->factory->fake(); $this->factory->get('http://foo.com/get', 'foo=bar'); $this->factory->assertSent(function (Request $request) { return $request->url() === 'http://foo.com/get?foo=bar' && $request['foo'] === 'bar'; }); } public function testRequestUriMethod() { $this->factory->fake(); $this->factory->get('http://foo.com/get?foo=bar&page=1'); $this->factory->assertSent(function (Request $request) { return $request->uri() instanceof Uri && (string) $request->uri() === 'http://foo.com/get?foo=bar&page=1'; }); } public function testGetWithQuery() { $this->factory->fake(); $this->factory->get('http://foo.com/get?foo=bar&page=1'); $this->factory->assertSent(function (Request $request) { return $request->url() === 'http://foo.com/get?foo=bar&page=1' && $request['foo'] === 'bar' && $request['page'] === '1'; }); } public function testGetWithQueryWontEncode() { $this->factory->fake(); $this->factory->get('http://foo.com/get?foo;bar;1;5;10&page=1'); $this->factory->assertSent(function (Request $request) { return $request->url() === 'http://foo.com/get?foo;bar;1;5;10&page=1' && ! isset($request['foo']) && ! isset($request['bar']) && $request['page'] === '1'; }); } public function testGetWithArrayQueryParamOverwrites() { $this->factory->fake(); $this->factory->get('http://foo.com/get?foo=bar&page=1', ['hello' => 'world']); $this->factory->assertSent(function (Request $request) { return $request->url() === 'http://foo.com/get?hello=world' && $request['hello'] === 'world'; }); } public function testGetWithArrayQueryParamEncodes() { $this->factory->fake(); $this->factory->get('http://foo.com/get', ['foo;bar; space test' => 'laravel']); $this->factory->assertSent(function (Request $request) { return $request->url() === 'http://foo.com/get?foo%3Bbar%3B%20space%20test=laravel' && $request['foo;bar; space test'] === 'laravel'; }); } public function testWithBaseUrl() { $this->factory->fake(); $this->factory->baseUrl('http://foo.com/')->get('get'); $this->factory->assertSent(function (Request $request) { return $request->url() === 'http://foo.com/get'; }); $this->factory->fake(); $this->factory->baseUrl('http://foo.com/')->get('http://bar.com/get'); $this->factory->assertSent(function (Request $request) { return $request->url() === 'http://bar.com/get'; }); } public function testCanConfirmManyHeaders() { $this->factory->fake(); $this->factory->withHeaders([ 'X-Test-Header' => 'foo', 'X-Test-ArrayHeader' => ['bar', 'baz'], ])->post('http://foo.com/json'); $this->factory->assertSent(function (Request $request) { return $request->url() === 'http://foo.com/json' && $request->hasHeaders([ 'X-Test-Header' => 'foo', 'X-Test-ArrayHeader' => ['bar', 'baz'], ]); }); } public function testCanConfirmManyHeadersUsingAString() { $this->factory->fake(); $this->factory->withHeaders([ 'X-Test-Header' => 'foo', 'X-Test-ArrayHeader' => ['bar', 'baz'], ])->post('http://foo.com/json'); $this->factory->assertSent(function (Request $request) { return $request->url() === 'http://foo.com/json' && $request->hasHeaders('X-Test-Header'); }); } public function testItMergesMultipleHeaders() { $this->factory->fake(); $this->factory->withHeaders([ 'X-Test-Header' => 'foo', ])->withHeaders([ 'X-Test-Header' => 'bar', ])->withHeaders([ 'X-Test-Header' => ['baz', 'qux'], ])->post('http://foo.com/json'); $this->factory->assertSent(function (Request $request) { return $request->url() === 'http://foo.com/json' && $request->hasHeaders(['X-Test-Header' => ['foo', 'bar', 'baz', 'qux']]); }); } public function testItCanReplaceHeaders() { $this->factory->fake(); $this->factory->withHeaders([ 'X-Test-Header' => 'foo', ])->replaceHeaders([ 'X-Test-Header' => 'baz', ])->post('http://foo.com/json'); $this->factory->assertSent(function (Request $request) { return $request->url() === 'http://foo.com/json' && $request->hasHeaders(['X-Test-Header' => ['baz']]); }); } public function testItCanReplaceHeadersWhenNoHeadersYetSet() { $this->factory->fake(); $this->factory->replaceHeaders([ 'X-Test-Header' => 'baz', ])->post('http://foo.com/json'); $this->factory->assertSent(function (Request $request) { return $request->url() === 'http://foo.com/json' && $request->hasHeaders(['X-Test-Header' => ['baz']]); }); } public function testCanConfirmSingleStringHeader() { $this->factory->fake(); $this->factory->withHeader('X-Test-Header', 'foo')->post('http://foo.com/json'); $this->factory->assertSent(function (Request $request) { return $request->url() === 'http://foo.com/json' && $request->hasHeaders([ 'X-Test-Header' => 'foo', ]); }); } public function testCanConfirmSingleArrayHeader() { $this->factory->fake(); $this->factory->withHeader('X-Test-ArrayHeader', ['bar', 'baz'])->post('http://foo.com/json'); $this->factory->assertSent(function (Request $request) { return $request->url() === 'http://foo.com/json' && $request->hasHeaders([ 'X-Test-ArrayHeader' => ['bar', 'baz'], ]); }); } public function testExceptionAccessorOnSuccess() { $resp = new Response(new Psr7Response()); $this->assertNull($resp->toException()); } public function testExceptionAccessorOnFailure() { $error = [ 'error' => [ 'code' => 403, 'message' => 'The Request can not be completed', ], ]; $response = new Psr7Response(403, [], json_encode($error)); $resp = new Response($response); $this->assertInstanceOf(RequestException::class, $resp->toException()); } public function testRequestExceptionSummary() { $this->expectException(RequestException::class); $this->expectExceptionMessage('{"error":{"code":403,"message":"The Request can not be completed"}}'); $error = [ 'error' => [ 'code' => 403, 'message' => 'The Request can not be completed', ], ]; $response = new Psr7Response(403, [], json_encode($error)); throw tap(new RequestException(new Response($response)), fn ($exception) => $exception->report()); } public function testRequestExceptionTruncatedSummary() { $this->expectException(RequestException::class); $this->expectExceptionMessage('{"error":{"code":403,"message":"The Request can not be completed because quota limit was exceeded. Please, check our sup (truncated...)'); $error = [ 'error' => [ 'code' => 403, 'message' => 'The Request can not be completed because quota limit was exceeded. Please, check our support team to increase your limit', ], ]; $response = new Psr7Response(403, [], json_encode($error)); throw tap(new RequestException(new Response($response)), fn ($exception) => $exception->report()); } public function testRequestExceptionWithoutTruncatedSummary() { RequestException::dontTruncate(); $this->expectException(RequestException::class); $this->expectExceptionMessage('{"error":{"code":403,"message":"The Request can not be completed because quota limit was exceeded. Please, check our support team to increase your limit'); $error = [ 'error' => [ 'code' => 403, 'message' => 'The Request can not be completed because quota limit was exceeded. Please, check our support team to increase your limit', ], ]; $response = new Psr7Response(403, [], json_encode($error)); throw tap(new RequestException(new Response($response)), fn ($exception) => $exception->report()); } public function testRequestExceptionWithCustomTruncatedSummary() { RequestException::truncateAt(60); $this->expectException(RequestException::class); $this->expectExceptionMessage('{"error":{"code":403,"message":"The Request can not be compl (truncated...)'); $error = [ 'error' => [ 'code' => 403, 'message' => 'The Request can not be completed because quota limit was exceeded. Please, check our support team to increase your limit', ], ]; $response = new Psr7Response(403, [], json_encode($error)); throw tap(new RequestException(new Response($response)), fn ($exception) => $exception->report()); } public function testRequestLevelTruncationLevelOnRequestException() { RequestException::truncateAt(60); $this->factory->fake([ '*' => $this->factory::response(['error'], 403), ]); $exception = null; try { $this->factory->throw()->truncateExceptionsAt(3)->get('http://foo.com/json'); } catch (RequestException $e) { $exception = $e; } // Ensure the exception message is truncated according to the request level truncation setting. $this->assertSame("HTTP request returned status code 403:\n[\"e (truncated...)\n", $exception->getMessage()); $exception->report(); // Ensure that the truncation level is not changed when reporting the exception. $this->assertSame("HTTP request returned status code 403:\n[\"e (truncated...)\n", $exception->getMessage()); $this->assertEquals(60, RequestException::$truncateAt); } public function testNoTruncationOnRequestLevel() { RequestException::truncateAt(60); $this->factory->fake([ '*' => $this->factory::response(['error'], 403), ]); $exception = null; try { $this->factory->throw()->dontTruncateExceptions()->get('http://foo.com/json'); } catch (RequestException $e) { $exception = $e; } $exception->report(); $this->assertSame("HTTP request returned status code 403:\nHTTP/1.1 403 Forbidden\r\nContent-Type: application/json\r\n\r\n[\"error\"]\n", $exception->getMessage()); $this->assertEquals(60, RequestException::$truncateAt); } public function testRequestExceptionDoesNotTruncateButRequestDoes() { RequestException::dontTruncate(); $this->factory->fake([ '*' => $this->factory::response(['error'], 403), ]); $exception = null; try { $this->factory->throw()->truncateExceptionsAt(3)->get('http://foo.com/json'); } catch (RequestException $e) { $exception = $e; } $exception->report(); $this->assertSame("HTTP request returned status code 403:\n[\"e (truncated...)\n", $exception->getMessage()); $this->assertFalse(RequestException::$truncateAt); } public function testAsyncRequestExceptionsRespectRequestTruncation() { RequestException::dontTruncate(); $this->factory->fake([ '*' => $this->factory::response(['error'], 403), ]); $exception = $this->factory->async()->throw()->truncateExceptionsAt(4)->get('http://foo.com/json')->wait(); $exception->report(); $this->assertInstanceOf(RequestException::class, $exception); $this->assertSame("HTTP request returned status code 403:\n[\"er (truncated...)\n", $exception->getMessage()); $this->assertFalse(RequestException::$truncateAt); } public function testRequestExceptionEmptyBody() { $this->expectException(RequestException::class); $this->expectExceptionMessageMatches('/HTTP request returned status code 403$/'); $response = new Psr7Response(403); throw new RequestException(new Response($response)); } public function testReportingExceptionTwiceDoesNotIncludeSummaryTwice() { RequestException::dontTruncate(); $error = [ 'error' => [ 'code' => 403, 'message' => 'The Request can not be completed', ], ]; $response = new Psr7Response(403, [], json_encode($error)); $exception = new RequestException(new Response($response)); $exception->report(); $exception->report(); $this->assertEquals(1, substr_count($exception->getMessage(), '{"error":{"code":403,"message":"The Request can not be completed"}}')); } #[TestWith([false])] #[TestWith([120])] public function testStreamingResponseExceptionMessageIsNotSummarizedWhenBodyIsNotSeekable(int|false $truncateAt) { RequestException::$truncateAt = $truncateAt; $this->factory->fake([ '*' => Create::promiseFor( new Psr7Response( 400, ['Content-Type' => 'application/json'], new NoSeekStream(Utils::streamFor(json_encode(['hello' => 'world']))) ) ), ]); $throwCallbackCalled = false; try { $this->factory ->withOptions(['stream' => true]) ->throw(function (Response $response, RequestException $exception) use (&$throwCallbackCalled) { $throwCallbackCalled = true; $this->assertNotNull($response->json()); $this->assertSame('HTTP request returned status code 400', $exception->getMessage()); }) ->get('http://example.com'); $this->fail('RequestException was not thrown.'); } catch (RequestException $exception) { $this->assertSame('HTTP request returned status code 400', $exception->getMessage()); } $this->assertTrue($throwCallbackCalled); } public function testOnErrorDoesntCallClosureOnInformational() { $status = 0; $client = $this->factory->fake([ 'laravel.com' => $this->factory::response('', 101), ]); $response = $client->get('laravel.com') ->onError(function ($response) use (&$status) { $status = $response->status(); }); $this->assertSame(0, $status); $this->assertSame(101, $response->status()); } public function testOnErrorDoesntCallClosureOnSuccess() { $status = 0; $client = $this->factory->fake([ 'laravel.com' => $this->factory::response('', 201), ]); $response = $client->get('laravel.com') ->onError(function ($response) use (&$status) { $status = $response->status(); }); $this->assertSame(0, $status); $this->assertSame(201, $response->status()); } public function testOnErrorDoesntCallClosureOnRedirection() { $status = 0; $client = $this->factory->fake([ 'laravel.com' => $this->factory::response('', 301), ]); $response = $client->get('laravel.com') ->onError(function ($response) use (&$status) { $status = $response->status(); }); $this->assertSame(0, $status); $this->assertSame(301, $response->status()); } public function testOnErrorCallsClosureOnClientError() { $status = 0; $client = $this->factory->fake([ 'laravel.com' => $this->factory::response('', 401), ]); $response = $client->get('laravel.com') ->onError(function ($response) use (&$status) { $status = $response->status(); }); $this->assertSame(401, $status); $this->assertSame(401, $response->status()); } public function testOnErrorCallsClosureOnServerError() { $status = 0; $client = $this->factory->fake([ 'laravel.com' => $this->factory::response('', 501), ]); $response = $client->get('laravel.com') ->onError(function ($response) use (&$status) { $status = $response->status(); }); $this->assertSame(501, $status); $this->assertSame(501, $response->status()); } public function testSinkToFile() { $this->factory->fakeSequence()->push('abc123'); $destination = __DIR__.'/fixtures/sunk.txt'; if (file_exists($destination)) { unlink($destination); } $this->factory->withOptions(['sink' => $destination])->get('https://example.com'); $this->assertFileExists($destination); $this->assertSame('abc123', file_get_contents($destination)); unlink($destination); } public function testSinkToResource() { $this->factory->fakeSequence()->push('abc123'); $resource = fopen('php://temp', 'w'); $this->factory->sink($resource)->get('https://example.com'); $this->assertSame(0, ftell($resource)); $this->assertSame('abc123', stream_get_contents($resource)); } public function testSinkWhenStubbedByPath() { $this->factory->fake([ 'foo.com/*' => ['page' => 'foo'], ]); $resource = fopen('php://temp', 'w'); $this->factory->sink($resource)->get('http://foo.com/test'); $this->assertSame(json_encode(['page' => 'foo']), stream_get_contents($resource)); } public function testCanAssertAgainstOrderOfHttpRequestsWithUrlStrings() { $this->factory->fake(); $exampleUrls = [ 'http://example.com/1', 'http://example.com/2', 'http://example.com/3', ]; foreach ($exampleUrls as $url) { $this->factory->get($url); } $this->factory->assertSentInOrder($exampleUrls); } public function testAssertionsSentOutOfOrderThrowAssertionFailed() { $this->factory->fake(); $exampleUrls = [ 'http://example.com/1', 'http://example.com/2', 'http://example.com/3', ]; $this->factory->get($exampleUrls[0]); $this->factory->get($exampleUrls[2]); $this->factory->get($exampleUrls[1]); $this->expectException(AssertionFailedError::class); $this->factory->assertSentInOrder($exampleUrls); } public function testWrongNumberOfRequestsThrowAssertionFailed() { $this->factory->fake(); $exampleUrls = [ 'http://example.com/1', 'http://example.com/2', 'http://example.com/3', ]; $this->factory->get($exampleUrls[0]); $this->factory->get($exampleUrls[1]); $this->expectException(AssertionFailedError::class); $this->factory->assertSentInOrder($exampleUrls); } public function testCanAssertAgainstOrderOfHttpRequestsWithCallables() { $this->factory->fake(); $exampleUrls = [ function ($request) { return $request->url() === 'http://example.com/1'; }, function ($request) { return $request->url() === 'http://example.com/2'; }, function ($request) { return $request->url() === 'http://example.com/3'; }, ]; $this->factory->get('http://example.com/1'); $this->factory->get('http://example.com/2'); $this->factory->get('http://example.com/3'); $this->factory->assertSentInOrder($exampleUrls); } public function testCanAssertAgainstOrderOfHttpRequestsWithCallablesAndHeaders() { $this->factory->fake(); $executionOrder = [ function (Request $request) { return $request->url() === 'http://foo.com/json' && $request->hasHeader('Content-Type', 'application/json') && $request->hasHeader('X-Test-Header', 'foo') && $request->hasHeader('X-Test-ArrayHeader', ['bar', 'baz']) && $request['name'] === 'Taylor'; }, function (Request $request) { return $request->url() === 'http://bar.com/json' && $request->hasHeader('Content-Type', 'application/json') && $request->hasHeader('X-Test-Header', 'bar') && $request->hasHeader('X-Test-ArrayHeader', ['bar', 'baz']) && $request['name'] === 'Taylor'; }, ]; $this->factory->withHeaders([ 'X-Test-Header' => 'foo', 'X-Test-ArrayHeader' => ['bar', 'baz'], ])->post('http://foo.com/json', [ 'name' => 'Taylor', ]); $this->factory->withHeaders([ 'X-Test-Header' => 'bar', 'X-Test-ArrayHeader' => ['bar', 'baz'], ])->post('http://bar.com/json', [ 'name' => 'Taylor', ]); $this->factory->assertSentInOrder($executionOrder); } public function testCanAssertAgainstOrderOfHttpRequestsWithCallablesAndHeadersFailsCorrectly() { $this->factory->fake(); $executionOrder = [ function (Request $request) { return $request->url() === 'http://bar.com/json' && $request->hasHeader('Content-Type', 'application/json') && $request->hasHeader('X-Test-Header', 'bar') && $request->hasHeader('X-Test-ArrayHeader', ['bar', 'baz']) && $request['name'] === 'Taylor'; }, function (Request $request) { return $request->url() === 'http://foo.com/json' && $request->hasHeader('Content-Type', 'application/json') && $request->hasHeader('X-Test-Header', 'foo') && $request->hasHeader('X-Test-ArrayHeader', ['bar', 'baz']) && $request['name'] === 'Taylor'; }, ]; $this->factory->withHeaders([ 'X-Test-Header' => 'foo', 'X-Test-ArrayHeader' => ['bar', 'baz'], ])->post('http://foo.com/json', [ 'name' => 'Taylor', ]); $this->factory->withHeaders([ 'X-Test-Header' => 'bar', 'X-Test-ArrayHeader' => ['bar', 'baz'], ])->post('http://bar.com/json', [ 'name' => 'Taylor', ]); $this->expectException(AssertionFailedError::class); $this->factory->assertSentInOrder($executionOrder); } public function testCanDump() { $dumped = []; VarDumper::setHandler(function ($value) use (&$dumped) { $dumped[] = $value; }); $this->factory->fake()->dump(1, 2, 3)->withOptions(['delay' => 1000])->get('http://foo.com'); $this->assertSame(1, $dumped[0]); $this->assertSame(2, $dumped[1]); $this->assertSame(3, $dumped[2]); $this->assertInstanceOf(Request::class, $dumped[3]); $this->assertSame(1000, $dumped[4]['delay']); VarDumper::setHandler(null); } public function testResponseCanDump() { $dumped = []; VarDumper::setHandler(function ($value) use (&$dumped) { $dumped[] = $value; }); $this->factory->fake([ '200.com' => $this->factory::response('hello', 200), ]); $this->factory->get('http://200.com')->dump(); $this->assertSame('"GET http://200.com" 200', $dumped[0]); $this->assertSame('hello', $dumped[1]); VarDumper::setHandler(null); } public function testResponseCanDumpWithKey() { $dumped = []; VarDumper::setHandler(function ($value) use (&$dumped) { $dumped[] = $value; }); $this->factory->fake([ '200.com' => $this->factory::response(['hello' => 'world'], 200), ]); $this->factory->get('http://200.com')->dump('hello'); $this->assertSame('"GET http://200.com" 200', $dumped[0]); $this->assertSame('world', $dumped[1]); VarDumper::setHandler(null); } public function testResponseCanDumpHeaders() { $dumped = []; VarDumper::setHandler(function ($value) use (&$dumped) { $dumped[] = $value; }); $this->factory->fake([ '200.com' => $this->factory::response('hello', 200, ['hello' => 'world']), ]); $this->factory->get('http://200.com')->dumpHeaders(); $this->assertSame(['hello' => ['world']], $dumped[0]); VarDumper::setHandler(null); } public function testResponseSequenceIsMacroable() { ResponseSequence::macro('customMethod', function () { return 'yes!'; }); $this->assertSame('yes!', $this->factory->fakeSequence()->customMethod()); } public function testRequestsCanBeAsync() { $request = new PendingRequest($this->factory); $promise = $request->async()->get('http://foo.com'); $this->assertInstanceOf(PromiseInterface::class, $promise); $this->assertSame($promise, $request->getPromise()); } public function testClientCanBeSet() { $client = $this->factory->buildClient(); $request = new PendingRequest($this->factory); $this->assertNotSame($client, $request->buildClient()); $request->setClient($client); $this->assertSame($client, $request->buildClient()); } public function testClientCanBeUsedExternally() { $this->factory->fake([ '200.com' => $this->factory::response('hello', 200), ]); $apiClient = new class($this->factory->buildClient()) { public function __construct( private GuzzleClient $client, ) { } public function sendGetRequest() { return $this->client->sendRequest(new GuzzleRequest('GET', '200.com')); } }; $response = $apiClient->sendGetRequest(); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('hello', $response->getBody()->getContents()); } public function testRequestsCanReplaceOptions() { $request = new PendingRequest($this->factory); $request = $request->withOptions(['http_errors' => true, 'connect_timeout' => 10]); $this->assertSame(['connect_timeout' => 10, 'crypto_method' => 33, 'http_errors' => true, 'timeout' => 30], $request->getOptions()); $request = $request->withOptions(['connect_timeout' => 20]); $this->assertSame(['connect_timeout' => 20, 'crypto_method' => 33, 'http_errors' => true, 'timeout' => 30], $request->getOptions()); } public function testGlobalConfigurationCanBeDisabledForRequestsCreatedWithinCallback() { $this->factory->fake(); $this->factory->globalOptions(['force_ip_resolve' => 'v4']); $this->factory->globalRequestMiddleware(fn ($request) => $request->withHeader('X-Global', 'Foo')); $request = $this->factory->withoutGlobalConfiguration(fn () => $this->factory->createPendingRequest()); $request->get('http://laravel.com/agent'); $this->factory->createPendingRequest()->get('http://laravel.com/global'); $this->assertArrayNotHasKey('force_ip_resolve', $request->getOptions()); $this->factory->assertSent(fn (Request $request) => $request->url() === 'http://laravel.com/agent' && ! $request->hasHeader('X-Global')); $this->factory->assertSent(fn (Request $request) => $request->url() === 'http://laravel.com/global' && $request->hasHeader('X-Global')); } public function testGlobalConfigurationIsRestoredAfterWithoutGlobalConfigurationCallback() { $middleware = fn ($handler) => $handler; $this->factory->globalOptions(['force_ip_resolve' => 'v4']); $this->factory->globalMiddleware($middleware); try { $this->factory->withoutGlobalConfiguration(function () { throw new Exception('boom'); }); } catch (Exception) { // } $this->assertSame('v4', $this->factory->createPendingRequest()->getOptions()['force_ip_resolve']); $this->assertSame([$middleware], $this->factory->getGlobalMiddleware()); } public function testMultipleRequestsAreSentInThePool() { $this->factory->fake([ '200.com' => $this->factory::response('', 200), '400.com' => $this->factory::response('', 400), '500.com' => $this->factory::response('', 500), ]); $responses = $this->factory->pool(function (Pool $pool) { return [ $pool->get('200.com'), $pool->get('400.com'), $pool->get('500.com'), ]; }); $this->assertSame(200, $responses[0]->status()); $this->assertSame(400, $responses[1]->status()); $this->assertSame(500, $responses[2]->status()); } public function testMultipleRequestsAreSentInThePoolWithKeys() { $this->factory->fake([ '200.com' => $this->factory::response('', 200), '400.com' => $this->factory::response('', 400), '500.com' => $this->factory::response('', 500), ]); $responses = $this->factory->pool(function (Pool $pool) { return [ $pool->as('test200')->get('200.com'), $pool->as('test400')->get('400.com'), $pool->as('test500')->get('500.com'), $pool->newRequest()->get('200.com'), ]; }); $this->assertSame(200, $responses['test200']->status()); $this->assertSame(200, $responses[0]->status()); $this->assertSame(400, $responses['test400']->status()); $this->assertSame(500, $responses['test500']->status()); } public function testMiddlewareRunsInPool() { $this->factory->fake(function (Request $request) { return $this->factory::response('Fake'); }); $history = []; $middleware = Middleware::history($history); $responses = $this->factory->pool(fn (Pool $pool) => [ $pool->withMiddleware($middleware)->post('https://example.com', ['hyped-for' => 'laravel-movie']), ]); $response = $responses[0]; $this->assertSame('Fake', $response->body()); $this->assertCount(1, $history); $this->assertSame('Fake', tap($history[0]['response']->getBody())->rewind()->getContents()); $this->assertSame(['hyped-for' => 'laravel-movie'], json_decode(tap($history[0]['request']->getBody())->rewind()->getContents(), true)); } public function testPoolConcurrency() { $this->factory->fake([ '200.com' => $this->factory::response('', 200), '400.com' => $this->factory::response('', 400), '500.com' => $this->factory::response('', 500), ]); $responses = $this->factory->pool(function (Pool $pool) { return [ $pool->get('200.com'), $pool->get('400.com'), $pool->get('500.com'), ]; }, 2); $this->assertSame(200, $responses[0]->status()); $this->assertSame(400, $responses[1]->status()); $this->assertSame(500, $responses[2]->status()); } public function testTheRequestSendingAndResponseReceivedEventsAreFiredWhenARequestIsSent() { $events = Mockery::mock(Dispatcher::class); $events->expects('dispatch')->times(5)->with(Mockery::type(RequestSending::class)); $events->expects('dispatch')->times(5)->with(Mockery::type(ResponseReceived::class)); $factory = new Factory($events); $factory->fake(); $factory->get('https://example.com'); $factory->head('https://example.com'); $factory->post('https://example.com'); $factory->patch('https://example.com'); $factory->delete('https://example.com'); } public function testTheRequestSendingAndResponseReceivedEventsAreFiredWhenARequestIsSentAsync() { $events = Mockery::mock(Dispatcher::class); $events->expects('dispatch')->times(5)->with(Mockery::type(RequestSending::class)); $events->expects('dispatch')->times(5)->with(Mockery::type(ResponseReceived::class)); $factory = new Factory($events); $factory->fake(); $factory->pool(function (Pool $pool) { return [ $pool->get('https://example.com'), $pool->head('https://example.com'), $pool->post('https://example.com'), $pool->patch('https://example.com'), $pool->delete('https://example.com'), ]; }); } public function testTheRequestSendingAndResponseReceivedEventsAreFiredForEveryRetry() { $events = Mockery::mock(Dispatcher::class); $events->expects('dispatch')->times(2)->with(Mockery::type(RequestSending::class)); $events->expects('dispatch')->times(2)->with(Mockery::type(ResponseReceived::class)); $factory = new Factory($events); $factory->fake([ '*' => $factory::response(['error'], 403), ]); $response = $factory->retry(2, 1000, null, false)->get('http://foo.com/get'); $this->assertTrue($response->failed()); $factory->assertSentCount(2); } public function testTheTransferStatsAreCalledSafelyWhenFakingTheRequest() { $this->factory->fake(['https://example.com' => ['world' => 'Hello world']]); $stats = $this->factory->get('https://example.com')->handlerStats(); $effectiveUri = $this->factory->get('https://example.com')->effectiveUri(); $this->assertIsArray($stats); $this->assertEmpty($stats); $this->assertNull($effectiveUri); } public function testTransferStatsArePresentWhenFakingTheRequestUsingAPromiseResponse() { $this->factory->fake(['https://example.com' => $this->factory::response()]); $effectiveUri = $this->factory->get('https://example.com')->effectiveUri(); $this->assertSame('https://example.com', (string) $effectiveUri); } public function testClonedClientsWorkSuccessfullyWithTheRequestObject() { $events = Mockery::mock(Dispatcher::class); $events->expects('dispatch')->with(Mockery::type(RequestSending::class)); $events->expects('dispatch')->with(Mockery::type(ResponseReceived::class)); $factory = new Factory($events); $factory->fake(['example.com' => $factory::response('foo', 200)]); $client = $factory->timeout(10); $clonedClient = clone $client; $clonedClient->get('https://example.com'); } public function testRequestIsMacroable() { Request::macro('customMethod', function () { return 'yes!'; }); $this->factory->fake(function (Request $request) { $this->assertSame('yes!', $request->customMethod()); return $this->factory::response(); }); $this->factory->get('https://example.com'); } public function testRequestExceptionIsThrownWhenRetriesExhausted() { $this->factory->fake([ '*' => $this->factory::response(['error'], 403), ]); $exception = null; try { $this->factory ->retry(2, 1000, null, true) ->get('http://foo.com/get'); } catch (RequestException $e) { $exception = $e; } $this->assertNotNull($exception); $this->assertInstanceOf(RequestException::class, $exception); $this->factory->assertSentCount(2); } public function testRequestExceptionIsThrownWhenRetriesExhaustedWithBackoffArray() { $this->factory->fake([ '*' => $this->factory::response(['error'], 403), ]); $exception = null; try { $this->factory ->retry([1], 0, null, true) ->get('http://foo.com/get'); } catch (RequestException $e) { $exception = $e; } $this->assertNotNull($exception); $this->assertInstanceOf(RequestException::class, $exception); $this->factory->assertSentCount(2); } public function testRequestExceptionIsThrownWithoutRetriesIfRetryNotNecessary() { $this->factory->fake([ '*' => $this->factory::response(['error'], 500), ]); $exception = null; $whenAttempts = 0; try { $this->factory ->retry(2, 1000, function ($exception) use (&$whenAttempts) { $whenAttempts++; return $exception->response->status() === 403; }, true) ->get('http://foo.com/get'); } catch (RequestException $e) { $exception = $e; } $this->assertNotNull($exception); $this->assertInstanceOf(RequestException::class, $exception); $this->assertSame(1, $whenAttempts); $this->factory->assertSentCount(1); } public function testRequestExceptionIsThrownWithoutRetriesIfRetryNotNecessaryWithBackoffArray() { $this->factory->fake([ '*' => $this->factory::response(['error'], 500), ]); $exception = null; $whenAttempts = 0; try { $this->factory ->retry([1000, 1000], 1000, function ($exception) use (&$whenAttempts) { $whenAttempts++; return $exception->response->status() === 403; }, true) ->get('http://foo.com/get'); } catch (RequestException $e) { $exception = $e; } $this->assertNotNull($exception); $this->assertInstanceOf(RequestException::class, $exception); $this->assertSame(1, $whenAttempts); $this->factory->assertSentCount(1); } public function testRequestExceptionIsNotThrownWhenDisabledAndRetriesExhausted() { $this->factory->fake([ '*' => $this->factory::response(['error'], 403), ]); $response = $this->factory ->retry(2, 1000, null, false) ->get('http://foo.com/get'); $this->assertTrue($response->failed()); $this->factory->assertSentCount(2); } public function testRequestExceptionIsNotThrownWhenDisabledAndRetriesExhaustedWithBackoffArray() { $this->factory->fake([ '*' => $this->factory::response(['error'], 403), ]); $response = $this->factory ->retry([1, 2], throw: false) ->get('http://foo.com/get'); $this->assertTrue($response->failed()); $this->factory->assertSentCount(3); } public function testAsyncRequestRetriesWithBackoffArray() { $this->factory->fake([ '*' => $this->factory::response(['error'], 403), ]); $response = $this->factory ->async() ->retry([1, 2], throw: false) ->get('http://foo.com/get') ->wait(); $this->assertTrue($response->failed()); $this->factory->assertSentCount(3); } public function testAsyncRequestRetriesWithIntegerTries() { $this->factory->fake([ '*' => $this->factory::response(['error'], 403), ]); $response = $this->factory ->async() ->retry(2, 1000, null, false) ->get('http://foo.com/get') ->wait(); $this->assertTrue($response->failed()); $this->factory->assertSentCount(2); } public function testAsyncRetryCallbackReceivesHttpMethod() { $method = null; $this->factory->fake([ '*' => $this->factory->sequence() ->push(['error'], 500) ->push(['ok'], 200), ]); $response = $this->factory ->async() ->retry(2, 0, function ($exception, $request, $requestMethod) use (&$method) { $method = $requestMethod; return true; }, false) ->get('http://foo.com/get') ->wait(); $this->assertSame('GET', $method); $this->assertTrue($response->successful()); } public function testRequestExceptionIsNotThrownWithoutRetriesIfRetryNotNecessary() { $this->factory->fake([ '*' => $this->factory::response(['error'], 500), ]); $whenAttempts = 0; $response = $this->factory ->retry(2, 1000, function ($exception) use (&$whenAttempts) { $whenAttempts++; return $exception->response->status() === 403; }, false) ->get('http://foo.com/get'); $this->assertTrue($response->failed()); $this->assertSame(1, $whenAttempts); $this->factory->assertSentCount(1); } public function testRequestExceptionIsNotThrownWithoutRetriesIfRetryNotNecessaryWithBackoffArray() { $this->factory->fake([ '*' => $this->factory::response(['error'], 500), ]); $whenAttempts = 0; $response = $this->factory ->retry([1, 2], 0, function ($exception) use (&$whenAttempts) { $whenAttempts++; return $exception->response->status() === 403; }, false) ->get('http://foo.com/get'); $this->assertTrue($response->failed()); $this->assertSame(1, $whenAttempts); $this->factory->assertSentCount(1); } public function testRequestCanBeModifiedInRetryCallback() { $this->factory->fake([ '*' => $this->factory->sequence() ->push(['error'], 500) ->push(['ok'], 200), ]); $response = $this->factory ->retry(2, 1000, function ($exception, $request) { $this->assertInstanceOf(PendingRequest::class, $request); $request->withHeaders(['Foo' => 'Bar']); return true; }, false) ->get('http://foo.com/get'); $this->assertTrue($response->successful()); $this->factory->assertSent(function (Request $request) { return $request->hasHeader('Foo') && $request->header('Foo') === ['Bar']; }); } public function testRequestCanBeModifiedInRetryCallbackWithBackoffArray() { $this->factory->fake([ '*' => $this->factory->sequence() ->push(['error'], 500) ->push(['ok'], 200), ]); $response = $this->factory ->retry([2], when: function ($exception, $request) { $this->assertInstanceOf(PendingRequest::class, $request); $request->withHeaders(['Foo' => 'Bar']); return true; }, throw: false) ->get('http://foo.com/get'); $this->assertTrue($response->successful()); $this->factory->assertSent(function (Request $request) { return $request->hasHeader('Foo') && $request->header('Foo') === ['Bar']; }); } public function testExceptionThrownInRetryCallbackWithoutRetrying() { $this->factory->fake([ '*' => $this->factory::response(['error'], 500), ]); $exception = null; try { $this->factory ->retry(2, 1000, function ($exception) use (&$whenAttempts) { throw new Exception('Foo bar'); }, false) ->get('http://foo.com/get'); } catch (Exception $e) { $exception = $e; } $this->assertNotNull($exception); $this->assertInstanceOf(Exception::class, $exception); $this->assertSame('Foo bar', $exception->getMessage()); $this->factory->assertSentCount(1); } public function testExceptionThrownInRetryCallbackWithoutRetryingWithBackoffArray() { $this->factory->fake([ '*' => $this->factory::response(['error'], 500), ]); $exception = null; try { $this->factory ->retry([1, 2, 3], when: function ($exception) use (&$whenAttempts) { throw new Exception('Foo bar'); }, throw: false) ->get('http://foo.com/get'); } catch (Exception $e) { $exception = $e; } $this->assertNotNull($exception); $this->assertInstanceOf(Exception::class, $exception); $this->assertSame('Foo bar', $exception->getMessage()); $this->factory->assertSentCount(1); } public function testRequestsWillBeWaitingSleepMillisecondsReceivedBeforeRetry() { Sleep::fake(); $this->factory->fake([ '*' => $this->factory->sequence() ->push(['error'], 500) ->push(['error'], 500) ->push(['ok'], 200), ]); $this->factory ->retry(3, function ($attempt, $exception) { $this->assertInstanceOf(RequestException::class, $exception); return $attempt * 100; }, null, true) ->get('http://foo.com/get'); $this->factory->assertSentCount(3); // Make sure we waited 300ms for the first two attempts Sleep::assertSleptTimes(2); Sleep::assertSequence([ Sleep::usleep(100_000), Sleep::usleep(200_000), ]); } public function testRequestExceptionReturnedWhenRetriesExhaustedInPool() { $this->factory->fake([ '*' => $this->factory::response(['error'], 403), ]); [$exception] = $this->factory->pool(fn ($pool) => [ $pool->retry(2, 1000, null, true)->get('http://foo.com/get'), ]); $this->assertNotNull($exception); $this->assertInstanceOf(RequestException::class, $exception); $this->factory->assertSentCount(2); } public function testRequestExceptionIsReturnedWithoutRetriesIfRetryNotNecessaryInPool() { $this->factory->fake([ '*' => $this->factory::response(['error'], 500), ]); $whenAttempts = collect(); [$exception] = $this->factory->pool(fn ($pool) => [ $pool->retry(2, 1000, function ($exception) use ($whenAttempts) { $whenAttempts->push($exception); return $exception->response->status() === 403; }, true)->get('http://foo.com/get'), ]); $this->assertNotNull($exception); $this->assertInstanceOf(RequestException::class, $exception); $this->assertCount(1, $whenAttempts); $this->factory->assertSentCount(1); } public function testRequestExceptionIsNotReturnedWhenDisabledAndRetriesExhaustedInPool() { $this->factory->fake([ '*' => $this->factory::response(['error'], 403), ]); [$response] = $this->factory->pool(fn ($pool) => [ $pool->retry(2, 1000, null, false)->get('http://foo.com/get'), ]); $this->assertNotNull($response); $this->assertInstanceOf(Response::class, $response); $this->assertTrue($response->failed()); $this->factory->assertSentCount(2); } public function testRequestExceptionIsNotReturnedWithoutRetriesIfRetryNotNecessaryInPool() { $this->factory->fake([ '*' => $this->factory::response(['error'], 500), ]); $whenAttempts = collect(); [$response] = $this->factory->pool(fn ($pool) => [ $pool->retry(2, 1000, function ($exception) use ($whenAttempts) { $whenAttempts->push($exception); return $exception->response->status() === 403; }, false)->get('http://foo.com/get'), ]); $this->assertNotNull($response); $this->assertInstanceOf(Response::class, $response); $this->assertTrue($response->failed()); $this->assertCount(1, $whenAttempts); $this->factory->assertSentCount(1); } public function testRequestCanBeModifiedInRetryCallbackInPool() { $this->factory->fake([ '*' => $this->factory->sequence() ->push(['error'], 500) ->push(['ok'], 200), ]); [$response] = $this->factory->pool(fn ($pool) => [ $pool->retry(2, 1000, function ($exception, $request) { $this->assertInstanceOf(PendingRequest::class, $request); $request->withHeaders(['Foo' => 'Bar']); return true; }, false)->get('http://foo.com/get'), ]); $this->assertTrue($response->successful()); $this->factory->assertSent(function (Request $request) { return $request->hasHeader('Foo') && $request->header('Foo') === ['Bar']; }); } public function testHandleRequestExeptionWithNoResponseInPoolConsideredConnectionException() { $requestException = new GuzzleRequestException('Error', new \GuzzleHttp\Psr7\Request('GET', '/')); $this->factory->fake([ 'noresponse.com' => new RejectedPromise($requestException), ]); $responses = $this->factory->pool(function (Pool $pool) { return [ $pool->get('noresponse.com'), ]; }); $this->assertInstanceOf(ConnectionException::class, $responses[0]); $this->assertSame($requestException, $responses[0]->getPrevious()); } public function testExceptionThrownInRetryCallbackIsReturnedWithoutRetryingInPool() { $this->factory->fake([ '*' => $this->factory::response(['error'], 500), ]); [$exception] = $this->factory->pool(fn ($pool) => [ $pool->retry(2, 1000, function ($exception) { throw new Exception('Foo bar'); }, false)->get('http://foo.com/get'), ]); $this->assertNotNull($exception); $this->assertInstanceOf(Exception::class, $exception); $this->assertSame('Foo bar', $exception->getMessage()); $this->factory->assertSentCount(1); } public function testExceptionThrowInMiddlewareAllowsRetry() { $middleware = Middleware::mapRequest(function (RequestInterface $request) { throw new RuntimeException; }); $this->expectException(RuntimeException::class); $this->factory->fake(function (Request $request) { return $this->factory::response('Fake'); })->withMiddleware($middleware) ->retry(3, 1, function (Exception $exception, PendingRequest $request) { return true; })->post('https://example.com'); } public function testRequestsWillBeWaitingSleepMillisecondsReceivedInBackoffArray() { Sleep::fake(); $this->factory->fake([ '*' => $this->factory->sequence() ->push(['error'], 500) ->push(['error'], 500) ->push(['error'], 500) ->push(['ok'], 200), ]); $this->factory ->retry([50, 100, 200], 0, null, true) ->get('http://foo.com/get'); $this->factory->assertSentCount(4); // Make sure we waited 300ms for the first two attempts Sleep::assertSleptTimes(3); Sleep::assertSequence([ Sleep::usleep(50_000), Sleep::usleep(100_000), Sleep::usleep(200_000), ]); } public function testFailedRequest() { $requestException = $this->factory::failedRequest(['code' => 'not_found'], 404, ['X-RateLimit-Remaining' => 199]); $this->assertInstanceOf(RequestException::class, $requestException); $this->assertEqualsCanonicalizing(['code' => 'not_found'], $requestException->response->json()); $this->assertEquals(404, $requestException->response->status()); $this->assertEquals(199, $requestException->response->header('X-RateLimit-Remaining')); } public function testFailedRequestHeaderValuesNormalizeNonFiniteFloats() { $exception = $this->factory::failedRequest('error', 500, [ 'X-Nan' => NAN, 'X-Inf' => INF, 'X-Negative-Inf' => -INF, ]); $this->assertSame('error', $exception->response->body()); $this->assertSame(500, $exception->response->status()); $this->assertSame('NAN', $exception->response->header('X-Nan')); $this->assertSame('INF', $exception->response->header('X-Inf')); $this->assertSame('-INF', $exception->response->header('X-Negative-Inf')); } public function testFakeConnectionException() { $this->factory->fake($this->factory::failedConnection('Fake')); $exception = null; try { $this->factory->post('https://example.com'); } catch (Throwable $e) { $exception = $e; } $this->assertNotNull($exception); $this->assertInstanceOf(ConnectionException::class, $exception); $this->assertSame('Fake', $exception->getMessage()); $this->factory->assertSentCount(1); $this->factory->assertSent(function (Request $request, ?Response $response) { return $request->url() === 'https://example.com' && $response === null; }); } public function testFakeConnectionExceptionWithinFakeClosure() { $this->factory->fake(fn () => $this->factory::failedConnection('Fake')); $exception = null; try { $this->factory->post('https://example.com'); } catch (Throwable $e) { $exception = $e; } $this->assertNotNull($exception); $this->assertInstanceOf(ConnectionException::class, $exception); $this->assertSame('Fake', $exception->getMessage()); $this->factory->assertSentCount(1); } public function testFakeConnectionExceptionWithinArray() { $this->factory->fake(['*' => $this->factory::failedConnection('Fake')]); $exception = null; try { $this->factory->post('https://example.com'); } catch (Throwable $e) { $exception = $e; } $this->assertNotNull($exception); $this->assertInstanceOf(ConnectionException::class, $exception); $this->assertSame('Fake', $exception->getMessage()); $this->factory->assertSentCount(1); } public function testFakeConnectionExceptionWithinSequence() { $this->factory->fake([ '*' => $this->factory->sequence() ->pushFailedConnection('Fake') ->push('Success'), ]); $exception = null; $response = $this->factory->retry(3, function ($attempt, $e) use (&$exception) { $exception = $e; return true; })->post('https://example.com'); $this->assertSame('Success', $response->body()); $this->assertNotNull($exception); $this->assertInstanceOf(ConnectionException::class, $exception); $this->assertSame('Fake', $exception->getMessage()); $this->factory->assertSentCount(2); } public function testMiddlewareRunsWhenFaked() { $this->factory->fake(function (Request $request) { return $this->factory::response('Fake'); }); $history = []; $pendingRequest = $this->factory->withMiddleware( Middleware::history($history) ); $response = $pendingRequest->post('https://example.com', ['hyped-for' => 'laravel-movie']); $this->assertSame('Fake', $response->body()); $this->assertCount(1, $history); $this->assertSame('Fake', tap($history[0]['response']->getBody())->rewind()->getContents()); $this->assertSame(['hyped-for' => 'laravel-movie'], json_decode(tap($history[0]['request']->getBody())->rewind()->getContents(), true)); } public function testMiddlewareRunsAndCanChangeRequestOnAssertSent() { $this->factory->fake(function (Request $request) { return $this->factory::response('Fake'); }); $pendingRequest = $this->factory->withMiddleware( Middleware::mapRequest(fn (RequestInterface $request) => $request->withHeader('X-Test-Header', 'Test')) ); $pendingRequest->post('https://laravel.example', ['laravel' => 'framework']); $this->factory->assertSent(function (Request $request) { return $request->url() === 'https://laravel.example' && $request->hasHeader('X-Test-Header', 'Test'); }); } public function testSslCertificateErrorsConvertedToConnectionException() { $this->factory->fake(function () { $request = new GuzzleRequest('HEAD', 'https://ssl-error.laravel.example'); throw new GuzzleRequestException( 'cURL error 60: SSL certificate problem: unable to get local issuer certificate', $request ); }); $this->expectExceptionObject(new ConnectionException('cURL error 60: SSL certificate problem: unable to get local issuer certificate')); $this->factory->head('https://ssl-error.laravel.example'); } public function testConnectExceptionIsConvertedToConnectionExceptionEvenWhenWithoutFactory() { $this->expectExceptionObject(new ConnectionException('cURL error 60: SSL certificate problem')); $pendingRequest = new PendingRequest(); $pendingRequest->setHandler(function () { throw new ConnectException( 'cURL error 60: SSL certificate problem: unable to get local issuer certificate', new GuzzleRequest('HEAD', 'https://ssl-error.laravel.example') ); }); $pendingRequest->head('https://ssl-error.laravel.example'); } public function testRequestExceptionWithoutResponseIsConvertedToConnectionExceptionEvenWhenWithoutFactory() { $this->expectExceptionObject(new ConnectionException('cURL error 28: Operation timed out')); $pendingRequest = new PendingRequest(); $pendingRequest->setHandler(function () { throw new GuzzleRequestException( 'cURL error 28: Operation timed out', new GuzzleRequest('GET', 'https://timeout-laravel.example') ); }); $pendingRequest->get('https://timeout-laravel.example'); } public function testRequestExceptionWithResponseIsConvertedToConnectionExceptionEvenWhenWithoutFactory() { $this->expectExceptionObject(new ConnectionException('cURL error 28: Operation timed out')); $pendingRequest = new PendingRequest(); $pendingRequest->setHandler(function () { throw new GuzzleRequestException( 'cURL error 28: Operation timed out', new GuzzleRequest('GET', 'https://timeout-laravel.example'), new Psr7Response(301) ); }); $pendingRequest->get('https://timeout-laravel.example'); } public function testTooManyRedirectsExceptionIsConvertedToConnectionExceptionEvenWhenWithoutFactory() { $this->expectExceptionObject(new ConnectionException('Maximum number of redirects (5) exceeded')); $pendingRequest = new PendingRequest(); $pendingRequest->setHandler(function () { throw new TooManyRedirectsException( 'Maximum number of redirects (5) exceeded', new GuzzleRequest('GET', 'https://redirect.laravel.example'), new Psr7Response(301) ); }); $pendingRequest->maxRedirects(5)->get('https://redirect.laravel.example'); } public function testTooManyRedirectsExceptionConvertedToConnectionException() { $this->factory->fake(function () { $request = new GuzzleRequest('GET', 'https://redirect.laravel.example'); $response = new Psr7Response(301, ['Location' => 'https://redirect2.laravel.example']); throw new TooManyRedirectsException( 'Maximum number of redirects (5) exceeded', $request, $response ); }); $this->expectExceptionObject(new ConnectionException('Maximum number of redirects (5) exceeded')); $this->factory->maxRedirects(5)->get('https://redirect.laravel.example'); } public function testTooManyRedirectsWithFakedRedirectChain() { $this->factory->fake([ '1.example.com' => $this->factory::response(null, 301, ['Location' => 'https://2.example.com']), '2.example.com' => $this->factory::response(null, 301, ['Location' => 'https://3.example.com']), '3.example.com' => $this->factory::response('', 200), ]); $this->expectException(ConnectionException::class); $this->factory->maxRedirects(1)->get('https://1.example.com'); } public function testRequestExceptionIsNotThrownIfThePendingRequestIsSetToThrowOnFailureButTheResponseIsSuccessful() { $this->factory->fake([ '*' => $this->factory::response(['success'], 200), ]); $response = $this->factory ->throw() ->get('http://foo.com/get'); $this->assertSame(200, $response->status()); } public function testRequestExceptionIsThrownIfThePendingRequestIsSetToThrowOnFailure() { $this->factory->fake([ '*' => $this->factory::response(['error'], 403), ]); $exception = null; try { $this->factory ->throw() ->get('http://foo.com/get'); } catch (RequestException $e) { $exception = $e; } $this->assertNotNull($exception); $this->assertInstanceOf(RequestException::class, $exception); } public function testRequestExceptionIsThrownIfTheThrowIfOnThePendingRequestIsSetToTrueOnFailure() { $this->factory->fake([ '*' => $this->factory::response(['error'], 403), ]); $exception = null; try { $this->factory ->throwIf(true) ->get('http://foo.com/get'); } catch (RequestException $e) { $exception = $e; } $this->assertNotNull($exception); $this->assertInstanceOf(RequestException::class, $exception); } public function testRequestExceptionIsNotThrownIfTheThrowIfOnThePendingRequestIsSetToFalseOnFailure() { $this->factory->fake([ '*' => $this->factory::response(['error'], 403), ]); $response = $this->factory ->throwIf(false) ->get('http://foo.com/get'); $this->assertSame(403, $response->status()); } public function testRequestExceptionIsThrownIfTheThrowIfClosureOnThePendingRequestReturnsTrue() { $this->factory->fake([ '*' => $this->factory::response(['error'], 403), ]); $exception = null; $hitThrowCallback = false; try { $this->factory ->throwIf(function ($response) { $this->assertInstanceOf(Response::class, $response); $this->assertSame(403, $response->status()); return true; }, function ($response, $e) use (&$hitThrowCallback) { $this->assertInstanceOf(Response::class, $response); $this->assertSame(403, $response->status()); $this->assertInstanceOf(RequestException::class, $e); $hitThrowCallback = true; }) ->get('http://foo.com/get'); } catch (RequestException $e) { $exception = $e; } $this->assertNotNull($exception); $this->assertInstanceOf(RequestException::class, $exception); $this->assertTrue($hitThrowCallback); } public function testRequestExceptionIsNotThrownIfTheThrowIfClosureOnThePendingRequestReturnsFalse() { $this->factory->fake([ '*' => $this->factory::response(['error'], 403), ]); $hitThrowCallback = false; $response = $this->factory ->throwIf(function ($response) { $this->assertInstanceOf(Response::class, $response); $this->assertSame(403, $response->status()); return false; }, function ($response, $e) use (&$hitThrowCallback) { $hitThrowCallback = true; }) ->get('http://foo.com/get'); $this->assertSame(403, $response->status()); $this->assertFalse($hitThrowCallback); } public function testRequestExceptionIsThrownWithCallbackIfThePendingRequestIsSetToThrowOnFailure() { $this->factory->fake([ '*' => $this->factory::response(['error'], 403), ]); $exception = null; $flag = false; try { $this->factory ->throw(function ($exception) use (&$flag) { $flag = true; }) ->get('http://foo.com/get'); } catch (RequestException $e) { $exception = $e; } $this->assertTrue($flag); $this->assertNotNull($exception); $this->assertInstanceOf(RequestException::class, $exception); } public function testRequestExceptionIsThrownIfTheRequestFails() { $this->factory->fake([ '*' => $this->factory::response('', 400), ]); $exception = null; try { $this->factory->get('http://foo.com/api')->throw(); } catch (RequestException $e) { $exception = $e; } $this->assertNotNull($exception); $this->assertInstanceOf(RequestException::class, $exception); } public function testRequestExceptionIsThrownWithCallbackIfTheRequestFails() { $this->factory->fake([ '*' => $this->factory::response('', 400), ]); $exception = null; $flag = false; try { $this->factory->get('http://foo.com/api')->throw(function () use (&$flag) { $flag = true; }); } catch (RequestException $e) { $exception = $e; } $this->assertTrue($flag); $this->assertNotNull($exception); $this->assertInstanceOf(RequestException::class, $exception); } public function testRequestExceptionIsNotThrownIfTheRequestDoesNotFail() { $this->factory->fake([ '*' => ['result' => ['foo' => 'bar']], ]); $response = $this->factory->get('http://foo.com/api')->throw(); $this->assertSame('{"result":{"foo":"bar"}}', $response->body()); } public function testRequestExceptionIsNotReturnedIfThePendingRequestIsSetToThrowOnFailureButTheResponseIsSuccessfulInPool() { $this->factory->fake([ '*' => $this->factory::response(['success'], 200), ]); [$response] = $this->factory->pool(fn ($pool) => [ $pool->throw()->get('http://foo.com/get'), ]); $this->assertInstanceOf(Response::class, $response); $this->assertSame(200, $response->status()); } public function testRequestExceptionIsReturnedIfThePendingRequestIsSetToThrowOnFailureInPool() { $this->factory->fake([ '*' => $this->factory::response(['error'], 403), ]); [$exception] = $this->factory->pool(fn ($pool) => [ $pool->throw()->get('http://foo.com/get'), ]); $this->assertNotNull($exception); $this->assertInstanceOf(RequestException::class, $exception); } public function testRequestExceptionIsReturnedIfTheThrowIfOnThePendingRequestIsSetToTrueOnFailureInPool() { $this->factory->fake([ '*' => $this->factory::response(['error'], 403), ]); [$exception] = $this->factory->pool(fn ($pool) => [ $pool->throwIf(true)->get('http://foo.com/get'), ]); $this->assertNotNull($exception); $this->assertInstanceOf(RequestException::class, $exception); } public function testRequestExceptionIsNotReturnedIfTheThrowIfOnThePendingRequestIsSetToFalseOnFailureInPool() { $this->factory->fake([ '*' => $this->factory::response(['error'], 403), ]); [$response] = $this->factory->pool(fn ($pool) => [ $pool->throwIf(false)->get('http://foo.com/get'), ]); $this->assertInstanceOf(Response::class, $response); $this->assertSame(403, $response->status()); } public function testRequestExceptionIsReturnedIfTheThrowIfClosureOnThePendingRequestReturnsTrueInPool() { $this->factory->fake([ '*' => $this->factory::response(['error'], 403), ]); $hitThrowCallback = collect(); [$exception] = $this->factory->pool(fn ($pool) => [ $pool->throwIf(function ($response) { $this->assertInstanceOf(Response::class, $response); $this->assertSame(403, $response->status()); return true; }, function ($response, $e) use (&$hitThrowCallback) { $this->assertInstanceOf(Response::class, $response); $this->assertSame(403, $response->status()); $this->assertInstanceOf(RequestException::class, $e); $hitThrowCallback->push(true); })->get('http://foo.com/get'), ]); $this->assertNotNull($exception); $this->assertInstanceOf(RequestException::class, $exception); $this->assertCount(1, $hitThrowCallback); } public function testRequestExceptionIsNotReturnedIfTheThrowIfClosureOnThePendingRequestReturnsFalseInPool() { $this->factory->fake([ '*' => $this->factory::response(['error'], 403), ]); $hitThrowCallback = collect(); [$response] = $this->factory->pool(fn ($pool) => [ $pool->throwIf(function ($response) { $this->assertInstanceOf(Response::class, $response); $this->assertSame(403, $response->status()); return false; }, function ($response, $e) use (&$hitThrowCallback) { $hitThrowCallback->push(true); })->get('http://foo.com/get'), ]); $this->assertCount(0, $hitThrowCallback); $this->assertSame(403, $response->status()); } public function testRequestExceptionIsReturnedWithCallbackIfThePendingRequestIsSetToThrowOnFailureInPool() { $this->factory->fake([ '*' => $this->factory::response(['error'], 403), ]); $flag = collect(); [$exception] = $this->factory->pool(fn ($pool) => [ $pool->throw(function ($exception) use (&$flag) { $flag->push(true); })->get('http://foo.com/get'), ]); $this->assertCount(1, $flag); $this->assertNotNull($exception); $this->assertInstanceOf(RequestException::class, $exception); } public function testRequestExceptionIsReturnedAfterLastRetryInPool() { $this->factory->fake([ '*' => $this->factory::response(['error'], 403), ]); [$exception] = $this->factory->pool(fn ($pool) => [ $pool->retry(3)->throw()->get('http://foo.com/get'), ]); $this->assertNotNull($exception); $this->assertInstanceOf(RequestException::class, $exception); $this->factory->assertSentCount(3); } public function testRequestExceptionIsThrowIfConditionIsSatisfied() { $this->factory->fake([ '*' => $this->factory::response('', 400), ]); $exception = null; try { $this->factory->get('http://foo.com/api')->throwIf(true); } catch (RequestException $e) { $exception = $e; } $this->assertNotNull($exception); $this->assertInstanceOf(RequestException::class, $exception); } public function testRequestExceptionIsNotThrownIfConditionIsNotSatisfied() { $this->factory->fake([ '*' => $this->factory::response(['result' => ['foo' => 'bar']], 400), ]); $response = $this->factory->get('http://foo.com/api')->throwIf(false); $this->assertSame('{"result":{"foo":"bar"}}', $response->body()); } public function testRequestExceptionIsThrownWhenUnlessConditionIsNotSatisfied() { $this->factory->fake([ '*' => $this->factory::response('', 400), ]); $exception = null; try { $this->factory->get('http://foo.com/api')->throwUnless(false); } catch (RequestException $e) { $exception = $e; } $this->assertNotNull($exception); $this->assertInstanceOf(RequestException::class, $exception); } public function testRequestExceptionIsNotThrownWhenUnlessConditionIsSatisfied() { $this->factory->fake([ '*' => $this->factory::response(['result' => ['foo' => 'bar']], 400), ]); $response = $this->factory->get('http://foo.com/api')->throwUnless(true); $this->assertSame('{"result":{"foo":"bar"}}', $response->body()); } public function testRequestExceptionIsThrowIfConditionClosureIsSatisfied() { $this->factory->fake([ '*' => $this->factory::response('', 400), ]); $exception = null; $hitThrowCallback = false; try { $this->factory->get('http://foo.com/api')->throwIf(function ($response) { $this->assertSame(400, $response->status()); return true; }, function ($response, $e) use (&$hitThrowCallback) { $this->assertSame(400, $response->status()); $this->assertInstanceOf(RequestException::class, $e); $hitThrowCallback = true; }); } catch (RequestException $e) { $exception = $e; } $this->assertNotNull($exception); $this->assertInstanceOf(RequestException::class, $exception); $this->assertTrue($hitThrowCallback); } public function testRequestExceptionIsNotThrownIfConditionClosureIsNotSatisfied() { $this->factory->fake([ '*' => $this->factory::response(['result' => ['foo' => 'bar']], 400), ]); $hitThrowCallback = false; $response = $this->factory->get('http://foo.com/api')->throwIf(function ($response) { $this->assertSame(400, $response->status()); return false; }, function ($response, $e) use (&$hitThrowCallback) { $hitThrowCallback = true; }); $this->assertSame('{"result":{"foo":"bar"}}', $response->body()); $this->assertFalse($hitThrowCallback); } public function testRequestExceptionIsThrownIfStatusCodeIsSatisfied() { $this->factory->fake([ '*' => $this->factory::response('', 400), ]); $exception = null; try { $this->factory->get('http://foo.com/api')->throwIfStatus(400); } catch (RequestException $e) { $exception = $e; } $this->assertNotNull($exception); $this->assertInstanceOf(RequestException::class, $exception); } public function testRequestExceptionIsThrownIfStatusCodeIsSatisfiedWithClosure() { $this->factory->fake([ '*' => $this->factory::response('', 400), ]); $exception = null; try { $this->factory->get('http://foo.com/api')->throwIfStatus(fn ($status) => $status === 400); } catch (RequestException $e) { $exception = $e; } $this->assertNotNull($exception); $this->assertInstanceOf(RequestException::class, $exception); } public function testRequestExceptionIsNotThrownIfStatusCodeIsNotSatisfied() { $this->factory->fake([ '*' => $this->factory::response('', 400), ]); $exception = null; try { $this->factory->get('http://foo.com/api')->throwIfStatus(500); } catch (RequestException $e) { $exception = $e; } $this->assertNull($exception); } public function testRequestExceptionIsThrownUnlessStatusCodeIsSatisfied() { $this->factory->fake([ 'http://foo.com/api/400' => $this->factory::response('', 400), 'http://foo.com/api/408' => $this->factory::response('', 408), 'http://foo.com/api/500' => $this->factory::response('', 500), ]); $exception = null; try { $this->factory->get('http://foo.com/api/400')->throwUnlessStatus(500); } catch (RequestException $e) { $exception = $e; } $this->assertNotNull($exception); $this->assertInstanceOf(RequestException::class, $exception); $exception = null; $this->factory->fake([ 'http://foo.com/api/400' => $this->factory::response('', 400), 'http://foo.com/api/408' => $this->factory::response('', 408), 'http://foo.com/api/500' => $this->factory::response('', 500), ]); $exception = null; try { $this->factory->get('http://foo.com/api/400')->throwUnlessStatus(fn ($status) => $status === 500); } catch (RequestException $e) { $exception = $e; } $this->assertNotNull($exception); $this->assertInstanceOf(RequestException::class, $exception); $exception = null; try { $this->factory->get('http://foo.com/api/408')->throwUnlessStatus(500); } catch (RequestException $e) { $exception = $e; } $this->assertNotNull($exception); $this->assertInstanceOf(RequestException::class, $exception); $exception = null; try { $this->factory->get('http://foo.com/api/500')->throwUnlessStatus(500); } catch (RequestException $e) { $exception = $e; } $this->assertNull($exception); $exception = null; try { $this->factory->get('http://foo.com/api/500')->throwUnlessStatus(fn ($status) => $status === 500); } catch (RequestException $e) { $exception = $e; } $this->assertNull($exception); } public function testRequestExceptionIsThrownIfIsClientError() { $this->factory->fake([ 'http://foo.com/api/400' => $this->factory::response('', 400), 'http://foo.com/api/408' => $this->factory::response('', 408), 'http://foo.com/api/500' => $this->factory::response('', 500), 'http://foo.com/api/504' => $this->factory::response('', 504), ]); $exception = null; try { $this->factory->get('http://foo.com/api/400')->throwIfClientError(); } catch (RequestException $e) { $exception = $e; } $this->assertNotNull($exception); $this->assertInstanceOf(RequestException::class, $exception); $exception = null; try { $this->factory->get('http://foo.com/api/408')->throwIfClientError(); } catch (RequestException $e) { $exception = $e; } $this->assertNotNull($exception); $this->assertInstanceOf(RequestException::class, $exception); $exception = null; try { $this->factory->get('http://foo.com/api/500')->throwIfClientError(); } catch (RequestException $e) { $exception = $e; } $this->assertNull($exception); $exception = null; try { $this->factory->get('http://foo.com/api/504')->throwIfClientError(); } catch (RequestException $e) { $exception = $e; } $this->assertNull($exception); } public function testThrowIfStatusWorksWithNonErrorStatusCodes() { $this->factory->fake([ '*' => $this->factory::response('', 201), ]); $exception = null; try { $this->factory->get('http://foo.com/api')->throwIfStatus(201); } catch (RequestException $e) { $exception = $e; } $this->assertNotNull($exception); $this->assertInstanceOf(RequestException::class, $exception); $exception = null; try { $this->factory->get('http://foo.com/api')->throwIfStatus(fn ($status) => $status === 201); } catch (RequestException $e) { $exception = $e; } $this->assertNotNull($exception); $this->assertInstanceOf(RequestException::class, $exception); } public function testThrowUnlessStatusWorksWithNonErrorStatusCodes() { $this->factory->fake([ '*' => $this->factory::response('', 201), ]); $exception = null; try { $this->factory->get('http://foo.com/api')->throwUnlessStatus(200); } catch (RequestException $e) { $exception = $e; } $this->assertNotNull($exception); $this->assertInstanceOf(RequestException::class, $exception); $exception = null; try { $this->factory->get('http://foo.com/api')->throwUnlessStatus(201); } catch (RequestException $e) { $exception = $e; } $this->assertNull($exception); $exception = null; try { $this->factory->get('http://foo.com/api')->throwUnlessStatus(fn ($status) => $status === 200); } catch (RequestException $e) { $exception = $e; } $this->assertNotNull($exception); $this->assertInstanceOf(RequestException::class, $exception); } public function testRequestExceptionIsThrownIfIsServerError() { $this->factory->fake([ 'http://foo.com/api/400' => $this->factory::response('', 400), 'http://foo.com/api/408' => $this->factory::response('', 408), 'http://foo.com/api/500' => $this->factory::response('', 500), 'http://foo.com/api/504' => $this->factory::response('', 504), ]); $exception = null; try { $this->factory->get('http://foo.com/api/400')->throwIfServerError(); } catch (RequestException $e) { $exception = $e; } $this->assertNull($exception); $exception = null; try { $this->factory->get('http://foo.com/api/408')->throwIfServerError(); } catch (RequestException $e) { $exception = $e; } $this->assertNull($exception); $exception = null; try { $this->factory->get('http://foo.com/api/500')->throwIfServerError(); } catch (RequestException $e) { $exception = $e; } $this->assertNotNull($exception); $this->assertInstanceOf(RequestException::class, $exception); $exception = null; try { $this->factory->get('http://foo.com/api/504')->throwIfServerError(); } catch (RequestException $e) { $exception = $e; } $this->assertNotNull($exception); $this->assertInstanceOf(RequestException::class, $exception); } public function testItCanEnforceFaking() { $this->factory->preventStrayRequests(); $this->factory->fake(['https://vapor.laravel.com' => Factory::response('ok', 200)]); $this->factory->fake(['https://forge.laravel.com' => Factory::response('ok', 200)]); $responses = []; $responses[] = $this->factory->get('https://vapor.laravel.com')->body(); $responses[] = $this->factory->get('https://forge.laravel.com')->body(); $this->assertSame(['ok', 'ok'], $responses); $this->expectExceptionObject(new StrayRequestException('https://laravel.com')); $this->factory->get('https://laravel.com'); } public function testItCanEnforceFakingInThePool() { $this->factory->preventStrayRequests(); $this->factory->fake(['https://vapor.laravel.com' => Factory::response('ok', 200)]); $this->factory->fake(['https://forge.laravel.com' => Factory::response('ok', 200)]); $responses = $this->factory->pool(function (Pool $pool) { return [ $pool->get('https://vapor.laravel.com'), $pool->get('https://forge.laravel.com'), ]; }); $this->assertSame(200, $responses[0]->status()); $this->assertSame(200, $responses[1]->status()); [$exception] = $this->factory->pool(function (Pool $pool) { return [ $pool->get('https://laravel.com'), ]; }); $this->assertInstanceOf(StrayRequestException::class, $exception); $this->assertSame('Attempted request to [https://laravel.com] without a matching fake.', $exception->getMessage()); } public function testPreventingStrayRequests() { $this->assertFalse($this->factory->preventingStrayRequests()); $this->factory->preventStrayRequests(); $this->assertTrue($this->factory->preventingStrayRequests()); } public function testAllowingStrayRequestUrls() { $this->assertFalse($this->factory->preventingStrayRequests()); $this->assertTrue($this->factory->isAllowedRequestUrl('127.0.0.1')); $this->factory->preventStrayRequests(); $this->assertFalse($this->factory->isAllowedRequestUrl('127.0.0.1')); $this->factory->allowStrayRequests([ '127.0.0.1', ]); $this->assertTrue($this->factory->preventingStrayRequests()); $this->assertTrue($this->factory->isAllowedRequestUrl('127.0.0.1')); } public function testItCanAddAuthorizationHeaderIntoRequestUsingBeforeSendingCallback() { $this->factory->fake(); $this->factory->beforeSending(function (Request $request) { $requestLine = sprintf( '%s %s HTTP/%s', $request->toPsrRequest()->getMethod(), $request->toPsrRequest()->getUri()->withScheme('')->withHost(''), $request->toPsrRequest()->getProtocolVersion() ); return $request->toPsrRequest()->withHeader('Authorization', 'Bearer '.$requestLine); })->get('http://foo.com/json'); $this->factory->assertSent(function (Request $request) { return $request->url() === 'http://foo.com/json' && $request->hasHeader('Authorization', 'Bearer GET /json HTTP/1.1'); }); } public function testItCanSetAllowMaxRedirects(): void { $request = new PendingRequest($this->factory); $request = $request->withOptions(['allow_redirects' => ['max' => 5]]); $this->assertSame(['connect_timeout' => 10, 'crypto_method' => 33, 'http_errors' => false, 'timeout' => 30, 'allow_redirects' => ['max' => 5]], $request->getOptions()); $request = $request->maxRedirects(10); $this->assertSame(['connect_timeout' => 10, 'crypto_method' => 33, 'http_errors' => false, 'timeout' => 30, 'allow_redirects' => ['max' => 10]], $request->getOptions()); } public function testPreventDuplicatedContentType(): void { $client = $this->factory->asJson(); $this->assertSame('application/json', Arr::get($client->getOptions(), 'headers.Content-Type')); $client->asJson(); $client->asJson(); $this->assertSame('application/json', Arr::get($client->getOptions(), 'headers.Content-Type')); $client->contentType('foo'); $this->assertSame('foo', Arr::get($client->getOptions(), 'headers.Content-Type')); } public function testItCanSubstituteUrlParams(): void { $this->factory->fake(); $this->factory->withUrlParameters([ 'endpoint' => 'https://laravel.com', 'page' => 'docs', 'version' => '9.x', 'thing' => 'validation', ])->get('{+endpoint}/{page}/{version}/{thing}'); $this->factory->assertSent(function (Request $request) { return $request->url() === 'https://laravel.com/docs/9.x/validation'; }); } public function testTheTransferStatsAreCustomizable(): void { $onStatsFunctionCalled = false; $stats = $this->factory ->withOptions([ 'on_stats' => function (TransferStats $stats) use (&$onStatsFunctionCalled) { $onStatsFunctionCalled = true; }, ]) ->get('http://example.com') ->handlerStats(); $this->assertIsArray($stats); $this->assertNotEmpty($stats); $this->assertTrue($onStatsFunctionCalled); } public function testTheTransferStatsAreCustomizableOnFake(): void { $onStatsFunctionCalled = false; $this->factory ->fake() ->withOptions([ 'on_stats' => function (TransferStats $stats) use (&$onStatsFunctionCalled) { $onStatsFunctionCalled = true; }, ]) ->get('https://foo.bar') ->handlerStats(); $this->assertTrue($onStatsFunctionCalled); } public function testItCanAddGlobalMiddleware() { Carbon::setTestNow(Carbon::today()); $requests = []; $responses = []; $this->factory->fake(function ($r) use (&$requests) { $requests[] = $r; Carbon::setTestNow(Carbon::now()->addSeconds(6 * count($requests))); return $this->factory::response('expected content'); }); $this->factory->globalMiddleware(Middleware::mapRequest(function ($request) { // Test manipulating headers on outgoing request... return $request->withHeader('User-Agent', 'Laravel Framework/1.0') ->withAddedHeader('shared', 'global') ->withHeader('list', ['item-1', 'item-2']) ->withAddedHeader('list', ['item-3']); }))->globalMiddleware(Middleware::mapResponse(function ($response) use (&$requests) { // Test adding headers in incoming response.. return $response->withHeader('X-Count', (string) count($requests)); }))->globalMiddleware(function ($handler) { // Test wrapping request in timing function... return function ($request, $options) use ($handler) { $startedAt = Carbon::now(); return $handler($request, $options)->then(function (ResponseInterface $response) use ($startedAt) { return $response->withHeader('X-Duration', "{$startedAt->diffInSeconds(Carbon::now())} seconds"); }); }; }); $responses[] = $this->factory->post('http://forge.laravel.com'); $responses[] = $this->factory->withHeader('shared', 'local')->post('http://vapor.laravel.com'); $this->assertCount(2, $requests); $this->assertCount(2, $responses); $this->assertSame(['Laravel Framework/1.0'], $requests[0]->header('User-Agent')); $this->assertSame(['item-1', 'item-2', 'item-3'], $requests[0]->header('list')); $this->assertSame(['global'], $requests[0]->header('shared')); $this->assertSame('1', $responses[0]->header('X-Count')); $this->assertSame('6 seconds', $responses[0]->header('X-Duration')); $this->assertSame(['Laravel Framework/1.0'], $requests[1]->header('User-Agent')); $this->assertSame(['item-1', 'item-2', 'item-3'], $requests[1]->header('list')); $this->assertSame(['local', 'global'], $requests[1]->header('shared')); $this->assertSame('2', $responses[1]->header('X-Count')); $this->assertSame('12 seconds', $responses[1]->header('X-Duration')); } public function testItCanAddGlobalRequestMiddleware() { $requests = []; $this->factory->fake(function ($r) use (&$requests) { $requests[] = $r; return Factory::response('expected content'); }); $this->factory->globalRequestMiddleware(function ($request) { return $request->withHeader('User-Agent', 'Laravel Framework/1.0'); }); $this->factory->post('http://forge.laravel.com'); $this->factory->post('http://laravel.com'); $this->assertSame(['Laravel Framework/1.0'], $requests[0]->header('User-Agent')); $this->assertSame(['Laravel Framework/1.0'], $requests[1]->header('User-Agent')); } public function testItCanAddGlobalResponseMiddleware() { $responses = []; $this->factory->fake(function ($r) use (&$request) { return Factory::response('expected content'); }); $this->factory->globalResponseMiddleware(function ($response) { return $response->withHeader('X-Foo', 'Bar'); }); $responses[] = $this->factory->post('http://forge.laravel.com'); $responses[] = $this->factory->post('http://laravel.com'); $this->assertSame('Bar', $responses[0]->header('X-Foo')); $this->assertSame('Bar', $responses[1]->header('X-Foo')); } public function testItCanGetTheGlobalMiddleware() { $this->factory->globalMiddleware($middleware = fn () => null); $this->assertEquals([$middleware], $this->factory->getGlobalMiddleware()); } public function testItCanAddRequestMiddleware() { $requests = []; $this->factory->fake(function ($r) use (&$requests) { $requests[] = $r; return Factory::response('expected content'); }); $this->factory->withRequestMiddleware(function ($request) { return $request->withHeader('User-Agent', 'Laravel Framework/1.0'); })->post('http://forge.laravel.com'); $this->factory->post('http://laravel.com'); $this->assertSame(['Laravel Framework/1.0'], $requests[0]->header('User-Agent')); $this->assertSame(['GuzzleHttp/7'], $requests[1]->header('User-Agent')); } public function testItCanAddResponseMiddleware() { $responses = []; $this->factory->fake(function ($r) use (&$request) { return Factory::response('expected content'); }); $responses[] = $this->factory->withResponseMiddleware(function ($response) { return $response->withHeader('X-Foo', 'Bar'); })->post('http://forge.laravel.com'); $responses[] = $this->factory->post('http://laravel.com'); $this->assertSame('Bar', $responses[0]->header('X-Foo')); $this->assertSame('', $responses[1]->header('X-Foo')); } public function testItReturnsResponse(): void { $this->factory->fake([ '*' => $this->factory::response('expected content'), ]); $response = $this->factory->get('http://laravel.com'); $this->assertInstanceOf(Response::class, $response); $this->assertSame('expected content', $response->body()); } public function testItCanReturnCustomResponseClass(): void { $factory = new CustomFactory; $factory->fake([ '*' => $factory::response('expected content'), ]); $response = $factory->get('http://laravel.fake'); $this->assertInstanceOf(TestResponse::class, $response); $this->assertSame('expected content', $response->body()); } public function testItCanHaveGlobalDefaultValues() { $factory = new Factory; $timeout = null; $allowRedirects = null; $headers = null; $factory->fake(function ($request, $options) use (&$timeout, &$allowRedirects, &$headers, $factory) { $timeout = $options['timeout']; $allowRedirects = $options['allow_redirects']; $headers = $request->headers(); return $factory::response(''); }); $factory->get('https://laravel.com'); $this->assertSame(30, $timeout); $this->assertSame(['max' => 5, 'protocols' => ['http', 'https'], 'strict' => false, 'referer' => false, 'track_redirects' => false], $allowRedirects); $this->assertNull($headers['X-Foo'] ?? null); $factory->globalOptions([ 'timeout' => 5, 'allow_redirects' => false, 'headers' => [ 'X-Foo' => 'true', ], ]); $factory->get('https://laravel.com'); $this->assertSame(5, $timeout); $this->assertFalse($allowRedirects); $this->assertSame(['true'], $headers['X-Foo']); $factory->globalOptions(fn () => [ 'timeout' => 10, 'headers' => [ 'X-Foo' => 'false', 'X-Bar' => 'true', ], ]); $factory->get('https://laravel.com'); $this->assertSame(10, $timeout); $this->assertSame(['max' => 5, 'protocols' => ['http', 'https'], 'strict' => false, 'referer' => false, 'track_redirects' => false], $allowRedirects); $this->assertSame(['false'], $headers['X-Foo']); $this->assertSame(['true'], $headers['X-Bar']); } public function testItCanCreatePendingRequest() { $factory = new Factory(); $this->assertInstanceOf(PendingRequest::class, $factory->createPendingRequest()); } public function testBatchNoCallbacks(): void { $this->factory->fake([ 'https://200.com' => $this->factory::response('OK', 200), 'https://201.com' => $this->factory::response('Created', 201), 'https://500.com' => $this->factory::response('Error', 500), ]); $batch = $this->factory->batch(function (Batch $batch) { return [ $batch->as('first')->get('https://200.com'), $batch->as('second')->get('https://201.com'), $batch->as('third')->get('https://500.com'), ]; }); $this->assertSame(3, $batch->totalRequests); $this->assertFalse($batch->finished()); $responses = $batch->send(); $this->assertSame(200, $responses['first']->status()); $this->assertSame(201, $responses['second']->status()); $this->assertSame(500, $responses['third']->status()); $this->assertSame(3, $batch->totalRequests); $this->assertSame(0, $batch->pendingRequests); $this->assertSame(1, $batch->failedRequests); $this->assertTrue($batch->hasFailures()); $this->assertTrue($batch->finished()); } public function testBatchDefer(): void { $this->factory->fake([ 'https://200.com' => $this->factory::response('OK', 200), 'https://201.com' => $this->factory::response('Created', 201), 'https://500.com' => $this->factory::response('Error', 500), ]); $batch = $this->factory->batch(function (Batch $batch) { return [ $batch->as('first')->get('https://200.com'), $batch->as('second')->get('https://201.com'), $batch->as('third')->get('https://500.com'), ]; }); $this->assertSame(3, $batch->totalRequests); $this->assertFalse($batch->finished()); $deferredCallback = $batch->defer(); $this->assertInstanceOf(DeferredCallback::class, $deferredCallback); $this->assertFalse($batch->finished()); } public function testCannotAddRequestsToInProgressBatch(): void { $this->expectException(BatchInProgressException::class); $this->factory->fake([ 'https://200.com' => $this->factory::response('OK', 200), 'https://201.com' => $this->factory::response('Created', 201), 'https://500.com' => $this->factory::response('Error', 500), ]); $responses = $this->factory->batch(function (Batch $batch) { return [ $batch->as('first')->get('https://200.com'), $batch->as('second')->get('https://201.com'), ]; })->progress(function (Batch $batch, int|string $key, Response $response) use (&$progressCallbacks) { $batch->as('third')->get('https://500.com'); })->send(); } public function testBatchBeforeHook(): void { $this->factory->fake([ 'https://200.com' => $this->factory::response('OK', 200), 'https://201.com' => $this->factory::response('Created', 201), ]); $beforeCallback = false; $responses = $this->factory->batch(function (Batch $batch) { return [ $batch->as('first')->get('https://200.com'), $batch->as('second')->get('https://201.com'), ]; })->before(function (Batch $batch) use (&$beforeCallback) { $beforeCallback = true; })->send(); $this->assertSame(200, $responses['first']->status()); $this->assertSame(201, $responses['second']->status()); $this->assertTrue($beforeCallback); } public function testBatchProgressHook(): void { $this->factory->fake([ 'https://200.com' => $this->factory::response('OK', 200), 'https://201.com' => $this->factory::response('Created', 201), 'https://500.com' => $this->factory::response('Error', 500), ]); $progressCallbacks = []; $responses = $this->factory->batch(function (Batch $batch) { return [ $batch->as('first')->get('https://200.com'), $batch->as('second')->get('https://201.com'), $batch->as('third')->get('https://500.com'), ]; })->progress(function (Batch $batch, int|string $key, Response $response) use (&$progressCallbacks) { $progressCallbacks[$key] = $response; })->send(); $this->assertSame(200, $responses['first']->status()); $this->assertSame(201, $responses['second']->status()); $this->assertSame(500, $responses['third']->status()); $this->assertCount(2, $progressCallbacks); $this->assertArrayHasKey('first', $progressCallbacks); $this->assertArrayHasKey('second', $progressCallbacks); $this->assertArrayNotHasKey('third', $progressCallbacks); $this->assertSame($responses['first'], $progressCallbacks['first']); $this->assertSame($responses['second'], $progressCallbacks['second']); } public function testBatchCatchHook(): void { $this->factory->fake([ 'https://200.com' => $this->factory::response('OK', 200), 'https://201.com' => $this->factory::response('Created', 201), 'https://500.com' => $this->factory::response('Error', 500), ]); $catchCallbacks = []; $responses = $this->factory->batch(function (Batch $batch) { return [ $batch->as('first')->get('https://200.com'), $batch->as('second')->get('https://201.com'), $batch->as('third')->get('https://500.com'), ]; })->catch(function (Batch $batch, int|string $key, Response|RequestException $response) use (&$catchCallbacks) { $catchCallbacks[$key] = $response; })->send(); $this->assertSame(200, $responses['first']->status()); $this->assertSame(201, $responses['second']->status()); $this->assertSame(500, $responses['third']->status()); $this->assertCount(1, $catchCallbacks); $this->assertArrayNotHasKey('first', $catchCallbacks); $this->assertArrayNotHasKey('second', $catchCallbacks); $this->assertArrayHasKey('third', $catchCallbacks); $this->assertSame($responses['third'], $catchCallbacks['third']); } public function testBatchThenHookIsCalled(): void { $this->factory->fake([ 'https://200.com' => $this->factory::response('OK', 200), 'https://201.com' => $this->factory::response('Created', 201), ]); $thenCallback = []; $responses = $this->factory->batch(function (Batch $batch) { return [ $batch->as('first')->get('https://200.com'), $batch->as('second')->get('https://201.com'), ]; })->then(function (Batch $batch, array $results) use (&$thenCallback) { $thenCallback = $results; })->send(); $this->assertSame(200, $responses['first']->status()); $this->assertSame(201, $responses['second']->status()); $this->assertCount(2, $thenCallback); $this->assertArrayHasKey('first', $thenCallback); $this->assertArrayHasKey('second', $thenCallback); $this->assertSame($responses['first'], $thenCallback['first']); $this->assertSame($responses['second'], $thenCallback['second']); } public function testBatchThenHookIsNotCalled(): void { $this->factory->fake([ 'https://200.com' => $this->factory::response('OK', 200), 'https://500.com' => $this->factory::response('Error', 500), ]); $thenCallback = []; $responses = $this->factory->batch(function (Batch $batch) { return [ $batch->as('first')->get('https://200.com'), $batch->as('second')->get('https://500.com'), ]; })->then(function (Batch $batch, array $results) use (&$thenCallback) { $thenCallback = $results; })->send(); $this->assertSame(200, $responses['first']->status()); $this->assertSame(500, $responses['second']->status()); $this->assertCount(0, $thenCallback); } public function testBatchFinallyHookIsCalledWithoutErrors(): void { $this->factory->fake([ 'https://200.com' => $this->factory::response('OK', 200), 'https://201.com' => $this->factory::response('Created', 201), ]); $finallyCallback = []; $responses = $this->factory->batch(function (Batch $batch) { return [ $batch->as('first')->get('https://200.com'), $batch->as('second')->get('https://201.com'), ]; })->finally(function (Batch $batch, array $results) use (&$finallyCallback) { $finallyCallback = $results; })->send(); $this->assertSame(200, $responses['first']->status()); $this->assertSame(201, $responses['second']->status()); $this->assertCount(2, $finallyCallback); $this->assertArrayHasKey('first', $finallyCallback); $this->assertArrayHasKey('second', $finallyCallback); $this->assertSame($responses['first'], $finallyCallback['first']); $this->assertSame($responses['second'], $finallyCallback['second']); } public function testBatchFinallyHookIsCalledWithErrors(): void { $this->factory->fake([ 'https://200.com' => $this->factory::response('OK', 200), 'https://500.com' => $this->factory::response('Error', 500), ]); $finallyCallback = []; $responses = $this->factory->batch(function (Batch $batch) { return [ $batch->as('first')->get('https://200.com'), $batch->as('second')->get('https://500.com'), ]; })->finally(function (Batch $batch, array $results) use (&$finallyCallback) { $finallyCallback = $results; })->send(); $this->assertSame(200, $responses['first']->status()); $this->assertSame(500, $responses['second']->status()); $this->assertCount(2, $finallyCallback); $this->assertArrayHasKey('first', $finallyCallback); $this->assertArrayHasKey('second', $finallyCallback); $this->assertSame($responses['first'], $finallyCallback['first']); $this->assertSame($responses['second'], $finallyCallback['second']); } public function testBatchConcurrency(): void { $this->factory->fake([ 'https://200.com' => $this->factory::response('OK', 200), 'https://201.com' => $this->factory::response('Created', 201), 'https://202.com' => $this->factory::response('Accepted', 202), 'https://203.com' => $this->factory::response('Non-Authoritative Information', 203), ]); $executionOrder = []; $batch = $this->factory->batch(function (Batch $batch) { return [ $batch->as('first')->get('https://200.com'), $batch->as('second')->get('https://201.com'), $batch->as('third')->get('https://202.com'), $batch->as('fourth')->get('https://203.com'), ]; })->progress(function (Batch $batch, int|string $key, Response $response) use (&$executionOrder) { $executionOrder[] = $key; })->concurrency(2); $this->assertSame(4, $batch->totalRequests); $responses = $batch->send(); $this->assertSame(200, $responses['first']->status()); $this->assertSame(201, $responses['second']->status()); $this->assertSame(202, $responses['third']->status()); $this->assertSame(203, $responses['fourth']->status()); $this->assertSame(4, $batch->totalRequests); $this->assertSame(0, $batch->pendingRequests); $this->assertSame(0, $batch->failedRequests); } public static function methodsReceivingArrayableDataProvider() { return [ 'patch' => ['patch'], 'put' => ['put'], 'post' => ['post'], 'delete' => ['delete'], ]; } public static function invalidHeaderValuesProvider() { return [ 'object' => [new stdClass], 'resource' => [fopen('php://temp', 'r')], 'array with object' => [['valid', new stdClass]], 'array with resource' => [['valid', fopen('php://temp', 'r')]], 'array with nested array' => [['valid', ['nested']]], ]; } public static function invalidMultipartHeaderValuesProvider() { return [ 'array' => [['nested']], 'object' => [new stdClass], 'resource' => [fopen('php://temp', 'r')], ]; } public static function invalidFakeResponseHeaderValuesProvider() { return [ 'object' => [new stdClass], 'resource' => [fopen('php://temp', 'r')], 'array with object' => [['valid', new stdClass]], 'array with resource' => [['valid', fopen('php://temp', 'r')]], 'array with nested array' => [['valid', ['nested']]], ]; } public function testAfterResponse() { $this->factory->fake([ 'http://200.com*' => $this->factory::response('OK'), ]); $response = $this->factory ->afterResponse(fn (Response $response): TestResponse => new TestResponse($response->toPsrResponse())) ->afterResponse(fn () => 'abc') ->afterResponse(function ($response, $request) { $this->assertInstanceOf(TestResponse::class, $response); $this->assertInstanceOf(Request::class, $request); $this->assertSame('http://200.com', (string) $request->url()); }) ->afterResponse(fn (Response $r) => new Response($r->toPsrResponse()->withBody(Utils::streamFor(strtolower($r->body()))))) ->get('http://200.com'); $this->assertInstanceOf(Response::class, $response); $this->assertSame('ok', $response->body()); } public function testAfterResponseWithThrows() { $this->factory->fake([ 'http://500.com*' => $this->factory::response('oh no', 500), ]); try { $this->factory->throw() ->afterResponse(fn ($response) => new TestResponse($response->toPsrResponse())) ->post('http://500.com'); } catch (RequestException $e) { $this->assertInstanceOf(TestResponse::class, $e->response); } } public function testAfterResponseWithAsync() { $this->factory->fake([ 'http://200.com*' => $this->factory::response('OK', 200), 'http://401.com*' => $this->factory::response('Unauthorized.', 401), ]); $requestReceived = null; $o = $this->factory->pool(function (Pool $pool) use (&$requestReceived): void { $pool->as('200')->afterResponse(function (Response $response, Request $request) use (&$requestReceived) { $requestReceived = $request; return new TestResponse($response->toPsrResponse()); })->get('http://200.com'); $pool->as('401-throwing')->throw()->afterResponse(fn (Response $response) => new TestResponse($response->toPsrResponse()))->get('http://401.com'); $pool->as('401-response')->afterResponse(fn (Response $response) => new TestResponse($response->toPsrResponse()->withBody(Utils::streamFor('different'))))->get('http://401.com'); }, 0); $this->assertInstanceOf(TestResponse::class, $o['200']); $this->assertInstanceOf(Request::class, $requestReceived); $this->assertSame('http://200.com', (string) $requestReceived->url()); $this->assertInstanceOf(TestResponse::class, $o['401-response']); $this->assertSame('different', $o['401-response']->body()); $this->assertInstanceOf(RequestException::class, $o['401-throwing']); $this->assertInstanceOf(TestResponse::class, $o['401-throwing']->response); } public function testRespectsDefaultFlags() { Response::$defaultJsonDecodingFlags = JSON_BIGINT_AS_STRING; // Create a response with a big integer that exceeds PHP_INT_MAX $bigInt = '9223372036854775808'; $body = '{"value":'.$bigInt.'}'; $response = new Response(Factory::psr7Response($body)); // With JSON_BIGINT_AS_STRING, it should be the exact string $this->assertSame($bigInt, $response->json('value')); $this->assertSame($bigInt, $response->object()->value); $this->assertSame($bigInt, $response->collect('value')->first()); $this->assertSame($bigInt, $response->fluent()->get('value')); // Default json_decode behavior (flags=0), big integers become floats (losing precision) $this->assertIsFloat($response->json('value', null, 0)); $this->assertIsFloat($response->object(0)->value); $this->assertIsFloat($response->collect('value', 0)->first()); $this->assertIsFloat($response->fluent(flags: 0)->get('value')); } public function testJsonDecodingIsCachedWhenFlagsMatch() { Response::$defaultJsonDecodingFlags = JSON_BIGINT_AS_STRING; $response = new BodyTrackingResponse(Factory::psr7Response('{"foo":"bar"}')); // First call decodes with default (JSON_BIGINT_AS_STRING) $response->json(); $this->assertSame(1, $response->bodyCallCount); // Second call with same (null) flags uses cache $response->json(); $this->assertSame(1, $response->bodyCallCount); // Explicit flags matching default still uses cache $response->json(flags: JSON_BIGINT_AS_STRING); $this->assertSame(1, $response->bodyCallCount); // Different flags triggers re-decode $response->json(flags: 0); $this->assertSame(2, $response->bodyCallCount); // Same explicit flags uses cache $response->json(flags: 0); $this->assertSame(2, $response->bodyCallCount); // Null flags means "use default", cached flags differ, so re-decode $response->json(); $this->assertSame(3, $response->bodyCallCount); } public function testJsonDecodingIsCachedForFalsyPayloads() { $payloads = [ ['[]', []], ['false', false], ['0', 0], ['null', null], ['""', ''], ]; foreach ($payloads as [$body, $expected]) { $response = new BodyTrackingResponse(Factory::psr7Response($body)); // First call decodes and caches $this->assertSame($expected, $response->json()); $this->assertSame(1, $response->bodyCallCount, "Failed for body: $body"); // Subsequent calls use cache (body() not called again) $this->assertSame($expected, $response->json()); $this->assertSame(1, $response->bodyCallCount, "body() called again for falsy payload: $body"); // Third call to be sure $this->assertSame($expected, $response->json()); $this->assertSame(1, $response->bodyCallCount, "body() called again for falsy payload: $body"); } } public function testJsonDecodingWithFalsyPayloadRespectsFlags() { $response = new BodyTrackingResponse(Factory::psr7Response('0')); // First call decodes with default flags $this->assertSame(0, $response->json()); $this->assertSame(1, $response->bodyCallCount); // Different flags triggers re-decode $response->json(flags: JSON_BIGINT_AS_STRING); $this->assertSame(2, $response->bodyCallCount); // Same flags uses cache $response->json(flags: JSON_BIGINT_AS_STRING); $this->assertSame(2, $response->bodyCallCount); } public function testJsonDecodingWithEmptyArrayRespectsKeyAccess() { $response = new BodyTrackingResponse(Factory::psr7Response('[]')); // Accessing a key on an empty array returns default $this->assertNull($response->json('missing')); $this->assertSame('fallback', $response->json('missing', 'fallback')); // body() should only be called once $response->json(); $this->assertSame(1, $response->bodyCallCount); } } class CustomFactory extends Factory { protected function newPendingRequest() { return new class extends PendingRequest { protected function newResponse($response) { return new TestResponse($response); } }; } } class TestResponse extends Response { } class BodyTrackingResponse extends Response { public int $bodyCallCount = 0; public function body() { $this->bodyCallCount++; return parent::body(); } }