Passed
Push — master ( c2b789...51b57c )
by Eric
03:37 queued 01:57
created

LibrariesIOTest::dataSubscriptionProvider()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 5
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 7
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * LibrariesIO - A simple API wrapper/client for the Libraries.io API.
7
 *
8
 * @author    Eric Sizemore <[email protected]>
9
 * @package   LibrariesIO
10
 * @link      https://www.secondversion.com/
11
 * @version   1.1.0
12
 * @copyright (C) 2023 Eric Sizemore
13
 * @license   The MIT License (MIT)
14
 */
15
namespace Esi\LibrariesIO\Tests;
16
17
use Esi\LibrariesIO\LibrariesIO;
18
use Esi\LibrariesIO\Exception\RateLimitExceededException;
19
20
use InvalidArgumentException;
21
use stdClass;
22
23
use PHPUnit\Framework\{
24
    TestCase,
25
    MockObject\MockObject,
26
    Attributes\CoversClass,
27
    Attributes\DataProvider
28
};
29
30
use GuzzleHttp\{
31
    Client,
32
    Handler\MockHandler,
33
    HandlerStack,
34
    Psr7\Response,
35
    Psr7\Request,
36
    Exception\ClientException
37
};
38
39
use function sys_get_temp_dir;
40
41
/**
42
 * LibrariesIO - A simple API wrapper/client for the Libraries.io API.
43
 *
44
 * @author    Eric Sizemore <[email protected]>
45
 * @package   LibrariesIO
46
 * @link      https://www.secondversion.com/
47
 * @version   1.1.0
48
 * @copyright (C) 2023 Eric Sizemore
49
 * @license   The MIT License (MIT)
50
 *
51
 * Copyright (C) 2023 Eric Sizemore. All rights reserved.
52
 *
53
 * Permission is hereby granted, free of charge, to any person obtaining a copy
54
 * of this software and associated documentation files (the "Software"), to
55
 * deal in the Software without restriction, including without limitation the
56
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
57
 * sell copies of the Software, and to permit persons to whom the Software is
58
 * furnished to do so, subject to the following conditions:
59
 *
60
 * The above copyright notice and this permission notice shall be included in
61
 * all copies or substantial portions of the Software.
62
 *
63
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
64
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
65
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
66
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
67
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
68
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
69
 * THE SOFTWARE.
70
 */
