Skip to content

Commit 2305ad8

Browse files
committed
Fix #170, #113, #162: allowedComponents read-side, OpenApi tests, URL generator tests
- #170: Skip pageDataProperty positions in ComponentPositionNormalizer when the resolved component's type is not in componentGroup.allowedComponents. Write-side validation already existed; this closes the read-side gap. - #113: Add PHPUnit test for OpenApiFactory.getExtendedVersion and two Behat scenarios verifying the version string format and concrete component path presence. - #162: Add PHPUnit unit tests for ApiUrlGenerator, PublicUrlGenerator, and TemporaryUrlGenerator covering URL format, snake_case conversion, config pass-through, and expiry handling.
1 parent 0741f6f commit 2305ad8

8 files changed

Lines changed: 295 additions & 0 deletions

File tree

features/bootstrap/DoctrineContext.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -831,6 +831,54 @@ public function thereIsAPageDataWithDraftComponentInPageDataPropertyPosition(str
831831
$this->manager->flush();
832832
}
833833

834+
/**
835+
* @Given there is a pageDataProperty position with a disallowed component type in a restricted group with route :path
836+
*/
837+
public function thereIsAPageDataPropertyPositionWithDisallowedComponentType(string $path): void
838+
{
839+
$componentGroup = new ComponentGroup();
840+
$componentGroup->reference = 'test';
841+
$componentGroup->location = 'test';
842+
$componentGroup->allowedComponents = ['/component/dummy_components'];
843+
$this->timestampedHelper->persistTimestampedFields($componentGroup, true);
844+
$this->manager->persist($componentGroup);
845+
$this->restContext->resources['component_group'] = $this->iriConverter->getIriFromResource($componentGroup);
846+
847+
$componentPosition = new ComponentPosition();
848+
$componentPosition->pageDataProperty = 'publishableComponent';
849+
$componentPosition->componentGroup = $componentGroup;
850+
$componentPosition->sortValue = 0;
851+
$this->timestampedHelper->persistTimestampedFields($componentPosition, true);
852+
$this->manager->persist($componentPosition);
853+
$this->restContext->resources['position_0'] = $this->iriConverter->getIriFromResource($componentPosition);
854+
855+
$page = new Page();
856+
$page->isTemplate = true;
857+
$page->reference = 'test page';
858+
$page->addComponentGroup($componentGroup);
859+
$this->timestampedHelper->persistTimestampedFields($page, true);
860+
$this->manager->persist($page);
861+
862+
$publishableComponent = new DummyPublishableComponent();
863+
$publishableComponent->setPublishedAt(new \DateTime());
864+
$this->manager->persist($publishableComponent);
865+
866+
$pageData = new PageDataWithComponent();
867+
$pageData->publishableComponent = $publishableComponent;
868+
$pageData->page = $page;
869+
$this->timestampedHelper->persistTimestampedFields($pageData, true);
870+
$this->manager->persist($pageData);
871+
$this->restContext->resources['page_data'] = $this->iriConverter->getIriFromResource($pageData);
872+
873+
$route = new Route();
874+
$route->setPath($path)->setName($path)->setPageData($pageData);
875+
$this->timestampedHelper->persistTimestampedFields($route, true);
876+
$this->manager->persist($route);
877+
$this->restContext->resources['page_data_route'] = $this->iriConverter->getIriFromResource($route);
878+
879+
$this->manager->flush();
880+
}
881+
834882
/**
835883
* @Given there is a PageData resource with the route path :childPath nested within the route :parentPath
836884
*/

features/main/component_groups.feature

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,14 @@ Feature: ComponentGroup resource
126126
Then the response status code should be 200
127127
And the resource "layout" should be purged from the cache
128128

129+
@loginUser
130+
Scenario: A pageDataProperty position is not populated when the resolved component type is not in allowedComponents
131+
Given there is a pageDataProperty position with a disallowed component type in a restricted group with route "/test-page"
132+
And I add "path" header equal to "/test-page"
133+
When I send a "GET" request to the resource "position_0"
134+
Then the response status code should be 200
135+
And the JSON node "component" should be null
136+
129137
@loginAdmin
130138
Scenario: Sending a PHP class name as allowedComponents is normalised to a collection IRI
131139
Given there is a ComponentGroup with 0 components

features/main/openapi_compatibility.feature

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,15 @@ Feature: API Platform Swagger Compatibility
66
Scenario: I can view the Swagger API Docs
77
When I send a "GET" request to "/"
88
Then the response status code should be 200
9+
10+
Scenario: The API version string is extended with the bundle package version in parentheses
11+
Given I add "Accept" header equal to "application/json"
12+
When I send a "GET" request to "/docs.json"
13+
Then the response status code should be 200
14+
And the JSON node "info.version" should contain "("
15+
16+
Scenario: Concrete component endpoints are included in the OpenAPI paths
17+
Given I add "Accept" header equal to "application/json"
18+
When I send a "GET" request to "/docs.json"
19+
Then the response status code should be 200
20+
And the JSON node "paths./component/dummy_components" should exist

