Skip to content

Commit

Permalink
Update tests to include fluent API as well
Browse files Browse the repository at this point in the history
  • Loading branch information
Thavarshan committed Sep 30, 2024
1 parent 0c09a31 commit 55db587
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 35 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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'))
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
42 changes: 38 additions & 4 deletions src/Fetch/Http/ClientHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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']);
}
}

/**
Expand Down Expand Up @@ -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
);

Expand All @@ -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()
);
}

/**
Expand Down Expand Up @@ -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.
*
Expand Down
25 changes: 18 additions & 7 deletions src/Fetch/Http/fetch.php
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
<?php

use Fetch\Http\ClientHandler;
use Fetch\Http\Response;
namespace Fetch\Http;

use GuzzleHttp\Exception\RequestException;
use Throwable;

if (! function_exists('fetch')) {
/**
* Perform an HTTP request similar to JavaScript's fetch API.
*
* @param string $url
* @param array $options
* @param string|null $url
* @param array|null $options
*
* @return \Fetch\Http\Response
* @return \Fetch\Http\Response|\Fetch\Http\ClientHandler
*/
function fetch(string $url, array $options = []): mixed
function fetch(?string $url = null, ?array $options = []): Response|ClientHandler
{
if (is_null($url)) {
return new ClientHandler(options: $options);
}

$options = array_merge(ClientHandler::getDefaultOptions(), $options);

// Uppercase the method
Expand All @@ -26,10 +31,16 @@ function fetch(string $url, array $options = []): mixed
$options['headers']['Content-Type'] = 'application/json';
}

// Handle baseUri if provided
if (isset($options['base_uri'])) {
$url = rtrim($options['base_uri'], '/') . '/' . ltrim($url, '/');
unset($options['base_uri']);
}

// Synchronous request handling
try {
return ClientHandler::handle($options['method'], $url, $options);
} catch (\Throwable $e) {
} catch (Throwable $e) {
// Handle exceptions and return the response
if ($e instanceof RequestException && $e->hasResponse()) {
return Response::createFromBase($e->getResponse());
Expand Down
37 changes: 37 additions & 0 deletions tests/Unit/ClientHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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.
*/
Expand Down
64 changes: 43 additions & 21 deletions tests/Unit/FetchTest.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

use function Fetch\Http\fetch;

use Fetch\Http\Response;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
Expand All @@ -17,11 +19,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(['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);
Expand All @@ -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);
Expand All @@ -48,20 +50,40 @@
});
});

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.
*/
test('fetch sends headers with a GET request', function () {
$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
]);
Expand All @@ -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
]);
Expand All @@ -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');
}
Expand All @@ -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');
});
Expand All @@ -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
Expand All @@ -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');
})
Expand Down

0 comments on commit 55db587

Please sign in to comment.