Passed
Push — main ( 6a339e...523722 )
by Eric
02:31
created

testClientWithRetriesRetryAfterHeaderServerError()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 10
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 16
rs 9.9332
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Esi\Api - A simple wrapper/builder using Guzzle for base API clients.
7
 *
8
 * @author    Eric Sizemore <[email protected]>
9
 * @version   1.0.0
10
 * @copyright (C) 2024 Eric Sizemore
11
 * @license   The MIT License (MIT)
12
 *
13
 * Copyright (C) 2024 Eric Sizemore <https://www.secondversion.com/>.
14
 *
15
 * Permission is hereby granted, free of charge, to any person obtaining a copy
16
 * of this software and associated documentation files (the "Software"), to
17
 * deal in the Software without restriction, including without limitation the
18
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
19
 * sell copies of the Software, and to permit persons to whom the Software is
20
 * furnished to do so, subject to the following conditions:
21
 *
22
 * The above copyright notice and this permission notice shall be included in
23
 * all copies or substantial portions of the Software.
24
 *
25
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
26
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
27
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
28
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
29
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
30
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
31
 * THE SOFTWARE.
32
 */
33
34
namespace Esi\Api\Tests;
35
36
use Esi\Api\Client;
37
use Esi\Api\Utils;
38
use Esi\Api\Exceptions\RateLimitExceededException;
39
use GuzzleHttp\Exception\ClientException;
40
use GuzzleHttp\Exception\ServerException;
41
use GuzzleHttp\Psr7\Response;
42
use InvalidArgumentException;
43
use PHPUnit\Framework\Attributes\CoversClass;
44
use PHPUnit\Framework\TestCase;
45
use ReflectionClass;
46
use RuntimeException;
47
use Symfony\Component\Filesystem\Filesystem;
48
use Symfony\Component\Filesystem\Path;
49
50
use function is_dir;
51
use function json_decode;
52
use function sys_get_temp_dir;
53
54
use const DIRECTORY_SEPARATOR;
55
56
use function serialize;
57
58
/**
59
 * Client Tests.
60
 *
61
 * These tests are... a mess. Needs some cleaning up.
62
 *
63
 * @todo I do not like testing against a live service. Though, in this case, it uses https://httpbin.org instead of
64
 *       an actual live API. Would likely be best to just do everything through Mocks.
65
 */
