Skip to content

Commit 3d40d7f

Browse files
committed
test(state): pagination state read from the Link header alone
1 parent 7f7a094 commit 3d40d7f

1 file changed

Lines changed: 170 additions & 0 deletions

File tree

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Functional;
15+
16+
use ApiPlatform\Test\ApiTestCase;
17+
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\PaginationLink\PaginationLinkResource;
18+
use ApiPlatform\Tests\SetupClassResourcesTrait;
19+
use Symfony\Component\WebLink\HttpHeaderParser;
20+
use Symfony\Contracts\HttpClient\ResponseInterface;
21+
22+
final class PaginationHeadersAcceptanceTest extends ApiTestCase
23+
{
24+
use SetupClassResourcesTrait;
25+
26+
private const JSON_LD = ['Accept' => 'application/ld+json'];
27+
28+
protected static ?bool $alwaysBootKernel = false;
29+
30+
/**
31+
* @return class-string[]
32+
*/
33+
public static function getResources(): array
34+
{
35+
return [PaginationLinkResource::class];
36+
}
37+
38+
public function testHeadTellsThePageCountWithoutAnyBodyButNotThePageSize(): void
39+
{
40+
$response = self::createClient()->request('HEAD', '/pagination_links?page=2', ['headers' => self::JSON_LD]);
41+
42+
$this->assertResponseStatusCodeSame(200);
43+
$this->assertSame('', $response->getContent(false));
44+
$state = self::paginationState($response);
45+
$this->assertSame(2, $state['current_page']);
46+
$this->assertSame(3, $state['page_count']);
47+
$this->assertNull($state['items_per_page']);
48+
$this->assertNull($state['total_min']);
49+
$this->assertNull($state['total_max']);
50+
}
51+
52+
public function testGetAdvertisesTheStateInTheLinkHeader(): void
53+
{
54+
$response = self::createClient()->request('GET', '/pagination_links?page=3', ['headers' => self::JSON_LD]);
55+
56+
$this->assertResponseStatusCodeSame(200);
57+
$state = self::paginationState($response);
58+
$this->assertSame(3, $state['current_page']);
59+
$this->assertSame(3, $state['page_count']);
60+
$this->assertArrayNotHasKey('next', $state['relations']);
61+
$this->assertArrayHasKey('prev', $state['relations']);
62+
}
63+
64+
public function testASinglePageCollectionStillTellsItsPageCountOnHead(): void
65+
{
66+
$response = self::createClient()->request('HEAD', '/pagination_links_single', ['headers' => self::JSON_LD]);
67+
68+
$this->assertResponseStatusCodeSame(200);
69+
$this->assertSame('', $response->getContent(false));
70+
$state = self::paginationState($response);
71+
$this->assertSame(1, $state['current_page']);
72+
$this->assertSame(1, $state['page_count']);
73+
$this->assertNull($state['items_per_page']);
74+
$this->assertNull($state['total_min']);
75+
$this->assertNull($state['total_max']);
76+
$this->assertArrayNotHasKey('prev', $state['relations']);
77+
$this->assertArrayNotHasKey('next', $state['relations']);
78+
}
79+
80+
public function testAPartialPaginatorAdmitsItDoesNotKnowTheTotal(): void
81+
{
82+
$response = self::createClient()->request('HEAD', '/pagination_links_partial?page=2', ['headers' => self::JSON_LD]);
83+
84+
$this->assertResponseStatusCodeSame(200);
85+
$this->assertSame('', $response->getContent(false));
86+
$state = self::paginationState($response);
87+
$this->assertArrayNotHasKey('last', $state['relations']);
88+
$this->assertNull($state['page_count']);
89+
$this->assertNull($state['total_min']);
90+
$this->assertNull($state['total_max']);
91+
$this->assertSame(2, $state['current_page']);
92+
$this->assertNull($state['items_per_page']);
93+
$this->assertArrayHasKey('next', $state['relations']);
94+
}
95+
96+
public function testTheStateIsIdenticalBetweenHeadAndGet(): void
97+
{
98+
$client = self::createClient();
99+
100+
$head = self::paginationState($client->request('HEAD', '/pagination_links?page=2', ['headers' => self::JSON_LD]));
101+
$get = self::paginationState($client->request('GET', '/pagination_links?page=2', ['headers' => self::JSON_LD]));
102+
103+
$this->assertSame($get, $head);
104+
}
105+
106+
public function testTheLinksCarryOnlyWhatTheClientSent(): void
107+
{
108+
$client = self::createClient();
109+
110+
$withoutAPageSize = self::paginationState($client->request('GET', '/pagination_links?page=2', ['headers' => self::JSON_LD]));
111+
$withAPageSize = self::paginationState($client->request('GET', '/pagination_links_client_size?itemsPerPage=5&page=2', ['headers' => self::JSON_LD]));
112+
113+
$this->assertSame([], self::pageSizes($withoutAPageSize['relations']));
114+
$this->assertSame(['self' => '5', 'first' => '5', 'prev' => '5', 'next' => '5', 'last' => '5'], self::pageSizes($withAPageSize['relations']));
115+
}
116+
117+
/**
118+
* @return array{current_page: ?int, items_per_page: ?int, page_count: ?int, total_min: ?int, total_max: ?int, relations: array<string, string>}
119+
*/
120+
private static function paginationState(ResponseInterface $response): array
121+
{
122+
$headers = $response->getHeaders(false);
123+
124+
$relations = [];
125+
foreach ((new HttpHeaderParser())->parse($headers['link'] ?? [])->getLinks() as $link) {
126+
foreach ($link->getRels() as $rel) {
127+
$relations[$rel] = $link->getHref();
128+
}
129+
}
130+
131+
$self = self::queryParameters($relations['self'] ?? null);
132+
$last = self::queryParameters($relations['last'] ?? null);
133+
134+
$itemsPerPage = isset($self['itemsPerPage']) ? (int) $self['itemsPerPage'] : null;
135+
$pageCount = isset($last['page']) ? (int) $last['page'] : null;
136+
137+
return [
138+
'current_page' => isset($self['page']) ? (int) $self['page'] : null,
139+
'items_per_page' => $itemsPerPage,
140+
'page_count' => $pageCount,
141+
'total_min' => null === $pageCount || null === $itemsPerPage ? null : ($pageCount - 1) * $itemsPerPage + 1,
142+
'total_max' => null === $pageCount || null === $itemsPerPage ? null : $pageCount * $itemsPerPage,
143+
'relations' => $relations,
144+
];
145+
}
146+
147+
/**
148+
* @param array<string, string> $relations
149+
*
150+
* @return array<string, string>
151+
*/
152+
private static function pageSizes(array $relations): array
153+
{
154+
return array_filter(array_map(static fn (string $href): ?string => self::queryParameters($href)['itemsPerPage'] ?? null, $relations), \is_string(...));
155+
}
156+
157+
/**
158+
* @return array<string, string>
159+
*/
160+
private static function queryParameters(?string $href): array
161+
{
162+
if (null === $href) {
163+
return [];
164+
}
165+
166+
parse_str(parse_url($href, \PHP_URL_QUERY) ?: '', $query);
167+
168+
return array_filter($query, \is_string(...));
169+
}
170+
}

0 commit comments

Comments
 (0)