Skip to content

Commit

Permalink
Improve code coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
cerbero90 committed Sep 10, 2024
1 parent e398e2f commit 577edd2
Show file tree
Hide file tree
Showing 16 changed files with 336 additions and 5 deletions.
6 changes: 4 additions & 2 deletions src/Concerns/RespectsRateLimits.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace Cerbero\LazyJsonPages\Concerns;

use Cerbero\LazyJsonPages\Services\ClientFactory;

/**
* The trait to respect rate limits of APIs.
*/
Expand All @@ -14,8 +16,8 @@ trait RespectsRateLimits
*/
protected function respectRateLimits(): void
{
if (microtime(true) < $timestamp = $this->config->rateLimits?->resetAt() ?? 0) {
time_sleep_until($timestamp);
if (microtime(true) < $timestamp = $this->config->rateLimits?->resetAt() ?? 0.0) {
ClientFactory::isFake() ? ClientFactory::$fakedRateLimits[] = $timestamp : time_sleep_until($timestamp);
}
}
}
2 changes: 2 additions & 0 deletions src/Paginations/Pagination.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ final public function __construct(

/**
* Determine whether this pagination matches the configuration.
*
* @codeCoverageIgnore
*/
public function matches(): bool
{
Expand Down
26 changes: 26 additions & 0 deletions src/Services/ClientFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,18 @@ final class ClientFactory
*/
private static array $globalMiddleware = [];

/**
* Whether HTTP requests are faked.
*/
private static bool $isFake = false;

/**
* The faked rate limits timestamps.
*
* @var float[]
*/
public static array $fakedRateLimits = [];

/**
* The tap middleware callbacks.
*/
Expand Down Expand Up @@ -84,6 +96,8 @@ public static function fake(array $responses, Closure $callback): array
{
$transactions = [];

self::$isFake = true;

$handler = HandlerStack::create(new MockHandler($responses));

$handler->push(Middleware::history($transactions));
Expand All @@ -94,9 +108,21 @@ public static function fake(array $responses, Closure $callback): array

unset(self::$defaultConfig['handler']);

self::$fakedRateLimits = [];

self::$isFake = false;

return $transactions;
}

/**
* Determine whether the HTTP requests are faked.
*/
public static function isFake(): bool
{
return self::$isFake;
}

/**
* Instantiate the class.
*/
Expand Down
2 changes: 2 additions & 0 deletions src/Sources/Source.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ final public function __construct(

/**
* Determine whether this class can handle the source.
*
* @codeCoverageIgnore
*/
public function matches(): bool
{
Expand Down
41 changes: 41 additions & 0 deletions tests/Feature/PaginationTest.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
<?php

use Cerbero\LazyJsonPages\Exceptions\InvalidKeyException;
use Cerbero\LazyJsonPages\Exceptions\InvalidPaginationException;
use Cerbero\LazyJsonPages\Exceptions\UnsupportedPaginationException;
use Cerbero\LazyJsonPages\LazyJsonPages;
use Cerbero\LazyJsonPages\Services\ClientFactory;
use GuzzleHttp\Psr7\Response;

it('supports length-aware paginations', function (Closure $configure) {
$lazyCollection = $configure(LazyJsonPages::from('https://example.com/api/v1/users'))
Expand Down Expand Up @@ -64,6 +68,27 @@
})());
});

it('loads only the first page if the link header does not contain links', function () {
$lazyCollection = LazyJsonPages::from('https://example.com/api/v1/users')
->linkHeader()
->collect('data.*');

$responses = [
new Response(body: file_get_contents(fixture('pagination/page1.json'))),
];

$transactions = ClientFactory::fake($responses, fn() => expect($lazyCollection)->sequence(
['name' => 'item1'],
['name' => 'item2'],
['name' => 'item3'],
['name' => 'item4'],
['name' => 'item5'],
));

expect($transactions)->toHaveCount(1);
expect((string) $transactions[0]['request']->getUri())->toBe('https://example.com/api/v1/users');
});

it('fails if an invalid custom pagination is provided', function () {
$lazyCollection = LazyJsonPages::from('https://example.com/api/v1/users')
->pagination('Invalid')
Expand All @@ -73,3 +98,19 @@
'https://example.com/api/v1/users' => 'pagination/page1.json',
]);
})->throws(InvalidPaginationException::class, 'The class [Invalid] should extend [Cerbero\LazyJsonPages\Paginations\Pagination].');