66
#[CoversClass(Client::class)]
67
#[CoversClass(Utils::class)]
68
final class ClientTest extends TestCase
69
{
70
    private static string $cacheDir;
71
72
    #[\Override]
73
    public static function setUpBeforeClass(): void
74
    {
75
        $filesystem = new FileSystem();
76
77
        self::$cacheDir = Path::normalize(__DIR__ . DIRECTORY_SEPARATOR . 'tmpCache');
78
79
        if (!is_dir(self::$cacheDir)) {
80
            $filesystem->mkdir(self::$cacheDir);
81
        }
82
    }
83
84
    #[\Override]
85
    public static function tearDownAfterClass(): void
86
    {
87
        $filesystem = new FileSystem();
88
89
        $filesystem->remove(self::$cacheDir);
90
91
        self::$cacheDir = '';
92
    }
93
94
    private function buildTestClient(string | bool | null ...$params): Client
95
    {
96
        static $called;
97
98
        // @phpstan-ignore-next-line
99
        return $called[serialize($params)] ??= new Client(...$params);
0 ignored issues
show
Bug introduced by
It seems like $params can also be of type boolean and null; however, parameter $apiUrl of Esi\Api\Client::__construct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

99
        return $called[serialize($params)] ??= new Client(/** @scrutinizer ignore-type */ ...$params);
Loading history...
100
    }
101
102
    public function testClientWithApiQuery(): void
103
    {
104
        $client = $this->buildTestClient('https://httpbin.org', 'test', sys_get_temp_dir(), true, 'api_key');
105
106
        $client->build(['persistentHeaders' => ['Accept' => 'application/json']]);
107
108
        $response = $client->send('GET', '/get', ['query' => ['foo' => 'bar']]);
109
110
        self::assertSame(200, $response->getStatusCode());
111
112
        $data = json_decode($response->getBody()->getContents(), true);
113
        self::assertNotEmpty($data);
114
        self::assertArrayHasKey('args', $data); // @phpstan-ignore-line
115
        self::assertSame(['api_key' => 'test', 'foo' => 'bar'], $data['args']);
116
    }
117
118
    public function testClientWithApiParamsAsHeaderNoQuery(): void
119
    {
120
        $client = $this->buildTestClient('https://httpbin.org', 'test', sys_get_temp_dir());
121
122
        $client->build([
123
            'persistentHeaders' => [
124
                'Accept'        => 'application/json',
125
                'Client-ID'     => 'apiKey',
126
                'Authorization' => 'someAccessToken',
127
128
        ]]);
129
130
        $response = $client->send('GET', '/anything');
131
132
        self::assertSame(200, $response->getStatusCode());
133
134
        /** @var array<string, array{}|string|null> $data **/
135
        $data = json_decode($response->getBody()->getContents(), true);
136
        self::assertNotEmpty($data);
137
138
        self::assertNotEmpty($data['headers']);
139
        self::assertArrayHasKey('Accept', $data['headers']); // @phpstan-ignore-line
140
        self::assertArrayHasKey('Client-Id', $data['headers']);
141
        self::assertArrayHasKey('Authorization', $data['headers']);
142
        self::assertSame('application/json', $data['headers']['Accept']);
143
        self::assertSame('apiKey', $data['headers']['Client-Id']);
144
        self::assertSame('someAccessToken', $data['headers']['Authorization']);
145
    }
146
147
    public function testClientWithApiParamsAsHeaderWithQuery(): void
148
    {
149
        $client = $this->buildTestClient('https://httpbin.org', 'test', sys_get_temp_dir());
150
151
        $client->build([
152
            'persistentHeaders' => [
153
                'Accept'        => 'application/json',
154
                'Client-ID'     => 'apiKey',
155
                'Authorization' => 'someAccessToken',
156
157
        ]]);
158
159
        $response = $client->send('GET', '/anything', ['query' => ['foo' => 'bar']]);
160
161
        self::assertSame(200, $response->getStatusCode());
162
163
        /** @var array<string, array{}|string|null> $data **/
164
        $data = json_decode($response->getBody()->getContents(), true);
165
        self::assertNotEmpty($data);
166
167
        self::assertNotEmpty($data['headers']);
168
        self::assertArrayHasKey('Accept', $data['headers']); // @phpstan-ignore-line
169
        self::assertArrayHasKey('Client-Id', $data['headers']);
170
        self::assertArrayHasKey('Authorization', $data['headers']);
171
        self::assertSame('application/json', $data['headers']['Accept']);
172
        self::assertSame('apiKey', $data['headers']['Client-Id']);
173
        self::assertSame('someAccessToken', $data['headers']['Authorization']);
174
175
        self::assertNotEmpty($data['args']);
176
        self::assertArrayHasKey('args', $data); // @phpstan-ignore-line
177
        self::assertSame(['foo' => 'bar'], $data['args']);
178
179
        //
180
        $client->build([
181
            'persistentHeaders' => [
182
                'Accept'        => 'application/json',
183
                'Client-ID'     => 'anotherApiKey',
184
                'Authorization' => 'someOtherAccessToken',
185
186
        ]]);
187
188
        $response = $client->send('GET', '/anything', ['query' => ['foo' => 'bar']]);
189
190
        self::assertSame(200, $response->getStatusCode());
191
192
        /** @var array<string, array{}|string|null> $data **/
193
        $data = json_decode($response->getBody()->getContents(), true);
194
        self::assertNotEmpty($data);
195
196
        self::assertNotEmpty($data['headers']);
197
        self::assertArrayHasKey('Accept', $data['headers']); // @phpstan-ignore-line
198
        self::assertArrayHasKey('Client-Id', $data['headers']);
199
        self::assertArrayHasKey('Authorization', $data['headers']);
200
        self::assertSame('application/json', $data['headers']['Accept']);
201
        self::assertSame('anotherApiKey', $data['headers']['Client-Id']);
202
        self::assertSame('someOtherAccessToken', $data['headers']['Authorization']);
203
204
        self::assertNotEmpty($data['args']);
205
        self::assertArrayHasKey('args', $data); // @phpstan-ignore-line
206
        self::assertSame(['foo' => 'bar'], $data['args']);
207
    }
208
209
    public function testClientWithRetries(): void
210
    {
211
        $client = $this->buildTestClient('https://httpbin.org', 'test', sys_get_temp_dir(), true, 'api_key');
212
        $client->enableRetryAttempts();
213
        $client->setMaxRetryAttempts(1);
214
215
        $client->build(['persistentHeaders' => ['Accept' => 'application/json']]);
216
217
        $this->expectException(ServerException::class);
218
        $response = $client->send('GET', '/status/500', ['query' => ['foo' => 'bar']]);
219
220
        self::assertSame(500, $response->getStatusCode());
221
222
        $reflectionClass = new ReflectionClass($client::class);
223
        $retryCalls      = $reflectionClass->getProperty('retryCalls')->getValue($client);
224
        self::assertSame(1, $retryCalls);
225
    }
226
227
    public function testClientWithRetriesRetryAfterHeaderRateLimited(): void
228
    {
229
        $client = $this->buildTestClient('https://esiapi.free.beeceptor.com/', 'test', sys_get_temp_dir());
230
        $client->enableRetryAttempts();
231
        $client->setMaxRetryAttempts(1);
232
233
        $client->build(['persistentHeaders' => ['Accept' => 'application/json'],]);
234
235
        $this->expectException(RateLimitExceededException::class);
236
        $response = $client->send('GET', '429');
237
238
        self::assertSame(429, $response->getStatusCode());
239
240
        $reflectionClass = new ReflectionClass($client::class);
241
        $retryCalls      = $reflectionClass->getProperty('retryCalls')->getValue($client);
242
        self::assertSame(1, $retryCalls);
243
    }
244
245
    public function testClientWithRetriesRetryAfterHeaderServerError(): void
246
    {
247
        $client = $this->buildTestClient('https://esiapi.free.beeceptor.com/', 'test', sys_get_temp_dir());
248
        $client->enableRetryAttempts();
249
        $client->setMaxRetryAttempts(1);
250
251
        $client->build(['persistentHeaders' => ['Accept' => 'application/json'],]);
252
253
        $this->expectException(ServerException::class);
254
        $response = $client->send('GET', '500');
255
256
        self::assertSame(500, $response->getStatusCode());
257
258
        $reflectionClass = new ReflectionClass($client::class);
259
        $retryCalls      = $reflectionClass->getProperty('retryCalls')->getValue($client);
260
        self::assertSame(1, $retryCalls);
261
    }
262
263
    public function testEnableRetryAttempts(): void
264
    {
265
        $client = $this->buildTestClient('https://httpbin.org', 'test', sys_get_temp_dir(), true, 'api_key');
266
        $client->enableRetryAttempts();
267
268
        $reflectionClass = new ReflectionClass($client::class);
269
        $attemptRetry    = $reflectionClass->getProperty('attemptRetry')->getValue($client);
270
        self::assertTrue($attemptRetry);
271
    }
272
273
    public function testDisableRetryAttempts(): void
274
    {
275
        $client = $this->buildTestClient('https://httpbin.org', 'test', sys_get_temp_dir(), true, 'api_key');
276
        $client->disableRetryAttempts();
277
278
        $reflectionClass = new ReflectionClass($client::class);
279
        $attemptRetry    = $reflectionClass->getProperty('attemptRetry')->getValue($client);
280
        self::assertFalse($attemptRetry);
281
    }
282
283
    public function testSetMaxRetryAttempts(): void
284
    {
285
        $client = $this->buildTestClient('https://httpbin.org', 'test', sys_get_temp_dir(), true, 'api_key');
286
        $client->setMaxRetryAttempts(10);
287
288
        $reflectionClass = new ReflectionClass($client::class);
289
        $maxRetries      = $reflectionClass->getProperty('maxRetries')->getValue($client);
290
        self::assertSame(10, $maxRetries);
291
    }
292
293
    public function testClientApiUrlEmptyString(): void
294
    {
295
        $this->expectException(InvalidArgumentException::class);
296
        $this->buildTestClient('', 'test', sys_get_temp_dir());
297
    }
298
299
    public function testClientApiKeyEmptyString(): void
300
    {
301
        $this->expectException(InvalidArgumentException::class);
302
        $this->buildTestClient('https://httpbin.org', '', sys_get_temp_dir());
303
    }
304
305
    public function testClientApiKeyNull(): void
306
    {
307
        $this->expectException(InvalidArgumentException::class);
308
        $this->buildTestClient('https://httpbin.org', null, sys_get_temp_dir());
309
    }
310
311
    public function testClientPersistentHeadersInvalid(): void
312
    {
313
        $client = $this->buildTestClient('https://httpbin.org', 'test', sys_get_temp_dir());
314
        $this->expectException(InvalidArgumentException::class);
315
        $client->build(['persistentHeaders' => ['Accept', 'application/json']]);
316
    }
317
318
    public function testClientBuildExtraOptions(): void
319
    {
320
        $client = $this->buildTestClient('https://httpbin.org', 'test', sys_get_temp_dir());
321
        $client->build(['persistentHeaders' => ['Accept' => 'application/json'], 'timeout' => '5.0']);
322
323
        $response = $client->send('GET', '/get', ['query' => ['foo' => 'bar']]);
324
325
        self::assertSame(200, $response->getStatusCode());
326
327
        $data = json_decode($response->getBody()->getContents(), true);
328
        self::assertNotEmpty($data);
329
        self::assertArrayHasKey('args', $data); // @phpstan-ignore-line
330
        self::assertSame(['foo' => 'bar'], $data['args']);
331
    }
332
333
    public function testClientBuildAndSend(): void
334
    {
335
        $client   = $this->buildTestClient('https://httpbin.org', 'test', sys_get_temp_dir());
336
        $response = $client->buildAndSend('GET', '/get', [
337
            'persistentHeaders' => ['Accept' => 'application/json'],
338
            'timeout'           => '5.0',
339
            'query'             => ['foo' => 'bar'],
340
        ]);
341
342
        self::assertSame(200, $response->getStatusCode());
343
344
        $data = json_decode($response->getBody()->getContents(), true);
345
        self::assertNotEmpty($data);
346
        self::assertArrayHasKey('args', $data); // @phpstan-ignore-line
347
        self::assertSame(['foo' => 'bar'], $data['args']);
348
    }
349
350
    public function testClientBuildNoOptionsNoQueryWithApi(): void
351
    {
352
        $client = $this->buildTestClient('https://httpbin.org', 'test', sys_get_temp_dir(), true, 'api_key');
353
        $client->build();
354
355
        $response = $client->send('GET', '/get');
356
357
        self::assertSame(200, $response->getStatusCode());
358
359
        $data = json_decode($response->getBody()->getContents(), true);
360
        self::assertNotEmpty($data);
361
        self::assertArrayHasKey('args', $data); // @phpstan-ignore-line
362
    }
363
364
    public function testClientBuildNoOptionsNoQueryWithoutApi(): void
365
    {
366
        $client = $this->buildTestClient('https://httpbin.org', 'test', sys_get_temp_dir());
367
        $client->build();
368
369
        $response = $client->send('GET', '/get');
370
371
        self::assertSame(200, $response->getStatusCode());
372
373
        $data = json_decode($response->getBody()->getContents(), true);
374
        self::assertNotEmpty($data);
375
        self::assertArrayHasKey('args', $data); // @phpstan-ignore-line
376
        self::assertEmpty($data['args']);
377
    }
378
379
    public function testClientSendInvalidMethod(): void
380
    {
381
        $client = $this->buildTestClient('https://httpbin.org', 'test', sys_get_temp_dir());
382
        $client->build(['persistentHeaders' => ['Accept' => 'application/json'], 'timeout' => '5.0']);
383
384
        $this->expectException(InvalidArgumentException::class);
385
        $client->send('INVALID', '/get', ['query' => ['foo' => 'bar']]);
386
    }
387
388
    public function testClientSendWithoutBuildFirst(): void
389
    {
390
        $client = $this->buildTestClient('https://httpbin.org', 'testnobuild', sys_get_temp_dir());
391
        $this->expectException(RuntimeException::class);
392
        $client->send('GET', '/get', ['query' => ['foo' => 'bar']]);
393
    }
394
395
    public function testRateLimitExceeded(): void
396
    {
397
        $client = $this->buildTestClient('https://httpbin.org', 'test', sys_get_temp_dir());
398
        $client->build(['persistentHeaders' => ['Accept' => 'application/json'], 'timeout' => '5.0']);
399
400
        $this->expectException(RateLimitExceededException::class);
401
        $client->send('GET', '/status/429');
402
    }
403
404
    public function testClientError(): void
405
    {
406
        $client = $this->buildTestClient('https://httpbin.org', 'test', sys_get_temp_dir());
407
        $client->build(['persistentHeaders' => ['Accept' => 'application/json'], 'timeout' => '5.0']);
408
409
        $this->expectException(ClientException::class);
410
        $client->send('GET', '/status/404');
411
    }
412
413
    public function testClientBuildWithInvalidOption(): void
414
    {
415
        $client = $this->buildTestClient('https://httpbin.org', 'test', sys_get_temp_dir());
416
        $this->expectException(InvalidArgumentException::class);
417
        $client->build(['persistentHeaders' => ['Accept' => 'application/json'], 'timut' => '5.0']);
418
    }
419
420
    public function testClientWithExtraQueryHeaders(): void
421
    {
422
        $client = $this->buildTestClient('https://httpbin.org', 'test', sys_get_temp_dir(), true, 'api_key');
423
        $client->build([
424
            'persistentHeaders' => ['Accept' => 'application/json'],
425
        ]);
426
        $response = $client->send('GET', '/get', ['query' => ['foo' => 'bar']]);
427
428
        self::assertSame(200, $response->getStatusCode());
429
430
        $data = json_decode($response->getBody()->getContents(), true);
431
        self::assertNotEmpty($data);
432
        self::assertArrayHasKey('args', $data); // @phpstan-ignore-line
433
        self::assertSame(['api_key' => 'test', 'foo' => 'bar'], $data['args']);
434
    }
435
436
    public function testClientWithQueryInBuildOptions(): void
437
    {
438
        $client = $this->buildTestClient('https://httpbin.org', 'test', sys_get_temp_dir(), true, 'api_key');
439
440
        $this->expectException(InvalidArgumentException::class);
441
        $client->build([
442
            'apiParamName'      => 'api_key',
443
            'apiRequiresQuery'  => true,
444
            'persistentHeaders' => ['Accept' => 'application/json'],
445
            'query'             => ['foo' => 'bar'],
446
        ]);
447
    }
448
449
    public function testParseWithJsonTraitRaw(): void
450
    {
451
        $client = $this->buildTestClient('https://httpbin.org', 'test', sys_get_temp_dir(), true, 'api_key');
452
        $client->build([
453
            'persistentHeaders' => ['Accept' => 'application/json'],
454
        ]);
455
        $response = $client->send('GET', '/get', ['query' => ['foo' => 'bar']]);
456
457
        self::assertSame(200, $response->getStatusCode());
458
        self::assertInstanceOf(Response::class, $response);
459
        self::assertNotEmpty($client->raw($response));
460
    }
461
462
    public function testParseWithJsonTraitToArray(): void
463
    {
464
        $client = $this->buildTestClient('https://httpbin.org', 'test', sys_get_temp_dir(), true, 'api_key');
465
        $client->build([
466
            'persistentHeaders' => ['Accept' => 'application/json'],
467
        ]);
468
        $response = $client->send('GET', '/get', ['query' => ['foo' => 'bar']]);
469
470
        self::assertSame(200, $response->getStatusCode());
471
        self::assertInstanceOf(Response::class, $response);
472
473
        $array = $client->toArray($response);
474
        self::assertArrayHasKey('args', $array);
475
        self::assertArrayHasKey('headers', $array);
476
        self::assertArrayHasKey('origin', $array);
477
        self::assertArrayHasKey('url', $array);
478
    }
479
480
    public function testParseWithJsonTraitToObject(): void
481
    {
482
        $client = $this->buildTestClient('https://httpbin.org', 'test', sys_get_temp_dir(), true, 'api_key');
483
        $client->build([
484
            'persistentHeaders' => ['Accept' => 'application/json'],
485
        ]);
486
        $response = $client->send('GET', '/get', ['query' => ['foo' => 'bar']]);
487
488
        self::assertSame(200, $response->getStatusCode());
489
        self::assertInstanceOf(Response::class, $response);
490
491
        $object = $client->toObject($response);
492
        self::assertObjectHasProperty('args', $object);
493
        self::assertObjectHasProperty('headers', $object);
494
        self::assertObjectHasProperty('origin', $object);
495
        self::assertObjectHasProperty('url', $object);
496
    }
497
}
498