Passed
Push — master ( 8a3a90...ff3ac2 )
by Eric
02:15
created

LibrariesIOTest::dataCachePathProvider()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 1
eloc 3
c 1
b 1
f 0
nc 1
nop 0
dl 0
loc 5
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * This file is part of Esi\LibrariesIO.
7
 *
8
 * (c) 2023-2024 Eric Sizemore <https://github.com/ericsizemore>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13
14
namespace Esi\LibrariesIO\Tests;
15
16
use Esi\LibrariesIO\AbstractClient;
17
use Esi\LibrariesIO\Exception\InvalidApiKeyException;
18
use Esi\LibrariesIO\Exception\InvalidEndpointException;
19
use Esi\LibrariesIO\Exception\InvalidEndpointOptionsException;
20
use Esi\LibrariesIO\Exception\RateLimitExceededException;
21
use Esi\LibrariesIO\LibrariesIO;
22
use Esi\LibrariesIO\Utils;
23
use GuzzleHttp\Exception\ClientException;
24
use GuzzleHttp\Handler\MockHandler;
25
use GuzzleHttp\Psr7\Request;
26
use GuzzleHttp\Psr7\Response;
27
use Iterator;
28
use PHPUnit\Framework\Attributes\CoversClass;
29
use PHPUnit\Framework\Attributes\DataProvider;
30
use PHPUnit\Framework\Attributes\TestDox;
31
use PHPUnit\Framework\MockObject\MockObject;
32
use PHPUnit\Framework\TestCase;
33
use stdClass;
34
35
use function md5;
36
use function sys_get_temp_dir;
37
38
/**
39
 * LibrariesIO Tests.
40
 *
41
 * @internal
42
 *
43
 * @psalm-internal Esi\LibrariesIO\Tests
44
 *
45
 * @todo Split tests into *Class*Test
46
 */
