From 55db58710a3aaa3033bc2231850f22f4c457f94e Mon Sep 17 00:00:00 2001 From: Jerome Thayananthajothy Date: Mon, 30 Sep 2024 18:27:50 +0100 Subject: [PATCH] Update tests to include fluent API as well --- README.md | 4 +- composer.json | 3 +- src/Fetch/Http/ClientHandler.php | 42 +++++++++++++++++++-- src/Fetch/Http/fetch.php | 25 +++++++++---- tests/Unit/ClientHandlerTest.php | 37 ++++++++++++++++++ tests/Unit/FetchTest.php | 64 +++++++++++++++++++++----------- 6 files changed, 140 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 3e821b0..f55020b 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,7 @@ The **fluent API** allows for a more flexible and readable way of building and s $response = fetch() ->baseUri('https://example.com') - ->withHeaders('Content-Type', 'application/json') + ->withHeaders(['Content-Type' => 'application/json']) ->withBody(json_encode(['key' => 'value'])) ->withToken('fake-bearer-auth-token') ->post('/posts'); @@ -200,7 +200,7 @@ $data = null; // Asynchronously send a POST request using the fluent API async(fn () => fetch() ->baseUri('https://example.com') - ->withHeaders('Content-Type', 'application/json') + ->withHeaders(['Content-Type' => 'application/json']) ->withBody(json_encode(['key' => 'value'])) ->withToken('fake-bearer-auth-token') ->post('/posts')) diff --git a/composer.json b/composer.json index ce44ff3..65eb7be 100644 --- a/composer.json +++ b/composer.json @@ -35,7 +35,8 @@ "mockery/mockery": "^1.6", "pestphp/pest": "^2.0|^3.0", "phpstan/phpstan": "^1.11.5", - "squizlabs/php_codesniffer": "^3.7" + "squizlabs/php_codesniffer": "^3.7", + "symfony/var-dumper": "^7.1" }, "autoload-dev": { "psr-4": { diff --git a/src/Fetch/Http/ClientHandler.php b/src/Fetch/Http/ClientHandler.php index 33e04aa..32a3377 100644 --- a/src/Fetch/Http/ClientHandler.php +++ b/src/Fetch/Http/ClientHandler.php @@ -7,6 +7,7 @@ use GuzzleHttp\Client as SyncClient; use GuzzleHttp\Cookie\CookieJarInterface; use GuzzleHttp\Exception\RequestException; +use InvalidArgumentException; use Matrix\AsyncHelper; use Matrix\Interfaces\AsyncHelper as AsyncHelperInterface; use Psr\Http\Client\ClientInterface; @@ -98,6 +99,10 @@ protected function applyOptions(array $options): void $this->retries = $options['retries'] ?? $this->retries; $this->retryDelay = $options['retry_delay'] ?? $this->retryDelay; $this->isAsync = ! empty($options['async']); + + if (isset($options['base_uri'])) { + $this->baseUri($options['base_uri']); + } } /** @@ -140,7 +145,7 @@ protected function sendSync(): ResponseInterface return $this->retryRequest(function (): ResponseInterface { $psrResponse = $this->getSyncClient()->request( $this->options['method'], - $this->options['uri'], + $this->getFullUri(), $this->options ); @@ -155,9 +160,9 @@ protected function sendSync(): ResponseInterface */ protected function sendAsync(): AsyncHelperInterface { - return new AsyncHelper(function (): ResponseInterface { - return $this->sendSync(); - }); + return new AsyncHelper( + promise: fn (): ResponseInterface => $this->sendSync() + ); } /** @@ -198,6 +203,35 @@ protected function isRetryableError(RequestException $e): bool return in_array($e->getCode(), [500, 502, 503, 504]); } + /** + * Get the full URI for the request. + * + * @return string + */ + protected function getFullUri(): string + { + $baseUri = $this->options['base_uri'] ?? ''; + $uri = $this->options['uri'] ?? ''; + + // If the URI is an absolute URL, return it as is + if (filter_var($uri, \FILTER_VALIDATE_URL)) { + return $uri; + } + + // If base URI is empty, return the URI with leading slashes trimmed + if (empty($baseUri)) { + return ltrim($uri, '/'); + } + + // Ensure base URI is a valid URL + if (! filter_var($baseUri, \FILTER_VALIDATE_URL)) { + throw new InvalidArgumentException("Invalid base URI: $baseUri"); + } + + // Concatenate base URI and URI ensuring no double slashes + return rtrim($baseUri, '/') . '/' . ltrim($uri, '/'); + } + /** * Reset the handler state. * diff --git a/src/Fetch/Http/fetch.php b/src/Fetch/Http/fetch.php index 1bdee8a..4e94165 100644 --- a/src/Fetch/Http/fetch.php +++ b/src/Fetch/Http/fetch.php @@ -1,20 +1,25 @@ hasResponse()) { return Response::createFromBase($e->getResponse()); diff --git a/tests/Unit/ClientHandlerTest.php b/tests/Unit/ClientHandlerTest.php index ff332e8..b583b21 100644 --- a/tests/Unit/ClientHandlerTest.php +++ b/tests/Unit/ClientHandlerTest.php @@ -32,6 +32,23 @@ expect($response->getStatusCode())->toBe(200); }); +test('makes successful synchronous GET request using fluent API', function () { + $mockClient = mock(Client::class, function (MockInterface $mock) { + $mock->shouldReceive('request') + ->once() + ->with('GET', 'http://localhost/', \Mockery::type('array')) + ->andReturn(new Response(200, [], json_encode(['success' => true]))); + }); + + $clientHandler = new ClientHandler(); + $response = $clientHandler->setSyncClient($mockClient) + ->baseUri('http://localhost') + ->get('/'); + + expect($response->json())->toBe(['success' => true]); + expect($response->getStatusCode())->toBe(200); +}); + /* * Test for a successful asynchronous GET request. */ @@ -57,6 +74,26 @@ }); }); +test('makes successful synchronous POST request using fluent API', function () { + $mockClient = mock(Client::class, function (MockInterface $mock) { + $mock->shouldReceive('request') + ->once() + ->with('POST', 'http://localhost/posts', \Mockery::type('array')) + ->andReturn(new Response(201, [], json_encode(['success' => true]))); + }); + + $clientHandler = new ClientHandler(); + $response = $clientHandler->setSyncClient($mockClient) + ->baseUri('http://localhost') + ->withHeaders(['Content-Type' => 'application/json']) + ->withBody(json_encode(['key' => 'value'])) + ->withToken('fake-bearer-auth-token') + ->post('/posts'); + + expect($response->json())->toBe(['success' => true]); + expect($response->getStatusCode())->toBe(201); +}); + /* * Test for sending headers with a GET request. */ diff --git a/tests/Unit/FetchTest.php b/tests/Unit/FetchTest.php index 4de1fcf..dee4430 100644 --- a/tests/Unit/FetchTest.php +++ b/tests/Unit/FetchTest.php @@ -1,5 +1,7 @@ shouldReceive('request') ->once() - ->with('GET', 'https://example.com', \Mockery::type('array')) + ->with('GET', 'http://localhost', \Mockery::type('array')) ->andReturn(new Response(200, [], json_encode(['success' => true]))); }); - $response = fetch('https://example.com', ['client' => $mockClient]); + $response = fetch('http://localhost', ['client' => $mockClient]); expect($response->json())->toBe(['success' => true]); expect($response->getStatusCode())->toBe(200); @@ -34,11 +36,11 @@ $mockClient = mock(Client::class, function (MockInterface $mock) { $mock->shouldReceive('request') ->once() - ->with('GET', 'https://example.com', \Mockery::type('array')) + ->with('GET', 'http://localhost', \Mockery::type('array')) ->andReturn(new Response(200, [], json_encode(['async' => 'result']))); }); - async(fn () => fetch('https://example.com', ['client' => $mockClient])) + async(fn () => fetch('http://localhost', ['client' => $mockClient])) ->then(function (Response $response) { expect($response->json())->toBe(['async' => 'result']); expect($response->getStatusCode())->toBe(200); @@ -48,6 +50,26 @@ }); }); +test('fetch makes successful synchronous POST request using fluent API', function () { + $mockClient = mock(Client::class, function (MockInterface $mock) { + $mock->shouldReceive('request') + ->once() + ->with('POST', 'http://localhost/posts', \Mockery::type('array')) + ->andReturn(new Response(201, [], json_encode(['success' => true]))); + }); + + $response = fetch() + ->setSyncClient($mockClient) // Set the mock client + ->baseUri('http://localhost') + ->withHeaders(['Content-Type' => 'application/json']) + ->withBody(json_encode(['key' => 'value'])) + ->withToken('fake-bearer-auth-token') + ->post('/posts'); + + expect($response->json())->toBe(['success' => true]); + expect($response->getStatusCode())->toBe(201); +}); + /* * Test for sending headers with a GET request using fetch. */ @@ -55,13 +77,13 @@ $mockClient = mock(Client::class, function (MockInterface $mock) { $mock->shouldReceive('request') ->once() - ->with('GET', 'https://example.com', \Mockery::on(function ($options) { + ->with('GET', 'http://localhost', \Mockery::on(function ($options) { return $options['headers']['Authorization'] === 'Bearer token'; })) ->andReturn(new Response(200, [], 'Headers checked')); }); - $response = fetch('https://example.com', [ + $response = fetch('http://localhost', [ 'headers' => ['Authorization' => 'Bearer token'], 'client' => $mockClient ]); @@ -76,13 +98,13 @@ $mockClient = mock(Client::class, function (MockInterface $mock) { $mock->shouldReceive('request') ->once() - ->with('GET', 'https://example.com', \Mockery::on(function ($options) { + ->with('GET', 'http://localhost', \Mockery::on(function ($options) { return $options['query'] === ['foo' => 'bar', 'baz' => 'qux']; })) ->andReturn(new Response(200, [], 'Query params checked')); }); - $response = fetch('https://example.com', [ + $response = fetch('http://localhost', [ 'query' => ['foo' => 'bar', 'baz' => 'qux'], 'client' => $mockClient ]); @@ -97,14 +119,14 @@ $mockClient = mock(Client::class, function (MockInterface $mock) { $mock->shouldReceive('request') ->once() - ->with('GET', 'https://example.com', \Mockery::on(function ($options) { + ->with('GET', 'http://localhost', \Mockery::on(function ($options) { return $options['timeout'] === 1; })) - ->andThrow(new RequestException('Timeout', new Request('GET', 'https://example.com'))); + ->andThrow(new RequestException('Timeout', new Request('GET', 'http://localhost'))); }); try { - fetch('https://example.com', ['timeout' => 1, 'client' => $mockClient]); + fetch('http://localhost', ['timeout' => 1, 'client' => $mockClient]); } catch (RequestException $e) { expect($e->getMessage())->toContain('Timeout'); } @@ -117,15 +139,15 @@ $mockClient = mock(Client::class, function (MockInterface $mock) { $mock->shouldReceive('request') ->times(1) // Expecting 2 calls: 1 failed, 1 retry - ->with('GET', 'https://example.com', \Mockery::type('array')) - ->andThrow(new RequestException('Failed request', new Request('GET', 'https://example.com'))); + ->with('GET', 'http://localhost', \Mockery::type('array')) + ->andThrow(new RequestException('Failed request', new Request('GET', 'http://localhost'))); $mock->shouldReceive('request') ->times(1) // Expecting 2 calls: 1 failed, 1 retry - ->with('GET', 'https://example.com', \Mockery::type('array')) + ->with('GET', 'http://localhost', \Mockery::type('array')) ->andReturn(new Response(200, [], 'Success after retry')); }); - $response = fetch('https://example.com', ['retries' => 2, 'client' => $mockClient]); + $response = fetch('http://localhost', ['retries' => 2, 'client' => $mockClient]); expect($response->text())->toBe('Success after retry'); }); @@ -137,13 +159,13 @@ $mockClient = mock(Client::class, function (MockInterface $mock) { $mock->shouldReceive('request') ->once() - ->with('POST', 'https://example.com/users', \Mockery::on(function ($options) { + ->with('POST', 'http://localhost/users', \Mockery::on(function ($options) { return $options['body'] === json_encode(['name' => 'John']); })) ->andReturn(new Response(201, [], 'Created')); }); - $response = fetch('https://example.com/users', [ + $response = fetch('http://localhost/users', [ 'method' => 'POST', 'body' => json_encode(['name' => 'John']), 'client' => $mockClient @@ -160,15 +182,15 @@ $mockClient = mock(Client::class, function (MockInterface $mock) { $mock->shouldReceive('request') ->times(1) - ->with('GET', 'https://example.com', \Mockery::type('array')) - ->andThrow(new RequestException('Failed request', new Request('GET', 'https://example.com'))); + ->with('GET', 'http://localhost', \Mockery::type('array')) + ->andThrow(new RequestException('Failed request', new Request('GET', 'http://localhost'))); $mock->shouldReceive('request') ->times(1) - ->with('GET', 'https://example.com', \Mockery::type('array')) + ->with('GET', 'http://localhost', \Mockery::type('array')) ->andReturn(new Response(200, [], 'Success after retry')); }); - async(fn () => fetch('https://example.com', ['retries' => 2, 'client' => $mockClient])) + async(fn () => fetch('http://localhost', ['retries' => 2, 'client' => $mockClient])) ->then(function (Response $response) { expect($response->text())->toBe('Success after retry'); })