Skip to content

Commit 55db587

Browse files
committed
Update tests to include fluent API as well
1 parent 0c09a31 commit 55db587

File tree

6 files changed

+140
-35
lines changed

6 files changed

+140
-35
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ The **fluent API** allows for a more flexible and readable way of building and s
180180

181181
$response = fetch()
182182
->baseUri('https://example.com')
183-
->withHeaders('Content-Type', 'application/json')
183+
->withHeaders(['Content-Type' => 'application/json'])
184184
->withBody(json_encode(['key' => 'value']))
185185
->withToken('fake-bearer-auth-token')
186186
->post('/posts');
@@ -200,7 +200,7 @@ $data = null;
200200
// Asynchronously send a POST request using the fluent API
201201
async(fn () => fetch()
202202
->baseUri('https://example.com')
203-
->withHeaders('Content-Type', 'application/json')
203+
->withHeaders(['Content-Type' => 'application/json'])
204204
->withBody(json_encode(['key' => 'value']))
205205
->withToken('fake-bearer-auth-token')
206206
->post('/posts'))

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
"mockery/mockery": "^1.6",
3636
"pestphp/pest": "^2.0|^3.0",
3737
"phpstan/phpstan": "^1.11.5",
38-
"squizlabs/php_codesniffer": "^3.7"
38+
"squizlabs/php_codesniffer": "^3.7",
39+
"symfony/var-dumper": "^7.1"
3940
},
4041
"autoload-dev": {
4142
"psr-4": {

src/Fetch/Http/ClientHandler.php

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use GuzzleHttp\Client as SyncClient;
88
use GuzzleHttp\Cookie\CookieJarInterface;
99
use GuzzleHttp\Exception\RequestException;
10+
use InvalidArgumentException;
1011
use Matrix\AsyncHelper;
1112
use Matrix\Interfaces\AsyncHelper as AsyncHelperInterface;
1213
use Psr\Http\Client\ClientInterface;
@@ -98,6 +99,10 @@ protected function applyOptions(array $options): void
9899
$this->retries = $options['retries'] ?? $this->retries;
99100
$this->retryDelay = $options['retry_delay'] ?? $this->retryDelay;
100101
$this->isAsync = ! empty($options['async']);
102+
103+
if (isset($options['base_uri'])) {
104+
$this->baseUri($options['base_uri']);
105+
}
101106
}
102107

103108
/**
@@ -140,7 +145,7 @@ protected function sendSync(): ResponseInterface
140145
return $this->retryRequest(function (): ResponseInterface {
141146
$psrResponse = $this->getSyncClient()->request(
142147
$this->options['method'],
143-
$this->options['uri'],
148+
$this->getFullUri(),
144149
$this->options
145150
);
146151

@@ -155,9 +160,9 @@ protected function sendSync(): ResponseInterface
155160
*/
156161
protected function sendAsync(): AsyncHelperInterface
157162
{
158-
return new AsyncHelper(function (): ResponseInterface {
159-
return $this->sendSync();
160-
});
163+
return new AsyncHelper(
164+
promise: fn (): ResponseInterface => $this->sendSync()
165+
);
161166
}
162167

163168
/**
@@ -198,6 +203,35 @@ protected function isRetryableError(RequestException $e): bool
198203
return in_array($e->getCode(), [500, 502, 503, 504]);
199204
}
200205

206+
/**
207+
* Get the full URI for the request.
208+
*
209+
* @return string
210+
*/
211+
protected function getFullUri(): string
212+
{
213+
$baseUri = $this->options['base_uri'] ?? '';
214+
$uri = $this->options['uri'] ?? '';
215+
216+
// If the URI is an absolute URL, return it as is
217+
if (filter_var($uri, \FILTER_VALIDATE_URL)) {
218+
return $uri;
219+
}
220+
221+
// If base URI is empty, return the URI with leading slashes trimmed
222+
if (empty($baseUri)) {
223+
return ltrim($uri, '/');
224+
}
225+
226+
// Ensure base URI is a valid URL
227+
if (! filter_var($baseUri, \FILTER_VALIDATE_URL)) {
228+
throw new InvalidArgumentException("Invalid base URI: $baseUri");
229+
}
230+
231+
// Concatenate base URI and URI ensuring no double slashes
232+
return rtrim($baseUri, '/') . '/' . ltrim($uri, '/');
233+
}
234+
201235
/**
202236
* Reset the handler state.
203237
*

src/Fetch/Http/fetch.php

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,25 @@
11
<?php
22

3-
use Fetch\Http\ClientHandler;
4-
use Fetch\Http\Response;
3+
namespace Fetch\Http;
4+
55
use GuzzleHttp\Exception\RequestException;
6+
use Throwable;
67

78
if (! function_exists('fetch')) {
89
/**
910
* Perform an HTTP request similar to JavaScript's fetch API.
1011
*
11-
* @param string $url
12-
* @param array $options
12+
* @param string|null $url
13+
* @param array|null $options
1314
*
14-
* @return \Fetch\Http\Response
15+
* @return \Fetch\Http\Response|\Fetch\Http\ClientHandler
1516
*/
16-
function fetch(string $url, array $options = []): mixed
17+
function fetch(?string $url = null, ?array $options = []): Response|ClientHandler
1718
{
19+
if (is_null($url)) {
20+
return new ClientHandler(options: $options);
21+
}
22+
1823
$options = array_merge(ClientHandler::getDefaultOptions(), $options);
1924

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

34+
// Handle baseUri if provided
35+
if (isset($options['base_uri'])) {
36+
$url = rtrim($options['base_uri'], '/') . '/' . ltrim($url, '/');
37+
unset($options['base_uri']);
38+
}
39+
2940
// Synchronous request handling
3041
try {
3142
return ClientHandler::handle($options['method'], $url, $options);
32-
} catch (\Throwable $e) {
43+
} catch (Throwable $e) {
3344
// Handle exceptions and return the response
3445
if ($e instanceof RequestException && $e->hasResponse()) {
3546
return Response::createFromBase($e->getResponse());

tests/Unit/ClientHandlerTest.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,23 @@
3232
expect($response->getStatusCode())->toBe(200);
3333
});
3434

35+
test('makes successful synchronous GET request using fluent API', function () {
36+
$mockClient = mock(Client::class, function (MockInterface $mock) {
37+
$mock->shouldReceive('request')
38+
->once()
39+
->with('GET', 'http://localhost/', \Mockery::type('array'))
40+
->andReturn(new Response(200, [], json_encode(['success' => true])));
41+
});
42+
43+
$clientHandler = new ClientHandler();
44+
$response = $clientHandler->setSyncClient($mockClient)
45+
->baseUri('http://localhost')
46+
->get('/');
47+
48+
expect($response->json())->toBe(['success' => true]);
49+
expect($response->getStatusCode())->toBe(200);
50+
});
51+
3552
/*
3653
* Test for a successful asynchronous GET request.
3754
*/
@@ -57,6 +74,26 @@
5774
});
5875
});
5976

