Skip to content

Commit 3312143

Browse files
authored
Merge pull request #8284 from pmattmann/feature/material-items-endpoint-require-filter
MaterialItem-Endpoint requires filter on camp, period, materialList or materialNode
2 parents cda61ae + 692713e commit 3312143

14 files changed

+702
-245
lines changed

api/config/services.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,14 @@ services:
8585
arguments:
8686
- '@api_platform.doctrine.orm.state.collection_provider'
8787

88+
App\State\ChecklistItemCollectionProvider:
89+
arguments:
90+
- '@api_platform.doctrine.orm.state.collection_provider'
91+
92+
App\State\MaterialItemCollectionProvider:
93+
arguments:
94+
- '@api_platform.doctrine.orm.state.collection_provider'
95+
8896
App\Serializer\SerializerContextBuilder:
8997
decorates: 'api_platform.serializer.context_builder'
9098

api/src/Entity/ChecklistItem.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use App\Entity\ContentNode\ChecklistNode;
1616
use App\InputFilter;
1717
use App\Repository\ChecklistItemRepository;
18+
use App\State\ChecklistItemCollectionProvider;
1819
use App\Util\EntityMap;
1920
use App\Validator\AssertNoLoop;
2021
use App\Validator\ChecklistItem\AssertBelongsToSameChecklist;
@@ -49,7 +50,8 @@
4950
validationContext: ['groups' => ['delete']],
5051
),
5152
new GetCollection(
52-
security: 'is_authenticated()'
53+
security: 'is_authenticated()',
54+
provider: ChecklistItemCollectionProvider::class
5355
),
5456
new Post(
5557
denormalizationContext: ['groups' => ['write', 'create']],

api/src/Entity/MaterialItem.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use App\Entity\ContentNode\MaterialNode;
1616
use App\InputFilter;
1717
use App\Repository\MaterialItemRepository;
18+
use App\State\MaterialItemCollectionProvider;
1819
use App\State\MaterialItemCreateProcessor;
1920
use App\Util\EntityMap;
2021
use App\Validator\AssertBelongsToSameCamp;
@@ -43,7 +44,8 @@
4344
security: 'is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object)'
4445
),
4546
new GetCollection(
46-
security: 'is_authenticated()'
47+
security: 'is_authenticated()',
48+
provider: MaterialItemCollectionProvider::class
4749
),
4850
new Post(
4951
processor: MaterialItemCreateProcessor::class,
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
namespace App\State;
4+
5+
use ApiPlatform\Metadata\Operation;
6+
use ApiPlatform\State\ProviderInterface;
7+
use App\Entity\ChecklistItem;
8+
use Symfony\Component\HttpFoundation\RequestStack;
9+
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
10+
11+
/**
12+
* @template-implements ProviderInterface<ChecklistItem>
13+
*/
14+
class ChecklistItemCollectionProvider implements ProviderInterface {
15+
public function __construct(
16+
private ProviderInterface $decorated,
17+
private RequestStack $requestStack
18+
) {}
19+
20+
public function provide(Operation $operation, array $uriVariables = [], array $context = []): array|object|null {
21+
$request = $this->requestStack->getCurrentRequest();
22+
$hasFilter = $request?->query->has('checklist') || $request?->query->has('checklist_camp') || $request?->query->has('checklistNodes');
23+
24+
if (!$hasFilter) {
25+
throw new BadRequestHttpException('Filter on checklist, checklist.camp or checklistNodes is required');
26+
}
27+
28+
return $this->decorated->provide($operation, $uriVariables, $context);
29+
}
30+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
namespace App\State;
4+
5+
use ApiPlatform\Metadata\Operation;
6+
use ApiPlatform\State\ProviderInterface;
7+
use App\Entity\MaterialItem;
8+
use Symfony\Component\HttpFoundation\RequestStack;
9+
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
10+
11+
/**
12+
* @template-implements ProviderInterface<MaterialItem>
13+
*/
14+
class MaterialItemCollectionProvider implements ProviderInterface {
15+
public function __construct(
16+
private ProviderInterface $decorated,
17+
private RequestStack $requestStack
18+
) {}
19+
20+
public function provide(Operation $operation, array $uriVariables = [], array $context = []): array|object|null {
21+
$request = $this->requestStack->getCurrentRequest();
22+
$hasFilter = $request?->query->has('camp') || $request?->query->has('period') || $request?->query->has('materialList') || $request?->query->has('materialNode');
23+
24+
if (!$hasFilter) {
25+
throw new BadRequestHttpException('Filter on camp, period, materialList or materialNode is required');
26+
}
27+
28+
return $this->decorated->provide($operation, $uriVariables, $context);
29+
}
30+
}

api/tests/Api/ChecklistItems/ListChecklistItemsTest.php

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,30 +17,12 @@ public function testListChecklistItemsIsDeniedForAnonymousUser() {
1717
]);
1818
}
1919

