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
22 changes: 17 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -727,25 +727,37 @@ The orchestrator's exit codes: `1` release already completed, `2` empty enumerat

### Testing the Rendering

For executing behavioral tests, install the `neos/behat` package and run `./flow behat:setup`. Then:
The behavioral tests need the `neos/behat` package (`composer require --dev neos/behat`), which brings Behat itself
along. Behat is used from the main composer installation:

```bash
cd Packages/Application/Flowpack.DecoupledContentStore/Tests/Behavior
../../../../bin/behat -c behat.yml.dist
../../../../../bin/behat -c behat.yml.dist
```

(five levels up is the installation root - adjust the path if the package sits somewhere else, for example as a symlink
into a `DistributionPackages` checkout)

The tests bootstrap the `Testing/Behat` context, so the database and the Redis instances they work on are the ones
configured in `Configuration/Testing/Behat/`.

**Every feature file is tagged `@resetRedis`, and that hook calls `FLUSHALL`** on the primary content store - not just
the configured database, but every database on that Redis server. Point the Behat context at a Redis instance whose
contents you are willing to lose; if it is the same server a development content store uses, running the tests wipes it,
including caches other applications keep there.

Behat also supports running single tests or single files - they need to be specified after the config file, e.g.

```bash

# run all scenarios in a given folder
../../../../bin/behat -c behat.yml.dist Features/ContentStore/
../../../../../bin/behat -c behat.yml.dist Features/ContentStore/

# run all scenarios in the single feature file
../../../../bin/behat -c behat.yml.dist Features/ContentStore/Basics.feature
../../../../../bin/behat -c behat.yml.dist Features/ContentStore/Basics.feature

# run the scenario starting at line 66
../../../../bin/behat -c behat.yml.dist Features/ContentStore/Basics.feature:66
../../../../../bin/behat -c behat.yml.dist Features/ContentStore/Basics.feature:66
```

In case of exceptions, it might be helpful to run the tests with `--stop-on-failure`, which stops the test cases at the first
Expand Down
118 changes: 96 additions & 22 deletions Tests/Behavior/Features/Bootstrap/FeatureContext.php
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
<?php

declare(strict_types=1);

use Behat\Behat\Context\Context;
use Behat\Gherkin\Node\PyStringNode;
use Composer\InstalledVersions;
use Flowpack\DecoupledContentStore\Command\ContentReleaseValidationCommandController;
use Flowpack\DecoupledContentStore\ContentReleaseManager;
use Flowpack\DecoupledContentStore\Core\ConcurrentBuildLockService;
use Flowpack\DecoupledContentStore\Core\Domain\ValueObject\ContentReleaseIdentifier;
use Flowpack\DecoupledContentStore\Core\Domain\ValueObject\PrunnerJobId;
use Flowpack\DecoupledContentStore\Core\Domain\ValueObject\RedisInstanceIdentifier;
use Flowpack\DecoupledContentStore\Exception as DecoupledContentStoreException;
use Flowpack\DecoupledContentStore\QuickPublish\Dto\NodeIdentifiers;
use Flowpack\DecoupledContentStore\QuickPublish\Infrastructure\RedisReleaseCopyService;
use Flowpack\DecoupledContentStore\QuickPublish\QuickPublishNodeEnumerator;
use Flowpack\DecoupledContentStore\Core\RedisKeyService;
use Flowpack\DecoupledContentStore\Core\Infrastructure\ContentReleaseLogger;
use Flowpack\DecoupledContentStore\Core\Infrastructure\RedisClientManager;
use Flowpack\DecoupledContentStore\Core\RedisKeyService;
use Flowpack\DecoupledContentStore\Exception as DecoupledContentStoreException;
use Flowpack\DecoupledContentStore\IncrementalContentReleaseHandler;
use Flowpack\DecoupledContentStore\NodeEnumeration\Domain\Repository\RedisEnumerationRepository;
use Flowpack\DecoupledContentStore\NodeEnumeration\Domain\Service\NodeContextCombinator;
Expand All @@ -32,15 +32,26 @@
use Flowpack\DecoupledContentStore\NodeRendering\ProcessEvents\RenderingQueueFilledEvent;
use Flowpack\DecoupledContentStore\NodeRendering\Render\CustomFusionView;
use Flowpack\DecoupledContentStore\PrepareContentRelease\Infrastructure\RedisContentReleaseService;
use Flowpack\DecoupledContentStore\QuickPublish\Dto\NodeIdentifiers;
use Flowpack\DecoupledContentStore\QuickPublish\Infrastructure\RedisReleaseCopyService;
use Flowpack\DecoupledContentStore\QuickPublish\QuickPublishNodeEnumerator;
use Flowpack\DecoupledContentStore\Tests\Behavior\Fixtures\StubPrunnerApiService;
use Neos\Behat\Tests\Behat\FlowContextTrait;
use Neos\Behat\FlowBootstrapTrait;
use Neos\Behat\FlowEntitiesTrait;
use Neos\ContentRepository\Domain\Model\NodeInterface;
use Neos\ContentRepository\Domain\Repository\WorkspaceRepository;
use Neos\ContentRepository\Domain\Service\NodeTypeManager;
use Neos\ContentRepository\Tests\Behavior\Features\Bootstrap\NodeOperationsTrait;
use Neos\Flow\Mvc\ActionRequest;
use Neos\Flow\ObjectManagement\ObjectManagerInterface;
use Neos\Flow\Persistence\PersistenceManagerInterface;
use Neos\Flow\Tests\Behavior\Features\Bootstrap\SecurityOperationsTrait;
use Neos\Flow\Security\Account;
use Neos\Flow\Security\Authentication\AuthenticationProviderManager;
use Neos\Flow\Security\Authentication\Provider\TestingProvider;
use Neos\Flow\Security\Authentication\TokenAndProviderFactoryInterface;
use Neos\Flow\Security\Authentication\TokenInterface;
use Neos\Flow\Security\Context as SecurityContext;
use Neos\Flow\Security\Policy\PolicyService;
use Neos\Neos\Domain\Model\Domain;
use Neos\Neos\Domain\Model\Site;
use Neos\Neos\Domain\Repository\DomainRepository;
Expand All @@ -49,44 +60,50 @@
use Neos\Utility\Arrays;
use Neos\Utility\ObjectAccess;
use PHPUnit\Framework\Assert;
use Psr\Http\Message\ServerRequestFactoryInterface;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\Yaml\Yaml;