77+
test('makes successful synchronous POST request using fluent API', function () {
78+
$mockClient = mock(Client::class, function (MockInterface $mock) {
79+
$mock->shouldReceive('request')
80+
->once()
81+
->with('POST', 'http://localhost/posts', \Mockery::type('array'))
82+
->andReturn(new Response(201, [], json_encode(['success' => true])));
83+
});
84+
85+
$clientHandler = new ClientHandler();
86+
$response = $clientHandler->setSyncClient($mockClient)
87+
->baseUri('http://localhost')
88+
->withHeaders(['Content-Type' => 'application/json'])
89+
->withBody(json_encode(['key' => 'value']))
90+
->withToken('fake-bearer-auth-token')
91+
->post('/posts');
92+
93+
expect($response->json())->toBe(['success' => true]);
94+
expect($response->getStatusCode())->toBe(201);
95+
});
96+
6097
/*
6198
* Test for sending headers with a GET request.
6299
*/

tests/Unit/FetchTest.php

Lines changed: 43 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<?php
22

3+
use function Fetch\Http\fetch;
4+
35
use Fetch\Http\Response;
46
use GuzzleHttp\Client;
57
use GuzzleHttp\Exception\RequestException;
@@ -17,11 +19,11 @@
1719
$mockClient = mock(Client::class, function (MockInterface $mock) {
1820
$mock->shouldReceive('request')
1921
->once()
20-
->with('GET', 'https://example.com', \Mockery::type('array'))
22+
->with('GET', 'http://localhost', \Mockery::type('array'))
2123
->andReturn(new Response(200, [], json_encode(['success' => true])));
2224
});
2325

