Passed
Push — master ( 5620bf...f38109 )
by Eric
01:56
created

LibrariesIOTest::dataEndpointProvider()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 4
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 6
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\{
24
    Exception\ClientException,
25
    HandlerStack,
26
    Handler\MockHandler,
27
    Psr7\Request,
28
    Psr7\Response
29
};
30
use InvalidArgumentException;
31
use Iterator;
32
use PHPUnit\Framework\{
33
    Attributes\CoversClass,
34
    Attributes\DataProvider,
35
    Attributes\TestDox,
1 ignored issue
show
Bug introduced by
The type PHPUnit\Framework\Attributes\TestDox was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
36
    MockObject\MockObject,
37
    TestCase
38
};
39
use stdClass;
40
41
use function sys_get_temp_dir;
42
43
/**
44
 * LibrariesIO Tests.
45
 *
46
 * @internal
47
 */
48
#[CoversClass(LibrariesIO::class)]
49
#[CoversClass(AbstractClient::class)]
50
#[CoversClass(Utils::class)]
51
#[CoversClass(RateLimitExceededException::class)]
52
final class LibrariesIOTest extends TestCase
53
{
54
    /**
55
     * @var array<string, ClientException|Response>
56
     */
57
    private array $responses;
58
59
    private string $testApiKey;
60
61
    #[\Override]
62
    protected function setUp(): void
63
    {
64
        $rateLimitHeaders = [
65
            'X-RateLimit-Limit'     => '60',
66
            'X-RateLimit-Remaining' => '0',
67
            'X-RateLimit-Reset'     => '',
68
        ];
69
70
        $this->testApiKey = md5('test');
71
72
        $this->responses = [
73
            'valid'       => new Response(200, body: '{"Hello":"World"}'),
74
            'clientError' => new ClientException('Error Communicating with Server', new Request('GET', 'test'), new Response(202, ['X-Foo' => 'Bar'])),
75
            'rateLimit'   => new ClientException('Rate Limit Exceeded', new Request('GET', 'test'), new Response(429, $rateLimitHeaders)),
76
            'rateLimiter' => new Response(429, $rateLimitHeaders, 'Rate Limit Exceeded'),
77
        ];
78
    }
79
80
    public static function dataEndpointProvider(): Iterator
81
    {
82
        yield ['project', '/project', 'https://libraries.io/api/'];
83
        yield ['project', 'project', 'https://libraries.io/api/'];
84
        yield ['/project', '/project', 'https://libraries.io/api'];
85
        yield ['/project', 'project', 'https://libraries.io/api'];
86
    }
87
88
    public static function dataMethodProvider(): Iterator
89
    {
90
        yield ['GET', 'GET'];
91
        yield ['POST', 'POST'];
92
        yield ['DELETE', 'DELETE'];
93
        yield ['PUT', 'PUT'];
94
        yield ['GET', 'PATCH'];
95
        yield ['GET', 'OPTIONS'];
96
        yield ['GET', 'HEAD'];
97
    }
98
99
    public static function dataProjectProvider(): Iterator
100
    {
101
        yield ['{"Hello":"World"}', 'contributors', ['platform' => 'npm', 'name' => 'utility']];
102
        yield ['{"Hello":"World"}', 'dependencies', ['platform' => 'npm', 'name' => 'utility', 'version' => 'latest']];
103
        yield ['{"Hello":"World"}', 'dependent_repositories', ['platform' => 'npm', 'name' => 'utility']];
104
        yield ['{"Hello":"World"}', 'dependents', ['platform' => 'npm', 'name' => 'utility']];
105
        yield ['{"Hello":"World"}', 'search', ['query' => 'grunt', 'sort' => 'rank', 'keywords' => 'wordpress']];
106
        yield ['{"Hello":"World"}', 'search', ['query' => 'grunt', 'sort' => 'notvalid', 'keywords' => 'wordpress']];
107
        yield ['{"Hello":"World"}', 'sourcerank', ['platform' => 'npm', 'name' => 'utility']];
108
        yield ['{"Hello":"World"}', 'project', ['platform' => 'npm', 'name' => 'utility', 'page' => 1, 'per_page' => 30]];
109
    }
110
111
    public static function dataRepositoryProvider(): Iterator
112
    {
113
        yield ['{"Hello":"World"}', 'dependencies', ['owner' => 'ericsizemore', 'name' => 'utility']];
114
        yield ['{"Hello":"World"}', 'projects', ['owner' => 'ericsizemore', 'name' => 'utility']];
115
        yield ['{"Hello":"World"}', 'repository', ['owner' => 'ericsizemore', 'name' => 'utility']];
116
        yield ['{"Hello":"World"}', 'dependencies', ['owner' => 'ericsizemore', 'name' => 'utility']];
117
        yield ['{"Hello":"World"}', 'projects', ['owner' => 'ericsizemore', 'name' => 'utility', 'page' => 1, 'per_page' => 30]];
118
        yield ['{"Hello":"World"}', 'repository', ['owner' => 'ericsizemore', 'name' => 'utility']];
119
    }
120
121
    public static function dataSubscriptionProvider(): Iterator
122
    {
123
        yield ['{"Hello":"World"}', 'subscribe', ['platform' => 'npm', 'name' => 'utility', 'include_prerelease' => 'true']];
124
        yield ['{"Hello":"World"}', 'check', ['platform' => 'npm', 'name' => 'utility']];
125
        yield ['{"Hello":"World"}', 'update', ['platform' => 'npm', 'name' => 'utility', 'include_prerelease' => 'false']];
126
        yield ['{"Hello":"World"}', 'unsubscribe', ['platform' => 'npm', 'name' => 'utility']];
127
    }
128
129
    public static function dataUserProvider(): Iterator
130
    {
131
        yield ['{"Hello":"World"}', 'dependencies', ['login' => 'ericsizemore']];
132
        yield ['{"Hello":"World"}', 'package_contributions', ['login' => 'ericsizemore']];
133
        yield ['{"Hello":"World"}', 'packages', ['login' => 'ericsizemore']];
134
        yield ['{"Hello":"World"}', 'repositories', ['login' => 'ericsizemore']];
135
        yield ['{"Hello":"World"}', 'repository_contributions', ['login' => 'ericsizemore', 'page' => 1, 'per_page' => 30]];
136
        yield ['{"Hello":"World"}', 'subscriptions', []];
137
    }
138
139
    #[TestDox('Client error throws a Guzzle ClientException')]
140
    public function testClientError(): void
141
    {
142
        $mockHandler = new MockHandler([$this->responses['clientError']]);
143
        $mockClient  = $this->mockClient($this->testApiKey, $mockHandler);
144
        $this->expectException(ClientException::class);
145
        $mockClient->platform();
146
    }
147
148
    #[TestDox('Providing an invalid API key results in an InvalidApiKeyException')]
149
    public function testInvalidApiKey(): void
150
    {
151
        $this->expectException(InvalidApiKeyException::class);
152
        $this->mockClient('notvalid');
153
    }
154
155
    #[DataProvider('dataEndpointProvider')]
156
    public function testNormalizeEndpoint(string $expected, string $endpoint, string $apiUrl): void
157
    {
158
        self::assertSame($expected, Utils::normalizeEndpoint($endpoint, $apiUrl));
159
    }
160
161
    #[DataProvider('dataMethodProvider')]
162
    public function testNormalizeMethod(string $expected, string $method): void
163
    {
164
        self::assertSame($expected, Utils::normalizeMethod($method));
165
    }
166
167
    #[TestDox('LibrariesIO::platform() returns expected response')]
168
    public function testPlatform(): void
169
    {
170
        $mockHandler = new MockHandler([$this->responses['valid']]);
171
        $mockClient  = $this->mockClient($this->testApiKey, $mockHandler);
172
        $response    = $mockClient->platform();
173
174
        self::assertInstanceOf(Response::class, $response);
175
        self::assertSame('{"Hello":"World"}', $response->getBody()->getContents());
176
    }
177
178
    /**
179
     * @param array<string, int|string> $options
180
     */
181
    #[DataProvider('dataProjectProvider')]
182
    #[TestDox('LibrariesIO::project() returns expected response')]
183
    public function testProject(string $expected, string $endpoint, array $options): void
184
    {
185
        $mockHandler = new MockHandler([$this->responses['valid']]);
186
        $mockClient  = $this->mockClient($this->testApiKey, $mockHandler);
187
        $response    = $mockClient->project($endpoint, $options);
188
189
        self::assertInstanceOf(Response::class, $response);
190
        self::assertSame($expected, $response->getBody()->getContents());
191
    }
192
193
    #[TestDox('LibrariesIO::project() with an invalid endpoint throws an InvalidEndpointException')]
194
    public function testProjectInvalidEndpoint(): void
195
    {
196
        $mockHandler = new MockHandler([$this->responses['valid']]);
197
        $mockClient  = $this->mockClient($this->testApiKey, $mockHandler);
198
199
        $this->expectException(InvalidEndpointException::class);
200
        $mockClient->project('notvalid', ['platform' => 'npm', 'name' => 'utility']);
201
    }
202
203
    #[TestDox('LibrariesIO::project() with an invalid options throws an InvalidEndpointOptionsException')]
204
    public function testProjectInvalidOptions(): void
205
    {
206
        $mockHandler = new MockHandler([$this->responses['valid']]);
207
        $mockClient  = $this->mockClient($this->testApiKey, $mockHandler);
208
209
        $this->expectException(InvalidEndpointOptionsException::class);
210
        $mockClient->project('search', ['huh' => 'what']);
211
    }
212
213
    #[TestDox('A response with status code 429 throws a RateLimitExceededException')]
214
    public function testRateLimitExceeded(): void
215
    {
216
        $response    = $this->responses['rateLimiter'];
217
        $mockHandler = new MockHandler([$response]);
218
        $mockClient  = $this->mockClient($this->testApiKey, $mockHandler);
219
220
        try {
221
            $mockClient->platform();
222
        } catch (RateLimitExceededException $rateLimitExceededException) {
223
            $response = $rateLimitExceededException->getResponse();
224
225
            self::assertSame('60', $response->getHeaderLine('x-ratelimit-limit'));
226
            self::assertSame('0', $response->getHeaderLine('x-ratelimit-remaining'));
227
            self::assertSame('', $response->getHeaderLine('x-ratelimit-reset'));
228
        }
229
    }
230
231
    #[TestDox('(ClientException) A response with status code 429 throws a RateLimitExceededException')]
232
    public function testRateLimitExceededClientException(): void
233
    {
234
        $response    = $this->responses['rateLimit'];
235
        $mockHandler = new MockHandler([$response]);
236
        $mockClient  = $this->mockClient($this->testApiKey, $mockHandler);
237
238
        try {
239
            $mockClient->platform();
240
        } catch (RateLimitExceededException $rateLimitExceededException) {
241
            $response = $rateLimitExceededException->getResponse();
242
243
            self::assertSame('60', $response->getHeaderLine('x-ratelimit-limit'));
244
            self::assertSame('0', $response->getHeaderLine('x-ratelimit-remaining'));
245
            self::assertSame('', $response->getHeaderLine('x-ratelimit-reset'));
246
        }
247
    }
248
249
    #[TestDox('Utils::toRaw() returns raw JSON and expected response')]
250
    public function testRaw(): void
251
    {
252
        $mockHandler = new MockHandler([$this->responses['valid']]);
253
        $mockClient  = $this->mockClient($this->testApiKey, $mockHandler);
254
        $response    = $mockClient->user('dependencies', ['login' => 'ericsizemore']);
255
256
        self::assertInstanceOf(Response::class, $response);
257
        self::assertSame('{"Hello":"World"}', Utils::raw($response));
258
    }
259
260
    /**
261
     * @param array<string, int|string> $options
262
     */
263
    #[DataProvider('dataRepositoryProvider')]
264
    #[TestDox('LibrariesIO::repository() returns expected response')]
265
    public function testRepository(string $expected, string $endpoint, array $options): void
266
    {
267
        $mockHandler = new MockHandler([$this->responses['valid']]);
268
        $mockClient  = $this->mockClient($this->testApiKey, $mockHandler);
269
        $response    = $mockClient->repository($endpoint, $options);
270
271
        self::assertInstanceOf(Response::class, $response);
272
        self::assertSame($expected, $response->getBody()->getContents());
273
    }
274
275
    /**
276
     * Test the repository endpoint with an invalid $endpoint arg specified.
277
     */
278
    public function testRepositoryInvalidEndpoint(): void
279
    {
280
        $mockHandler = new MockHandler([$this->responses['valid']]);
281
        $mockClient  = $this->mockClient($this->testApiKey, $mockHandler);
282
        $this->expectException(InvalidEndpointException::class);
283
        $mockClient->repository('notvalid', ['owner' => 'ericsizemore', 'name' => 'utility']);
284
    }
285
286
    /**
287
     * Test the repository endpoint with a valid subset $endpoint arg and invalid options specified.
288
     */
289
    public function testRepositoryInvalidOptions(): void
290
    {
291
        $mockHandler = new MockHandler([$this->responses['valid']]);
292
        $mockClient  = $this->mockClient($this->testApiKey, $mockHandler);
293
        $this->expectException(InvalidArgumentException::class);
294
        $mockClient->repository('repository', ['huh' => 'what']);
295
    }
296
297
    /**
298
     * Test the subscription endpoint.
299
     *
300
     * @param array<string> $options
301
     */
302
    #[DataProvider('dataSubscriptionProvider')]
303
    public function testSubscription(string $expected, string $endpoint, array $options): void
304
    {
305
        $mockHandler = new MockHandler([$this->responses['valid']]);
306
        $mockClient  = $this->mockClient($this->testApiKey, $mockHandler);
307
        $response    = $mockClient->subscription($endpoint, $options);
308
309
        self::assertInstanceOf(Response::class, $response);
310
        self::assertSame($expected, $response->getBody()->getContents());
311
    }
312
313
    /**
314
     * Test the subscription endpoint with an invalid $endpoint arg specified.
315
     */
316
    public function testSubscriptionInvalidEndpoint(): void
317
    {
318
        $mockHandler = new MockHandler([$this->responses['valid']]);
319
        $mockClient  = $this->mockClient($this->testApiKey, $mockHandler);
320
        $this->expectException(InvalidEndpointException::class);
321
        $mockClient->subscription('notvalid', ['platform' => 'npm', 'name' => 'utility']);
322
    }
323
324
    /**
325
     * Test the subscription endpoint with a valid $endpoint arg and invalid $options specified.
326
     */
327
    public function testSubscriptionInvalidOptions(): void
328
    {
329
        $mockHandler = new MockHandler([$this->responses['valid']]);
330
        $mockClient  = $this->mockClient($this->testApiKey, $mockHandler);
331
        $this->expectException(InvalidEndpointOptionsException::class);
332
        $mockClient->subscription('check', ['huh' => 'what']);
333
    }
334
335
    /**
336
     * Test the toArray function. It decodes the raw json data into an associative array.
337
     */
338
    public function testToArray(): void
339
    {
340
        $mockHandler = new MockHandler([$this->responses['valid']]);
341
        $mockClient  = $this->mockClient($this->testApiKey, $mockHandler);
342
        $response    = $mockClient->user('dependencies', ['login' => 'ericsizemore']);
343
344
        self::assertInstanceOf(Response::class, $response);
345
        self::assertSame(['Hello' => 'World'], Utils::toArray($response));
346
    }
347
348
    /**
349
     * Test the toObject function. It decodes the raw json data and creates a \stdClass object.
350
     */
351
    public function testToObject(): void
352
    {
353
        $mockHandler = new MockHandler([$this->responses['valid']]);
354
        $mockClient  = $this->mockClient($this->testApiKey, $mockHandler);
355
        $response    = $mockClient->user('dependencies', ['login' => 'ericsizemore']);
356
357
        self::assertInstanceOf(Response::class, $response);
358
359
        $expected        = new stdClass();
360
        $expected->Hello = 'World';
361
362
        self::assertEquals($expected, Utils::toObject($response));
363
    }
364
365
    /**
366
     * Test the user endpoint.
367
     *
368
     * @param array<string, int|string> $options
369
     */
370
    #[DataProvider('dataUserProvider')]
371
    public function testUser(string $expected, string $endpoint, array $options): void
372
    {
373
        $mockHandler = new MockHandler([$this->responses['valid']]);
374
        $mockClient  = $this->mockClient($this->testApiKey, $mockHandler);
375
        $response    = $mockClient->user($endpoint, $options);
376
377
        self::assertInstanceOf(Response::class, $response);
378
        self::assertSame($expected, $response->getBody()->getContents());
379
    }
380
381
    /**
382
     * Test the user endpoint with an invalid $endpoint arg specified.
383
     */
384
    public function testUserInvalidEndpoint(): void
385
    {
386
        $mockHandler = new MockHandler([$this->responses['valid']]);
387
        $mockClient  = $this->mockClient($this->testApiKey, $mockHandler);
388
        $this->expectException(InvalidEndpointException::class);
389
        $mockClient->user('notvalid', ['login' => 'ericsizemore']);
390
    }
391
392
    /**
393
     * Test the user endpoint with a valid $endpoint arg and invalid $options specified.
394
     */
395
    public function testUserInvalidOptions(): void
396
    {
397
        $mockHandler = new MockHandler([$this->responses['valid']]);
398
        $mockClient  = $this->mockClient($this->testApiKey, $mockHandler);
399
        $this->expectException(InvalidEndpointOptionsException::class);
400
        $mockClient->user('packages', ['huh' => 'what']);
401
    }
402
403
    /**
404
     * Creates a mock for testing.
405
     */
406
    private function mockClient(string $apiKey, ?MockHandler $mockHandler = null): LibrariesIO&MockObject
407
    {
408
        // Create a mock
0 ignored issues
show
Unused Code Comprehensibility introduced by
42% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
409
        //$handlerStack = HandlerStack::create($mockHandler);
410
411
        return $this
412
            ->getMockBuilder(LibrariesIO::class)
413
            ->setConstructorArgs([$apiKey, sys_get_temp_dir(), ['_mockHandler' => $mockHandler]])
414
            ->onlyMethods([])
415
            ->getMock();
416
    }
417
}
418