20-
public function testListChecklistItemsIsAllowedForLoggedInUserButFiltered() {
20+
public function testListChecklistItemsWithoutFilterIsNotAllowedForLoggedInUser() {
2121
// precondition: There is a checklist-item that the user doesn't have access to
2222
$this->assertNotEmpty(static::$fixtures['checklistItemUnrelated_1_1']);
2323

2424
$response = static::createClientWithCredentials()->request('GET', '/checklist_items');
25-
$this->assertResponseStatusCodeSame(200);
26-
$this->assertJsonContains([
27-
'totalItems' => 7,
28-
'_links' => [
29-
'items' => [],
30-
],
31-
'_embedded' => [
32-
'items' => [],
33-
],
34-
]);
35-
$this->assertEqualsCanonicalizing([
36-
['href' => $this->getIriFor('checklistItem1_1_1')],
37-
['href' => $this->getIriFor('checklistItem1_1_2')],
38-
['href' => $this->getIriFor('checklistItem1_1_2_3')],
39-
['href' => $this->getIriFor('checklistItem1_1_2_3_4')],
40-
['href' => $this->getIriFor('checklistItem2_1_1')],
41-
['href' => $this->getIriFor('checklistItemCampPrototype_1_1')],
42-
['href' => $this->getIriFor('checklistItemCampShared_1_1')],
43-
], $response->toArray()['_links']['items']);
25+
$this->assertResponseStatusCodeSame(400);
4426
}
4527

4628
public function testListChecklistItemsFilteredByChecklistIsAllowedForCollaborator() {

api/tests/Api/MaterialItems/ListMaterialItemsTest.php

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,28 +17,12 @@ public function testListMaterialItemsIsDeniedForAnonymousUser() {
1717
]);
1818
}
1919

