Skip to content

Commit

Permalink
Merge branch 'components-web-app:main' into feature/components-web-ap…
Browse files Browse the repository at this point in the history
…p#113-additional-tests
  • Loading branch information
bofalke authored Oct 26, 2022
2 parents 884a7c0 + 384c4a3 commit a7476c9
Show file tree
Hide file tree
Showing 48 changed files with 1,696 additions and 918 deletions.
634 changes: 187 additions & 447 deletions .github/workflows/ci.yml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion .symfony.insight.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
php_version: 8.0
php_version: 8.1
rules:
doctrine.public_doctrine_property:
enabled: false
16 changes: 13 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,18 @@
"type": "symfony-bundle",
"description": "Creates a flexible API for a website's structure, reusable components and common functionality.",
"license": "MIT",
"homepage": "https://cwa.rocks",
"authors": [
{
"name": "Daniel West",
"email": "[email protected]",
"homepage": "https://silverback.is"
}
],
"repositories": [
{
"type": "vcs",
"url": "https://github.com/silverbackdan/contexts"
"type": "git",
"url": "git@github.com:silverbackdan/contexts.git"
}
],
"require": {
Expand All @@ -16,7 +24,7 @@
"ext-json": "*",
"ext-pdo": "*",
"ext-simplexml": "*",
"api-platform/core": "^3.0",
"api-platform/core": "^3.0.3",
"cocur/slugify": "^4.1",
"doctrine/annotations": "^1.7.0",
"doctrine/dbal": "^3.4",
Expand Down Expand Up @@ -80,6 +88,7 @@
"symfony/http-client": "^6.1",
"symfony/maker-bundle": "^1.0",
"symfony/mercure-bundle": "^0.3.4",
"symfony/messenger": "^6.1",
"symfony/monolog-bundle": "^3.8",
"symfony/phpunit-bridge": "^6.1.3",
"symfony/stopwatch": "^6.1",
Expand Down Expand Up @@ -128,6 +137,7 @@
"willdurand/negotiation": "^2",
"symfony/proxy-manager-bridge": "<5.4",
"symfony/serializer": "<=6.1.2",
"symfony/var-exporter": "<6.1",
"symfony/web-link": "<=6.0",
"doctrine/collections": "<1.7",
"doctrine/orm": "<2.13"
Expand Down
12 changes: 6 additions & 6 deletions docs/Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ GEM
execjs
coffee-script-source (1.11.1)
colorator (1.1.0)
commonmarker (0.23.5)
commonmarker (0.23.6)
concurrent-ruby (1.1.10)
dnsruby (1.61.9)
simpleidn (~> 0.1)
Expand All @@ -25,10 +25,10 @@ GEM
ffi (>= 1.15.0)
eventmachine (1.2.7)
execjs (2.8.1)
faraday (2.5.2)
faraday (2.6.0)
faraday-net_http (>= 2.0, < 3.1)
ruby2_keywords (>= 0.0.4)
faraday-net_http (3.0.0)
faraday-net_http (3.0.1)
ffi (1.15.5)
forwardable-extended (2.6.0)
gemoji (3.0.1)
Expand Down Expand Up @@ -83,7 +83,7 @@ GEM
octokit (~> 4.0)
public_suffix (>= 3.0, < 5.0)
typhoeus (~> 1.3)
html-pipeline (2.14.2)
html-pipeline (2.14.3)
activesupport (>= 2)
nokogiri (>= 1.4)
http_parser.rb (0.8.0)
Expand Down Expand Up @@ -212,7 +212,7 @@ GEM
jekyll-feed (~> 0.9)
jekyll-seo-tag (~> 2.1)
minitest (5.16.3)
nokogiri (1.13.8)
nokogiri (1.13.9)
mini_portile2 (~> 2.8.0)
racc (~> 1.4)
octokit (4.25.1)
Expand Down Expand Up @@ -251,7 +251,7 @@ GEM
unf_ext
unf_ext (0.0.8.2)
unicode-display_width (1.8.0)
zeitwerk (2.6.0)
zeitwerk (2.6.1)

PLATFORMS
ruby
Expand Down
54 changes: 50 additions & 4 deletions features/bootstrap/JsonContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use Behatch\Json\Json;
use Behatch\Json\JsonInspector;
use Behatch\Json\JsonSchema;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWSProvider\JWSProviderInterface;
use PHPUnit\Framework\Assert;
use Symfony\Component\HttpFoundation\Cookie;

Expand All @@ -29,10 +30,12 @@ class JsonContext implements Context
private JsonInspector $inspector;
private ?BehatchJsonContext $jsonContext;
private ?RestContext $restContext;
private JWSProviderInterface $jwsProvider;

public function __construct()
public function __construct(JWSProviderInterface $jwsProvider)
{
$this->inspector = new JsonInspector('javascript');
$this->jwsProvider = $jwsProvider;
}

/**
Expand Down Expand Up @@ -147,9 +150,52 @@ public function theResponseShouldBeTheResource($name): void
*/
public function theResponseShouldHaveACookie(string $name): void
{
$cookie = Cookie::fromString($this->jsonContext->getSession()->getResponseHeader('set-cookie'));
$realName = $cookie->getName();
Assert::assertEquals($realName, $name, sprintf('The cookie "%s" was not found in the response headers.', $name));
$responseHeaders = $this->jsonContext->getSession()->getResponseHeaders();
$setCookieHeaders = $responseHeaders['set-cookie'];
foreach ($setCookieHeaders as $setCookieHeader) {
$cookie = Cookie::fromString($setCookieHeader);
$realName = $cookie->getName();
if ($realName === $name) {
return;
}
}
throw new \Exception(sprintf('The cookie "%s" was not found in the response headers.', $name));
}

private function getMercureCookieDraftTopics(): array
{
$responseHeaders = $this->jsonContext->getSession()->getResponseHeaders();
$setCookieHeaders = $responseHeaders['set-cookie'];
foreach ($setCookieHeaders as $setCookieHeader) {
$cookie = Cookie::fromString($setCookieHeader);
$realName = $cookie->getName();
if ('mercureAuthorization' === $realName) {
$token = $this->jwsProvider->load($cookie->getValue());
$payload = $token->getPayload();

return array_filter($payload['mercure']['subscribe'], static function ($topic) {
return str_ends_with($topic, '?draft=1');
});
}
}

return [];
}

/**
* @Then the mercure cookie should not contain draft resource topics
*/
public function theMercureCookieShouldNotContainDraftResources()
{
Assert::assertCount(0, $this->getMercureCookieDraftTopics(), 'The cookie allows a user to be subscribed to draft resources');
}

/**
* @Then the mercure cookie should contain draft resource topics
*/
public function theMercureCookieShouldContainDraftResources()
{
Assert::assertGreaterThan(0, $this->getMercureCookieDraftTopics(), 'The cookie does not allow a user to subscribe to any draft resources');
}

/**
Expand Down
53 changes: 53 additions & 0 deletions features/bootstrap/ProfilerContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@
use Silverback\ApiComponentsBundle\Factory\User\Mailer\WelcomeEmailFactory;
use Silverback\ApiComponentsBundle\Tests\Functional\TestBundle\Entity\User;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Bundle\MercureBundle\DataCollector\MercureDataCollector;
use Symfony\Component\BrowserKit\AbstractBrowser;
use Symfony\Component\HttpClient\DataCollector\HttpClientDataCollector;
use Symfony\Component\HttpKernel\Profiler\Profile as HttpProfile;
use Symfony\Component\Mailer\DataCollector\MessageDataCollector;
use Symfony\Component\Mercure\Update;
use Symfony\Component\Mime\Header\Headers;
use Symfony\Component\VarDumper\Cloner\Data;

Expand All @@ -53,6 +55,54 @@ public function getContexts(BeforeScenarioScope $scope)
$this->client = $this->minkContext->getSession()->getDriver()->getClient();
}

/**
* @return Update[]
*/
private function getMercureMessageObjects(): array
{
$objects = [];
/** @var MercureDataCollector $collector */
$collector = $this->getProfile()->getCollector('mercure');
$hubs = $collector->getHubs();
foreach ($hubs['default']['messages'] as $message) {
$objects[] = $message['object'];
}

return $objects;
}

/**
* @Then there should be :count mercure messages
*/
public function thereShouldBeAPublishedMercureUpdatePublished(int $count)
{
$messageObjects = $this->getMercureMessageObjects();
if (\count($messageObjects) !== $count) {
throw new ExpectationException(sprintf('%d updates were published but %d were expected', \count($messageObjects), $count), $this->minkContext->getSession()->getDriver());
}
}

/**
* @Then there should be :count mercure messages for draft resources
*/
public function thereShouldMercureMessagesForDraftResources(int $count)
{
$messageObjects = $this->getMercureMessageObjects();
$draftCount = 0;
foreach ($messageObjects as $messageObject) {
$iri = $messageObject->getTopics()[0];
if (str_ends_with($iri, '?draft=1')) {
if (!$messageObject->isPrivate()) {
throw new ExpectationException('Draft resource messages must be private', $this->minkContext->getSession()->getDriver());
}
++$draftCount;
}
}
if ($draftCount !== $count) {
throw new ExpectationException(sprintf('%d draft updates were published but %d were expected', $draftCount, $count), $this->minkContext->getSession()->getDriver());
}
}

/**
* @Then the resource :resource_name should be purged from the cache
*/
Expand Down Expand Up @@ -100,12 +150,15 @@ public function iShouldGetAnEmail(string $emailType, string $emailAddress = 'use
{
/** @var MessageDataCollector $collector */
$collector = $this->getProfile()->getCollector('mailer');
/** @var TemplatedEmail[] $messages */
$messages = $collector->getEvents()->getMessages();

Assert::assertCount(1, $messages);
Assert::assertInstanceOf(TemplatedEmail::class, $email = $messages[0]);

/** @var TemplatedEmail $email */
$context = $email->getContext();
Assert::assertArrayHasKey('website_name', $context);
Assert::assertEquals('New Website', $context['website_name']);
Assert::assertInstanceOf(User::class, $context['user']);

Expand Down
21 changes: 21 additions & 0 deletions features/mercure/mercure.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
Feature: Mercure authorization cookies and messages are published
In order to restrict access to draft components
As a an application developer
I must be able to configure the ability to access the resource

Background:
Given I add "Accept" header equal to "application/ld+json"
And I add "Content-Type" header equal to "application/ld+json"

Scenario: A Mercure authorization cookie is set WITHOUT topic draft access
When I send a "GET" request to "/docs.jsonld"
Then the response status code should be 200
And the response should have a "mercureAuthorization" cookie
And the mercure cookie should not contain draft resource topics

@loginAdmin
Scenario: A Mercure authorization cookie is set WITH topic draft access
When I send a "GET" request to "/docs.jsonld"
Then the response status code should be 200
And the response should have a "mercureAuthorization" cookie
And the mercure cookie should contain draft resource topics
3 changes: 3 additions & 0 deletions features/publishable/publishable.feature
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ Feature: Access to unpublished/draft resources should be configurable
And the JSON should be valid according to the schema file "publishable.schema.json"
And the JSON node publishedAt should not exist
And the JSON node "_metadata.publishable.published" should be false
And there should be 1 mercure messages for draft resources

@loginAdmin
Scenario Outline: As a user with draft access, when I create a resource, I should be able to set the publishedAt date to specify if it is draft/published
Expand All @@ -70,6 +71,8 @@ Feature: Access to unpublished/draft resources should be configurable
Then the response status code should be 201
And the JSON node publishedAt should exist
And the JSON node "_metadata.publishable.published" should be true
And there should be 1 mercure messages
And there should be 0 mercure messages for draft resources

@loginUser
Scenario Outline: As a user with draft access to a specific resource, when I create a resource, I should be able to set the publishedAt date to specify if it is draft/published
Expand Down
49 changes: 49 additions & 0 deletions src/ApiPlatform/Api/MercureIriConverter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

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

declare(strict_types=1);

namespace Silverback\ApiComponentsBundle\ApiPlatform\Api;

use ApiPlatform\Api\IriConverterInterface;
use ApiPlatform\Api\UrlGeneratorInterface;
use ApiPlatform\Metadata\Operation;
use Silverback\ApiComponentsBundle\Helper\Publishable\PublishableStatusChecker;

/**
* @author Daniel West <[email protected]>
*/
class MercureIriConverter implements IriConverterInterface
{
public function __construct(private IriConverterInterface $decorated, private PublishableStatusChecker $publishableStatusChecker)
{
}

public function getResourceFromIri(string $iri, array $context = [], ?Operation $operation = null): object
{
return $this->decorated->getResourceFromIri($iri, $context, $operation);
}

public function getIriFromResource($resource, int $referenceType = UrlGeneratorInterface::ABS_PATH, ?Operation $operation = null, array $context = []): ?string
{
$iri = $this->decorated->getIriFromResource($resource, $referenceType, $operation, $context);

if (\is_string($resource)) {
return $iri;
}

if ($this->publishableStatusChecker->getAnnotationReader()->isConfigured($resource) && !$this->publishableStatusChecker->isActivePublishedAt($resource)) {
$iri .= '?draft=1';
}

return $iri;
}
}
2 changes: 1 addition & 1 deletion src/DataProvider/PageDataProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public function __construct(
$this->managerRegistry = $managerRegistry;
}

private function getOriginalRequestPath(): ?string
public function getOriginalRequestPath(): ?string
{
$request = $this->requestStack->getCurrentRequest();
if (!$request) {
Expand Down
14 changes: 13 additions & 1 deletion src/DependencyInjection/CompilerPass/ApiPlatformCompilerPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,20 @@ public function process(ContainerBuilder $container): void
$container->getDefinition(CollectionApiEventListener::class)->setArgument('$itemsPerPageParameterName', $itemsPerPageParameterName);
$purgeListener = 'silverback.api_components.event_listener.doctrine.purge_http_cache_listener';

if (!$container->hasAlias('api_platform.http_cache.purger')) {
if ($container->hasAlias('api_platform.http_cache.purger')) {
// we have implemented fully custom logic
$container->removeDefinition('api_platform.doctrine.listener.http_cache.purge');
} else {
$container->removeDefinition($purgeListener);
}

$publishListener = 'silverback.api_components.event_listener.doctrine.mercure_publish_listener';
$apiPlatformMercurePublishListener = 'api_platform.doctrine.orm.listener.mercure.publish';
if ($container->hasDefinition($apiPlatformMercurePublishListener)) {
// we have implemented fully custom logic
$container->removeDefinition($apiPlatformMercurePublishListener);
} else {
$container->removeDefinition($publishListener);
}
}
}
Loading

0 comments on commit a7476c9

Please sign in to comment.