Skip to content

Commit e238098

Browse files
committed
Needs review
1 parent 56e28be commit e238098

1 file changed

Lines changed: 115 additions & 3 deletions

File tree

tests/HttpClient/CircuitBreakerHttpClientTest.php

Lines changed: 115 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
namespace Bizkit\CircuitBreakerBundle\Tests\HttpClient;
66

7+
use Bizkit\CircuitBreakerBundle\FailureChecker\DefaultFailureChecker;
8+
use Bizkit\CircuitBreakerBundle\FailureChecker\FailureCheckerInterface;
79
use Bizkit\CircuitBreakerBundle\HttpClient\CircuitBreakerHttpClient;
810
use GabrielAnhaia\PhpCircuitBreaker\CircuitBreaker;
911
use GabrielAnhaia\PhpCircuitBreaker\CircuitBreakerConfig;
@@ -17,7 +19,9 @@
1719
use Symfony\Component\HttpClient\Exception\ServerException;
1820
use Symfony\Component\HttpClient\Exception\TransportException;
1921
use Symfony\Component\HttpClient\MockHttpClient;
22+
use Symfony\Component\HttpClient\Response\AsyncContext;
2023
use Symfony\Component\HttpClient\Response\MockResponse;
24+
use Symfony\Contracts\HttpClient\ChunkInterface;
2125