24-
$response = fetch('https://example.com', ['client' => $mockClient]);
26+
$response = fetch('http://localhost', ['client' => $mockClient]);
2527

2628
expect($response->json())->toBe(['success' => true]);
2729
expect($response->getStatusCode())->toBe(200);
@@ -34,11 +36,11 @@
3436
$mockClient = mock(Client::class, function (MockInterface $mock) {
3537
$mock->shouldReceive('request')
3638
->once()
37-
->with('GET', 'https://example.com', \Mockery::type('array'))
39+
->with('GET', 'http://localhost', \Mockery::type('array'))
3840
->andReturn(new Response(200, [], json_encode(['async' => 'result'])));
3941
});
4042

41-
async(fn () => fetch('https://example.com', ['client' => $mockClient]))
43+
async(fn () => fetch('http://localhost', ['client' => $mockClient]))
4244
->then(function (Response $response) {
4345
expect($response->json())->toBe(['async' => 'result']);
4446
expect($response->getStatusCode())->toBe(200);
@@ -48,20 +50,40 @@
4850
});
4951
});
5052

53+
test('fetch makes successful synchronous POST request using fluent API', function () {
54+
$mockClient = mock(Client::class, function (MockInterface $mock) {
55+
$mock->shouldReceive('request')
56+
->once()
57+
->with('POST', 'http://localhost/posts', \Mockery::type('array'))
58+
->andReturn(new Response(201, [], json_encode(['success' => true])));
59+
});
60+
61+
$response = fetch()
62+
->setSyncClient($mockClient) // Set the mock client
63+
->baseUri('http://localhost')
64+
->withHeaders(['Content-Type' => 'application/json'])
65+
->withBody(json_encode(['key' => 'value']))
66+
->withToken('fake-bearer-auth-token')
67+
->post('/posts');
68+
69+
expect($response->json())->toBe(['success' => true]);
70+
expect($response->getStatusCode())->toBe(201);
71+
});
72+
5173
/*
5274
* Test for sending headers with a GET request using fetch.
5375
*/
5476
test('fetch sends headers with a GET request', function () {
5577
$mockClient = mock(Client::class, function (MockInterface $mock) {
5678
$mock->shouldReceive('request')
5779
->once()
58-
->with('GET', 'https://example.com', \Mockery::on(function ($options) {
80+
->with('GET', 'http://localhost', \Mockery::on(function ($options) {
5981
return $options['headers']['Authorization'] === 'Bearer token';
6082
}))
6183
->andReturn(new Response(200, [], 'Headers checked'));
6284
});
6385

64-
$response = fetch('https://example.com', [
86+
$response = fetch('http://localhost', [
6587
'headers' => ['Authorization' => 'Bearer token'],
6688
'client' => $mockClient
6789
]);
@@ -76,13 +98,13 @@
7698
$mockClient = mock(Client::class, function (MockInterface $mock) {
7799
$mock->shouldReceive('request')
78100
->once()
79-
->with('GET', 'https://example.com', \Mockery::on(function ($options) {
101+
->with('GET', 'http://localhost', \Mockery::on(function ($options) {
80102
return $options['query'] === ['foo' => 'bar', 'baz' => 'qux'];
81103
}))
82104
->andReturn(new Response(200, [], 'Query params checked'));
83105
});
84106

85-
$response = fetch('https://example.com', [
107+
$response = fetch('http://localhost', [
86108
'query' => ['foo' => 'bar', 'baz' => 'qux'],
87109
'client' => $mockClient
88110
]);
@@ -97,14 +119,14 @@
97119
$mockClient = mock(Client::class, function (MockInterface $mock) {
98120
$mock->shouldReceive('request')
99121
->once()
100-
->with('GET', 'https://example.com', \Mockery::on(function ($options) {
122+
->with('GET', 'http://localhost', \Mockery::on(function ($options) {
101123
return $options['timeout'] === 1;
102124
}))
103-
->andThrow(new RequestException('Timeout', new Request('GET', 'https://example.com')));
125+
->andThrow(new RequestException('Timeout', new Request('GET', 'http://localhost')));
104126
});
105127

106128
try {
107-
fetch('https://example.com', ['timeout' => 1, 'client' => $mockClient]);
129+
fetch('http://localhost', ['timeout' => 1, 'client' => $mockClient]);
108130
} catch (RequestException $e) {
109131
expect($e->getMessage())->toContain('Timeout');
110132
}
@@ -117,15 +139,15 @@
117139
$mockClient = mock(Client::class, function (MockInterface $mock) {
118140
$mock->shouldReceive('request')
119141
->times(1) // Expecting 2 calls: 1 failed, 1 retry
120-
->with('GET', 'https://example.com', \Mockery::type('array'))
121-
->andThrow(new RequestException('Failed request', new Request('GET', 'https://example.com')));
142+
->with('GET', 'http://localhost', \Mockery::type('array'))
143+
->andThrow(new RequestException('Failed request', new Request('GET', 'http://localhost')));
122144
$mock->shouldReceive('request')
123145
->times(1) // Expecting 2 calls: 1 failed, 1 retry
124-
->with('GET', 'https://example.com', \Mockery::type('array'))
146+
->with('GET', 'http://localhost', \Mockery::type('array'))
125147
->andReturn(new Response(200, [], 'Success after retry'));
126148
});
127149

128-
$response = fetch('https://example.com', ['retries' => 2, 'client' => $mockClient]);
150+
$response = fetch('http://localhost', ['retries' => 2, 'client' => $mockClient]);
129151

130152
expect($response->text())->toBe('Success after retry');
131153
});
@@ -137,13 +159,13 @@
137159
$mockClient = mock(Client::class, function (MockInterface $mock) {
138160
$mock->shouldReceive('request')
139161
->once()
140-
->with('POST', 'https://example.com/users', \Mockery::on(function ($options) {
162+
->with('POST', 'http://localhost/users', \Mockery::on(function ($options) {
141163
return $options['body'] === json_encode(['name' => 'John']);
142164
}))
143165
->andReturn(new Response(201, [], 'Created'));
144166
});
145167

146-
$response = fetch('https://example.com/users', [
168+
$response = fetch('http://localhost/users', [
147169
'method' => 'POST',
148170
'body' => json_encode(['name' => 'John']),
149171
'client' => $mockClient
@@ -160,15 +182,15 @@
160182
$mockClient = mock(Client::class, function (MockInterface $mock) {
161183
$mock->shouldReceive('request')
162184
->times(1)
163-
->with('GET', 'https://example.com', \Mockery::type('array'))
164-
->andThrow(new RequestException('Failed request', new Request('GET', 'https://example.com')));
185+
->with('GET', 'http://localhost', \Mockery::type('array'))
186+
->andThrow(new RequestException('Failed request', new Request('GET', 'http://localhost')));
165187
$mock->shouldReceive('request')
166188
->times(1)
167-
->with('GET', 'https://example.com', \Mockery::type('array'))
189+
->with('GET', 'http://localhost', \Mockery::type('array'))
168190
->andReturn(new Response(200, [], 'Success after retry'));
169191
});
170192

171-
async(fn () => fetch('https://example.com', ['retries' => 2, 'client' => $mockClient]))
193+
async(fn () => fetch('http://localhost', ['retries' => 2, 'client' => $mockClient]))
172194
->then(function (Response $response) {
173195
expect($response->text())->toBe('Success after retry');
174196
})

0 commit comments

Comments
 (0)