20-
public function testListMaterialItemsIsAllowedForLoggedInUserButFiltered() {
20+
public function testListMaterialItemsWithoutFilterIsNotAllowedForLoggedInUser() {
2121
// precondition: There is a material item that the user doesn't have access to
2222
$this->assertNotEmpty(static::$fixtures['materialItem1period1campUnrelated']);
2323

2424
$response = static::createClientWithCredentials()->request('GET', '/material_items');
25-
$this->assertResponseStatusCodeSame(200);
26-
$this->assertJsonContains([
27-
'totalItems' => 5,
28-
'_links' => [
29-
'items' => [],
30-
],
31-
'_embedded' => [
32-
'items' => [],
33-
],
34-
]);
35-
$this->assertEqualsCanonicalizing([
36-
['href' => $this->getIriFor('materialItem1')],
37-
['href' => $this->getIriFor('materialItem1period1')],
38-
['href' => $this->getIriFor('materialItem1period1camp2')],
39-
['href' => $this->getIriFor('materialItem1period1campPrototype')],
40-
['href' => $this->getIriFor('materialItem1period1campShared')],
41-
], $response->toArray()['_links']['items']);
25+
$this->assertResponseStatusCodeSame(400);
4226
}
4327

4428
public function testListMaterialItemsFilteredByMaterialListIsAllowedForCollaborator() {

api/tests/Api/SnapshotTests/EndpointPerformanceTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,9 @@ private static function getCollectionEndpoints() {
243243
'/auth/reset_password' => false,
244244
'/auth/resend_activation' => false,
245245
'/content_nodes' => false,
246+
'/checklist_items' => false,
246247
'/invitations' => false,
248+
'/material_items' => false,
247249
'/personal_invitations' => false,
248250
'/token/refresh' => false,
249251
default => true

api/tests/Api/SnapshotTests/ResponseSnapshotTest.php

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ public function testOpenApiSpecMatchesSnapshot() {
8181
* @throws TransportExceptionInterface
8282
*/
8383
#[DataProvider('getCollectionEndpoints')]
84+
#[DataProvider('getCollectionEndpointsFiltered')]
8485
public function testGetCollectionMatchesStructure(Client $client, string $endpoint) {
8586
$response = $client->request('GET', $endpoint);
8687

@@ -116,7 +117,9 @@ public static function getCollectionEndpoints() {
116117
'/auth/reset_password' => false,
117118
'/auth/resend_activation' => false,
118119
'/content_nodes' => false,
120+
'/checklist_items' => false,
119121
'/invitations' => false,
122+
'/material_items' => false,
120123
'/personal_invitations' => false,
121124
'/token/refresh' => false,
122125
'/users' => false,
@@ -135,6 +138,26 @@ public static function getCollectionEndpoints() {
135138
return $withUrlAsKey;
136139
}
137140

141+
/**
142+
* @throws RedirectionExceptionInterface
143+
* @throws DecodingExceptionInterface
144+
* @throws ClientExceptionInterface
145+
* @throws TransportExceptionInterface
146+
* @throws ServerExceptionInterface
147+
*/
148+
public static function getCollectionEndpointsFiltered() {
149+
static::bootKernel();
150+
$client = static::createClientWithCredentials();
151+
$client->disableReboot();
152+
$response = $client->request('GET', '/');
153+
154+
return [
155+
[$client, '/content_nodes?camp=/camps/'.self::getFixtureFor('/camps')->getId()],
156+
[$client, '/checklist_items?checklist=/checklists/'.self::getFixtureFor('/checklists')->getId()],
157+
[$client, '/material_items?camp=/camps/'.self::getFixtureFor('/camps')->getId()],
158+
];
159+
}
160+
138161
/**
139162
* @throws ClientExceptionInterface
140163
* @throws DecodingExceptionInterface
@@ -145,7 +168,7 @@ public static function getCollectionEndpoints() {
145168
#[DataProvider('getItemEndpoints')]
146169
public function testGetItemMatchesStructure(Client $client, string $endpoint) {
147170
/** @var BaseEntity $fixtureFor */
148-
$fixtureFor = $this->getFixtureFor($endpoint);
171+
$fixtureFor = self::getFixtureFor($endpoint);
149172

150173
$itemResponse = $client->request('GET', "{$endpoint}/{$fixtureFor->getId()}");
151174

@@ -163,7 +186,6 @@ public function testGetItemMatchesStructure(Client $client, string $endpoint) {
163186
public static function getItemEndpoints() {
164187
return array_filter(self::getCollectionEndpoints(), function (array $endpoint) {
165188
return match ($endpoint[1]) {
166-
'/content_nodes' => false,
167189
default => true,
168190
};
169191
});
@@ -179,7 +201,7 @@ public static function getItemEndpoints() {
179201
#[DataProvider('getPatchEndpoints')]
180202
public function testPatchResponseMatchesGetItemResponse(Client $client, string $endpoint) {
181203
/** @var BaseEntity $fixtureFor */
182-
$fixtureFor = $this->getFixtureFor($endpoint);
204+
$fixtureFor = self::getFixtureFor($endpoint);
183205

184206
$itemResponse = $client->request('GET', "{$endpoint}/{$fixtureFor->getId()}");
185207
assertThat($itemResponse->getStatusCode(), equalTo(200));
@@ -238,7 +260,7 @@ public static function getPatchEndpoints() {
238260
#[TestWith(['/periods', '/days'], '/periods_{campId}_days')]
239261
#[TestWith(['/periods', '/schedule_entries'], '/periods_{campId}_schedule_entries')]
240262
public function testSubResourceUrlMatchesSnapshot(string $endpoint, string $subresource) {
241-
$fixture = $this->getFixtureFor($endpoint);
263+
$fixture = self::getFixtureFor($endpoint);
242264
$uri = "{$endpoint}/{$fixture->getId()}{$subresource}";
243265

244266
$response = static::createClientWithCredentials()->request('GET', $uri);
@@ -247,7 +269,7 @@ public function testSubResourceUrlMatchesSnapshot(string $endpoint, string $subr
247269
$this->assertMatchesEscapedResponseSnapshot($response);
248270
}
249271

250-
private function getFixtureFor(string $collectionEndpoint) {
272+
private static function getFixtureFor(string $collectionEndpoint) {
251273
$fixtures = FixtureStore::getFixtures();
252274

253275
return ReadItemFixtureMap::get($collectionEndpoint, $fixtures);

0 commit comments

Comments
 (0)