src/Serializer/Normalizer/ComponentPositionNormalizer.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111

1212
namespace Silverback\ApiComponentsBundle\Serializer\Normalizer;
1313

14+
use ApiPlatform\Metadata\GetCollection;
1415
use ApiPlatform\Metadata\IriConverterInterface;
16+
use ApiPlatform\Metadata\UrlGeneratorInterface;
1517
use Doctrine\ORM\Mapping\ClassMetadata;
1618
use Doctrine\Persistence\ManagerRegistry;
1719
use Silverback\ApiComponentsBundle\DataProvider\PageDataProvider;
@@ -190,6 +192,19 @@ private function normalizeForPageData(ComponentPosition $object, array $context)
190192
return $object;
191193
}
192194

195+
// skip if the resolved component type is not in the group's allowedComponents
196+
if ($object->componentGroup && null !== $object->componentGroup->allowedComponents) {
197+
$resourceClass = $component::class;
198+
$iri = $this->iriConverter->getIriFromResource(
199+
$resourceClass,
200+
UrlGeneratorInterface::ABS_PATH,
201+
(new GetCollection())->withClass($resourceClass),
202+
);
203+
if (!\in_array($iri, $object->componentGroup->allowedComponents, true)) {
204+
return $object;
205+
}
206+
}
207+
193208
// populate the position
194209
$object->setComponent($component);
195210

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Silverback API Components Bundle Project
5+
*
6+
* (c) Daniel West <daniel@silverback.is>
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+
namespace Silverback\ApiComponentsBundle\Tests\Factory\Uploadable;
13+
14+
use ApiPlatform\Metadata\IriConverterInterface;
15+
use League\Flysystem\Filesystem;
16+
use PHPUnit\Framework\TestCase;
17+
use Silverback\ApiComponentsBundle\Factory\Uploadable\ApiUrlGenerator;
18+
use Symfony\Component\HttpFoundation\Request;
19+
use Symfony\Component\HttpFoundation\RequestStack;
20+
use Symfony\Component\HttpFoundation\UrlHelper;
21+
22+
class ApiUrlGeneratorTest extends TestCase
23+
{
24+
private function buildUrlHelper(string $baseUrl = 'https://example.com'): UrlHelper
25+
{
26+
$requestStack = new RequestStack();
27+
$requestStack->push(Request::create($baseUrl));
28+
29+
return new UrlHelper($requestStack);
30+
}
31+
32+
public function testGeneratesDownloadUrlFromResourceIriAndPropertyName(): void
33+
{
34+
$object = new \stdClass();
35+
36+
$iriConverter = $this->createMock(IriConverterInterface::class);
37+
$iriConverter->method('getIriFromResource')->with($object)->willReturn('/_/component_groups/abc-123');
38+
39+
$generator = new ApiUrlGenerator($iriConverter, $this->buildUrlHelper());
40+
41+
$result = $generator->generateUrl($object, 'fileName', $this->createMock(Filesystem::class), '/path/to/file.png');
42+
43+
$this->assertSame('https://example.com/_/component_groups/abc-123/download/file_name', $result);
44+
}
45+
46+
public function testConvertsPropertyNameToSnakeCase(): void
47+
{
48+
$object = new \stdClass();
49+
50+
$iriConverter = $this->createMock(IriConverterInterface::class);
51+
$iriConverter->method('getIriFromResource')->willReturn('/resource/1');
52+
53+
$generator = new ApiUrlGenerator($iriConverter, $this->buildUrlHelper());
54+
55+
$result = $generator->generateUrl($object, 'myUploadedFile', $this->createMock(Filesystem::class), 'file.png');
56+
57+
$this->assertStringEndsWith('/download/my_uploaded_file', $result);
58+
}
59+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Silverback API Components Bundle Project
5+
*
6+
* (c) Daniel West <daniel@silverback.is>
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+
namespace Silverback\ApiComponentsBundle\Tests\Factory\Uploadable;
13+
14+
use League\Flysystem\Filesystem;
15+
use PHPUnit\Framework\TestCase;
16+
use Silverback\ApiComponentsBundle\Factory\Uploadable\PublicUrlGenerator;
17+
18+
class PublicUrlGeneratorTest extends TestCase
19+
{
20+
public function testGeneratesPublicUrlFromFilesystem(): void
21+
{
22+
$filesystem = $this->createMock(Filesystem::class);
23+
$filesystem->method('publicUrl')
24+
->with('/uploads/image.png', [])
25+
->willReturn('https://cdn.example.com/uploads/image.png');
26+
27+
$generator = new PublicUrlGenerator();
28+
29+
$result = $generator->generateUrl(new \stdClass(), 'file', $filesystem, '/uploads/image.png');
30+
31+
$this->assertSame('https://cdn.example.com/uploads/image.png', $result);
32+
}
33+
34+
public function testPassesConfigToFilesystem(): void
35+
{
36+
$config = ['visibility' => 'public'];
37+
38+
$filesystem = $this->createMock(Filesystem::class);
39+
$filesystem->method('publicUrl')
40+
->with('/file.png', $config)
41+
->willReturn('https://cdn.example.com/file.png');
42+
43+
$generator = new PublicUrlGenerator($config);
44+
45+
$result = $generator->generateUrl(new \stdClass(), 'file', $filesystem, '/file.png');
46+
47+
$this->assertSame('https://cdn.example.com/file.png', $result);
48+
}
49+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Silverback API Components Bundle Project
5+
*
6+
* (c) Daniel West <daniel@silverback.is>
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+
namespace Silverback\ApiComponentsBundle\Tests\Factory\Uploadable;
13+
14+
use League\Flysystem\Filesystem;
15+
use PHPUnit\Framework\TestCase;
16+
use Silverback\ApiComponentsBundle\Factory\Uploadable\TemporaryUrlGenerator;
17+
18+
class TemporaryUrlGeneratorTest extends TestCase
19+
{
20+
public function testGeneratesTemporaryUrlFromFilesystem(): void
21+
{
22+
$filesystem = $this->createMock(Filesystem::class);
23+
$filesystem->method('temporaryUrl')
24+
->willReturn('https://s3.example.com/file.png?signed=abc');
25+
26+
$generator = new TemporaryUrlGenerator();
27+
28+
$result = $generator->generateUrl(new \stdClass(), 'file', $filesystem, '/uploads/file.png');
29+
30+
$this->assertSame('https://s3.example.com/file.png?signed=abc', $result);
31+
}
32+
33+
public function testUsesConfiguredExpiryString(): void
34+
{
35+
$capturedExpiry = null;
36+
$filesystem = $this->createMock(Filesystem::class);
37+
$filesystem->method('temporaryUrl')
38+
->willReturnCallback(function (string $path, \DateTimeInterface $expiry, array $config) use (&$capturedExpiry): string {
39+
$capturedExpiry = $expiry;
40+
41+
return 'https://s3.example.com/signed';
42+
});
43+
44+
$generator = new TemporaryUrlGenerator(expires: '+1 hour');
45+
$before = new \DateTime();
46+
$generator->generateUrl(new \stdClass(), 'file', $filesystem, 'file.png');
47+
$after = new \DateTime('+1 hour');
48+
49+
$this->assertGreaterThan($before->getTimestamp(), $capturedExpiry->getTimestamp());
50+
$this->assertLessThanOrEqual($after->getTimestamp(), $capturedExpiry->getTimestamp());
51+
}
52+
53+
public function testPassesConfigToFilesystem(): void
54+
{
55+
$config = ['ServerSideEncryption' => 'AES256'];
56+
57+
$capturedConfig = null;
58+
$filesystem = $this->createMock(Filesystem::class);
59+
$filesystem->method('temporaryUrl')
60+
->willReturnCallback(function (string $path, \DateTimeInterface $expiry, array $config) use (&$capturedConfig): string {
61+
$capturedConfig = $config;
62+
63+
return 'https://s3.example.com/signed';
64+
});
65+
66+
$generator = new TemporaryUrlGenerator($config);
67+
$generator->generateUrl(new \stdClass(), 'file', $filesystem, 'file.png');
68+
69+
$this->assertSame($config, $capturedConfig);
70+
}
71+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Silverback API Components Bundle Project
5+
*
6+
* (c) Daniel West <daniel@silverback.is>
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+
namespace Silverback\ApiComponentsBundle\Tests\OpenApi;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Silverback\ApiComponentsBundle\OpenApi\OpenApiFactory;
16+
17+
class OpenApiFactoryTest extends TestCase
18+
{
19+
public function testGetExtendedVersionAppendsParenthesizedBundleVersion(): void
20+
{
21+
$extended = OpenApiFactory::getExtendedVersion('3.1.0');
22+
23+
$this->assertStringStartsWith('3.1.0 (', $extended);
24+
$this->assertStringEndsWith(')', $extended);
25+
}
26+
27+
public function testGetExtendedVersionPreservesOriginalVersion(): void
28+
{
29+
$extended = OpenApiFactory::getExtendedVersion('2.0');
30+
31+
$this->assertStringStartsWith('2.0', $extended);
32+
}
33+
}

0 commit comments

Comments
 (0)