From 1ef14ee0d3affe751ecc0ee40b9f619924bc9c15 Mon Sep 17 00:00:00 2001 From: Gregory Haddow Date: Tue, 6 Aug 2024 12:35:11 +0100 Subject: [PATCH] fix: ensure resource id's are always encoded/decoded when appropriate within a cursor test: test cursor id encoding/decoding --- src/Pagination/Cursor/CursorBuilder.php | 21 +- src/Pagination/Cursor/CursorPaginator.php | 6 +- src/Pagination/Cursor/CursorParser.php | 54 +++ src/Pagination/CursorPagination.php | 3 +- .../Pagination/CursorPaginationTest.php | 312 +++++++++++++++--- tests/lib/EncodedId.php | 91 +++++ 6 files changed, 427 insertions(+), 60 deletions(-) create mode 100644 src/Pagination/Cursor/CursorParser.php create mode 100644 tests/lib/EncodedId.php diff --git a/src/Pagination/Cursor/CursorBuilder.php b/src/Pagination/Cursor/CursorBuilder.php index adf4c6f..43c11ad 100644 --- a/src/Pagination/Cursor/CursorBuilder.php +++ b/src/Pagination/Cursor/CursorBuilder.php @@ -14,7 +14,7 @@ class CursorBuilder { private Builder|Relation $query; - private ?ID $id = null; + private ID $id; private string $keyName; @@ -26,6 +26,8 @@ class CursorBuilder private bool $keySort = true; + private CursorParser $parser; + /** * CursorBuilder constructor. * @@ -34,14 +36,16 @@ class CursorBuilder * @param string|null $key * the key column that the before/after cursors related to */ - public function __construct($query, string $key = null) + public function __construct($query, ID $id, string $key = null) { if (!$query instanceof Builder && !$query instanceof Relation) { throw new \InvalidArgumentException('Expecting an Eloquent query builder or relation.'); } $this->query = $query; + $this->id = $id; $this->keyName = $key ?: $this->guessKey(); + $this->parser = new CursorParser(IdParser::make($this->id), $this->keyName); } /** @@ -58,15 +62,6 @@ public function withDefaultPerPage(?int $perPage): self return $this; } - /** - * @return $this - */ - public function withIdField(?ID $id): self - { - $this->id = $id; - - return $this; - } public function withKeySort(bool $keySort): self { @@ -108,8 +103,8 @@ public function paginate(Cursor $cursor, array $columns = ['*']): CursorPaginato $this->applyKeySort(); $total = $this->getTotal(); - $laravelPaginator = $this->query->cursorPaginate($cursor->getLimit(), $columns, 'cursor', $this->convertCursor($cursor)); - $paginator = new CursorPaginator($laravelPaginator, $cursor, $total); + $laravelPaginator = $this->query->cursorPaginate($cursor->getLimit(), $columns, 'cursor', $this->parser->decode($cursor)); + $paginator = new CursorPaginator($this->parser, $laravelPaginator, $cursor, $total); return $paginator->withCurrentPath(); } diff --git a/src/Pagination/Cursor/CursorPaginator.php b/src/Pagination/Cursor/CursorPaginator.php index 03552f4..8cd848f 100644 --- a/src/Pagination/Cursor/CursorPaginator.php +++ b/src/Pagination/Cursor/CursorPaginator.php @@ -17,7 +17,7 @@ class CursorPaginator implements \IteratorAggregate, \Countable /** * CursorPaginator constructor. */ - public function __construct(private readonly LaravelCursorPaginator $laravelPaginator, private readonly Cursor $cursor, private readonly int|null $total = null) + public function __construct(private readonly CursorParser $parser, private readonly LaravelCursorPaginator $laravelPaginator, private readonly Cursor $cursor, private readonly int|null $total = null) { $this->items = Collection::make($this->laravelPaginator->items()); } @@ -34,7 +34,7 @@ public function firstItem(): ?string return null; } - return $this->laravelPaginator->getCursorForItem($this->items->first(), false)->encode(); + return $this->parser->encode($this->laravelPaginator->getCursorForItem($this->items->first(), false)); } public function lastItem(): ?string @@ -43,7 +43,7 @@ public function lastItem(): ?string return null; } - return $this->laravelPaginator->getCursorForItem($this->items->last())->encode(); + return $this->parser->encode($this->laravelPaginator->getCursorForItem($this->items->last())); } public function hasMorePages(): bool diff --git a/src/Pagination/Cursor/CursorParser.php b/src/Pagination/Cursor/CursorParser.php new file mode 100644 index 0000000..3b8b71a --- /dev/null +++ b/src/Pagination/Cursor/CursorParser.php @@ -0,0 +1,54 @@ +parameter($this->keyName); + if (!$key) { + return $cursor->encode(); + } + + $encodedId = $this->idParser->encode($key); + $parameters = $cursor->toArray(); + unset($parameters['_pointsToNextItems']); + $parameters[$this->keyName] = $encodedId; + + $newCursor = new LaravelCursor($parameters, $cursor->pointsToNextItems()); + return $newCursor->encode(); + } + + public function decode(Cursor $cursor): ?LaravelCursor + { + $encodedCursor = $cursor->isBefore() ? $cursor->getBefore() : $cursor->getAfter(); + if (!is_string($encodedCursor)) { + return null; + } + + $parameters = json_decode(base64_decode(str_replace(['-', '_'], ['+', '/'], $encodedCursor)), true); + + if (json_last_error() !== JSON_ERROR_NONE) { + return null; + } + + $pointsToNextItems = $parameters['_pointsToNextItems']; + unset($parameters['_pointsToNextItems']); + if (isset($parameters[$this->keyName])) { + $parameters[$this->keyName] = $this->idParser->decode( + $parameters[$this->keyName], + ); + } + + return new LaravelCursor($parameters, $pointsToNextItems); + } +} diff --git a/src/Pagination/CursorPagination.php b/src/Pagination/CursorPagination.php index e7214a3..b010f9c 100644 --- a/src/Pagination/CursorPagination.php +++ b/src/Pagination/CursorPagination.php @@ -206,7 +206,6 @@ public function paginate($query, array $page): Page $paginator = $this ->query($query) - ->withIdField($this->id) ->withDirection($this->direction) ->withKeySort($this->keySort) ->withDefaultPerPage($this->defaultPerPage) @@ -227,7 +226,7 @@ public function paginate($query, array $page): Page */ private function query(Builder|Relation $query): CursorBuilder { - return new CursorBuilder($query, $this->primaryKey); + return new CursorBuilder($query, $this->id, $this->primaryKey); } /** diff --git a/tests/lib/Acceptance/Pagination/CursorPaginationTest.php b/tests/lib/Acceptance/Pagination/CursorPaginationTest.php index 8ccb101..7f4ee89 100644 --- a/tests/lib/Acceptance/Pagination/CursorPaginationTest.php +++ b/tests/lib/Acceptance/Pagination/CursorPaginationTest.php @@ -25,6 +25,7 @@ use LaravelJsonApi\Eloquent\Fields\ID; use LaravelJsonApi\Eloquent\Pagination\CursorPagination; use LaravelJsonApi\Eloquent\Tests\Acceptance\TestCase; +use LaravelJsonApi\Eloquent\Tests\EncodedId; use PHPUnit\Framework\MockObject\MockObject; class CursorPaginationTest extends TestCase @@ -35,6 +36,11 @@ class CursorPaginationTest extends TestCase */ private CursorPagination $paginator; + /** + * @var EncodedId|null + */ + private ?EncodedId $encodedId = null; + /** * @var PostSchema|MockObject */ @@ -66,8 +72,8 @@ protected function setUp(): void ->setConstructorArgs(['server' => $this->server()]) ->getMock(); - $this->posts->method('pagination')->willReturn($this->paginator); - $this->videos->method('pagination')->willReturn($this->paginator); + $this->posts->method('pagination')->willReturnCallback(fn () => $this->paginator); + $this->videos->method('pagination')->willReturnCallback(fn () => $this->paginator); $this->app->instance(PostSchema::class, $this->posts); $this->app->instance(VideoSchema::class, $this->videos); @@ -85,10 +91,16 @@ public function testDefaultPagination(): void $this->posts->method('defaultPagination')->willReturn(['limit' => 10]); $meta = [ - 'from' => 'eyJpZCI6NCwiX3BvaW50c1RvTmV4dEl0ZW1zIjpmYWxzZX0', + 'from' => $this->encodeCursor([ + "id" => "4", + "_pointsToNextItems" => false + ]), 'hasMore' => false, 'perPage' => 10, - 'to' => 'eyJpZCI6MSwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ', + 'to' => $this->encodeCursor([ + "id" => "1", + "_pointsToNextItems" => true + ]), ]; $links = [ @@ -106,7 +118,6 @@ public function testDefaultPagination(): void ->repository() ->queryAll() ->firstOrPaginate(null); - $this->assertInstanceOf(Page::class, $page); $this->assertSame(['page' => $meta], $page->meta()); $this->assertSame($links, $page->links()->toArray()); @@ -220,10 +231,16 @@ public function testWithoutCursor(): void $posts = Post::factory()->count(4)->create(); $meta = [ - 'from' => 'eyJpZCI6NCwiX3BvaW50c1RvTmV4dEl0ZW1zIjpmYWxzZX0', + 'from' => $this->encodeCursor([ + "id" => "4", + "_pointsToNextItems" => false + ]), 'hasMore' => true, 'perPage' => 3, - 'to' => 'eyJpZCI6MiwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ', + 'to' => $this->encodeCursor([ + "id" => "2", + "_pointsToNextItems" => true + ]) ]; $links = [ @@ -234,7 +251,10 @@ public function testWithoutCursor(): void ], 'next' => [ 'href' => 'http://localhost/api/v1/posts?' . Arr::query([ - 'page' => ['after' => 'eyJpZCI6MiwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ', 'limit' => '3'] + 'page' => ['after' => $this->encodeCursor([ + "id" => "2", + "_pointsToNextItems" => true + ]), 'limit' => '3'] ]), ] ]; @@ -253,10 +273,16 @@ public function testAfter(): void $this->paginator->withCamelCaseMeta(); $meta = [ - 'from' => 'eyJpZCI6MSwiX3BvaW50c1RvTmV4dEl0ZW1zIjpmYWxzZX0', + 'from' => $this->encodeCursor([ + "id" => "1", + "_pointsToNextItems" => false + ]), 'hasMore' => false, 'perPage' => 3, - 'to' => 'eyJpZCI6MSwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ', + 'to' => $this->encodeCursor([ + "id" => "1", + "_pointsToNextItems" => true + ]) ]; $links = [ @@ -267,18 +293,79 @@ public function testAfter(): void ], 'prev' => [ 'href' => 'http://localhost/api/v1/posts?' . Arr::query([ - 'page' => ['before' => 'eyJpZCI6MSwiX3BvaW50c1RvTmV4dEl0ZW1zIjpmYWxzZX0', 'limit' => '3'] + 'page' => ['before' => $this->encodeCursor([ + "id" => "1", + "_pointsToNextItems" => false + ]), 'limit' => '3'] ]), ], ]; - $page = $this->posts->repository()->queryAll()->paginate(['after' => 'eyJpZCI6MiwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ', 'limit' => '3']); + $page = $this->posts->repository()->queryAll()->paginate(['after' => $this->encodeCursor([ + "id" => "2", + "_pointsToNextItems" => true + ]), 'limit' => '3']); $this->assertSame(['page' => $meta], $page->meta()); $this->assertSame($links, $page->links()->toArray()); $this->assertPage([$posts->first()], $page); } + public function testAfterWithIdEncoding(): void + { + $this->withIdEncoding(); + + $posts = Post::factory()->count(10)->create()->values(); + + $expected = [$posts[6], $posts[5], $posts[4]]; + + $meta = [ + 'from' => $this->encodeCursor([ + "id" => 'TEST-7', + "_pointsToNextItems" => false + ]), + 'hasMore' => true, + 'perPage' => 3, + 'to' => $this->encodeCursor([ + "id" => 'TEST-5', + "_pointsToNextItems" => true + ]), + ]; + + $links = [ + 'first' => [ + 'href' => 'http://localhost/api/v1/posts?' . Arr::query([ + 'page' => ['limit' => '3'] + ]), + ], + 'next' => [ + 'href' => 'http://localhost/api/v1/posts?' . Arr::query([ + 'page' => ['after' => $this->encodeCursor([ + "id" => "TEST-5", + "_pointsToNextItems" => true + ]),'limit' => '3'] + ]),], + 'prev' => [ + 'href' => 'http://localhost/api/v1/posts?' . Arr::query([ + 'page' => ['before' => $this->encodeCursor([ + "id" => "TEST-7", + "_pointsToNextItems" => false + ]),'limit' => '3'] + ]),], + ]; + + $page = $this->posts->repository()->queryAll()->paginate([ + 'after' => $this->encodeCursor([ + "id" => 'TEST-8', + "_pointsToNextItems" => true + ]), + 'limit' => 3, + ]); + $this->assertSame(['page' => $meta], $page->meta()); + $this->assertSame($links, $page->links()->toArray()); + $this->assertPage($expected, $page); + } + public function testBefore(): void { $posts = Post::factory()->count(4)->create(); @@ -286,10 +373,16 @@ public function testBefore(): void $this->paginator->withCamelCaseMeta(); $meta = [ - 'from' => 'eyJpZCI6NCwiX3BvaW50c1RvTmV4dEl0ZW1zIjpmYWxzZX0', + 'from' => $this->encodeCursor([ + "id" => "4", + "_pointsToNextItems" => false + ]), 'hasMore' => true, 'perPage' => 3, - 'to' => 'eyJpZCI6MiwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ', + 'to' => $this->encodeCursor([ + "id" => "2", + "_pointsToNextItems" => true + ]), ]; $links = [ @@ -300,18 +393,72 @@ public function testBefore(): void ], 'prev' => [ 'href' => 'http://localhost/api/v1/posts?' . Arr::query([ - 'page' => ['before' => 'eyJpZCI6NCwiX3BvaW50c1RvTmV4dEl0ZW1zIjpmYWxzZX0', 'limit' => '3'] + 'page' => ['before' => $this->encodeCursor([ + "id" => "4", + "_pointsToNextItems" => false + ]), 'limit' => '3'] ]), ], ]; - $page = $this->posts->repository()->queryAll()->paginate(['before' => 'eyJpZCI6MSwiX3BvaW50c1RvTmV4dEl0ZW1zIjpmYWxzZX0', 'limit' => '3']); + $page = $this->posts->repository()->queryAll()->paginate(['before' => $this->encodeCursor([ + "id" => "1", + "_pointsToNextItems" => false + ]), 'limit' => '3']); $this->assertSame(['page' => $meta], $page->meta()); $this->assertSame($links, $page->links()->toArray()); $this->assertPage($posts->reverse()->take(3), $page); } + public function testBeforeWithIdEncoding(): void + { + $this->withIdEncoding(); + + $posts = Post::factory()->count(10)->create()->values(); + + $expected = [$posts[6], $posts[5], $posts[4]]; + + $meta = [ + 'from' => $this->encodeCursor([ + "id" => 'TEST-7', + "_pointsToNextItems" => false + ]), + 'hasMore' => true, + 'perPage' => 3, + 'to' => $this->encodeCursor([ + "id" => 'TEST-5', + "_pointsToNextItems" => true + ]), + ]; + + $links = [ + 'first' => [ + 'href' => 'http://localhost/api/v1/posts?' . Arr::query([ + 'page' => ['limit' => '3'] + ]), + ], + 'prev' => [ + 'href' => 'http://localhost/api/v1/posts?' . Arr::query([ + 'page' => ['before' => $this->encodeCursor([ + "id" => "TEST-7", + "_pointsToNextItems" => false + ]),'limit' => '3'] + ]),], + ]; + + $page = $this->posts->repository()->queryAll()->paginate([ + 'before' => $this->encodeCursor([ + "id" => 'TEST-4', + "_pointsToNextItems" => false + ]), + 'limit' => 3, + ]); + $this->assertSame(['page' => $meta], $page->meta()); + $this->assertSame($links, $page->links()->toArray()); + $this->assertPage($expected, $page); + } + /** * When no page size is provided, the default is used from the model. */ @@ -321,10 +468,16 @@ public function testItUsesModelDefaultPerPage(): void $posts = Post::factory()->count($expected + 1)->create(); $meta = [ - 'from' => 'eyJpZCI6MTYsIl9wb2ludHNUb05leHRJdGVtcyI6ZmFsc2V9', + 'from' => $this->encodeCursor([ + "id" => (string) ($expected + 1), + "_pointsToNextItems" => false + ]), 'hasMore' => true, 'perPage' => $expected, - 'to' => 'eyJpZCI6MiwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ', + 'to' => $this->encodeCursor([ + "id" => '2', + "_pointsToNextItems" => true + ]), ]; @@ -336,7 +489,10 @@ public function testItUsesModelDefaultPerPage(): void ], 'next' => [ 'href' => 'http://localhost/api/v1/posts?' . Arr::query([ - 'page' => ['after' => 'eyJpZCI6MiwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ', 'limit' => $expected] + 'page' => ['after' => $this->encodeCursor([ + "id" => '2', + "_pointsToNextItems" => true + ]), 'limit' => $expected] ]), ], ]; @@ -360,10 +516,16 @@ public function testItUsesDefaultPerPage(): void $posts = Post::factory()->count($expected + 1)->create(); $meta = [ - 'from' => 'eyJpZCI6MTEsIl9wb2ludHNUb05leHRJdGVtcyI6ZmFsc2V9', + 'from' => $this->encodeCursor([ + "id" => (string) ($expected + 1), + "_pointsToNextItems" => false + ]), 'hasMore' => true, - 'perPage' => 10, - 'to' => 'eyJpZCI6MiwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ', + 'perPage' => $expected, + 'to' => $this->encodeCursor([ + "id" => '2', + "_pointsToNextItems" => true + ]), ]; $links = [ @@ -374,7 +536,10 @@ public function testItUsesDefaultPerPage(): void ], 'next' => [ 'href' => 'http://localhost/api/v1/posts?' . Arr::query([ - 'page' => ['after' => 'eyJpZCI6MiwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ', 'limit' => $expected] + 'page' => ['after' => $this->encodeCursor([ + "id" => '2', + "_pointsToNextItems" => true + ]), 'limit' => $expected] ]), ], ]; @@ -391,10 +556,10 @@ public function testPageWithReverseKey(): void $posts = Post::factory()->count(4)->create(); $page = $this->posts->repository()->queryAll() - ->sort('-id') + ->sort('id') ->paginate(['limit' => '3']); - $this->assertPage($posts->reverse()->take(3), $page); + $this->assertPage($posts->take(3), $page); } /** @@ -521,7 +686,9 @@ public function testCustomPageKeys(): void ], 'next' => [ 'href' => 'http://localhost/api/v1/posts?' . Arr::query([ - 'page' => ['next' => 'eyJpZCI6MiwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ', 'perPage' => '3'] + 'page' => ['next' => $this->encodeCursor( + ["id" => "2", "_pointsToNextItems" => true] + ), 'perPage' => '3'] ]), ], ]; @@ -539,12 +706,16 @@ public function testCustomPageKeys(): void ], 'prev' => [ 'href' => 'http://localhost/api/v1/posts?' . Arr::query([ - 'page' => ['perPage' => '3', 'prev' => 'eyJpZCI6MSwiX3BvaW50c1RvTmV4dEl0ZW1zIjpmYWxzZX0'] + 'page' => ['perPage' => '3', 'prev' => $this->encodeCursor( + ["id" => "1", "_pointsToNextItems" => false] + )] ]), ], ]; - $page = $this->posts->repository()->queryAll()->paginate(['next' => 'eyJpZCI6MiwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ', 'perPage' => '3']); + $page = $this->posts->repository()->queryAll()->paginate(['next' => $this->encodeCursor( + ["id" => "2", "_pointsToNextItems" => true] + ), 'perPage' => '3']); $this->assertSame($links, $page->links()->toArray()); } @@ -556,10 +727,16 @@ public function testSnakeCaseMetaAndCustomMetaKey(): void $this->paginator->withMetaKey('paginator')->withSnakeCaseMeta(); $meta = [ - 'from' => 'eyJpZCI6NCwiX3BvaW50c1RvTmV4dEl0ZW1zIjpmYWxzZX0', + 'from' => $this->encodeCursor([ + "id" => "4", + "_pointsToNextItems" => false + ]), 'has_more' => true, 'per_page' => 3, - 'to' => 'eyJpZCI6MiwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ', + 'to' => $this->encodeCursor([ + "id" => "2", + "_pointsToNextItems" => true + ]) ]; $page = $this->posts->repository()->queryAll()->paginate(['limit' => '3']); @@ -575,10 +752,16 @@ public function testDashCaseMeta(): void $this->paginator->withDashCaseMeta(); $meta = [ - 'from' => 'eyJpZCI6NCwiX3BvaW50c1RvTmV4dEl0ZW1zIjpmYWxzZX0', + 'from' => $this->encodeCursor([ + "id" => "4", + "_pointsToNextItems" => false + ]), 'has-more' => true, 'per-page' => 3, - 'to' => 'eyJpZCI6MiwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ', + 'to' => $this->encodeCursor([ + "id" => "2", + "_pointsToNextItems" => true + ]) ]; $page = $this->posts->repository()->queryAll()->paginate(['limit' => '3']); @@ -594,10 +777,16 @@ public function testMetaNotNested(): void $this->paginator->withoutNestedMeta(); $meta = [ - 'from' => 'eyJpZCI6NCwiX3BvaW50c1RvTmV4dEl0ZW1zIjpmYWxzZX0', + 'from' => $this->encodeCursor([ + "id" => "4", + "_pointsToNextItems" => false + ]), 'hasMore' => true, 'perPage' => 3, - 'to' => 'eyJpZCI6MiwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ', + 'to' => $this->encodeCursor([ + "id" => "2", + "_pointsToNextItems" => true + ]) ]; $page = $this->posts->repository()->queryAll()->paginate(['limit' => '3']); @@ -620,7 +809,10 @@ public function testItCanRemoveMeta(): void ], 'next' => [ 'href' => 'http://localhost/api/v1/posts?' . Arr::query([ - 'page' => ['after' => 'eyJpZCI6MiwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ', 'limit' => '3'] + 'page' => ['after' => $this->encodeCursor([ + "id" => "2", + "_pointsToNextItems" => true + ]), 'limit' => '3'] ]), ], ]; @@ -654,7 +846,10 @@ public function testUrlsIncludeOtherQueryParameters(): void 'fields' => $fields, 'filter' => ['slugs' => $slugs], 'include' => 'author', - 'page' => ['after' => 'eyJpZCI6MiwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ', 'limit' => '3'], + 'page' => ['after' => $this->encodeCursor([ + "id" => "2", + "_pointsToNextItems" => true + ]), 'limit' => '3'], ]), ], ]; @@ -678,10 +873,16 @@ public function testWithTotal() $this->paginator->withTotal(); $meta = [ - 'from' => 'eyJpZCI6NCwiX3BvaW50c1RvTmV4dEl0ZW1zIjpmYWxzZX0', + 'from' => $this->encodeCursor([ + "id" => "4", + "_pointsToNextItems" => false + ]), 'hasMore' => true, 'perPage' => 3, - 'to' => 'eyJpZCI6MiwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ', + 'to' => $this->encodeCursor([ + "id" => "2", + "_pointsToNextItems" => true + ]), 'total' => 4, ]; @@ -698,7 +899,10 @@ public function testWithTotal() $page = $this->posts ->repository() ->queryAll() - ->paginate(['after' => 'eyJpZCI6MiwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ', 'limit' => 3]); + ->paginate(['after' => $this->encodeCursor([ + "id" => "2", + "_pointsToNextItems" => true + ]), 'limit' => 3]); $this->assertInstanceOf(Page::class, $page); $this->assertArrayHasKey('page', $page->meta()); @@ -712,10 +916,16 @@ public function testWithTotalOnFirstPage() $this->paginator->withTotalOnFirstPage(); $meta = [ - 'from' => 'eyJpZCI6NCwiX3BvaW50c1RvTmV4dEl0ZW1zIjpmYWxzZX0', + 'from' => $this->encodeCursor([ + "id" => "4", + "_pointsToNextItems" => false + ]), 'hasMore' => true, 'perPage' => 3, - 'to' => 'eyJpZCI6MiwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ', + 'to' => $this->encodeCursor([ + "id" => "2", + "_pointsToNextItems" => true + ]), 'total' => 4, ]; @@ -732,7 +942,10 @@ public function testWithTotalOnFirstPage() $page = $this->posts ->repository() ->queryAll() - ->paginate(['after' => 'eyJpZCI6MiwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ', 'limit' => 3]); + ->paginate(['after' => $this->encodeCursor([ + "id" => "2", + "_pointsToNextItems" => true + ]), 'limit' => 3]); $this->assertInstanceOf(Page::class, $page); $this->assertArrayHasKey('page', $page->meta()); @@ -753,4 +966,19 @@ private function assertPage($expected, $actual): void $this->assertSame(array_values($expected), array_values($actual)); } + /** + * @return void + */ + private function withIdEncoding(): void + { + $this->paginator = CursorPagination::make( + $this->encodedId = new EncodedId() + ); + } + + private function encodeCursor(array $params) : string + { + return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode(json_encode($params))); + } + } diff --git a/tests/lib/EncodedId.php b/tests/lib/EncodedId.php new file mode 100644 index 0000000..4afc4cd --- /dev/null +++ b/tests/lib/EncodedId.php @@ -0,0 +1,91 @@ +