it('fails if an invalid JSON key is provided', function () {
$lazyCollection = LazyJsonPages::from('https://example.com/api/v1/users')
->totalPages('invalid')
->collect('data.*');

expect($lazyCollection)->toLoadItemsViaRequests([
'https://example.com/api/v1/users' => 'pagination/page1.json',
]);
})->throws(InvalidKeyException::class, 'The key [invalid] does not contain a valid value.');

it('fails if a pagination is not supported', function () {
LazyJsonPages::from('https://example.com/api/v1/users')
->collect('data.*')
->each(fn() => true);
})->throws(UnsupportedPaginationException::class, 'The provided configuration does not match with any supported pagination.');
74 changes: 74 additions & 0 deletions tests/Feature/RequestsOptimizationTest.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
<?php

use Cerbero\LazyJsonPages\Exceptions\OutOfAttemptsException;
use Cerbero\LazyJsonPages\LazyJsonPages;
use Cerbero\LazyJsonPages\Services\ClientFactory;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Middleware;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;

it('adds middleware for Guzzle', function () {
$log = collect();
Expand Down Expand Up @@ -67,3 +73,71 @@

expect($log)->sequence('sending', 'sent', 'sending', 'sending', 'sent', 'sent');
});

it('handles failures when sending HTTP requests asynchronously', function () {
$lazyCollection = LazyJsonPages::from('https://example.com/api/v1/users')
->async(3)
->attempts(3)
->backoff(fn() => 0)
->totalPages('meta.total_pages')
->collect('data.*');

$responses = [
new Response(body: file_get_contents(fixture('paginationWith5Pages/page1.json'))),
new RequestException('connection failed', new Request('GET', 'https://example.com/api/v1/users?page=2')),
new RequestException('connection failed', new Request('GET', 'https://example.com/api/v1/users?page=2')),
new RequestException('connection failed', new Request('GET', 'https://example.com/api/v1/users?page=2')),
new RequestException('connection failed', new Request('GET', 'https://example.com/api/v1/users?page=2')),
new RequestException('connection failed', new Request('GET', 'https://example.com/api/v1/users?page=2')),
new RequestException('connection failed', new Request('GET', 'https://example.com/api/v1/users?page=2')),
$e = new RequestException('connection failed', new Request('GET', 'https://example.com/api/v1/users?page=2')),
new Response(body: file_get_contents(fixture('paginationWith5Pages/page3.json'))),
];

ClientFactory::fake($responses, function() use ($lazyCollection, $e) {
try {
$lazyCollection->toArray();
} catch (Throwable $exception) {
expect($exception)
->toBeInstanceOf(OutOfAttemptsException::class)
->getPrevious()->toBe($e)
->failedPages->toContain(2)
->items->pluck('name')->all()->toContain('item11', 'item12', 'item13', 'item14', 'item15');
}
});
});

it('sets the timeouts', function () {
LazyJsonPages::from('https://nonexisting.test')
->totalPages('meta.total_pages')
->connectionTimeout(0.01)
->requestTimeout(0.01)
->collect('data.*')
->each(fn() => true);
})->throws(ConnectException::class, 'Connection refused for URI https://nonexisting.test');

it('respects rate limits', function () {
$lazyCollection = LazyJsonPages::from('https://example.com/api/v1/users')
->totalPages('meta.total_pages')
->throttle(requests: 1, perSeconds: 1)
->collect('data.*');

$responses = [
new Response(body: file_get_contents(fixture('pagination/page1.json'))),
new Response(body: file_get_contents(fixture('pagination/page2.json'))),
new Response(body: file_get_contents(fixture('pagination/page3.json'))),
];

$transactions = ClientFactory::fake($responses, function () use ($lazyCollection) {
expect($lazyCollection)->sequence(...require fixture('items.php'));
expect(ClientFactory::$fakedRateLimits)->toHaveCount(3);
});

$actualUris = array_map(fn(array $transaction) => (string) $transaction['request']->getUri(), $transactions);

expect($actualUris)->toBe([
'https://example.com/api/v1/users',
'https://example.com/api/v1/users?page=2',
'https://example.com/api/v1/users?page=3',
]);
});
20 changes: 20 additions & 0 deletions tests/Feature/SourceTest.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
<?php

use Cerbero\LazyJsonPages\Exceptions\RequestNotSentException;
use Cerbero\LazyJsonPages\Exceptions\UnsupportedSourceException;
use Cerbero\LazyJsonPages\LazyJsonPages;
use GuzzleHttp\Psr7\Response as Psr7Response;
use Illuminate\Http\Client\Response;

