Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ Every change that reaches `main` adds a line under **Unreleased**. When a versio

## Unreleased

### Fixed
- `GET /_/contexts/<ShortName>` (the JSON-LD context) no longer fails with a TypeError in the publishable response listener ([#583](https://github.com/components-web-app/api-components-bundle/pull/583))

## [2.0.0-alpha.40](https://github.com/components-web-app/api-components-bundle/compare/2.0.0-alpha.39...2.0.0-alpha.40) - 2026-10-31

### Breaking
Expand Down
1 change: 1 addition & 0 deletions src/EventListener/Api/PublishableEventListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public function onPostRespond(ResponseEvent $event): void

if (
null === $data
|| null === $attributes['class']
|| !$this->publishableAttributeReader->isConfigured($attributes['class'])
|| $attributes['operation'] instanceof CollectionOperationInterface
) {
Expand Down
127 changes: 127 additions & 0 deletions tests/EventListener/Api/PublishableEventListenerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<?php

/*
* This file is part of the Silverback API Components Bundle Project
*
* (c) Daniel West <daniel@silverback.is>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Silverback\ApiComponentsBundle\Tests\EventListener\Api;

use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Validator\ValidatorInterface;
use PHPUnit\Framework\TestCase;
use Silverback\ApiComponentsBundle\AttributeReader\PublishableAttributeReader;
use Silverback\ApiComponentsBundle\Entity\Core\Layout;
use Silverback\ApiComponentsBundle\EventListener\Api\PublishableEventListener;
use Silverback\ApiComponentsBundle\Helper\Publishable\PublishableStatusChecker;
use Silverback\ApiComponentsBundle\Tests\Functional\TestBundle\Entity\DummyPublishableComponent;
use Silverback\ApiComponentsBundle\Tests\Helper\Publishable\PublishableDatabaseTrait;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;

final class PublishableEventListenerTest extends TestCase
{
use PublishableDatabaseTrait;

private const string PUBLISHED_AT = '2999-01-01 00:00:00 UTC';

/** @var list<string> */
private array $checked = [];

protected function setUp(): void
{
$this->setUpPublishableDatabase();
$this->checked = [];
}

public function test_a_response_whose_data_is_not_an_object_and_has_no_resource_class_is_left_unchanged(): void
{
$response = $this->dispatch(['@context' => ['@vocab' => 'http://example.com/docs.jsonld#']], new Get());

self::assertSame(['cache-control' => ['no-cache, private']], $this->headersWithoutDate($response));
self::assertSame([], $this->checked);
}

public function test_a_resource_whose_class_is_not_publishable_is_left_unchanged(): void
{
$response = $this->dispatch(new Layout(), new Get());

self::assertSame(['cache-control' => ['no-cache, private']], $this->headersWithoutDate($response));
self::assertSame([], $this->checked);
}

public function test_a_publishable_resource_from_a_collection_operation_is_left_unchanged(): void
{
$response = $this->dispatch($this->scheduledComponent(), new GetCollection());

self::assertSame(['cache-control' => ['no-cache, private']], $this->headersWithoutDate($response));
self::assertSame([], $this->checked);
}

public function test_a_scheduled_publishable_resource_from_an_item_operation_expires_at_its_publication_date(): void
{
$response = $this->dispatch($this->scheduledComponent(), new Get());

self::assertSame([
'cache-control' => ['private, must-revalidate'],
'expires' => ['Tue, 01 Jan 2999 00:00:00 GMT'],
], $this->headersWithoutDate($response));
self::assertSame(["is_granted('ROLE_ADMIN')"], $this->checked);
}

private function scheduledComponent(): DummyPublishableComponent
{
$component = new DummyPublishableComponent();
$component->setPublishedAt(new \DateTime(self::PUBLISHED_AT));

return $component;
}

private function dispatch(mixed $data, Operation $operation): Response
{
$authorizationChecker = $this->createStub(AuthorizationCheckerInterface::class);
$authorizationChecker->method('isGranted')->willReturnCallback(function (mixed $attribute): bool {
$this->checked[] = (string) $attribute;

return false;
});
$validator = $this->createMock(ValidatorInterface::class);
$validator->expects(self::never())->method('validate');
$listener = new PublishableEventListener(
new PublishableStatusChecker($this->registry, new PublishableAttributeReader($this->registry), $authorizationChecker, "is_granted('ROLE_ADMIN')"),
$this->registry,
$validator,
);

$request = Request::create('/resource');
$request->attributes->set('_api_operation', $operation);
$request->attributes->set('data', $data);
$request->attributes->set('_api_normalization_context', []);
$response = new Response('{}');

$listener->onPostRespond(new ResponseEvent($this->createStub(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST, $response));

return $response;
}

/**
* @return array<string, list<string|null>>
*/
private function headersWithoutDate(Response $response): array
{
$headers = $response->headers->all();
unset($headers['date']);
ksort($headers);

return $headers;
}
}
57 changes: 57 additions & 0 deletions tests/Functional/Publishable/PublishableJsonLdContextTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

/*
* This file is part of the Silverback API Components Bundle Project
*
* (c) Daniel West <daniel@silverback.is>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Silverback\ApiComponentsBundle\Tests\Functional\Publishable;

use PHPUnit\Framework\Attributes\DataProvider;
use Silverback\ApiComponentsBundle\Tests\Functional\FunctionalTestCase;
use Silverback\ApiComponentsBundle\Tests\Functional\Trait\ApiRequestTrait;
use Silverback\ApiComponentsBundle\Tests\Functional\Trait\AuthenticationTrait;
use Silverback\ApiComponentsBundle\Tests\Functional\Trait\JsonTrait;

final class PublishableJsonLdContextTest extends FunctionalTestCase
{
use ApiRequestTrait;
use AuthenticationTrait;
use JsonTrait;

#[DataProvider('contexts')]
public function test_a_json_ld_context_is_served_to_an_anonymous_caller(string $shortName): void
{
$client = static::createClient();

self::requestJsonLd($client, 'GET', '/contexts/' . $shortName);

self::assertResponseStatusCodeSame(200);
self::assertJsonNodeExists('@context');
}

#[DataProvider('contexts')]
public function test_a_json_ld_context_is_served_to_an_admin(string $shortName): void
{
$client = static::createClient();
self::loginAsAdmin($client);

self::requestJsonLd($client, 'GET', '/contexts/' . $shortName);

self::assertResponseStatusCodeSame(200);
self::assertJsonNodeExists('@context');
}

/**
* @return iterable<string, array{string}>
*/
public static function contexts(): iterable
{
yield 'a publishable component' => ['DummyPublishableComponent'];
yield 'a page' => ['Page'];
}
}
Loading