Skip to content

Commit edfe5be

Browse files
test(DMD-987): add unit tests for HandlerStack
- Test retry behavior for 5xx errors - Test no retry for 501 Not Implemented - Test 503 maintenance retry with retryOnMaintenance flag - Test 409 versionConflict retry handling - Test no retry for other 409 errors - Test max retries limit - Test default backoffMaxTries is zero Co-Authored-By: Martin Zajic <ja@mzajic.cz>
1 parent 4f91b95 commit edfe5be

1 file changed

Lines changed: 312 additions & 0 deletions

File tree

tests/Client/HandlerStackTest.php

Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Keboola\UnitTest\Client;
6+
7+
use GuzzleHttp\Client as GuzzleClient;
8+
use GuzzleHttp\Handler\MockHandler;
9+
use GuzzleHttp\Psr7\Request;
10+
use GuzzleHttp\Psr7\Response;
11+
use Keboola\StorageApi\HandlerStack;
12+
use PHPUnit\Framework\TestCase;
13+
14+
class HandlerStackTest extends TestCase
15+
{
16+
public function testCreateReturnsHandlerStack(): void
17+
{
18+
$handlerStack = HandlerStack::create();
19+
20+
self::assertInstanceOf(\GuzzleHttp\HandlerStack::class, $handlerStack);
21+
}
22+
23+
public function testCreateWithCustomHandler(): void
24+
{
25+
$mockHandler = new MockHandler([
26+
new Response(200, [], 'ok'),
27+
]);
28+
29+
$handlerStack = HandlerStack::create(['handler' => $mockHandler]);
30+
$client = new GuzzleClient(['handler' => $handlerStack]);
31+
32+
$response = $client->request('GET', 'http://example.com');
33+
self::assertSame(200, $response->getStatusCode());
34+
}
35+
36+
public function testNoRetryWhen501NotImplemented(): void
37+
{
38+
$mockHandler = new MockHandler([
39+
new Response(501, [], 'Not Implemented'),
40+
new Response(200, [], 'ok'),
41+
]);
42+
43+
$handlerStack = HandlerStack::create([
44+
'handler' => $mockHandler,
45+
'backoffMaxTries' => 3,
46+
]);
47+
$client = new GuzzleClient(['handler' => $handlerStack, 'http_errors' => false]);
48+
49+
$response = $client->request('GET', 'http://example.com');
50+
self::assertSame(501, $response->getStatusCode());
51+
self::assertSame(1, $mockHandler->count(), 'Second response should not be consumed');
52+
}
53+
54+
public function testNoRetryWhen503AndRetryOnMaintenanceDisabled(): void
55+
{
56+
$mockHandler = new MockHandler([
57+
new Response(503, [], 'Service Unavailable'),
58+
new Response(200, [], 'ok'),
59+
]);
60+
61+
$handlerStack = HandlerStack::create([
62+
'handler' => $mockHandler,
63+
'backoffMaxTries' => 3,
64+
'retryOnMaintenance' => false,
65+
]);
66+
$client = new GuzzleClient(['handler' => $handlerStack, 'http_errors' => false]);
67+
68+
$response = $client->request('GET', 'http://example.com');
69+
self::assertSame(503, $response->getStatusCode());
70+
self::assertSame(1, $mockHandler->count(), 'Second response should not be consumed');
71+
}
72+
73+
public function testRetryWhen503AndRetryOnMaintenanceEnabled(): void
74+
{
75+
$mockHandler = new MockHandler([
76+
new Response(503, [], 'Service Unavailable'),
77+
new Response(200, [], 'ok'),
78+
]);
79+
80+
$handlerStack = HandlerStack::create([
81+
'handler' => $mockHandler,
82+
'backoffMaxTries' => 3,
83+
'retryOnMaintenance' => true,
84+
]);
85+
$client = new GuzzleClient(['handler' => $handlerStack, 'http_errors' => false]);
86+
87+
$response = $client->request('GET', 'http://example.com');
88+
self::assertSame(200, $response->getStatusCode());
89+
self::assertSame(0, $mockHandler->count(), 'Both responses should be consumed');
90+
}
91+
92+
public function testRetryOn5xxErrors(): void
93+
{
94+
$mockHandler = new MockHandler([
95+
new Response(500, [], 'Internal Server Error'),
96+
new Response(200, [], 'ok'),
97+
]);
98+
99+
$handlerStack = HandlerStack::create([
100+
'handler' => $mockHandler,
101+
'backoffMaxTries' => 3,
102+
]);
103+
$client = new GuzzleClient(['handler' => $handlerStack, 'http_errors' => false]);
104+
105+
$response = $client->request('GET', 'http://example.com');
106+
self::assertSame(200, $response->getStatusCode());
107+
self::assertSame(0, $mockHandler->count(), 'Both responses should be consumed');
108+
}
109+
110+
public function testNoRetryWhenMaxRetriesExceeded(): void
111+
{
112+
$mockHandler = new MockHandler([
113+
new Response(500, [], 'Internal Server Error'),
114+
new Response(500, [], 'Internal Server Error'),
115+
new Response(500, [], 'Internal Server Error'),
116+
new Response(200, [], 'ok'),
117+
]);
118+
119+
$handlerStack = HandlerStack::create([
120+
'handler' => $mockHandler,
121+
'backoffMaxTries' => 2,
122+
]);
123+
$client = new GuzzleClient(['handler' => $handlerStack, 'http_errors' => false]);
124+
125+
$response = $client->request('GET', 'http://example.com');
126+
self::assertSame(500, $response->getStatusCode());
127+
self::assertSame(1, $mockHandler->count(), 'Should stop after max retries');
128+
}
129+
130+
public function testRetryOn409VersionConflict(): void
131+
{
132+
$versionConflictBody = json_encode([
133+
'code' => 'storage.components.configurations.versionConflict',
134+
'message' => 'Configuration row creation conflict. Please retry the operation.',
135+
]);
136+
137+
$mockHandler = new MockHandler([
138+
new Response(409, [], $versionConflictBody),
139+
new Response(200, [], 'ok'),
140+
]);
141+
142+
$handlerStack = HandlerStack::create([
143+
'handler' => $mockHandler,
144+
'backoffMaxTries' => 3,
145+
]);
146+
$client = new GuzzleClient(['handler' => $handlerStack, 'http_errors' => false]);
147+
148+
$response = $client->request('GET', 'http://example.com');
149+
self::assertSame(200, $response->getStatusCode());
150+
self::assertSame(0, $mockHandler->count(), 'Both responses should be consumed');
151+
}
152+
153+
public function testNoRetryOn409WithDifferentErrorCode(): void
154+
{
155+
$otherConflictBody = json_encode([
156+
'code' => 'storage.buckets.alreadyExists',
157+
'message' => 'Bucket already exists.',
158+
]);
159+
160+
$mockHandler = new MockHandler([
161+
new Response(409, [], $otherConflictBody),
162+
new Response(200, [], 'ok'),
163+
]);
164+
165+
$handlerStack = HandlerStack::create([
166+
'handler' => $mockHandler,
167+
'backoffMaxTries' => 3,
168+
]);
169+
$client = new GuzzleClient(['handler' => $handlerStack, 'http_errors' => false]);
170+
171+
$response = $client->request('GET', 'http://example.com');
172+
self::assertSame(409, $response->getStatusCode());
173+
self::assertSame(1, $mockHandler->count(), 'Second response should not be consumed');
174+
}
175+
176+
public function testNoRetryOn409WithInvalidJson(): void
177+
{
178+
$mockHandler = new MockHandler([
179+
new Response(409, [], 'not valid json'),
180+
new Response(200, [], 'ok'),
181+
]);
182+
183+
$handlerStack = HandlerStack::create([
184+
'handler' => $mockHandler,
185+
'backoffMaxTries' => 3,
186+
]);
187+
$client = new GuzzleClient(['handler' => $handlerStack, 'http_errors' => false]);
188+
189+
$response = $client->request('GET', 'http://example.com');
190+
self::assertSame(409, $response->getStatusCode());
191+
self::assertSame(1, $mockHandler->count(), 'Second response should not be consumed');
192+
}
193+
194+
public function testNoRetryOn409WithEmptyBody(): void
195+
{
196+
$mockHandler = new MockHandler([
197+
new Response(409, [], ''),
198+
new Response(200, [], 'ok'),
199+
]);
200+
201+
$handlerStack = HandlerStack::create([
202+
'handler' => $mockHandler,
203+
'backoffMaxTries' => 3,
204+
]);
205+
$client = new GuzzleClient(['handler' => $handlerStack, 'http_errors' => false]);
206+
207+
$response = $client->request('GET', 'http://example.com');
208+
self::assertSame(409, $response->getStatusCode());
209+
self::assertSame(1, $mockHandler->count(), 'Second response should not be consumed');
210+
}
211+
212+
public function testNoRetryOn409WithMissingCodeField(): void
213+
{
214+
$bodyWithoutCode = json_encode([
215+
'message' => 'Some conflict error.',
216+
]);
217+
218+
$mockHandler = new MockHandler([
219+
new Response(409, [], $bodyWithoutCode),
220+
new Response(200, [], 'ok'),
221+
]);
222+
223+
$handlerStack = HandlerStack::create([
224+
'handler' => $mockHandler,
225+
'backoffMaxTries' => 3,
226+
]);
227+
$client = new GuzzleClient(['handler' => $handlerStack, 'http_errors' => false]);
228+
229+
$response = $client->request('GET', 'http://example.com');
230+
self::assertSame(409, $response->getStatusCode());
231+
self::assertSame(1, $mockHandler->count(), 'Second response should not be consumed');
232+
}
233+
234+
public function testNoRetryOnSuccessfulResponse(): void
235+
{
236+
$mockHandler = new MockHandler([
237+
new Response(200, [], 'ok'),
238+
new Response(200, [], 'second'),
239+
]);
240+
241+
$handlerStack = HandlerStack::create([
242+
'handler' => $mockHandler,
243+
'backoffMaxTries' => 3,
244+
]);
245+
$client = new GuzzleClient(['handler' => $handlerStack, 'http_errors' => false]);
246+
247+
$response = $client->request('GET', 'http://example.com');
248+
self::assertSame(200, $response->getStatusCode());
249+
self::assertSame('ok', (string) $response->getBody());
250+
self::assertSame(1, $mockHandler->count(), 'Second response should not be consumed');
251+
}
252+
253+
public function testNoRetryOn4xxErrors(): void
254+
{
255+
$mockHandler = new MockHandler([
256+
new Response(400, [], 'Bad Request'),
257+
new Response(200, [], 'ok'),
258+
]);
259+
260+
$handlerStack = HandlerStack::create([
261+
'handler' => $mockHandler,
262+
'backoffMaxTries' => 3,
263+
]);
264+
$client = new GuzzleClient(['handler' => $handlerStack, 'http_errors' => false]);
265+
266+
$response = $client->request('GET', 'http://example.com');
267+
self::assertSame(400, $response->getStatusCode());
268+
self::assertSame(1, $mockHandler->count(), 'Second response should not be consumed');
269+
}
270+
271+
public function testDefaultBackoffMaxTriesIsZero(): void
272+
{
273+
$mockHandler = new MockHandler([
274+
new Response(500, [], 'Internal Server Error'),
275+
new Response(200, [], 'ok'),
276+
]);
277+
278+
$handlerStack = HandlerStack::create([
279+
'handler' => $mockHandler,
280+
]);
281+
$client = new GuzzleClient(['handler' => $handlerStack, 'http_errors' => false]);
282+
283+
$response = $client->request('GET', 'http://example.com');
284+
self::assertSame(500, $response->getStatusCode());
285+
self::assertSame(1, $mockHandler->count(), 'Should not retry when backoffMaxTries is 0');
286+
}
287+
288+
public function testRetryOn409VersionConflictRespectsMaxRetries(): void
289+
{
290+
$versionConflictBody = json_encode([
291+
'code' => 'storage.components.configurations.versionConflict',
292+
'message' => 'Configuration row creation conflict. Please retry the operation.',
293+
]);
294+
295+
$mockHandler = new MockHandler([
296+
new Response(409, [], $versionConflictBody),
297+
new Response(409, [], $versionConflictBody),
298+
new Response(409, [], $versionConflictBody),
299+
new Response(200, [], 'ok'),
300+
]);
301+
302+
$handlerStack = HandlerStack::create([
303+
'handler' => $mockHandler,
304+
'backoffMaxTries' => 2,
305+
]);
306+
$client = new GuzzleClient(['handler' => $handlerStack, 'http_errors' => false]);
307+
308+
$response = $client->request('GET', 'http://example.com');
309+
self::assertSame(409, $response->getStatusCode());
310+
self::assertSame(1, $mockHandler->count(), 'Should stop after max retries');
311+
}
312+
}

0 commit comments

Comments
 (0)