require_once __DIR__ . '/../../../../../../Packages/Application/Neos.Behat/Tests/Behat/FlowContextTrait.php';
require_once
__DIR__
. '/../../../../../../Packages/Application/Neos.ContentRepository/Tests/Behavior/Features/Bootstrap/NodeOperationsTrait.php';
require_once
__DIR__
. '/../../../../../../Packages/Framework/Neos.Flow/Tests/Behavior/Features/Bootstrap/SecurityOperationsTrait.php';
// The content repository keeps its step definitions in a directory no autoloader knows about, so the file has to be
// included by hand. Composer knows where the package was installed, which holds wherever this package itself sits -
// inside a Neos installation as well as in a checkout of its own.
require_once InstalledVersions::getInstallPath('neos/content-repository')
. '/Tests/Behavior/Features/Bootstrap/NodeOperationsTrait.php';

/**
* Features context
*/
class FeatureContext implements Context
{
use FlowContextTrait;
use SecurityOperationsTrait;
use FlowBootstrapTrait;
use FlowEntitiesTrait;
use NodeOperationsTrait;

/**
* The step definitions of the content repository run their steps in a sub process where this is TRUE, which none
* of the features here ask for.
*/
protected $isolated = false;

/**
* @var ObjectManagerInterface
*/
protected $objectManager;

private SecurityContext $securityContext;
private ActionRequest $securityActionRequest;
private AuthenticationProviderManager $authenticationManager;
private TestingProvider $testingProvider;
private PolicyService $policyService;

private ?InterruptibleProcessRuntime $renderOrchestratorProcess;
private ?InterruptibleProcessRuntimeEventInterface $renderOrchestratorProcessLastEvent;
private StubPrunnerApiService $stubPrunnerApiService;
private BufferedOutput $renderOrchestratorProcessBufferedOutput;

public function __construct()
{
if (self::$bootstrap === null) {
self::$bootstrap = $this->initializeFlow();
}
$this->objectManager = self::$bootstrap->getObjectManager();
$this->objectManager = self::bootstrapFlow()->getObjectManager();
$this->setupSecurity();

// for testing, we use Private/EndToEndTestFusion as fusion folder to load.
Expand Down Expand Up @@ -116,6 +133,63 @@ public function getObjectManager(): ObjectManagerInterface
return $this->objectManager;
}

/**
* Persist any changes - part of what the content repository's step definitions expect from their context.
*/
public function persistAll(): void
{
$persistenceManager = $this->getObject(PersistenceManagerInterface::class);
$persistenceManager->persistAll();
$persistenceManager->clearState();

// roles are read through the policy service, which holds on to them across a database reset
$this->policyService->reset();
}

/**
* The authentication a scenario gets through "I am authenticated with role" needs a security context which has
* a request, and a TestingProvider to hand the account to.
*/
private function setupSecurity(): void
{
$this->policyService = $this->getObject(PolicyService::class);
$this->authenticationManager = $this->getObject(AuthenticationProviderManager::class);

// asking for the providers is what builds them, and with them the singleton TestingProvider
$providers = $this->getObject(TokenAndProviderFactoryInterface::class)->getProviders();
$this->testingProvider = $providers['TestingProvider'];

$httpRequest = $this->getObject(ServerRequestFactoryInterface::class)
->createServerRequest('GET', 'http://localhost/');
$this->securityActionRequest = ActionRequest::fromHttpRequest($httpRequest);

$this->securityContext = $this->getObject(SecurityContext::class);
$this->securityContext->clearContext();
$this->securityContext->setRequest($this->securityActionRequest);
}

/**
* @Given /^I am authenticated with role "([^"]*)"$/
*/
public function iAmAuthenticatedWithRole(string $roleIdentifier): void
{
$roles = [];
foreach (Arrays::trimExplode(',', $roleIdentifier) as $roleName) {
$roles[] = $this->policyService->getRole($roleName);
}

$account = new Account();
$account->setAccountIdentifier('TestAccount');
$account->setRoles($roles);

$this->testingProvider->setAuthenticationStatus(TokenInterface::AUTHENTICATION_SUCCESSFUL);
$this->testingProvider->setAccount($account);

$this->securityContext->clearContext();
$this->securityContext->setRequest($this->securityActionRequest);
$this->authenticationManager->authenticate();
}

/**
* @BeforeScenario @resetRedis
*/
Expand Down Expand Up @@ -331,7 +405,7 @@ public function theEnumerationContainsNode($contentReleaseIdentifier, $expectedC
} )()
);