2226
#[CoversClass(CircuitBreakerHttpClient::class)]
2327
final class CircuitBreakerHttpClientTest extends TestCase
@@ -35,6 +39,7 @@ public function testRecordsSuccessForSuccessfulResponseAfterBodyCompletes(array
3539
$client = new CircuitBreakerHttpClient(
3640
new MockHttpClient(new MockResponse('ok', ['http_code' => 200])),
3741
$circuitBreaker,
42+
new DefaultFailureChecker(),
3843
'api',
3944
);
4045

@@ -54,6 +59,7 @@ public function testRecordsSuccessForClientErrorResponseAfterBodyCompletes(): vo
5459
$client = new CircuitBreakerHttpClient(
5560
new MockHttpClient(new MockResponse('not found', ['http_code' => 404])),
5661
$circuitBreaker,
62+
new DefaultFailureChecker(),
5763
'api',
5864
);
5965

@@ -77,20 +83,98 @@ public function testRecordsFailureForServerErrorResponse(array $options, string
7783
$client = new CircuitBreakerHttpClient(
7884
new MockHttpClient(new MockResponse('', ['http_code' => 500])),
7985
$circuitBreaker,
86+
new DefaultFailureChecker(),
8087
'api',
8188
);
8289

8390
self::assertSame(500, $client->request('GET', 'https://example.com', $options)->getStatusCode());
8491
self::assertSame(CircuitState::OPEN, $circuitBreaker->getState($serviceName));
8592
}
8693

94+
public function testInjectedFailureCheckerCanRecordClientErrorAsFailure(): void
95+
{
96+
$storage = new InMemoryStorage();
97+
$circuitBreaker = new CircuitBreaker($storage, new CircuitBreakerConfig(failureThreshold: 1));
98+
$failureChecker = new class implements FailureCheckerInterface {
99+
public function __invoke(ChunkInterface $chunk, AsyncContext $context, string $serviceName): bool
100+
{
101+
return $chunk->isFirst() && $context->getStatusCode() >= 400;
102+
}
103+
};
104+
$client = new CircuitBreakerHttpClient(
105+
new MockHttpClient(new MockResponse('', ['http_code' => 404])),
106+
$circuitBreaker,
107+
$failureChecker,
108+
'api',
109+
);
110+
111+
self::assertSame(404, $client->request('GET', 'https://example.com')->getStatusCode());
112+
self::assertSame(CircuitState::OPEN, $circuitBreaker->getState('api'));
113+
}
114+
115+
public function testInjectedFailureCheckerCanIgnoreServerError(): void
116+
{
117+
$storage = new InMemoryStorage();
118+
$storage->setHalfOpen('api', 30);
119+
$circuitBreaker = new CircuitBreaker($storage, new CircuitBreakerConfig(failureThreshold: 1));
120+
$failureChecker = new class implements FailureCheckerInterface {
121+
public function __invoke(ChunkInterface $chunk, AsyncContext $context, string $serviceName): bool
122+
{
123+
return false;
124+
}
125+
};
126+
$client = new CircuitBreakerHttpClient(
127+
new MockHttpClient(new MockResponse('server error', ['http_code' => 500])),
128+
$circuitBreaker,
129+
$failureChecker,
130+
'api',
131+
);
132+
133+
$response = $client->request('GET', 'https://example.com');
134+
135+
self::assertSame(500, $response->getStatusCode());
136+
self::assertSame(CircuitState::HALF_OPEN, $circuitBreaker->getState('api'));
137+
self::assertSame('server error', $response->getContent(false));
138+
self::assertSame(CircuitState::CLOSED, $circuitBreaker->getState('api'));
139+
}
140+
141+
public function testRequestFailureCheckerOverridesInjectedFailureChecker(): void
142+
{
143+
$storage = new InMemoryStorage();
144+
$circuitBreaker = new CircuitBreaker($storage, new CircuitBreakerConfig(failureThreshold: 1));
145+
$failureChecker = new class implements FailureCheckerInterface {
146+
public function __invoke(ChunkInterface $chunk, AsyncContext $context, string $serviceName): bool
147+
{
148+
return false;
149+
}
150+
};
151+
$client = new CircuitBreakerHttpClient(
152+
new MockHttpClient(new MockResponse('', ['http_code' => 404])),
153+
$circuitBreaker,
154+
$failureChecker,
155+
'api',
156+
);
157+
158+
$response = $client->request('GET', 'https://example.com', [
159+
'extra' => [
160+
'circuit_breaker' => [
161+
'failure_checker' => static fn (ChunkInterface $chunk, AsyncContext $context, string $serviceName): bool => $chunk->isFirst() && 404 === $context->getStatusCode(),
162+
],
163+
],
164+
]);
165+
166+
self::assertSame(404, $response->getStatusCode());
167+
self::assertSame(CircuitState::OPEN, $circuitBreaker->getState('api'));
168+
}
169+
87170
public function testRecordsFailureForServerErrorResponseWhenStreaming(): void
88171
{
89172
$storage = new InMemoryStorage();
90173
$circuitBreaker = new CircuitBreaker($storage, new CircuitBreakerConfig(failureThreshold: 1));
91174
$client = new CircuitBreakerHttpClient(
92175
new MockHttpClient(new MockResponse('error', ['http_code' => 500])),
93176
$circuitBreaker,
177+
new DefaultFailureChecker(),
94178
'api',
95179
);
96180
$response = $client->request('GET', 'https://example.com');
@@ -115,6 +199,7 @@ public function testRecordsFailureForTransportException(MockResponse $response):
115199
$client = new CircuitBreakerHttpClient(
116200
new MockHttpClient($response),
117201
$circuitBreaker,
202+
new DefaultFailureChecker(),
118203
'api',
119204
);
120205

@@ -134,6 +219,7 @@ public function testRecordsFailureOnlyOnceWhenServerErrorResponseAlsoFailsWhileS
134219
$client = new CircuitBreakerHttpClient(
135220
new MockHttpClient(new MockResponse([new TransportException('Network failure')], ['http_code' => 500])),
136221
$circuitBreaker,
222+
new DefaultFailureChecker(),
137223
'api',
138224
);
139225

@@ -158,7 +244,7 @@ public function testReturnsSyntheticServiceUnavailableResponseWithoutCallingDeco
158244
$circuitBreaker->forceState($serviceName, CircuitState::OPEN);
159245
$innerClient = new MockHttpClient();
160246

161-
$client = new CircuitBreakerHttpClient($innerClient, $circuitBreaker, 'api');
247+
$client = new CircuitBreakerHttpClient($innerClient, $circuitBreaker, new DefaultFailureChecker(), 'api');
162248
$response = $client->request('GET', 'https://example.com', $options);
163249

164250
self::assertSame(503, $response->getStatusCode());
@@ -174,7 +260,7 @@ public function testThrowsWhenCircuitIsOpenAndExceptionsAreEnabled(): void
174260
$circuitBreaker = new CircuitBreaker($storage, new CircuitBreakerConfig(exceptionsEnabled: true));
175261
$circuitBreaker->forceState('api', CircuitState::OPEN);
176262

177-
$client = new CircuitBreakerHttpClient(new MockHttpClient(), $circuitBreaker, 'api');
263+
$client = new CircuitBreakerHttpClient(new MockHttpClient(), $circuitBreaker, new DefaultFailureChecker(), 'api');
178264

179265
$this->expectException(OpenCircuitException::class);
180266

@@ -187,7 +273,7 @@ public function testResetResetsDecoratedClientWithoutClearingCircuitStorage(): v
187273
$circuitBreaker = new CircuitBreaker($storage, new CircuitBreakerConfig(failureThreshold: 1));
188274
$innerClient = new MockHttpClient(new MockResponse());
189275

190-
$client = new CircuitBreakerHttpClient($innerClient, $circuitBreaker, 'api');
276+
$client = new CircuitBreakerHttpClient($innerClient, $circuitBreaker, new DefaultFailureChecker(), 'api');
191277
$client->request('GET', 'https://example.com')->getStatusCode();
192278
self::assertSame(1, $innerClient->getRequestsCount());
193279

@@ -198,6 +284,31 @@ public function testResetResetsDecoratedClientWithoutClearingCircuitStorage(): v
198284
self::assertSame(0, $innerClient->getRequestsCount());
199285
}
200286

287+
public function testRejectsInvalidRequestFailureChecker(): void
288+
{
289+
$storage = new InMemoryStorage();
290+
$client = new CircuitBreakerHttpClient(
291+
new MockHttpClient(new MockResponse('ok')),
292+
new CircuitBreaker($storage),
293+
new DefaultFailureChecker(),
294+
'api',
295+
);
296+
297+
/** @var array<string, mixed> $options */
298+
$options = [
299+
'extra' => [
300+
'circuit_breaker' => [
301+
'failure_checker' => 'not_a_function',
302+
],
303+
],
304+
];
305+
306+
$this->expectException(InvalidArgumentException::class);
307+
$this->expectExceptionMessage('Option "extra.circuit_breaker.failure_checker" must be callable.');
308+
309+
$client->request('GET', 'https://example.com', $options);
310+
}
311+
201312
/**
202313
* @param array<string, mixed> $options
203314
*/
@@ -209,6 +320,7 @@ public function testRejectsInvalidOverriddenServiceName(array $options): void
209320
$client = new CircuitBreakerHttpClient(
210321
new MockHttpClient(new MockResponse('ok')),
211322
new CircuitBreaker($storage),
323+
new DefaultFailureChecker(),
212324
'api',
213325
);
214326

0 commit comments

Comments
 (0)