71
#[CoversClass(LibrariesIO::class)]
72
class LibrariesIOTest extends TestCase
73
{
74
    /**
75
     * A mock'ed GuzzleHttp client we can inject for testing.
76
     *
77
     * @var Client
78
     */
79
    protected Client $client;
80
81
    /**
82
     * The mock/stub of the main class.
83
     *
84
     * @var LibrariesIO&MockObject
85
     */
86
    protected LibrariesIO&MockObject $stub;
87
88
    /**
89
     * Creates the mock to be used throughout testing.
90
     */
91
    public function setUp(): void
92
    {
93
        // Create a mock and queue two responses.
94
        $mock = new MockHandler([
95
            new Response(200, body: '{"Hello":"World"}')
96
        ]);
97
        
98
        $handlerStack = HandlerStack::create($mock);
99
        $this->client = new Client(['handler' => $handlerStack]);
100
101
        $this->stub = $this
102
            ->getMockBuilder(LibrariesIO::class)
103
            ->setConstructorArgs([md5('test'), sys_get_temp_dir()])
104
            ->onlyMethods([])
105
            ->getMock();
106
    }
107
108
    /**
109
     * Mock a client error via Guzzle's ClientException
110
     */
111
    public function testClientError(): void
112
    {
113
        // Create a mock and queue two responses.
114
        $mock = new MockHandler([
115
            new ClientException('Error Communicating with Server', new Request('GET', 'test'), new Response(202, ['X-Foo' => 'Bar']))
116
        ]);
117
        
118
        $handlerStack = HandlerStack::create($mock);
119
        $client = new Client(['handler' => $handlerStack]);
120
121
        $stub = $this
122
            ->getMockBuilder(LibrariesIO::class)
123
            ->setConstructorArgs([md5('test'), sys_get_temp_dir()])
124
            ->onlyMethods([])
125
            ->getMock();
126
        $this->expectException(ClientException::class);
127
        $stub->client = $client;
0 ignored issues
show
Bug introduced by
Accessing client on the interface PHPUnit\Framework\MockObject\MockObject suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
128
        $response = $stub->platform();
0 ignored issues
show
Unused Code introduced by
The assignment to $response is dead and can be removed.
Loading history...
129
    }
130
131
    /**
132
     * Tests library handling of HTTP 429, which can be returned by libraries.io if rate limit
133
     * is exceeded.
134
     */
135
    public function testRateLimitExceeded(): void
136
    {
137
        // Create a mock and queue two responses.
138
        $mock = new MockHandler([
139
            new ClientException('Error Communicating with Server', new Request('GET', 'test'), new Response(429, ['X-Foo' => 'Bar']))
140
        ]);
141
        
142
        $handlerStack = HandlerStack::create($mock);
143
        $client = new Client(['handler' => $handlerStack]);
144
145
        $stub = $this
146
            ->getMockBuilder(LibrariesIO::class)
147
            ->setConstructorArgs([md5('test'), sys_get_temp_dir()])
148
            ->onlyMethods([])
149
            ->getMock();
150
        $this->expectException(RateLimitExceededException::class);
151
        $stub->client = $client;
0 ignored issues
show
Bug introduced by
Accessing client on the interface PHPUnit\Framework\MockObject\MockObject suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
152
        $response = $stub->platform();
0 ignored issues
show
Unused Code introduced by
The assignment to $response is dead and can be removed.
Loading history...
153
    }
154
155
    /**
156
     * Test providing an invalid API key.
157
     */
158
    public function testInvalidApiKey(): void
159
    {
160
        $this->expectException(InvalidArgumentException::class);
161
        $stub = $this
0 ignored issues
show
Unused Code introduced by
The assignment to $stub is dead and can be removed.
Loading history...
162
            ->getMockBuilder(LibrariesIO::class)
163
            ->setConstructorArgs(['notvalid', sys_get_temp_dir()])
164
            ->onlyMethods([])
165
            ->getMock();
166
    }
167
168
    /**
169
     * Test the platform endpoint
170
     */
171
    public function testPlatform(): void
172
    {
173
        $this->stub->client = $this->client;
174
        $response = $this->stub->platform();
175
        self::assertInstanceOf(Response::class, $response);
176
        self::assertEquals('{"Hello":"World"}', $response->getBody()->getContents());
177
    }
178
179
    /**
180
     * Test the platform endpoint with an invalid $endpoint arg specified.
181
     */
182
    public function testPlatformInvalid(): void
183
    {
184
        $this->stub->client = $this->client;
185
        $this->expectException(InvalidArgumentException::class);
186
        $response = $this->stub->platform('notvalid');
0 ignored issues
show
Unused Code introduced by
The assignment to $response is dead and can be removed.
Loading history...
187
    }
188
189
    /**
190
     * Provides testing data for the project endpoint testing.
191
     *
192
     * @return array<int, array<int, array<string, int|string>|bool|string>>
193
     */
194
    public static function dataProjectProvider(): array
195
    {
196
        return [
197
        //contributors, dependencies, dependent_repositories, dependents, search, sourcerank, or project
198
            ['{"Hello":"World"}', 'contributors'          , ['platform' => 'npm'  , 'name' => 'utility']],
199
            ['{"Hello":"World"}', 'dependencies'          , ['platform' => 'npm'  , 'name' => 'utility', 'version' => 'latest']],
200
            ['{"Hello":"World"}', 'dependent_repositories', ['platform' => 'npm'  , 'name' => 'utility']],
201
            ['{"Hello":"World"}', 'dependents'            , ['platform' => 'npm'  , 'name' => 'utility']],
202
            ['{"Hello":"World"}', 'search'                , ['query'    => 'grunt', 'sort' => 'rank', 'keywords' => 'wordpress']],
203
            ['{"Hello":"World"}', 'search'                , ['query'    => 'grunt', 'sort' => 'notvalid', 'keywords' => 'wordpress']],
204
            ['{"Hello":"World"}', 'sourcerank'            , ['platform' => 'npm'  , 'name' => 'utility']],
205
            ['{"Hello":"World"}', 'project'               , ['platform' => 'npm'  , 'name' => 'utility', 'page' => 1, 'per_page' => 30]]
206
        ];
207
    }
208
209
    /**
210
     * Tests the project endpoint
211
     *
212
     * @param string $expected
213
     * @param string $endpoint
214
     * @param array<string, int|string> $options
215
     */
216
    #[DataProvider('dataProjectProvider')] 
217
    public function testProject(string $expected, string $endpoint, array $options): void
218
    {
219
        $this->stub->client = $this->client;
220
        $response = $this->stub->project($endpoint, $options);
221
        self::assertInstanceOf(Response::class, $response);
222
        self::assertEquals($expected, $response->getBody()->getContents());
223
    }
224
225
    /**
226
     * Test the project endpoint with an invalid subset $endpoint arg specified.
227
     */
228
    public function testProjectInvalidEndpoint(): void
229
    {
230
        $this->stub->client = $this->client;
231
        $this->expectException(InvalidArgumentException::class);
232
        $response = $this->stub->project('notvalid', ['platform' => 'npm'  , 'name' => 'utility']);
0 ignored issues
show
Unused Code introduced by
The assignment to $response is dead and can be removed.
Loading history...
233
    }
234
235
    /**
236
     * Test the platform endpoint with n valid subset $endpoint arg and invalid $options specified.
237
     */
238
    public function testProjectInvalidOptions(): void
239
    {
240
        $this->stub->client = $this->client;
241
        $this->expectException(InvalidArgumentException::class);
242
        $response = $this->stub->project('search', ['huh' => 'what']);
0 ignored issues
show
Unused Code introduced by
The assignment to $response is dead and can be removed.
Loading history...
243
    }
244
245
    /**
246
     * Provides testing data for the repository endpoint.
247
     *
248
     * @return array<int, array<int, array<string, int|string>|bool|string>>
249
     */
250
    public static function dataRepositoryProvider(): array
251
    {
252
        return [
253
            ['{"Hello":"World"}', 'dependencies' , ['owner' => 'ericsizemore', 'name' => 'utility']],
254
            ['{"Hello":"World"}', 'projects'     , ['owner' => 'ericsizemore', 'name' => 'utility']],
255
            ['{"Hello":"World"}', 'repository'   , ['owner' => 'ericsizemore', 'name' => 'utility']],
256
            ['{"Hello":"World"}', 'dependencies' , ['owner' => 'ericsizemore', 'name' => 'utility']],
257
            ['{"Hello":"World"}', 'projects'     , ['owner' => 'ericsizemore', 'name' => 'utility', 'page' => 1, 'per_page' => 30]],
258
            ['{"Hello":"World"}', 'repository'   , ['owner' => 'ericsizemore', 'name' => 'utility']],
259
        ];
260
    }
261
262
    /**
263
     * Test the repository endpoint
264
     *
265
     * @param string $expected
266
     * @param string $endpoint
267
     * @param array<string, int|string> $options
268
     */
269
    #[DataProvider('dataRepositoryProvider')] 
270
    public function testRepository(string $expected, string $endpoint, array $options): void
271
    {
272
        $this->stub->client = $this->client;
273
        $response = $this->stub->repository($endpoint, $options);
274
        self::assertInstanceOf(Response::class, $response);
275
        self::assertEquals($expected, $response->getBody()->getContents());
276
    }
277
278
    /**
279
     * Test the repository endpoint with an invalid $endpoint arg specified.
280
     */
281
    public function testRepositoryInvalidEndpoint(): void
282
    {
283
        $this->stub->client = $this->client;
284
        $this->expectException(InvalidArgumentException::class);
285
        $response = $this->stub->repository('notvalid', ['owner' => 'ericsizemore', 'name' => 'utility']);
0 ignored issues
show
Unused Code introduced by
The assignment to $response is dead and can be removed.
Loading history...
286
    }
287
288
    /**
289
     * Test the repository endpoint with a valid subset $endpoint arg and invalid options specified.
290
     */
291
    public function testRepositoryInvalidOptions(): void
292
    {
293
        $this->stub->client = $this->client;
294
        $this->expectException(InvalidArgumentException::class);
295
        $response = $this->stub->repository('repository', ['huh' => 'what']);
0 ignored issues
show
Unused Code introduced by
The assignment to $response is dead and can be removed.
Loading history...
296
    }
297
298
    /**
299
     * Provides the data for testing the user endpoint
300
     *
301
     * @return array<int, array<int, array<string, int|string>|bool|string>>
302
     */
303
    public static function dataUserProvider(): array
304
    {
305
        return [
306
            ['{"Hello":"World"}', 'dependencies'            , ['login' => 'ericsizemore']],
307
            ['{"Hello":"World"}', 'package_contributions'   , ['login' => 'ericsizemore']],
308
            ['{"Hello":"World"}', 'packages'                , ['login' => 'ericsizemore']],
309
            ['{"Hello":"World"}', 'repositories'            , ['login' => 'ericsizemore']],
310
            ['{"Hello":"World"}', 'repository_contributions', ['login' => 'ericsizemore', 'page' => 1, 'per_page' => 30]],
311
            ['{"Hello":"World"}', 'subscriptions'           , []]
312
        ];
313
    }
314
315
    /**
316
     * Test the user endpoint
317
     *
318
     * @param string $expected
319
     * @param string $endpoint
320
     * @param array<string, int|string> $options
321
     */
322
    #[DataProvider('dataUserProvider')] 
323
    public function testUser(string $expected, string $endpoint, array $options): void
324
    {
325
        $this->stub->client = $this->client;
326
        $response = $this->stub->user($endpoint, $options);
327
        self::assertInstanceOf(Response::class, $response);
328
        self::assertEquals($expected, $response->getBody()->getContents());
329
    }
330
331
    /**
332
     * Test the user endpoint with an invalid $endpoint arg specified.
333
     */
334
    public function testUserInvalidEndpoint(): void
335
    {
336
        $this->stub->client = $this->client;
337
        $this->expectException(InvalidArgumentException::class);
338
        $response = $this->stub->user('notvalid', ['login' => 'ericsizemore']);
0 ignored issues
show
Unused Code introduced by
The assignment to $response is dead and can be removed.
Loading history...
339
    }
340
341
    /**
342
     * Test the user endpoint with a valid $endpoint arg and invalid $options specified.
343
     */
344
    public function testUserInvalidOptions(): void
345
    {
346
        $this->stub->client = $this->client;
347
        $this->expectException(InvalidArgumentException::class);
348
        $response = $this->stub->user('packages', ['huh' => 'what']);
0 ignored issues
show
Unused Code introduced by
The assignment to $response is dead and can be removed.
Loading history...
349
    }
350
351
    /**
352
     * Provides the data for testing the subscription endpoint
353
     *
354
     * @return array<int, array<int, array<string, bool|string>|string>>
355
     */
356
    public static function dataSubscriptionProvider(): array
357
    {
358
        return [
359
            ['{"Hello":"World"}', 'subscribe'  , ['platform' => 'npm', 'name' => 'utility', 'include_prerelease' => 'true']],
360
            ['{"Hello":"World"}', 'check'      , ['platform' => 'npm', 'name' => 'utility']],
361
            ['{"Hello":"World"}', 'update'     , ['platform' => 'npm', 'name' => 'utility', 'include_prerelease' => 'false']],
362
            ['{"Hello":"World"}', 'unsubscribe', ['platform' => 'npm', 'name' => 'utility']]
363
        ];
364
    }
365
366
    /**
367
     * Test the subscription endpoint
368
     *
369
     * @param string $expected
370
     * @param string $endpoint
371
     * @param array<string, int|string> $options
372
     */
373
    #[DataProvider('dataSubscriptionProvider')] 
374
    public function testSubscription(string $expected, string $endpoint, array $options): void
375
    {
376
        $this->stub->client = $this->client;
377
        $response = $this->stub->subscription($endpoint, $options);
378
        self::assertInstanceOf(Response::class, $response);
379
        self::assertEquals($expected, $response->getBody()->getContents());
380
    }
381
382
    /**
383
     * Test the subscription endpoint with an invalid $endpoint arg specified.
384
     */
385
    public function testSubscriptionInvalidEndpoint(): void
386
    {
387
        $this->stub->client = $this->client;
388
        $this->expectException(InvalidArgumentException::class);
389
        $response = $this->stub->subscription('notvalid', ['platform' => 'npm', 'name' => 'utility']);
0 ignored issues
show
Unused Code introduced by
The assignment to $response is dead and can be removed.
Loading history...
390
    }
391
392
    /**
393
     * Test the subscription endpoint with a valid $endpoint arg and invalid $options specified.
394
     */
395
    public function testSubscriptionInvalidOptions(): void
396
    {
397
        $this->stub->client = $this->client;
398
        $this->expectException(InvalidArgumentException::class);
399
        $response = $this->stub->subscription('check', ['huh' => 'what']);
0 ignored issues
show
Unused Code introduced by
The assignment to $response is dead and can be removed.
Loading history...
400
    }
401
402
    /**
403
     * Test the toRaw function. It should return the raw response json
404
     */
405
    public function testRaw(): void
406
    {
407
        $this->stub->client = $this->client;
408
        $response = $this->stub->user('dependencies', ['login' => 'ericsizemore']);
409
        self::assertInstanceOf(Response::class, $response);
410
        self::assertEquals('{"Hello":"World"}', $this->stub->raw($response));
411
    }
412
413
    /**
414
     * Test the toArray function. It decodes the raw json data into an associative array.
415
     */
416
    public function testToArray(): void
417
    {
418
        $this->stub->client = $this->client;
419
        $response = $this->stub->user('dependencies', ['login' => 'ericsizemore']);
420
        self::assertInstanceOf(Response::class, $response);
421
        self::assertEquals(['Hello' => 'World'], $this->stub->toArray($response));
422
    }
423
424
    /**
425
     * Test the toObject function. It decodes the raw json data and creates a \stdClass object.
426
     */
427
    public function testToObject(): void
428
    {
429
        $this->stub->client = $this->client;
430
        $response = $this->stub->user('dependencies', ['login' => 'ericsizemore']);
431
        self::assertInstanceOf(Response::class, $response);
432
433
        $expected = new stdClass;
434
        $expected->Hello = 'World';
435
        self::assertEquals($expected, $this->stub->toObject($response));
436
    }
437
}
438