Assert::assertCount($expectedCount, $enumerationAsArray);
Assert::assertCount((int)$expectedCount, $enumerationAsArray);
}

/**
Expand Down Expand Up @@ -488,7 +562,7 @@ public function duringRenderingOfContentReleaseSomeErrorsOccured($contentRelease
$contentReleaseIdentifier = ContentReleaseIdentifier::fromString($contentReleaseIdentifier);
$redisRenderingErrorManager = $this->getObjectManager()->get(RedisRenderingErrorManager::class);
$renderingErrors = $redisRenderingErrorManager->getRenderingErrors($contentReleaseIdentifier);
Assert::assertCount($expectedNumberOfErrors, $renderingErrors);
Assert::assertCount((int)$expectedNumberOfErrors, $renderingErrors);
}

private const DEFAULT_NODETYPES_CONFIG = <<<EOF
Expand Down
1 change: 1 addition & 0 deletions Tests/Behavior/Features/ContentStore/Basics.feature
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@fixtures
@flowEntities
@resetRedis
Feature: Basic Rendering

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@fixtures
@flowEntities
@resetRedis
Feature: Errors while rendering

Expand Down
15 changes: 10 additions & 5 deletions Tests/Behavior/Features/ContentStore/IncrementalRendering.feature
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@fixtures
@flowEntities
@resetRedis
Feature: Incremental Rendering

Expand Down Expand Up @@ -77,11 +78,15 @@ Feature: Incremental Rendering
BEFOREUnterseite2AFTER
"""
Then I expect the content release "6" to not contain anything for URI "http://test.de/de/nested"
# TODO: right now, a rerendering of the homepage is still needed - would be nice to get rid of this sometime in the future.
Then I expect the content release "6" to not contain anything for URI "http://test.de/de"
# /sites/test/sub
# /sites/test right now (TODO debatable whether this makes sense)
And the rendering queue for content release "6" contains 2 documents
# The homepage keeps its rendering: the cache entry of a content collection is tagged with the collection node
# (Neos.Neos:ContentCollection sets @context.node to the nearest content collection), so a change inside a
# document further down the tree flushes neither the homepage nor its collection.
Then I expect the content release "6" to contain the following content for URI "http://test.de/de" at CSS selector "body .neos-contentcollection":
"""
BEFOREHallo - this is rendered.AFTER
"""
# only /sites/test/sub
And the rendering queue for content release "6" contains 1 document

# however, when we re-run the rendering (in the next iteration), the rendering should converge and work out.
And I run the renderer for content release "6" until the queue is empty
Expand Down
1 change: 1 addition & 0 deletions Tests/Behavior/Features/ContentStore/QuickRelease.feature
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@fixtures
@flowEntities
@resetRedis
Feature: Quick Release

Expand Down
19 changes: 11 additions & 8 deletions Tests/Behavior/Features/ContentStore/Renamings.feature
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@fixtures
@flowEntities
@resetRedis
Feature: Renamings before rendering, and during a rendering.

Expand Down Expand Up @@ -259,14 +260,15 @@ Feature: Renamings before rendering, and during a rendering.
And I flush the content cache depending on the modified nodes
And a next content release was triggered

# - here, /sites/test is not in the cache (DescendantOf_ modification), /sites/test/sub2 is not in the cache (DescendantOf_ modification)
# and /sites/test/sub is IN THE CACHE.
# - here, /sites/test/sub2 is not in the cache (DescendantOf_ modification of its content collection),
# while /sites/test and /sites/test/sub ARE IN THE CACHE: a content collection is tagged with the collection
# node, so a change below another document does not reach them.

# NOW, start a new enumeration and a new content release.
When I create a content release "6"
When I enumerate all nodes for content release "6"
Then the enumeration for content release "6" contains 3 nodes
# - Render Orchestrator would add /sites/test/sub to Content Release, and schedule /sites/test and /sites/test/sub2 for rendering
# - Render Orchestrator would add /sites/test and /sites/test/sub to Content Release, and schedule /sites/test/sub2 for rendering
When I run the render-orchestrator control loop once for content release "6"

# - Rename Uri Path Semgment to "foo" for the sub2 page
Expand All @@ -278,18 +280,19 @@ Feature: Renamings before rendering, and during a rendering.
And I flush the content cache depending on the modified nodes

# - Cache is flushed for sub2
# - Cache should also be flushed for /sites/site and /sites/site/sub because of FixedNodeLinkHandlingInContentCacheFlusherAspect
# - however, /sites/site/sub has already a been added to the content release with the old URI -> BROKEN LINK which goes live.
# - Cache is also flushed for /sites/test and /sites/test/sub, because both link to sub2 and therefore carry its
# NodeDynamicTag_ (added by Neos.Neos:ConvertUris while rendering the link)
# - however, both have already been added to the content release with the old URI -> BROKEN LINKS which go live.
And I run the renderer for content release "6" until the queue is empty
When I continue running the render-orchestrator control loop
Then I expect the render-orchestrator control loop to exit with status code 0

Then during rendering of content release "6", 0 errors occured
Then I expect the content release "6" to not contain anything for URI "http://test.de/de/nested2"
# no broken link :-)
# THIS IS THE BROKEN LINK
Then I expect the content release "6" to contain the following HTML content for URI "http://test.de/de" at CSS selector "body .neos-contentcollection":
"""
BEFOREHallo - this is rendered. <a href="/de/foo">Link to /nested2</a>AFTER
BEFOREHallo - this is rendered. <a href="/de/nested2">Link to /nested2</a>AFTER
"""
# THIS IS THE BROKEN LINK
Then I expect the content release "6" to contain the following HTML content for URI "http://test.de/de/nested" at CSS selector "body .neos-contentcollection":
Expand All @@ -314,7 +317,7 @@ Feature: Renamings before rendering, and during a rendering.
Then I expect the render-orchestrator control loop to exit with status code 0
Then during rendering of content release "7", 0 errors occured
Then I expect the content release "6" to not contain anything for URI "http://test.de/de/nested2"
# no broken link :-)
# BROKEN LINK -> fixed
Then I expect the content release "7" to contain the following HTML content for URI "http://test.de/de" at CSS selector "body .neos-contentcollection":
"""
BEFOREHallo - this is rendered. <a href="/de/foo">Link to /nested2</a>AFTER
Expand Down
11 changes: 8 additions & 3 deletions Tests/Behavior/Fixtures/StubPrunnerApiService.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,22 @@
use Flowpack\Prunner\PrunnerApiService;
use Flowpack\Prunner\ValueObject\JobId;
use Flowpack\Prunner\ValueObject\PipelineName;
use Flowpack\Prunner\ValueObject\QueuePartitionName;

class StubPrunnerApiService extends PrunnerApiService
{
public array $calls = [];

public function schedulePipeline(PipelineName $pipeline, array $variables): JobId
{
public function schedulePipeline(
PipelineName $pipeline,
array $variables,
?QueuePartitionName $queuePartition = null
): JobId {
$this->calls[] = [
'method' => 'schedulePipeline',
'pipeline' => $pipeline,
'variables' => $variables
'variables' => $variables,
'queuePartition' => $queuePartition
];

return JobId::create('STUB' . count($this->calls));
Expand Down