47
#[CoversClass(LibrariesIO::class)]
48
#[CoversClass(AbstractClient::class)]
49
#[CoversClass(Utils::class)]
50
#[CoversClass(RateLimitExceededException::class)]
51
final class LibrariesIOTest extends TestCase
52
{
53
    /**
54
     * @var array<string, ClientException|Response>
55
     */
56
    private array $responses;
57
58
    private string $testApiKey;
59
60
    #[\Override]
61
    protected function setUp(): void
62
    {
63
        $rateLimitHeaders = [
64
            'X-RateLimit-Limit'     => '60',
65
            'X-RateLimit-Remaining' => '0',
66
            'X-RateLimit-Reset'     => '',
67
        ];
68
69
        $this->testApiKey = md5('test');
70
71
        $this->responses = [
72
            'valid'       => new Response(200, body: '{"Hello":"World"}'),
73
            'clientError' => new ClientException('Error Communicating with Server', new Request('GET', 'test'), new Response(202, ['X-Foo' => 'Bar'])),
74
            'rateLimit'   => new ClientException('Rate Limit Exceeded', new Request('GET', 'test'), new Response(429, $rateLimitHeaders)),
75
            'rateLimiter' => new Response(429, $rateLimitHeaders, 'Rate Limit Exceeded'),
76
        ];
77
    }
78
79
    #[TestDox('Client error throws a Guzzle ClientException')]
80
    public function testClientError(): void
81
    {
82
        $mockHandler = new MockHandler([$this->responses['clientError']]);
83
        $mockClient  = $this->mockClient($this->testApiKey, $mockHandler);
84
        $this->expectException(ClientException::class);
85
        $mockClient->platform();
86
    }
87
88
    #[TestDox('Providing an invalid API key results in an InvalidApiKeyException')]
89
    public function testInvalidApiKey(): void
90
    {
91
        $this->expectException(InvalidApiKeyException::class);
92
        $this->mockClient('notvalid');
93
    }
94
95
    #[DataProvider('dataEndpointProvider')]
96
    public function testNormalizeEndpoint(string $expected, string $endpoint, string $apiUrl): void
97
    {
98
        self::assertSame($expected, Utils::normalizeEndpoint($endpoint, $apiUrl));
99
    }
100
101
    #[DataProvider('dataMethodProvider')]
102
    public function testNormalizeMethod(string $expected, string $method): void
103
    {
104
        self::assertSame($expected, Utils::normalizeMethod($method));
105
    }
106
107
    #[TestDox('LibrariesIO::platform() returns expected response')]
108
    public function testPlatform(): void
109
    {
110
        $mockHandler = new MockHandler([$this->responses['valid']]);
111
        $mockClient  = $this->mockClient($this->testApiKey, $mockHandler);
112
        $response    = $mockClient->platform();
113
114
        self::assertInstanceOf(Response::class, $response);
115
        self::assertSame('{"Hello":"World"}', $response->getBody()->getContents());
116
    }
117
118
    /**
119
     * @param array<string, int|string> $options
120
     */
121
    #[DataProvider('dataProjectProvider')]
122
    #[TestDox('LibrariesIO::project() returns expected response')]
123
    public function testProject(string $expected, string $endpoint, array $options): void
124
    {
125
        $mockHandler = new MockHandler([$this->responses['valid']]);
126
        $mockClient  = $this->mockClient($this->testApiKey, $mockHandler);
127
        $response    = $mockClient->project($endpoint, $options);
128
129
        self::assertInstanceOf(Response::class, $response);
130
        self::assertSame($expected, $response->getBody()->getContents());
131
    }
132
133
    #[TestDox('LibrariesIO::project() with an invalid endpoint throws an InvalidEndpointException')]
134
    public function testProjectInvalidEndpoint(): void
135
    {
136
        $mockHandler = new MockHandler([$this->responses['valid']]);
137
        $mockClient  = $this->mockClient($this->testApiKey, $mockHandler);
138
139
        $this->expectException(InvalidEndpointException::class);
140
        $mockClient->project('notvalid', ['platform' => 'npm', 'name' => 'utility']);
141
    }
142
143
    #[TestDox('LibrariesIO::project() with an invalid options throws an InvalidEndpointOptionsException')]
144
    public function testProjectInvalidOptions(): void
145
    {
146
        $mockHandler = new MockHandler([$this->responses['valid']]);
147
        $mockClient  = $this->mockClient($this->testApiKey, $mockHandler);
148
149
        $this->expectException(InvalidEndpointOptionsException::class);
150
        $mockClient->project('search', ['huh' => 'what']);
151
    }
152
153
    #[TestDox('A response with status code 429 throws a RateLimitExceededException')]
154
    public function testRateLimitExceeded(): void
155
    {
156
        $response    = $this->responses['rateLimiter'];
157
        $mockHandler = new MockHandler([$response]);
158
        $mockClient  = $this->mockClient($this->testApiKey, $mockHandler);
159
160
        try {
161
            $mockClient->platform();
162
        } catch (RateLimitExceededException $rateLimitExceededException) {
163
            $response = $rateLimitExceededException->getResponse();
164
165
            self::assertSame('60', $response->getHeaderLine('x-ratelimit-limit'));
166
            self::assertSame('0', $response->getHeaderLine('x-ratelimit-remaining'));
167
            self::assertSame('', $response->getHeaderLine('x-ratelimit-reset'));
168
        }
169
    }
170
171
    #[TestDox('(ClientException) A response with status code 429 throws a RateLimitExceededException')]
172
    public function testRateLimitExceededClientException(): void
173
    {
174
        $response    = $this->responses['rateLimit'];
175
        $mockHandler = new MockHandler([$response]);
176
        $mockClient  = $this->mockClient($this->testApiKey, $mockHandler);
177
178
        try {
179
            $mockClient->platform();
180
        } catch (RateLimitExceededException $rateLimitExceededException) {
181
            $response = $rateLimitExceededException->getResponse();
182
183
            self::assertSame('60', $response->getHeaderLine('x-ratelimit-limit'));
184
            self::assertSame('0', $response->getHeaderLine('x-ratelimit-remaining'));
185
            self::assertSame('', $response->getHeaderLine('x-ratelimit-reset'));
186
        }
187
    }
188
189
    #[TestDox('Utils::toRaw() returns raw JSON and expected response')]
190
    public function testRaw(): void
191
    {
192
        $mockHandler = new MockHandler([$this->responses['valid']]);
193
        $mockClient  = $this->mockClient($this->testApiKey, $mockHandler);
194
        $response    = $mockClient->user('dependencies', ['login' => 'ericsizemore']);
195
196
        self::assertInstanceOf(Response::class, $response);
197
        self::assertSame('{"Hello":"World"}', Utils::raw($response));
198
    }
199
200
    /**
201
     * @param array<string, int|string> $options
202
     */
203
    #[DataProvider('dataRepositoryProvider')]
204
    #[TestDox('LibrariesIO::repository() returns expected response')]
205
    public function testRepository(string $expected, string $endpoint, array $options): void
206
    {
207
        $mockHandler = new MockHandler([$this->responses['valid']]);
208
        $mockClient  = $this->mockClient($this->testApiKey, $mockHandler);
209
        $response    = $mockClient->repository($endpoint, $options);
210
211
        self::assertInstanceOf(Response::class, $response);
212
        self::assertSame($expected, $response->getBody()->getContents());
213
    }
214
215
    /**
216
     * Test the repository endpoint with an invalid $endpoint arg specified.
217
     */
218
    public function testRepositoryInvalidEndpoint(): void
219
    {
220
        $mockHandler = new MockHandler([$this->responses['valid']]);
221
        $mockClient  = $this->mockClient($this->testApiKey, $mockHandler);
222
        $this->expectException(InvalidEndpointException::class);
223
        $mockClient->repository('notvalid', ['owner' => 'ericsizemore', 'name' => 'utility']);
224
    }
225
226
    /**
227
     * Test the repository endpoint with a valid subset $endpoint arg and invalid options specified.
228
     */
229
    public function testRepositoryInvalidOptions(): void
230
    {
231
        $mockHandler = new MockHandler([$this->responses['valid']]);
232
        $mockClient  = $this->mockClient($this->testApiKey, $mockHandler);
233
        $this->expectException(InvalidEndpointOptionsException::class);
234
        $mockClient->repository('repository', ['huh' => 'what']);
235
    }
236
237
    /**
238
     * Test the subscription endpoint.
239
     *
240
     * @param array<string> $options
241
     */
242
    #[DataProvider('dataSubscriptionProvider')]
243
    public function testSubscription(string $expected, string $endpoint, array $options): void
244
    {
245
        $mockHandler = new MockHandler([$this->responses['valid']]);
246
        $mockClient  = $this->mockClient($this->testApiKey, $mockHandler);
247
        $response    = $mockClient->subscription($endpoint, $options);
248
249
        self::assertInstanceOf(Response::class, $response);
250
        self::assertSame($expected, $response->getBody()->getContents());
251
    }
252
253
    /**
254
     * Test the subscription endpoint with an invalid $endpoint arg specified.
255
     */
256
    public function testSubscriptionInvalidEndpoint(): void
257
    {
258
        $mockHandler = new MockHandler([$this->responses['valid']]);
259
        $mockClient  = $this->mockClient($this->testApiKey, $mockHandler);
260
        $this->expectException(InvalidEndpointException::class);
261
        $mockClient->subscription('notvalid', ['platform' => 'npm', 'name' => 'utility']);
262
    }
263
264
    /**
265
     * Test the subscription endpoint with a valid $endpoint arg and invalid $options specified.
266
     */
267
    public function testSubscriptionInvalidOptions(): void
268
    {
269
        $mockHandler = new MockHandler([$this->responses['valid']]);
270
        $mockClient  = $this->mockClient($this->testApiKey, $mockHandler);
271
        $this->expectException(InvalidEndpointOptionsException::class);
272
        $mockClient->subscription('check', ['huh' => 'what']);
273
    }
274
275
    /**
276
     * Test the toArray function. It decodes the raw json data into an associative array.
277
     */
278
    public function testToArray(): void
279
    {
280
        $mockHandler = new MockHandler([$this->responses['valid']]);
281
        $mockClient  = $this->mockClient($this->testApiKey, $mockHandler);
282
        $response    = $mockClient->user('dependencies', ['login' => 'ericsizemore']);
283
284
        self::assertInstanceOf(Response::class, $response);
285
        self::assertSame(['Hello' => 'World'], Utils::toArray($response));
286
    }
287
288
    /**
289
     * Test the toObject function. It decodes the raw json data and creates a \stdClass object.
290
     */
291
    public function testToObject(): void
292
    {
293
        $mockHandler = new MockHandler([$this->responses['valid']]);
294
        $mockClient  = $this->mockClient($this->testApiKey, $mockHandler);
295
        $response    = $mockClient->user('dependencies', ['login' => 'ericsizemore']);
296
297
        self::assertInstanceOf(Response::class, $response);
298
299
        $expected        = new stdClass();
300
        $expected->Hello = 'World';
301
302
        self::assertEquals($expected, Utils::toObject($response));
303
    }
304
305
    /**
306
     * Test the user endpoint.
307
     *
308
     * @param array<string, int|string> $options
309
     */
310
    #[DataProvider('dataUserProvider')]
311
    public function testUser(string $expected, string $endpoint, array $options): void
312
    {
313
        $mockHandler = new MockHandler([$this->responses['valid']]);
314
        $mockClient  = $this->mockClient($this->testApiKey, $mockHandler);
315
        $response    = $mockClient->user($endpoint, $options);
316
317
        self::assertInstanceOf(Response::class, $response);
318
        self::assertSame($expected, $response->getBody()->getContents());
319
    }
320
321
    /**
322
     * Test the user endpoint with an invalid $endpoint arg specified.
323
     */
324
    public function testUserInvalidEndpoint(): void
325
    {
326
        $mockHandler = new MockHandler([$this->responses['valid']]);
327
        $mockClient  = $this->mockClient($this->testApiKey, $mockHandler);
328
        $this->expectException(InvalidEndpointException::class);
329
        $mockClient->user('notvalid', ['login' => 'ericsizemore']);
330
    }
331
332
    /**
333
     * Test the user endpoint with a valid $endpoint arg and invalid $options specified.
334
     */
335
    public function testUserInvalidOptions(): void
336
    {
337
        $mockHandler = new MockHandler([$this->responses['valid']]);
338
        $mockClient  = $this->mockClient($this->testApiKey, $mockHandler);
339
        $this->expectException(InvalidEndpointOptionsException::class);
340
        $mockClient->user('packages', ['huh' => 'what']);
341
    }
342
343
    /**
344
     * Test the Utils::validateCachePath() with valid and invalid $cachePath's
345
     */
346
    #[DataProvider('dataCachePathProvider')]
347
    public function testValidateCachePathReturnValues(?string $expected, ?string $cachePath): void
348
    {
349
        self::assertSame($expected, Utils::validateCachePath($cachePath));
350
    }
351
352
    /**
353
     * @psalm-suppress PossiblyUnusedMethod
354
     */
355
    public static function dataCachePathProvider(): Iterator
356
    {
357
        yield [null, null];
358
        yield [null, '/path/does/not/exist'];
359
        yield [sys_get_temp_dir(), sys_get_temp_dir()];
360
    }
361
362
    /**
363
     * @psalm-suppress PossiblyUnusedMethod
364
     */
365
    public static function dataEndpointProvider(): Iterator
366
    {
367
        yield ['project', '/project', 'https://libraries.io/api/'];
368
        yield ['project', 'project', 'https://libraries.io/api/'];
369
        yield ['/project', '/project', 'https://libraries.io/api'];
370
        yield ['/project', 'project', 'https://libraries.io/api'];
371
    }
372
373
    /**
374
     * @psalm-suppress PossiblyUnusedMethod
375
     */
376
    public static function dataMethodProvider(): Iterator
377
    {
378
        yield ['GET', 'GET'];
379
        yield ['POST', 'POST'];
380
        yield ['DELETE', 'DELETE'];
381
        yield ['PUT', 'PUT'];
382
        yield ['GET', 'PATCH'];
383
        yield ['GET', 'OPTIONS'];
384
        yield ['GET', 'HEAD'];
385
    }
386
387
    /**
388
     * @psalm-suppress PossiblyUnusedMethod
389
     */
390
    public static function dataProjectProvider(): Iterator
391
    {
392
        yield ['{"Hello":"World"}', 'contributors', ['platform' => 'npm', 'name' => 'utility']];
393
        yield ['{"Hello":"World"}', 'dependencies', ['platform' => 'npm', 'name' => 'utility', 'version' => 'latest']];
394
        yield ['{"Hello":"World"}', 'dependent_repositories', ['platform' => 'npm', 'name' => 'utility']];
395
        yield ['{"Hello":"World"}', 'dependents', ['platform' => 'npm', 'name' => 'utility']];
396
        yield ['{"Hello":"World"}', 'search', ['query' => 'grunt', 'sort' => 'rank', 'keywords' => 'wordpress']];
397
        yield ['{"Hello":"World"}', 'search', ['query' => 'grunt', 'sort' => 'notvalid', 'keywords' => 'wordpress']];
398
        yield ['{"Hello":"World"}', 'sourcerank', ['platform' => 'npm', 'name' => 'utility']];
399
        yield ['{"Hello":"World"}', 'project', ['platform' => 'npm', 'name' => 'utility', 'page' => 1, 'per_page' => 30]];
400
    }
401
402
    /**
403
     * @psalm-suppress PossiblyUnusedMethod
404
     */
405
    public static function dataRepositoryProvider(): Iterator
406
    {
407
        yield ['{"Hello":"World"}', 'dependencies', ['owner' => 'ericsizemore', 'name' => 'utility']];
408
        yield ['{"Hello":"World"}', 'projects', ['owner' => 'ericsizemore', 'name' => 'utility']];
409
        yield ['{"Hello":"World"}', 'repository', ['owner' => 'ericsizemore', 'name' => 'utility']];
410
        yield ['{"Hello":"World"}', 'dependencies', ['owner' => 'ericsizemore', 'name' => 'utility']];
411
        yield ['{"Hello":"World"}', 'projects', ['owner' => 'ericsizemore', 'name' => 'utility', 'page' => 1, 'per_page' => 30]];
412
        yield ['{"Hello":"World"}', 'repository', ['owner' => 'ericsizemore', 'name' => 'utility']];
413
    }
414
415
    /**
416
     * @psalm-suppress PossiblyUnusedMethod
417
     */
418
    public static function dataSubscriptionProvider(): Iterator
419
    {
420
        yield ['{"Hello":"World"}', 'subscribe', ['platform' => 'npm', 'name' => 'utility', 'include_prerelease' => 'true']];
421
        yield ['{"Hello":"World"}', 'check', ['platform' => 'npm', 'name' => 'utility']];
422
        yield ['{"Hello":"World"}', 'update', ['platform' => 'npm', 'name' => 'utility', 'include_prerelease' => 'false']];
423
        yield ['{"Hello":"World"}', 'unsubscribe', ['platform' => 'npm', 'name' => 'utility']];
424
    }
425
426
    /**
427
     * @psalm-suppress PossiblyUnusedMethod
428
     */
429
    public static function dataUserProvider(): Iterator
430
    {
431
        yield ['{"Hello":"World"}', 'dependencies', ['login' => 'ericsizemore']];
432
        yield ['{"Hello":"World"}', 'package_contributions', ['login' => 'ericsizemore']];
433
        yield ['{"Hello":"World"}', 'packages', ['login' => 'ericsizemore']];
434
        yield ['{"Hello":"World"}', 'repositories', ['login' => 'ericsizemore']];
435
        yield ['{"Hello":"World"}', 'repository_contributions', ['login' => 'ericsizemore', 'page' => 1, 'per_page' => 30]];
436
        yield ['{"Hello":"World"}', 'subscriptions', []];
437
    }
438
439
    /**
440
     * Creates a mock for testing.
441
     */
442
    private function mockClient(string $apiKey, ?MockHandler $mockHandler = null): LibrariesIO&MockObject
443
    {
444
        return $this
445
            ->getMockBuilder(LibrariesIO::class)
446
            ->setConstructorArgs([$apiKey, sys_get_temp_dir(), ['_mockHandler' => $mockHandler]])
447
            ->onlyMethods([])
448
            ->getMock();
449
    }
450
}
451