it('supports multiple sources', function (mixed $source, bool $requestsFirstPage) {
$lazyCollection = LazyJsonPages::from($source)
Expand All @@ -13,3 +17,19 @@
'https://example.com/api/v1/users?page=3' => 'pagination/page3.json',
]);
})->with('sources');

it('fails if a source is not supported', function () {
LazyJsonPages::from(123)
->totalPages('total_pages')
->collect('data.*')
->each(fn() => true);
})->throws(UnsupportedSourceException::class, 'The provided source is not supported.');

it('fails if a Laravel client response did not send the request', function () {
$response = new Response(new Psr7Response(body: '{"cursor":"abc"}'));

LazyJsonPages::from($response)
->cursor('cursor')
->collect('data.*')
->each(fn() => true);
})->throws(RequestNotSentException::class, 'The source did not send any HTTP request.');
26 changes: 26 additions & 0 deletions tests/Feature/StructureTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,19 @@
]);
});

it('supports paginations with the current page in the URI path and last page as a URI', function () {
$lazyCollection = LazyJsonPages::from('https://example.com/api/v1/users/page/1')
->pageInPath()
->lastPage('meta.last_page_uri')
->collect('data.*');

expect($lazyCollection)->toLoadItemsViaRequests([
'https://example.com/api/v1/users/page/1' => 'pagination/page1.json',
'https://example.com/api/v1/users/page/2' => 'pagination/page2.json',
'https://example.com/api/v1/users/page/3' => 'pagination/page3.json',
]);
});

it('supports a custom pattern for paginations with the current page in the URI path', function () {
$lazyCollection = LazyJsonPages::from('https://example.com/api/v1/users/page1')
->pageInPath('~/page(\d+)$~')
Expand Down Expand Up @@ -92,3 +105,16 @@
'https://example.com/api/v1/users?skip=10' => 'pagination/page3.json',
]);
});

it('supports paginations with custom page name', function () {
$lazyCollection = LazyJsonPages::from('https://example.com/api/v1/users')
->pageName('current_page')
->totalPages('meta.total_pages')
->collect('data.*');

expect($lazyCollection)->toLoadItemsViaRequests([
'https://example.com/api/v1/users' => 'pagination/page1.json',
'https://example.com/api/v1/users?current_page=2' => 'pagination/page2.json',
'https://example.com/api/v1/users?current_page=3' => 'pagination/page3.json',
]);
});
4 changes: 3 additions & 1 deletion tests/fixtures/pagination/page1.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
"total_pages": 3,
"total_items": 14,
"last_page": 3,
"cursor": "cursor1"
"last_page_uri": "https://example.com/api/v1/users/page/3",
"cursor": "cursor1",
"cursor_uri": "https://example.com/api/v1/users/page/cursor1"
}
}
4 changes: 3 additions & 1 deletion tests/fixtures/pagination/page2.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
"total_pages": 3,
"total_items": 14,
"last_page": 3,
"cursor": "cursor2"
"last_page_uri": "https://example.com/api/v1/users/page/3",
"cursor": "cursor2",
"cursor_uri": "https://example.com/api/v1/users/page/cursor2"
}
}
4 changes: 3 additions & 1 deletion tests/fixtures/pagination/page3.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
"total_pages": 3,
"total_items": 14,
"last_page": 3,
"cursor": null
"last_page_uri": "https://example.com/api/v1/users/page/3",
"cursor": null,
"cursor_uri": null
}
}
27 changes: 27 additions & 0 deletions tests/fixtures/paginationWith5Pages/page1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"data": [
{
"name": "item1"
},
{
"name": "item2"
},
{
"name": "item3"
},
{
"name": "item4"
},
{
"name": "item5"
}
],
"meta": {
"total_pages": 5,
"total_items": 24,
"last_page": 5,
"last_page_uri": "https://example.com/api/v1/users/page/5",
"cursor": "cursor1",
"cursor_uri": "https://example.com/api/v1/users/page/cursor1"
}
}
27 changes: 27 additions & 0 deletions tests/fixtures/paginationWith5Pages/page2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"data": [
{
"name": "item6"
},
{
"name": "item7"
},
{
"name": "item8"
},
{
"name": "item9"
},
{
"name": "item10"
}
],
"meta": {
"total_pages": 5,
"total_items": 24,
"last_page": 5,
"last_page_uri": "https://example.com/api/v1/users/page/5",
"cursor": "cursor2",
"cursor_uri": "https://example.com/api/v1/users/page/cursor2"
}
}
Loading

0 comments on commit 577edd2

Please sign in to comment.