From 43c84b8950f4bc5db05da4b928317dd5d87d6be6 Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Mon, 1 May 2023 14:15:56 +0200 Subject: [PATCH 01/32] Update composer.json --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index f992b38..885f1dc 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,7 @@ "license": "GPL-3.0-or-later", "require": { "neos/redirecthandler": "~3.0 || ~4.0 || ~5.0 || dev-master", - "neos/neos": "~4.3 || ~5.0 || ~7.0 || ~8.0 || dev-master" + "neos/neos": "~4.3 || ~5.0 || ~7.0 || ~8.0 || ~9.0 || dev-master" }, "autoload": { "psr-4": { From 24ad45271891562af607390527e97986debcf63d Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Mon, 1 May 2023 15:00:26 +0200 Subject: [PATCH 02/32] FEATURE: Set version constraints to Neos and Flow 9.0 --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 885f1dc..41ba162 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,8 @@ "license": "GPL-3.0-or-later", "require": { "neos/redirecthandler": "~3.0 || ~4.0 || ~5.0 || dev-master", - "neos/neos": "~4.3 || ~5.0 || ~7.0 || ~8.0 || ~9.0 || dev-master" + "neos/neos": "~9.0 || dev-master", + "neos/flow": "~9.0 || dev-master" }, "autoload": { "psr-4": { From cb6ce031d627db3162f706fe3cdaa4d2b6f868c6 Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Tue, 9 May 2023 11:00:14 +0200 Subject: [PATCH 03/32] FEATURE: Refactor Redirect Adapter for Neos 9.0 --- Classes/Package.php | 30 +- Classes/Service/NodeRedirectService.php | 486 +++--------------- Configuration/Settings.yaml | 3 - .../Behat/NodeTypes.Test.Redirect.yaml | 17 + .../Testing/Behat/Settings.Restictions.yaml | 10 + Documentation/index.rst | 17 - .../Features/Bootstrap/FeatureContext.php | 144 +++++- .../Bootstrap/RedirectOperationTrait.php | 38 +- Tests/Behavior/Features/Redirect.feature | 347 +++++++++---- Tests/Behavior/behat.yml | 25 - Tests/Behavior/behat.yml.dist | 28 +- .../Service/NodeRedirectServiceTest.php | 16 +- composer.json | 4 +- 13 files changed, 535 insertions(+), 630 deletions(-) create mode 100644 Configuration/Testing/Behat/NodeTypes.Test.Redirect.yaml create mode 100644 Configuration/Testing/Behat/Settings.Restictions.yaml delete mode 100644 Tests/Behavior/behat.yml diff --git a/Classes/Package.php b/Classes/Package.php index 3d0b686..e0d56d3 100644 --- a/Classes/Package.php +++ b/Classes/Package.php @@ -13,11 +13,12 @@ * source code. */ -use Neos\Flow\Persistence\Doctrine\PersistenceManager; -use Neos\RedirectHandler\NeosAdapter\Service\NodeRedirectService; +use Neos\EventStore\Model\EventEnvelope; use Neos\Flow\Core\Bootstrap; use Neos\Flow\Package\Package as BasePackage; -use Neos\ContentRepository\Domain\Model\Workspace; +use Neos\Neos\Domain\Model\SiteNodeName; +use Neos\Neos\FrontendRouting\Projection\DocumentUriPathProjection; +use Neos\RedirectHandler\NeosAdapter\Service\NodeRedirectService; /** * The Neos RedirectHandler NeosAdapter Package @@ -32,7 +33,26 @@ public function boot(Bootstrap $bootstrap): void { $dispatcher = $bootstrap->getSignalSlotDispatcher(); - $dispatcher->connect(Workspace::class, 'beforeNodePublishing', NodeRedirectService::class, 'collectPossibleRedirects'); - $dispatcher->connect(PersistenceManager::class, 'allObjectsPersisted', NodeRedirectService::class, 'createPendingRedirects'); + $dispatcher->connect(DocumentUriPathProjection::class, 'afterNodeAggregateWasMoved', function ( + string $oldUriPath, string $newUriPath, SiteNodeName $siteNodeName, $_, + ) use ($bootstrap) { + $nodeRedirectService = $bootstrap->getObjectManager()->get(NodeRedirectService::class); + $nodeRedirectService->createRedirect($oldUriPath, $newUriPath, $siteNodeName); + }); + + $dispatcher->connect(DocumentUriPathProjection::class, 'afterNodeAggregateWasRemoved', function ( + string $oldUriPath, SiteNodeName $siteNodeName, $_, + ) use ($bootstrap) { + $nodeRedirectService = $bootstrap->getObjectManager()->get(NodeRedirectService::class); + $nodeRedirectService->createRedirect($oldUriPath, null, $siteNodeName); + }); + + $dispatcher->connect(DocumentUriPathProjection::class, 'afterDocumentUriPathChanged', function ( + string $oldUriPath, string $newUriPath, SiteNodeName $siteNodeName, $_, EventEnvelope $eventEnvelope, + ) use ($bootstrap) { + $nodeRedirectService = $bootstrap->getObjectManager()->get(NodeRedirectService::class); + $nodeRedirectService->createRedirect($oldUriPath, $newUriPath, $siteNodeName); + }); + } } diff --git a/Classes/Service/NodeRedirectService.php b/Classes/Service/NodeRedirectService.php index e7ab051..f49eaf5 100644 --- a/Classes/Service/NodeRedirectService.php +++ b/Classes/Service/NodeRedirectService.php @@ -13,25 +13,14 @@ * source code. */ -use GuzzleHttp\Psr7\ServerRequest; -use Neos\ContentRepository\Domain\Factory\NodeFactory; -use Neos\ContentRepository\Domain\Model\NodeInterface; -use Neos\ContentRepository\Domain\Model\Workspace; -use Neos\ContentRepository\Domain\Service\ContentDimensionCombinator; -use Neos\ContentRepository\Domain\Service\ContextFactoryInterface; +use Neos\ContentRepository\Core\Projection\ContentGraph\Node; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; +use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; -use Neos\Flow\Cli\CommandRequestHandler; -use Neos\Flow\Core\Bootstrap; -use Neos\Flow\Http\Exception as HttpException; -use Neos\Flow\Http\HttpRequestHandlerInterface; -use Neos\Flow\Mvc\ActionRequest; -use Neos\Flow\Mvc\Routing\Dto\RouteParameters; -use Neos\Flow\Mvc\Routing\Exception\MissingActionNameException; -use Neos\Flow\Mvc\Routing\RouterCachingService; -use Neos\Flow\Mvc\Routing\UriBuilder; use Neos\Flow\Persistence\PersistenceManagerInterface; -use Neos\Neos\Controller\CreateContentContextTrait; use Neos\Neos\Domain\Model\Domain; +use Neos\Neos\Domain\Model\SiteNodeName; +use Neos\Neos\Domain\Repository\SiteRepository; use Neos\RedirectHandler\Storage\RedirectStorageInterface; use Psr\Log\LoggerInterface; @@ -44,12 +33,8 @@ */ class NodeRedirectService { - use CreateContentContextTrait; - - /** - * @var UriBuilder - */ - protected $uriBuilder; + const STATUS_CODE_TYPE_REDIRECT = 'redirect'; + const STATUS_CODE_TYPE_GONE = 'gone'; /** * @Flow\Inject @@ -57,42 +42,18 @@ class NodeRedirectService */ protected $redirectStorage; - /** - * @Flow\Inject - * @var RouterCachingService - */ - protected $routerCachingService; - /** * @Flow\Inject * @var PersistenceManagerInterface */ protected $persistenceManager; - /** - * @Flow\Inject - * @var ContextFactoryInterface - */ - protected $contextFactory; - - /** - * @Flow\Inject - * @var NodeFactory - */ - protected $nodeFactory; - /** * @Flow\Inject * @var LoggerInterface */ protected $logger; - /** - * @var Bootstrap - * @Flow\Inject - */ - protected $bootstrap; - /** * @Flow\InjectConfiguration(path="statusCode", package="Neos.RedirectHandler") * @var array @@ -100,10 +61,10 @@ class NodeRedirectService protected $defaultStatusCode; /** - * @Flow\Inject - * @var ContentDimensionCombinator + * @Flow\InjectConfiguration(path="enableAutomaticRedirects", package="Neos.RedirectHandler.NeosAdapter") + * @var array */ - protected $contentDimensionCombinator; + protected $enableAutomaticRedirects; /** * @Flow\InjectConfiguration(path="enableRemovedNodeRedirect", package="Neos.RedirectHandler.NeosAdapter") @@ -111,12 +72,6 @@ class NodeRedirectService */ protected $enableRemovedNodeRedirect; - /** - * @Flow\InjectConfiguration(path="restrictByPathPrefix", package="Neos.RedirectHandler.NeosAdapter") - * @var array - */ - protected $restrictByPathPrefix; - /** * @Flow\InjectConfiguration(path="restrictByOldUriPrefix", package="Neos.RedirectHandler.NeosAdapter") * @var array @@ -130,261 +85,64 @@ class NodeRedirectService protected $restrictByNodeType; /** - * @Flow\InjectConfiguration(path="enableAutomaticRedirects", package="Neos.RedirectHandler.NeosAdapter") - * @var array + * @Flow\Inject + * @var SiteRepository */ - protected $enableAutomaticRedirects; + protected $siteRepository; - /** - * @Flow\InjectConfiguration(path="http.baseUri", package="Neos.Flow") - * @var string - */ - protected $baseUri; + #[\Neos\Flow\Annotations\Inject] + protected ContentRepositoryRegistry $contentRepositoryRegistry; - /** - * @var array - */ - protected $pendingRedirects = []; /** - * @var ActionRequest - */ - protected $actionRequestForUriBuilder; - - /** - * Collects the node for redirection if it is a 'Neos.Neos:Document' node and its URI has changed - * - * @param NodeInterface $node The node that is about to be published - * @param Workspace $targetWorkspace + * @param string $oldUriPath + * @param string|null $newUriPath + * @param SiteNodeName $siteNodeName * @return void - * @throws MissingActionNameException */ - public function collectPossibleRedirects(NodeInterface $node, Workspace $targetWorkspace): void - { + public function createRedirect( + string $oldUriPath, + ?string $newUriPath, + SiteNodeName $siteNodeName, + ): void { + if (!$this->enableAutomaticRedirects) { return; } - $nodeType = $node->getNodeType(); - if ($targetWorkspace->isPublicWorkspace() === false || $nodeType->isOfType('Neos.Neos:Document') === false) { + // TODO: Restrict by NodeType + if (/*$this->isRestrictedByNodeType($targetNodeInfo->node)|| */ $this->isRestrictedByOldUri($oldUriPath)) { return; } - $this->appendNodeAndChildrenDocumentsToPendingRedirects($node, $targetWorkspace); - } - - /** - * Returns the current http request or a generated http request - * based on a configured baseUri to allow redirect generation - * for CLI requests. - * - * @return ActionRequest - */ - protected function getActionRequestForUriBuilder(): ?ActionRequest - { - if ($this->actionRequestForUriBuilder) { - return $this->actionRequestForUriBuilder; - } - - /** @var HttpRequestHandlerInterface $requestHandler */ - $requestHandler = $this->bootstrap->getActiveRequestHandler(); - - if ($requestHandler instanceof CommandRequestHandler) { - // Generate a custom request when the current request was triggered from CLI - $baseUri = $this->baseUri ?? 'http://localhost'; - - // Prevent `index.php` appearing in generated redirects - putenv('FLOW_REWRITEURLS=1'); - - $httpRequest = new ServerRequest('POST', $baseUri); + $oldUriPath = $this->buildUri($oldUriPath); + if ($newUriPath !== null) { + $newUriPath = $this->buildUri($newUriPath); + $this->createRedirectWithNewTarget($oldUriPath, $newUriPath, $siteNodeName); } else { - $httpRequest = $requestHandler->getHttpRequest(); - } - - if (method_exists(ActionRequest::class, 'fromHttpRequest')) { - $routeParameters = $httpRequest->getAttribute('routingParameters') ?? RouteParameters::createEmpty(); - $httpRequest = $httpRequest->withAttribute('routingParameters', $routeParameters->withParameter('requestUriHost', $httpRequest->getUri()->getHost())); - // From Flow 6+ we have to use a static method to create an ActionRequest. Earlier versions use the constructor. - $this->actionRequestForUriBuilder = ActionRequest::fromHttpRequest($httpRequest); - } else { - /* @deprecated This case can be removed up when this package only supports Flow 6+. */ - if ($httpRequest instanceof ServerRequest) { - $httpRequest = new \Neos\Flow\Http\Request([], [], [], [ - 'HTTP_HOST' => $httpRequest->getHeaderLine('host'), - 'HTTPS' => $httpRequest->getHeaderLine('scheme') === 'https', - 'REQUEST_URI' => $httpRequest->getHeaderLine('path'), - ]); - } - $this->actionRequestForUriBuilder = new ActionRequest($httpRequest); - } - - return $this->actionRequestForUriBuilder; - } - - /** - * Creates the queued redirects provided we can find the node. - * - * @return void - * @throws MissingActionNameException - */ - public function createPendingRedirects(): void - { - if (!$this->enableAutomaticRedirects) { - return; - } - - $this->nodeFactory->reset(); - foreach ($this->pendingRedirects as $nodeIdentifierAndWorkspace => $oldUriPerDimensionCombination) { - [$nodeIdentifier, $workspaceName] = explode('@', $nodeIdentifierAndWorkspace); - $this->buildRedirects($nodeIdentifier, $workspaceName, $oldUriPerDimensionCombination); + $this->createRedirectForRemovedTarget($oldUriPath, $siteNodeName); } - $this->pendingRedirects = []; $this->persistenceManager->persistAll(); } /** - * @param NodeInterface $node - * @param Workspace $targetWorkspace - * @return void - * @throws MissingActionNameException - */ - protected function appendNodeAndChildrenDocumentsToPendingRedirects(NodeInterface $node, Workspace $targetWorkspace): void - { - $identifierAndWorkspaceKey = $node->getIdentifier() . '@' . $targetWorkspace->getName(); - if (isset($this->pendingRedirects[$identifierAndWorkspaceKey])) { - return; - } - - if (!$this->hasNodeUriChanged($node, $targetWorkspace)) { - return; - } - - $this->pendingRedirects[$identifierAndWorkspaceKey] = $this->createUriPathsAcrossDimensionsForNode($node->getIdentifier(), $targetWorkspace); - - foreach ($node->getChildNodes('Neos.Neos:Document') as $childNode) { - $this->appendNodeAndChildrenDocumentsToPendingRedirects($childNode, $targetWorkspace); - } - } - - /** - * @param string $nodeIdentifier - * @param Workspace $targetWorkspace - * @return array - * @throws MissingActionNameException - */ - protected function createUriPathsAcrossDimensionsForNode(string $nodeIdentifier, Workspace $targetWorkspace): array - { - $result = []; - foreach ($this->contentDimensionCombinator->getAllAllowedCombinations() as $allowedCombination) { - $nodeInDimensions = $this->getNodeInWorkspaceAndDimensions($nodeIdentifier, $targetWorkspace->getName(), $allowedCombination); - if ($nodeInDimensions === null) { - continue; - } - - try { - $nodeUriPath = $this->buildUriPathForNode($nodeInDimensions); - } catch (\Exception $_) { - continue; - } - $nodeUriPath = $this->removeContextInformationFromRelativeNodeUri($nodeUriPath); - $result[] = [ - $nodeUriPath, - $allowedCombination - ]; - } - - return $result; - } - - /** - * Has the Uri changed at all. + * Adds a redirect for given $oldUriPath to $newUriPath for all domains set up for $siteNode * - * @param NodeInterface $node - * @param Workspace $targetWorkspace + * @param NodeAggregateId $nodeAggregateId + * @param string $oldUriPath + * @param string $newUriPath * @return bool - * @throws MissingActionNameException */ - protected function hasNodeUriChanged(NodeInterface $node, Workspace $targetWorkspace): bool + protected function createRedirectWithNewTarget(string $oldUriPath, string $newUriPath, SiteNodeName $siteNodeName): bool { - $nodeInTargetWorkspace = $this->getNodeInWorkspace($node, $targetWorkspace); - if (!$nodeInTargetWorkspace) { - return false; - } - try { - $newUriPath = $this->buildUriPathForNode($node); - } catch (\Exception $exception) { - $this->logger->info(sprintf('Failed to build new URI for updated node "%s": %s', $node->getContextPath(), $exception->getMessage())); + if ($oldUriPath === $newUriPath) { return false; } - $newUriPath = $this->removeContextInformationFromRelativeNodeUri($newUriPath); - try { - $oldUriPath = $this->buildUriPathForNode($nodeInTargetWorkspace); - } catch (\Exception $exception) { - $this->logger->info(sprintf('Failed to build previous URI for updated node "%s": %s', $node->getContextPath(), $exception->getMessage())); - return false; - } - $oldUriPath = $this->removeContextInformationFromRelativeNodeUri($oldUriPath); - - return ($newUriPath !== $oldUriPath); - } - /** - * Build redirects in all dimensions for a given node. - * - * @param string $nodeIdentifier - * @param string $workspaceName - * @param $oldUriPerDimensionCombination - * @return void - * @throws MissingActionNameException - */ - protected function buildRedirects(string $nodeIdentifier, string $workspaceName, array $oldUriPerDimensionCombination): void - { - foreach ($oldUriPerDimensionCombination as [$oldRelativeUri, $dimensionCombination]) { - $this->createRedirectFrom($oldRelativeUri, $nodeIdentifier, $workspaceName, $dimensionCombination); - } - } + $hosts = $this->getHostnames($siteNodeName); + $statusCode = (integer)$this->defaultStatusCode[self::STATUS_CODE_TYPE_REDIRECT]; - /** - * Gets the node in the given dimensions and workspace and redirects the oldUri to the new one. - * - * @param string $oldUri - * @param string $nodeIdentifer - * @param string $workspaceName - * @param array $dimensionCombination - * @return bool - * @throws MissingActionNameException - */ - protected function createRedirectFrom(string $oldUri, string $nodeIdentifer, string $workspaceName, array $dimensionCombination): bool - { - $node = $this->getNodeInWorkspaceAndDimensions($nodeIdentifer, $workspaceName, $dimensionCombination); - if ($node === null) { - return false; - } - - if ($this->isRestrictedByNodeType($node) || $this->isRestrictedByPath($node) || $this->isRestrictedByOldUri($oldUri, $node)) { - return false; - } - - try { - $newUri = $this->buildUriPathForNode($node); - } catch (\Exception $exception) { - $this->logger->info(sprintf('Redirect creation skipped since URL for node "%s" could not be created and led to an exception: %s', $node->getContextPath(), $exception->getMessage())); - return false; - } - - if ($node->isRemoved()) { - return $this->removeNodeRedirectIfNeeded($node, $newUri); - } - - if ($oldUri === $newUri) { - return false; - } - - $hosts = $this->getHostnames($node); - $this->flushRoutingCacheForNode($node); - $statusCode = (integer)$this->defaultStatusCode['redirect']; - - $this->redirectStorage->addRedirect($oldUri, $newUri, $statusCode, $hosts); + $this->redirectStorage->addRedirect($oldUriPath, $newUriPath, $statusCode, $hosts); return true; } @@ -392,20 +150,19 @@ protected function createRedirectFrom(string $oldUri, string $nodeIdentifer, str /** * Removes a redirect * - * @param NodeInterface $node - * @param string $newUri + * @param string $oldUriPath + * @param SiteNodeName $siteNodeName * @return bool */ - protected function removeNodeRedirectIfNeeded(NodeInterface $node, string $newUri): bool + protected function createRedirectForRemovedTarget(string $oldUriPath, SiteNodeName $siteNodeName): bool { // By default the redirect handling for removed nodes is activated. // If it is deactivated in your settings you will be able to handle the redirects on your own. // For example redirect to dedicated landing pages for deleted campaign NodeTypes if ($this->enableRemovedNodeRedirect) { - $hosts = $this->getHostnames($node); - $this->flushRoutingCacheForNode($node); - $statusCode = (integer)$this->defaultStatusCode['gone']; - $this->redirectStorage->addRedirect($newUri, '', $statusCode, $hosts); + $hosts = $this->getHostnames($siteNodeName); + $statusCode = (integer)$this->defaultStatusCode[self::STATUS_CODE_TYPE_GONE]; + $this->redirectStorage->addRedirect($oldUriPath, '', $statusCode, $hosts); return true; } @@ -413,25 +170,13 @@ protected function removeNodeRedirectIfNeeded(NodeInterface $node, string $newUr return false; } - /** - * Removes any context information appended to a node Uri. - * - * @param string $relativeNodeUri - * @return string - */ - protected function removeContextInformationFromRelativeNodeUri(string $relativeNodeUri): string - { - // FIXME: Uses the same regexp than the ContentContextBar Ember View, but we can probably find something better. - return (string)preg_replace('/@[A-Za-z0-9;&,\-_=]+/', '', $relativeNodeUri); - } - /** * Check if the current node type is restricted by Settings * - * @param NodeInterface $node + * @param Node $node * @return bool */ - protected function isRestrictedByNodeType(NodeInterface $node): bool + protected function isRestrictedByNodeType(Node $node): bool { if (!isset($this->restrictByNodeType)) { return false; @@ -441,10 +186,10 @@ protected function isRestrictedByNodeType(NodeInterface $node): bool if ($status !== true) { continue; } - if ($node->getNodeType()->isOfType($disabledNodeType)) { + if ($node->nodeType->isOfType($disabledNodeType)) { $this->logger->debug(vsprintf('Redirect skipped based on the current node type (%s) for node %s because is of type %s', [ - $node->getNodeType()->getName(), - $node->getContextPath(), + $node->nodeType->name->value, + $node->nodeAggregateId->value, $disabledNodeType ])); @@ -455,45 +200,13 @@ protected function isRestrictedByNodeType(NodeInterface $node): bool return false; } - /** - * Check if the current node path is restricted by Settings - * - * @param NodeInterface $node - * @return bool - */ - protected function isRestrictedByPath(NodeInterface $node): bool - { - if (!isset($this->restrictByPathPrefix)) { - return false; - } - - foreach ($this->restrictByPathPrefix as $pathPrefix => $status) { - if ($status !== true) { - continue; - } - $pathPrefix = rtrim($pathPrefix, '/') . '/'; - if (mb_strpos($node->getPath(), $pathPrefix) === 0) { - $this->logger->debug(vsprintf('Redirect skipped based on the current node path (%s) for node %s because prefix matches %s', [ - $node->getPath(), - $node->getContextPath(), - $pathPrefix - ])); - - return true; - } - } - - return false; - } - /** * Check if the old URI is restricted by Settings * - * @param string $oldUri - * @param NodeInterface $node + * @param string $oldUriPath * @return bool */ - protected function isRestrictedByOldUri(string $oldUri, NodeInterface $node): bool + protected function isRestrictedByOldUri(string $oldUriPath): bool { if (!isset($this->restrictByOldUriPrefix)) { return false; @@ -504,10 +217,10 @@ protected function isRestrictedByOldUri(string $oldUri, NodeInterface $node): bo continue; } $uriPrefix = rtrim($uriPrefix, '/') . '/'; - if (mb_strpos($oldUri, $uriPrefix) === 0) { - $this->logger->debug(vsprintf('Redirect skipped based on the old URI (%s) for node %s because prefix matches %s', [ - $oldUri, - $node->getContextPath(), + $oldUriPath = rtrim($oldUriPath, '/') . '/'; + if (mb_strpos($oldUriPath, $uriPrefix) === 0) { + $this->logger->debug(vsprintf('Redirect skipped based on the old URI (%s) because prefix matches %s', [ + $oldUriPath, $uriPrefix ])); @@ -521,14 +234,15 @@ protected function isRestrictedByOldUri(string $oldUri, NodeInterface $node): bo /** * Collects all hostnames from the Domain entries attached to the current site. * - * @param NodeInterface $node + * @param SiteNodeName $siteNodeName * @return array */ - protected function getHostnames(NodeInterface $node): array + protected function getHostnames(SiteNodeName $siteNodeName): array { - $contentContext = $this->createContextMatchingNodeData($node->getNodeData()); + // TODO: Caching + $site = $this->siteRepository->findOneByNodeName($siteNodeName); + $domains = []; - $site = $contentContext->getCurrentSite(); if ($site === null) { return $domains; } @@ -542,79 +256,15 @@ protected function getHostnames(NodeInterface $node): array } /** - * Removes all routing cache entries for the given $nodeData - * - * @param NodeInterface $node - * @return void - */ - protected function flushRoutingCacheForNode(NodeInterface $node): void - { - $nodeData = $node->getNodeData(); - $nodeDataIdentifier = $this->persistenceManager->getIdentifierByObject($nodeData); - if ($nodeDataIdentifier === null) { - return; - } - $this->routerCachingService->flushCachesByTag($nodeDataIdentifier); - } - - /** - * Creates a (relative) URI for the given $nodeContextPath removing the "@workspace-name" from the result + * Creates a (relative) URI for the given $nodeInfo * - * @param NodeInterface $node + * @param string $uriPath * @return string the resulting (relative) URI - * @throws MissingActionNameException - * @throws HttpException - */ - protected function buildUriPathForNode(NodeInterface $node): string - { - return $this->getUriBuilder() - ->uriFor('show', ['node' => $node], 'Frontend\\Node', 'Neos.Neos'); - } - - /** - * Creates an UriBuilder instance for the current request - * - * @return UriBuilder */ - protected function getUriBuilder(): UriBuilder + protected function buildUri(string $uriPath): string { - if ($this->uriBuilder !== null) { - return $this->uriBuilder; - } - - $this->uriBuilder = new UriBuilder(); - $this->uriBuilder - ->setFormat('html') - ->setCreateAbsoluteUri(false) - ->setRequest($this->getActionRequestForUriBuilder()); - - return $this->uriBuilder; - } - - /** - * @param NodeInterface $node - * @param Workspace $targetWorkspace - * @return NodeInterface|null - */ - protected function getNodeInWorkspace(NodeInterface $node, Workspace $targetWorkspace): ?NodeInterface - { - return $this->getNodeInWorkspaceAndDimensions($node->getIdentifier(), $targetWorkspace->getName(), $node->getContext()->getDimensions()); - } - - /** - * @param string $nodeIdentifier - * @param string $workspaceName - * @param array $dimensionCombination - * @return NodeInterface|null - */ - protected function getNodeInWorkspaceAndDimensions(string $nodeIdentifier, string $workspaceName, array $dimensionCombination): ?NodeInterface - { - $context = $this->contextFactory->create([ - 'workspaceName' => $workspaceName, - 'dimensions' => $dimensionCombination, - 'invisibleContentShown' => true, - ]); - - return $context->getNodeByIdentifier($nodeIdentifier); + // TODO: Add dimension prefix + // TODO: Add uriSuffix + return $uriPath; } } diff --git a/Configuration/Settings.yaml b/Configuration/Settings.yaml index bb9f2fb..42d0436 100755 --- a/Configuration/Settings.yaml +++ b/Configuration/Settings.yaml @@ -6,9 +6,6 @@ Neos: # For example redirect to dedicated landing pages for deleted campaign NodeTypes enableRemovedNodeRedirect: true - pathPrefixConfiguration: [] - # '/sites/neosdemo/': false - restrictByNodeType: [] # Neos.Neos:Document: true diff --git a/Configuration/Testing/Behat/NodeTypes.Test.Redirect.yaml b/Configuration/Testing/Behat/NodeTypes.Test.Redirect.yaml new file mode 100644 index 0000000..682fd71 --- /dev/null +++ b/Configuration/Testing/Behat/NodeTypes.Test.Redirect.yaml @@ -0,0 +1,17 @@ +# Those node type definitions are required for the Redirect Behat tests + +'Neos.Neos:Test.Redirect.Page': + superTypes: + 'Neos.Neos:Document': true + constraints: + nodeTypes: + '*': true + 'Neos.Neos:Test.Redirect.Page': true + +'Neos.Neos:Test.Redirect.RestrictedPage': + superTypes: + 'Neos.Neos:Document': true + constraints: + nodeTypes: + '*': true + 'Neos.Neos:Test.Redirect.Page': true \ No newline at end of file diff --git a/Configuration/Testing/Behat/Settings.Restictions.yaml b/Configuration/Testing/Behat/Settings.Restictions.yaml new file mode 100644 index 0000000..742ea15 --- /dev/null +++ b/Configuration/Testing/Behat/Settings.Restictions.yaml @@ -0,0 +1,10 @@ +Neos: + RedirectHandler: + NeosAdapter: + enableRemovedNodeRedirect: true + enableAutomaticRedirects: true + restrictByNodeType: + 'Neos.Neos:Test.Redirect.RestrictedPage': true + restrictByOldUriPrefix: + 'restricted-by-path': true + diff --git a/Documentation/index.rst b/Documentation/index.rst index a0c8095..71e84d6 100644 --- a/Documentation/index.rst +++ b/Documentation/index.rst @@ -58,23 +58,6 @@ Restrict redirect generation by node type. restrictByNodeType: Neos.Neos:Document: true -restrictByPathPrefix -^^^^^^^^^^^^^^^^^^^^ - -Restrict redirect generation by node path prefix. - -**Note**: No redirect will be created if you move a node within the restricted path or if you move it away from the -restricted path. But if you move a node into the restricted path the restriction rule will not apply, because the -restriction is based on the source node path. - -.. code-block:: yaml - - Neos: - RedirectHandler: - NeosAdapter: - restrictByPathPrefix: - - '/sites/neosdemo': true - restrictByOldUriPrefix ^^^^^^^^^^^^^^^^^^^^^^ diff --git a/Tests/Behavior/Features/Bootstrap/FeatureContext.php b/Tests/Behavior/Features/Bootstrap/FeatureContext.php index e12e29f..3af7b4c 100644 --- a/Tests/Behavior/Features/Bootstrap/FeatureContext.php +++ b/Tests/Behavior/Features/Bootstrap/FeatureContext.php @@ -1,45 +1,157 @@ initializeFlow(); } $this->objectManager = self::$bootstrap->getObjectManager(); + $this->environment = $this->objectManager->get(Environment::class); + $this->nodeAuthorizationService = $this->objectManager->get(AuthorizationService::class); - $this->nodeTypeManager = $this->objectManager->get(NodeTypeManager::class); $this->setupSecurity(); + $this->setupEventSourcedTrait(true); + } + + protected function getContentRepositoryRegistry(): ContentRepositoryRegistry + { + /** @var ContentRepositoryRegistry $contentRepositoryRegistry */ + $contentRepositoryRegistry = $this->objectManager->get(ContentRepositoryRegistry::class); + + return $contentRepositoryRegistry; + } + + protected function getContentRepositoryService(ContentRepositoryId $contentRepositoryId, ContentRepositoryServiceFactoryInterface $factory): ContentRepositoryServiceInterface + { + return $this->getContentRepositoryRegistry()->getService($contentRepositoryId, $factory); + } + + /** + * @param array $adapterKeys "DoctrineDBAL" if + * @return void + */ + protected function initCleanContentRepository(array $adapterKeys): void + { + $this->logToRaceConditionTracker(['msg' => 'initCleanContentRepository']); + + $configurationManager = $this->getObjectManager()->get(ConfigurationManager::class); + $registrySettings = $configurationManager->getConfiguration( + ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, + 'Neos.ContentRepositoryRegistry' + ); + + if (!in_array('Postgres', $adapterKeys)) { + // in case we do not have tests annotated with @adapters=Postgres, we + // REMOVE the Postgres projection from the Registry settings. This way, we won't trigger + // Postgres projection catchup for tests which are not yet postgres-aware. + // + // This is to make the testcases more stable and deterministic. We can remove this workaround + // once the Postgres adapter is fully ready. + unset($registrySettings['presets'][$this->contentRepositoryId->value]['projections']['Neos.ContentGraph.PostgreSQLAdapter:Hypergraph']); + } + $registrySettings['presets'][$this->contentRepositoryId->value]['userIdProvider']['factoryObjectName'] = FakeUserIdProviderFactory::class; + $registrySettings['presets'][$this->contentRepositoryId->value]['clock']['factoryObjectName'] = FakeClockFactory::class; + + $this->contentRepositoryRegistry = new ContentRepositoryRegistry( + $registrySettings, + $this->getObjectManager() + ); + + + $this->contentRepository = $this->contentRepositoryRegistry->get($this->contentRepositoryId); + // Big performance optimization: only run the setup once - DRAMATICALLY reduces test time + if ($this->alwaysRunContentRepositorySetup || !self::$wasContentRepositorySetupCalled) { + $this->contentRepository->setUp(); + self::$wasContentRepositorySetupCalled = true; + } + $this->contentRepositoryInternals = $this->contentRepositoryRegistry->getService($this->contentRepositoryId, new ContentRepositoryInternalsFactory()); + + $availableContentGraphs = []; + $availableContentGraphs['DoctrineDBAL'] = $this->contentRepository->getContentGraph(); + // NOTE: to disable a content graph (do not run the tests for it), you can use "null" as value. + if (in_array('Postgres', $adapterKeys)) { + $availableContentGraphs['Postgres'] = $this->contentRepository->projectionState(ContentHypergraph::class); + } + + if (count($availableContentGraphs) === 0) { + throw new \RuntimeException('No content graph active during testing. Please set one in settings in activeContentGraphs'); + } + $this->availableContentGraphs = new ContentGraphs($availableContentGraphs); } } diff --git a/Tests/Behavior/Features/Bootstrap/RedirectOperationTrait.php b/Tests/Behavior/Features/Bootstrap/RedirectOperationTrait.php index abb3b78..f28410e 100755 --- a/Tests/Behavior/Features/Bootstrap/RedirectOperationTrait.php +++ b/Tests/Behavior/Features/Bootstrap/RedirectOperationTrait.php @@ -39,23 +39,29 @@ public function iHaveTheFollowingRedirects($table): void } /** - * @Then /^A redirect should be created for the node with path "([^"]*)" and with the following context:$/ + * @Given /^I should have a redirect with sourceUri "([^"]*)" and targetUri "([^"]*)"$/ */ - public function aRedirectShouldBeCreatedForTheNodeWithPathAndWithTheFollowingContext($path, $table): void + public function iShouldHaveARedirectWithSourceUriAndTargetUri($sourceUri, $targetUri): void { - $rows = $table->getHash(); - $context = $this->getContextForProperties($rows[0]); - $workspace = $context->getWorkspace(); - $redirectNode = $context->getNode($path); - $redirectService = $this->objectManager->get(NodeRedirectService::class); + $nodeRedirectStorage = $this->objectManager->get(RedirectStorage::class); + + $redirect = $nodeRedirectStorage->getOneBySourceUriPathAndHost($sourceUri); - $redirectService->createRedirectsForPublishedNode($redirectNode, $workspace); + if ($redirect !== null) { + Assert::assertEquals( + $targetUri, + $redirect->getTargetUriPath(), + 'A redirect was created, but the target URI does not match' + ); + } else { + Assert::assertNotNull($redirect, 'No redirect was created for asserted sourceUri'); + } } /** - * @Given /^I should have a redirect with sourceUri "([^"]*)" and targetUri "([^"]*)"$/ + * @Given /^I should have a redirect with sourceUri "([^"]*)" and statusCode "([^"]*)"$/ */ - public function iShouldHaveARedirectWithSourceUriAndTargetUri($sourceUri, $targetUri): void + public function iShouldHaveARedirectWithSourceUriAndStatus($sourceUri, $statusCode): void { $nodeRedirectStorage = $this->objectManager->get(RedirectStorage::class); @@ -63,9 +69,9 @@ public function iShouldHaveARedirectWithSourceUriAndTargetUri($sourceUri, $targe if ($redirect !== null) { Assert::assertEquals( - $targetUri, - $redirect->getTargetUriPath(), - 'A redirect was created, but the target URI does not match' + $statusCode, + $redirect->getStatusCode(), + 'A redirect was created, but the status code does not match' ); } else { Assert::assertNotNull($redirect, 'No redirect was created for asserted sourceUri'); @@ -84,11 +90,11 @@ public function iShouldHaveNoRedirectWithSourceUriAndTargetUri($sourceUri, $targ Assert::assertNotEquals( $targetUri, $redirect->getTargetUriPath(), - 'An untwanted redirect was created for given source and target URI' + 'An unwanted redirect was created for given source and target URI' ); + } else { + Assert::assertNull($redirect); } - - Assert::assertNull($redirect); } /** diff --git a/Tests/Behavior/Features/Redirect.feature b/Tests/Behavior/Features/Redirect.feature index d14265f..ca3f44d 100755 --- a/Tests/Behavior/Features/Redirect.feature +++ b/Tests/Behavior/Features/Redirect.feature @@ -1,129 +1,254 @@ -Feature: Redirects are created automatically when the URI of an existing node is changed +@fixtures @contentrepository +Feature: Basic redirect handling with document nodes in one dimension + Background: - Given I am authenticated with role "Neos.Neos:Editor" - And I have the following content dimensions: - | Identifier | Default | Presets | - | language | en | en=en; de=de,en; fr=fr | - And I have the following nodes: - | Identifier | Path | Node Type | Properties | Workspace | Hidden | Language | - | ecf40ad1-3119-0a43-d02e-55f8b5aa3c70 | /sites | unstructured | | live | | | - | fd5ba6e1-4313-b145-1004-dad2f1173a35 | /sites/behat | Neos.Neos:Document | {"uriPathSegment": "home"} | live | | en | - | 68ca0dcd-2afb-ef0e-1106-a5301e65b8a0 | /sites/behat/company | Neos.Neos:Document | {"uriPathSegment": "company"} | live | | en | - | 52540602-b417-11e3-9358-14109fd7a2dd | /sites/behat/service | Neos.Neos:Document | {"uriPathSegment": "service"} | live | | en | - | dc48851c-f653-ebd5-4d35-3feac69a3e09 | /sites/behat/about | Neos.Neos:Document | {"uriPathSegment": "about"} | live | | en | - | 511e9e4b-2193-4100-9a91-6fde2586ae95 | /sites/behat/imprint | Neos.Neos:Document | {"uriPathSegment": "impressum"} | live | | de | - | 511e9e4b-2193-4100-9a91-6fde2586ae95 | /sites/behat/imprint | Neos.Neos:Document | {"uriPathSegment": "imprint"} | live | | en | - | 511e9e4b-2193-4100-9a91-6fde2586ae95 | /sites/behat/imprint | Neos.Neos:Document | {"uriPathSegment": "empreinte"} | live | | fr | - | 4bba27c8-5029-4ae6-8371-0f2b3e1700a9 | /sites/behat/buy | Neos.Neos:Document | {"uriPathSegment": "buy", "title": "Buy"} | live | | en | - | 4bba27c8-5029-4ae6-8371-0f2b3e1700a9 | /sites/behat/buy | Neos.Neos:Document | {"uriPathSegment": "acheter"} | live | | fr | - | 4bba27c8-5029-4ae6-8371-0f2b3e1700a9 | /sites/behat/buy | Neos.Neos:Document | {"uriPathSegment": "kaufen"} | live | true | de | - | 81dc6c8c-f478-434c-9ac9-bd5d1781cd95 | /sites/behat/mail | Neos.Neos:Document | {"uriPathSegment": "mail"} | live | | en | - | 81dc6c8c-f478-434c-9ac9-bd5d1781cd95 | /sites/behat/mail | Neos.Neos:Document | {"uriPathSegment": "mail"} | live | true | de | + Given I have no content dimensions + And I am user identified by "initiating-user-identifier" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | newContentStreamId | "cs-identifier" | + And the event RootNodeAggregateWithNodeWasCreated was published with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "site-root" | + | nodeTypeName | "Neos.Neos:Sites" | + | coveredDimensionSpacePoints | [{}] | + | nodeAggregateClassification | "root" | + And the graph projection is fully up to date + + # site-root + # behat + # company + # service + # about + # imprint + # buy + # mail + And I am in content stream "cs-identifier" and dimension space point {} + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | parentNodeAggregateId | nodeTypeName | initialPropertyValues | nodeName | + | behat | site-root | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "home"} | node1 | + | company | behat | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "company"} | node2 | + | service | company | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "service"} | node3 | + | about | company | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "about"} | node4 | + | imprint | behat | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "imprint"} | node5 | + | buy | behat | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "buy", "title": "Buy"} | node6 | + | mail | behat | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "mail"} | node7 | + | restricted-by-nodetype | behat | Neos.Neos:Test.Redirect.RestrictedPage | {"uriPathSegment": "restricted-by-nodetype"} | node8 | + | restricted-by-path | behat | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "restricted-by-path"} | node9 | + And A site exists for node name "node1" + And the sites configuration is: + """ + Neos: + Neos: + sites: + '*': + contentRepository: default + contentDimensions: + resolver: + factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\NoopResolverFactory + """ + And The documenturipath projection is up to date @fixtures - Scenario: Move a node into different node and a redirect will be created - When I get a node by path "/sites/behat/service" with the following context: - | Workspace | - | user-testaccount | - And I move the node into the node with path "/sites/behat/company" - And I publish the node - Then I should have a redirect with sourceUri "en/service.html" and targetUri "en/company/service.html" + Scenario: Move a node down into different node and a redirect will be created + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "imprint" | + | dimensionSpacePoint | {} | + | newParentNodeAggregateId | "company" | + | newSucceedingSiblingNodeAggregateId | null | + And The documenturipath projection is up to date + Then I should have a redirect with sourceUri "imprint" and targetUri "company/imprint" + + Scenario: Move a node up into different node and a redirect will be created + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "service" | + | dimensionSpacePoint | {} | + | newParentNodeAggregateId | "behat" | + | newSucceedingSiblingNodeAggregateId | null | + And The documenturipath projection is up to date + Then I should have a redirect with sourceUri "company/service" and targetUri "service" @fixtures Scenario: Change the the `uriPathSegment` and a redirect will be created - When I get a node by path "/sites/behat/company" with the following context: - | Workspace | - | user-testaccount | - And I set the node property "uriPathSegment" to "evil-corp" - And I publish the node - Then I should have a redirect with sourceUri "en/company.html" and targetUri "en/evil-corp.html" - - #fixed in 1.0.2 - @fixtures - Scenario: Retarget an existing redirect when the target URI matches the source URI of the new redirect - When I get a node by path "/sites/behat/about" with the following context: - | Workspace | - | user-testaccount | - And I have the following redirects: - | sourceuripath | targeturipath | - | en/about.html | en/about-you.html | - And I set the node property "uriPathSegment" to "about-me" - And I publish the node - And I should have a redirect with sourceUri "en/about.html" and targetUri "en/about-me.html" + When the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "company" | + | originDimensionSpacePoint | {} | + | propertyValues | {"uriPathSegment": "evil-corp"} | + And The documenturipath projection is up to date + Then I should have a redirect with sourceUri "company" and targetUri "evil-corp" + And I should have a redirect with sourceUri "company/service" and targetUri "evil-corp/service" @fixtures - Scenario: Redirects should aways be created in the same dimension the node is in - When I get a node by path "/sites/behat/imprint" with the following context: - | Workspace | Language | - | user-testaccount | fr | - And I set the node property "uriPathSegment" to "empreinte-nouveau" - And I publish the node - Then I should have a redirect with sourceUri "fr/empreinte.html" and targetUri "fr/empreinte-nouveau.html" - - #fixed in 1.0.3 - @fixtures - Scenario: Redirects should aways be created in the same dimension the node is in and not the fallback dimension - When I get a node by path "/sites/behat/imprint" with the following context: - | Workspace | Language | - | user-testaccount | de,en | - And I set the node property "uriPathSegment" to "impressum-neu" - And I publish the node - Then I should have a redirect with sourceUri "de/impressum.html" and targetUri "de/impressum-neu.html" - And I should have no redirect with sourceUri "en/impressum.html" and targetUri "de/impressum-neu.html" - - #fixed in 1.0.3 + Scenario: Change the the `uriPathSegment` mutiple times and mutliple redirects will be created + When the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "company" | + | originDimensionSpacePoint | {} | + | propertyValues | {"uriPathSegment": "evil-corp"} | + And the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "company" | + | originDimensionSpacePoint | {} | + | propertyValues | {"uriPathSegment": "more-evil-corp"} | + And The documenturipath projection is up to date + + Then I should have a redirect with sourceUri "company" and targetUri "more-evil-corp" + And I should have a redirect with sourceUri "company/service" and targetUri "more-evil-corp/service" + And I should have a redirect with sourceUri "evil-corp" and targetUri "more-evil-corp" + And I should have a redirect with sourceUri "evil-corp/service" and targetUri "more-evil-corp/service" + + @fixtures - Scenario: I have an existing redirect and it should never be overwritten for a node variant from a different dimension + Scenario: Retarget an existing redirect when the source URI matches the source URI of the new redirect When I have the following redirects: - | sourceuripath | targeturipath | - | important-page-from-the-old-site | en/mail.html | - When I get a node by path "/sites/behat/mail" with the following context: - | Workspace | Language | - | user-testaccount | de,en | - And I unhide the node - And I publish the node - Then I should have a redirect with sourceUri "important-page-from-the-old-site" and targetUri "en/mail.html" - And I should have no redirect with sourceUri "en/mail.html" and targetUri "de/mail.html" + | sourceuripath | targeturipath | + | company | company-old | + And the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "company" | + | originDimensionSpacePoint | {} | + | propertyValues | {"uriPathSegment": "my-company"} | + And The documenturipath projection is up to date + Then I should have a redirect with sourceUri "company" and targetUri "my-company" + And I should have no redirect with sourceUri "company" and targetUri "company-old" + And I should have a redirect with sourceUri "company/service" and targetUri "my-company/service" @fixtures Scenario: No redirect should be created for an existing node if any non URI related property changes - When I get a node by path "/sites/behat/buy" with the following context: - | Workspace | - | user-testaccount | - And I set the node property "title" to "Buy later" - And I publish the node - Then I should have no redirect with sourceUri "en/buy.html" + When the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "buy" | + | originDimensionSpacePoint | {} | + | propertyValues | {"title": "my-buy"} | + And The documenturipath projection is up to date + Then I should have no redirect with sourceUri "buy" @fixtures - Scenario: Redirects should be created for a hidden node - When I get a node by path "/sites/behat/buy" with the following context: - | Workspace | Language | - | user-testaccount | de,en | - And I set the node property "uriPathSegment" to "nicht-kaufen" - And I publish the node - Then I should have a redirect with sourceUri "de/kaufen.html" and targetUri "de/nicht-kaufen.html" + Scenario: No redirect should be created for an restricted node by nodetype + When the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "restricted-by-nodetype" | + | originDimensionSpacePoint | {} | + | propertyValues | {"uriPathSegment": "restricted-by-nodetype-new"} | + And The documenturipath projection is up to date + Then I should have no redirect with sourceUri "restricted" + @fixtures - Scenario: Create redirects for nodes published in different dimensions - When I get a node by path "/sites/behat/buy" with the following context: - | Workspace | - | user-testaccount | - And I move the node into the node with path "/sites/behat/company" - And I publish the node - When I get a node by path "/sites/behat/company/buy" with the following context: - | Workspace | Language | - | user-testaccount | de,en | - And I publish the node - Then I should have a redirect with sourceUri "en/buy.html" and targetUri "en/company/buy.html" - And I should have a redirect with sourceUri "de/kaufen.html" and targetUri "de/company/kaufen.html" - - #fixed in 1.0.4 + Scenario: No redirect should be created for an restricted node by path + When the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "restricted-by-path" | + | originDimensionSpacePoint | {} | + | propertyValues | {"uriPathSegment": "restricted-by-path-new"} | + And The documenturipath projection is up to date + Then I should have no redirect with sourceUri "restricted-by-path" + +# @fixtures +# Scenario: Redirects should always be created in the same dimension the node is in +# When I get a node by path "/sites/behat/imprint" with the following context: +# | Workspace | Language | +# | user-testaccount | fr | +# And I set the node property "uriPathSegment" to "empreinte-nouveau" +# And I publish the node +# Then I should have a redirect with sourceUri "fr/empreinte" and targetUri "fr/empreinte-nouveau" +# +# #fixed in 1.0.3 +# @fixtures +# Scenario: Redirects should aways be created in the same dimension the node is in and not the fallback dimension +# When I get a node by path "/sites/behat/imprint" with the following context: +# | Workspace | Language | +# | user-testaccount | de,en | +# And I set the node property "uriPathSegment" to "impressum-neu" +# And I publish the node +# Then I should have a redirect with sourceUri "de/impressum" and targetUri "de/impressum-neu" +# And I should have no redirect with sourceUri "en/impressum" and targetUri "de/impressum-neu" +# +# #fixed in 1.0.3 +# @fixtures +# Scenario: I have an existing redirect and it should never be overwritten for a node variant from a different dimension +# When I have the following redirects: +# | sourceuripath | targeturipath | +# | important-page-from-the-old-site | en/mail | +# When I get a node by path "/sites/behat/mail" with the following context: +# | Workspace | Language | +# | user-testaccount | de,en | +# And I unhide the node +# And I publish the node +# Then I should have a redirect with sourceUri "important-page-from-the-old-site" and targetUri "en/mail" +# And I should have no redirect with sourceUri "en/mail" and targetUri "de/mail" +# + @fixtures - Scenario: Create redirects for nodes that use the current dimension as fallback - When I get a node by path "/sites/behat/company" with the following context: - | Workspace | Language | - | user-testaccount | en | - And I move the node into the node with path "/sites/behat/service" - And I publish the node - Then I should have a redirect with sourceUri "en/company.html" and targetUri "en/service/company.html" - And I should have a redirect with sourceUri "de/company.html" and targetUri "de/service/company.html" + Scenario: Redirects should be created for a hidden node + When the command DisableNodeAggregate is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "mail" | + | originDimensionSpacePoint | {} | + | nodeVariantSelectionStrategy | "allVariants" | + And the graph projection is fully up to date + When the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "mail" | + | originDimensionSpacePoint | {} | + | propertyValues | {"uriPathSegment": "not-mail"} | + And The documenturipath projection is up to date + Then I should have a redirect with sourceUri "mail" and targetUri "not-mail" + +# @fixtures +# Scenario: Create redirects for nodes published in different dimensions +# When I get a node by path "/sites/behat/buy" with the following context: +# | Workspace | +# | user-testaccount | +# And I move the node into the node with path "/sites/behat/company" +# And I publish the node +# When I get a node by path "/sites/behat/company/buy" with the following context: +# | Workspace | Language | +# | user-testaccount | de,en | +# And I publish the node +# Then I should have a redirect with sourceUri "en/buy" and targetUri "en/company/buy" +# And I should have a redirect with sourceUri "de/kaufen" and targetUri "de/company/kaufen" +# +# #fixed in 1.0.4 +# @fixtures +# Scenario: Create redirects for nodes that use the current dimension as fallback +# When I get a node by path "/sites/behat/company" with the following context: +# | Workspace | Language | +# | user-testaccount | en | +# And I move the node into the node with path "/sites/behat/service" +# And I publish the node +# Then I should have a redirect with sourceUri "en/company" and targetUri "en/service/company" +# And I should have a redirect with sourceUri "de/company" and targetUri "de/service/company" + + + @fixtures + Scenario: A removed node should lead to a GONE response with empty target uri + Given the event NodeAggregateWasRemoved was published with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "company" | + | affectedOccupiedDimensionSpacePoints | [] | + | affectedCoveredDimensionSpacePoints | [[]] | + And the graph projection is fully up to date + And The documenturipath projection is up to date + + Then I should have a redirect with sourceUri "company" and statusCode "410" + And I should have a redirect with sourceUri "company" and targetUri "" + And I should have a redirect with sourceUri "company/service" and statusCode "410" + And I should have a redirect with sourceUri "company/service" and targetUri "" \ No newline at end of file diff --git a/Tests/Behavior/behat.yml b/Tests/Behavior/behat.yml deleted file mode 100644 index c950ae8..0000000 --- a/Tests/Behavior/behat.yml +++ /dev/null @@ -1,25 +0,0 @@ -# Behat distribution configuration -# -# Override with behat.yml for local configuration. -# -default: - autoload: - '': "%paths.base%/Features/Bootstrap" - suites: - content: - paths: - - "%paths.base%/Features" - contexts: - - FeatureContext - extensions: - Behat\MinkExtension: - files_path: features/Resources - show_cmd: 'open %s' - goutte: ~ - selenium2: ~ - - # Project base URL - # - # Use BEHAT_PARAMS="extensions[Behat\MinkExtension\Extension][base_url]=http://example.local/" for configuration during runtime. - # - base_url: http://neos5.behat.test/ diff --git a/Tests/Behavior/behat.yml.dist b/Tests/Behavior/behat.yml.dist index d27ab4b..a97facc 100644 --- a/Tests/Behavior/behat.yml.dist +++ b/Tests/Behavior/behat.yml.dist @@ -2,6 +2,7 @@ # # Override with behat.yml for local configuration. # + default: autoload: '': "%paths.base%/Features/Bootstrap" @@ -10,16 +11,25 @@ default: paths: - "%paths.base%/Features" contexts: - - FeatureContext - extensions: - Behat\MinkExtension: - files_path: features/Resources - show_cmd: 'open %s' - goutte: ~ - selenium2: ~ + - FeatureContext # Project base URL # - # Use BEHAT_PARAMS="extensions[Behat\MinkExtension\Extension][base_url]=http://example.local/" for configuration during runtime. + # Use BEHAT_PARAMS="extensions[Behat\MinkExtension\Extension][base_url]=http://neos.local/" for configuration during + # runtime. + # + # base_url: http://localhost/ + + # Saucelabs configuration + # + # Use this configuration, if you want to use saucelabs for your @javascript-tests # - base_url: http://localhost/ + #javascript_session: saucelabs + #saucelabs: + #username: + #access_key: + +# Import a bunch of browser configurations for saucelab tests +# +#imports: + #- saucelabsBrowsers.yml diff --git a/Tests/Functional/Service/NodeRedirectServiceTest.php b/Tests/Functional/Service/NodeRedirectServiceTest.php index 360e5bc..12433d3 100644 --- a/Tests/Functional/Service/NodeRedirectServiceTest.php +++ b/Tests/Functional/Service/NodeRedirectServiceTest.php @@ -62,27 +62,27 @@ class NodeRedirectServiceTest extends FunctionalTestCase protected $nodeDataRepository; /** - * @var NodeTypeManager + * @var \Neos\ContentRepository\Core\NodeType\NodeTypeManager */ protected $nodeTypeManager; /** - * @var Workspace + * @var \Neos\ContentRepository\Core\Projection\Workspace\Workspace */ protected $liveWorkspace; /** - * @var Workspace + * @var \Neos\ContentRepository\Core\Projection\Workspace\Workspace */ protected $userWorkspace; /** - * @var ContentContext + * @var \Neos\Rector\ContentRepository90\Legacy\LegacyContextStub */ protected $userContext; /** - * @var NodeInterface + * @var \Neos\ContentRepository\Core\Projection\ContentGraph\Node */ protected $site; @@ -111,10 +111,10 @@ public function setUp(): void $this->mockRedirectStorage = $this->getMockBuilder(RedirectStorageInterface::class)->getMock(); $this->inject($this->nodeRedirectService, 'redirectStorage', $this->mockRedirectStorage); $this->contentContextFactory = $this->objectManager->get(ContentContextFactory::class); - $this->nodeTypeManager = $this->objectManager->get(NodeTypeManager::class); + $this->nodeTypeManager = $this->objectManager->get(\Neos\ContentRepository\Core\NodeType\NodeTypeManager::class); $this->workspaceRepository = $this->objectManager->get(WorkspaceRepository::class); - $this->liveWorkspace = new Workspace('live'); - $this->userWorkspace = new Workspace('user-me', $this->liveWorkspace); + $this->liveWorkspace = new \Neos\ContentRepository\Core\Projection\Workspace\Workspace('live'); + $this->userWorkspace = new \Neos\ContentRepository\Core\Projection\Workspace\Workspace('user-me', $this->liveWorkspace); $this->workspaceRepository->add($this->liveWorkspace); $this->workspaceRepository->add($this->userWorkspace); $liveContext = $this->contentContextFactory->create([ diff --git a/composer.json b/composer.json index 41ba162..2972f80 100644 --- a/composer.json +++ b/composer.json @@ -5,8 +5,8 @@ "license": "GPL-3.0-or-later", "require": { "neos/redirecthandler": "~3.0 || ~4.0 || ~5.0 || dev-master", - "neos/neos": "~9.0 || dev-master", - "neos/flow": "~9.0 || dev-master" + "neos/neos": "^9.0", + "neos/flow": "^9.0" }, "autoload": { "psr-4": { From 0d5613c837350c78f25cc389aff99aea8b53e910 Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Mon, 22 May 2023 20:50:00 +0200 Subject: [PATCH 04/32] FEATURE: Add content repository id and nodetype name to signal --- Classes/Package.php | 30 ++++++++++++++++++++----- Classes/Service/NodeRedirectService.php | 19 +++++++--------- composer.json | 2 +- 3 files changed, 33 insertions(+), 18 deletions(-) diff --git a/Classes/Package.php b/Classes/Package.php index e0d56d3..faf3587 100644 --- a/Classes/Package.php +++ b/Classes/Package.php @@ -19,6 +19,9 @@ use Neos\Neos\Domain\Model\SiteNodeName; use Neos\Neos\FrontendRouting\Projection\DocumentUriPathProjection; use Neos\RedirectHandler\NeosAdapter\Service\NodeRedirectService; +use Neos\ContentRepository\Core\NodeType\NodeTypeName; +use Neos\ContentRepository\Core\Factory\ContentRepositoryId; +use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; /** * The Neos RedirectHandler NeosAdapter Package @@ -34,24 +37,39 @@ public function boot(Bootstrap $bootstrap): void $dispatcher = $bootstrap->getSignalSlotDispatcher(); $dispatcher->connect(DocumentUriPathProjection::class, 'afterNodeAggregateWasMoved', function ( - string $oldUriPath, string $newUriPath, SiteNodeName $siteNodeName, $_, + ContentRepositoryId $contentRepositoryId, string $oldUriPath, string $newUriPath, NodeTypeName $nodeTypeName, SiteNodeName $siteNodeName, $_, ) use ($bootstrap) { $nodeRedirectService = $bootstrap->getObjectManager()->get(NodeRedirectService::class); - $nodeRedirectService->createRedirect($oldUriPath, $newUriPath, $siteNodeName); + + $contentRepository = $bootstrap->getObjectManager()->get(ContentRepositoryRegistry::class)->get($contentRepositoryId); + $nodeTypeManager = $contentRepository->getNodeTypeManager(); + $nodeType = $nodeTypeManager->getNodeType($nodeTypeName); + + $nodeRedirectService->createRedirect($oldUriPath, $newUriPath, $nodeType, $siteNodeName); }); $dispatcher->connect(DocumentUriPathProjection::class, 'afterNodeAggregateWasRemoved', function ( - string $oldUriPath, SiteNodeName $siteNodeName, $_, + ContentRepositoryId $contentRepositoryId, string $oldUriPath, NodeTypeName $nodeTypeName, SiteNodeName $siteNodeName, $_, ) use ($bootstrap) { $nodeRedirectService = $bootstrap->getObjectManager()->get(NodeRedirectService::class); - $nodeRedirectService->createRedirect($oldUriPath, null, $siteNodeName); + + $contentRepository = $bootstrap->getObjectManager()->get(ContentRepositoryRegistry::class)->get($contentRepositoryId); + $nodeTypeManager = $contentRepository->getNodeTypeManager(); + $nodeType = $nodeTypeManager->getNodeType($nodeTypeName); + + $nodeRedirectService->createRedirect($oldUriPath, null, $nodeType, $siteNodeName); }); $dispatcher->connect(DocumentUriPathProjection::class, 'afterDocumentUriPathChanged', function ( - string $oldUriPath, string $newUriPath, SiteNodeName $siteNodeName, $_, EventEnvelope $eventEnvelope, + ContentRepositoryId $contentRepositoryId, string $oldUriPath, string $newUriPath, NodeTypeName $nodeTypeName, SiteNodeName $siteNodeName, $_, EventEnvelope $eventEnvelope, ) use ($bootstrap) { $nodeRedirectService = $bootstrap->getObjectManager()->get(NodeRedirectService::class); - $nodeRedirectService->createRedirect($oldUriPath, $newUriPath, $siteNodeName); + + $contentRepository = $bootstrap->getObjectManager()->get(ContentRepositoryRegistry::class)->get($contentRepositoryId); + $nodeTypeManager = $contentRepository->getNodeTypeManager(); + $nodeType = $nodeTypeManager->getNodeType($nodeTypeName); + + $nodeRedirectService->createRedirect($oldUriPath, $newUriPath, $nodeType, $siteNodeName); }); } diff --git a/Classes/Service/NodeRedirectService.php b/Classes/Service/NodeRedirectService.php index f49eaf5..6036a8d 100644 --- a/Classes/Service/NodeRedirectService.php +++ b/Classes/Service/NodeRedirectService.php @@ -15,7 +15,6 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; -use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; use Neos\Flow\Persistence\PersistenceManagerInterface; use Neos\Neos\Domain\Model\Domain; @@ -23,6 +22,7 @@ use Neos\Neos\Domain\Repository\SiteRepository; use Neos\RedirectHandler\Storage\RedirectStorageInterface; use Psr\Log\LoggerInterface; +use Neos\ContentRepository\Core\NodeType\NodeType; /** * Service that creates redirects for moved / deleted nodes. @@ -90,9 +90,6 @@ class NodeRedirectService */ protected $siteRepository; - #[\Neos\Flow\Annotations\Inject] - protected ContentRepositoryRegistry $contentRepositoryRegistry; - /** * @param string $oldUriPath @@ -103,6 +100,7 @@ class NodeRedirectService public function createRedirect( string $oldUriPath, ?string $newUriPath, + NodeType $nodeType, SiteNodeName $siteNodeName, ): void { @@ -110,8 +108,7 @@ public function createRedirect( return; } - // TODO: Restrict by NodeType - if (/*$this->isRestrictedByNodeType($targetNodeInfo->node)|| */ $this->isRestrictedByOldUri($oldUriPath)) { + if ($this->isRestrictedByNodeType($nodeType) || $this->isRestrictedByOldUri($oldUriPath)) { return; } $oldUriPath = $this->buildUri($oldUriPath); @@ -176,7 +173,7 @@ protected function createRedirectForRemovedTarget(string $oldUriPath, SiteNodeNa * @param Node $node * @return bool */ - protected function isRestrictedByNodeType(Node $node): bool + protected function isRestrictedByNodeType(NodeType $nodeType): bool { if (!isset($this->restrictByNodeType)) { return false; @@ -186,10 +183,10 @@ protected function isRestrictedByNodeType(Node $node): bool if ($status !== true) { continue; } - if ($node->nodeType->isOfType($disabledNodeType)) { - $this->logger->debug(vsprintf('Redirect skipped based on the current node type (%s) for node %s because is of type %s', [ - $node->nodeType->name->value, - $node->nodeAggregateId->value, + + if ($nodeType->isOfType($disabledNodeType)) { + $this->logger->debug(vsprintf('Redirect skipped based on the current node type (%s) for a node because is of type %s', [ + $nodeType->name->value, $disabledNodeType ])); diff --git a/composer.json b/composer.json index 2972f80..8377f1e 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,7 @@ "description": "Neos Redirect Handler", "license": "GPL-3.0-or-later", "require": { - "neos/redirecthandler": "~3.0 || ~4.0 || ~5.0 || dev-master", + "neos/redirecthandler": "~6.0 || dev-master", "neos/neos": "^9.0", "neos/flow": "^9.0" }, From f7e6440b62e457aff35e3a881469d5720afdab27 Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Tue, 13 Jun 2023 10:01:45 +0200 Subject: [PATCH 05/32] FEATURE: Redirect NeosAdapter for new ContentRepository --- .../DocumentUriPathProjectionHook.php | 263 +++++++++++++++++ .../DocumentUriPathProjectionHookFactory.php | 27 ++ Classes/Package.php | 76 ----- Classes/Service/NodeRedirectService.php | 232 ++++++++------- Configuration/Settings.yaml | 10 + .../Features/Bootstrap/FeatureContext.php | 5 + Tests/Behavior/Features/OneDimension.feature | 272 ++++++++++++++++++ ...rect.feature => WithoutDimensions.feature} | 78 +---- 8 files changed, 706 insertions(+), 257 deletions(-) create mode 100644 Classes/CatchUpHook/DocumentUriPathProjectionHook.php create mode 100644 Classes/CatchUpHook/DocumentUriPathProjectionHookFactory.php delete mode 100644 Classes/Package.php create mode 100644 Tests/Behavior/Features/OneDimension.feature rename Tests/Behavior/Features/{Redirect.feature => WithoutDimensions.feature} (72%) diff --git a/Classes/CatchUpHook/DocumentUriPathProjectionHook.php b/Classes/CatchUpHook/DocumentUriPathProjectionHook.php new file mode 100644 index 0000000..7d1dc95 --- /dev/null +++ b/Classes/CatchUpHook/DocumentUriPathProjectionHook.php @@ -0,0 +1,263 @@ +> + */ + private array $documentNodeInfosBeforeRemoval; + + public function __construct( + private readonly ContentRepository $contentRepository, + private readonly ContentRepositoryRegistry $contentRepositoryRegistry, + private readonly NodeRedirectService $nodeRedirectService, + ) { + } + + public function onBeforeCatchUp(): void + { + // Nothing to do here + } + + public function onBeforeEvent(EventInterface $eventInstance, EventEnvelope $eventEnvelope): void + { + match ($eventInstance::class) { + NodeAggregateWasRemoved::class => $this->onBeforeNodeAggregateWasRemoved($eventInstance), + NodePropertiesWereSet::class => $this->onBeforeNodePropertiesWereSet($eventInstance), + NodeAggregateWasMoved::class => $this->onBeforeNodeAggregateWasMoved($eventInstance), + default => null + }; + + } + + public function onAfterEvent(EventInterface $eventInstance, EventEnvelope $eventEnvelope): void + { + match ($eventInstance::class) { + NodeAggregateWasRemoved::class => $this->onAfterNodeAggregateWasRemoved($eventInstance), + NodePropertiesWereSet::class => $this->onAfterNodePropertiesWereSet($eventInstance), + NodeAggregateWasMoved::class => $this->onAfterNodeAggregateWasMoved($eventInstance), + default => null + }; + } + + public function onBeforeBatchCompleted(): void + { + // Nothing to do here + } + + public function onAfterCatchUp(): void + { + // Nothing to do here + } + + private function onBeforeNodeAggregateWasRemoved(NodeAggregateWasRemoved $event): void + { + if (!$this->isLiveContentStream($event->contentStreamId)) { + return; + } + + $this->documentNodeInfosBeforeRemoval = []; + + foreach ($event->affectedCoveredDimensionSpacePoints as $dimensionSpacePoint) { + $node = $this->tryGetNode(fn() => $this->getState()->getByIdAndDimensionSpacePointHash( + $event->nodeAggregateId, + $dimensionSpacePoint->hash + )); + if ($node === null) { + // Probably not a document node + continue; + } + + $this->nodeRedirectService->appendAffectedNode( + $node, + $this->getNodeAddress($event->contentStreamId, $dimensionSpacePoint, $node->getNodeAggregateId()), + $this->getContentRepositoryId() + ); + $this->documentNodeInfosBeforeRemoval[$dimensionSpacePoint->hash][] = $node; + + $children = $this->getState()->getAllChildrenOfNode($node); + array_map( + function ($childNode) use ($event, $dimensionSpacePoint) { + $this->nodeRedirectService->appendAffectedNode( + $childNode, + $this->getNodeAddress($event->contentStreamId, $dimensionSpacePoint, $childNode->getNodeAggregateId()), + $this->getContentRepositoryId() + ); + $this->documentNodeInfosBeforeRemoval[$dimensionSpacePoint->hash][] = $childNode; + }, + $children); + } + } + + private function onAfterNodeAggregateWasRemoved(NodeAggregateWasRemoved $event): void + { + if (!$this->isLiveContentStream($event->contentStreamId)) { + return; + } + + foreach ($event->affectedCoveredDimensionSpacePoints as $dimensionSpacePoint) { + $documentNodeInfosBeforeRemoval = $this->documentNodeInfosBeforeRemoval[$dimensionSpacePoint->hash]; + unset($this->documentNodeInfosBeforeRemoval[$dimensionSpacePoint->hash]); + + array_map( + fn($node) => $this->nodeRedirectService->createRedirectForRemovedAffectedNode( + $node, + $this->getContentRepositoryId() + ), + $documentNodeInfosBeforeRemoval + ); + } + } + + private function onBeforeNodePropertiesWereSet(NodePropertiesWereSet $event): void + { + $this->handleNodePropertiesWereSet( + $event, + fn( + DocumentNodeInfo $node, NodeAddress $nodeAddress, + ) => $this->nodeRedirectService->appendAffectedNode($node, $nodeAddress, $this->getContentRepositoryId()) + ); + } + + private function onAfterNodePropertiesWereSet(NodePropertiesWereSet $event): void + { + $this->handleNodePropertiesWereSet( + $event, + fn( + DocumentNodeInfo $node, NodeAddress $nodeAddress, + ) => $this->nodeRedirectService->createRedirectForAffectedNode($node, $nodeAddress, $this->getContentRepositoryId()) + ); + } + + private function handleNodePropertiesWereSet(NodePropertiesWereSet $event, \Closure $closure): void + { + if (!$this->isLiveContentStream($event->contentStreamId)) { + return; + } + + $newPropertyValues = $event->propertyValues->getPlainValues(); + if (!isset($newPropertyValues['uriPathSegment'])) { + return; + } + + foreach ($event->affectedDimensionSpacePoints as $affectedDimensionSpacePoint) { + $node = $this->tryGetNode(fn() => $this->getState()->getByIdAndDimensionSpacePointHash( + $event->nodeAggregateId, + $affectedDimensionSpacePoint->hash + )); + file_put_contents('/var/www/html/Data/Logs/foo.log', $node->getUriPath() . "\n", flags: FILE_APPEND); + if ($node === null) { + // probably not a document node + continue; + } + + $closure($node, $this->getNodeAddress($event->contentStreamId, $affectedDimensionSpacePoint, $node->getNodeAggregateId())); + + $children = $this->getState()->getAllChildrenOfNode($node); + array_map(fn($childNode) => $closure($childNode, $this->getNodeAddress($event->contentStreamId, $affectedDimensionSpacePoint, $childNode->getNodeAggregateId())), $children); + } + } + + private function onBeforeNodeAggregateWasMoved(NodeAggregateWasMoved $event): void + { + $this->handleNodeWasMoved( + $event, + fn( + DocumentNodeInfo $node, NodeAddress $nodeAddress, + ) => $this->nodeRedirectService->appendAffectedNode($node, $nodeAddress, $this->getContentRepositoryId()) + ); + } + + private function onAfterNodeAggregateWasMoved(NodeAggregateWasMoved $event): void + { + $this->handleNodeWasMoved( + $event, + fn( + DocumentNodeInfo $node, NodeAddress $nodeAddress, + ) => $this->nodeRedirectService->createRedirectForAffectedNode($node, $nodeAddress, $this->getContentRepositoryId()) + ); + } + + private function handleNodeWasMoved(NodeAggregateWasMoved $event, \Closure $closure): void + { + if (!$this->isLiveContentStream($event->contentStreamId)) { + return; + } + + foreach ($event->nodeMoveMappings as $moveMapping) { + /* @var \Neos\ContentRepository\Core\Feature\NodeMove\Dto\OriginNodeMoveMapping $moveMapping */ + foreach ($moveMapping->newLocations as $newLocation) { + /* @var $newLocation CoverageNodeMoveMapping */ + $node = $this->tryGetNode(fn() => $this->getState()->getByIdAndDimensionSpacePointHash( + $event->nodeAggregateId, + $newLocation->coveredDimensionSpacePoint->hash + )); + + if (!$node) { + // node probably no document node, skip + continue; + } + + $closure($node, $this->getNodeAddress($event->contentStreamId, $newLocation->coveredDimensionSpacePoint, $node->getNodeAggregateId())); + + $children = $this->getState()->getAllChildrenOfNode($node); + array_map(fn($childNode) => $closure($childNode, $this->getNodeAddress($event->contentStreamId, $newLocation->coveredDimensionSpacePoint, $childNode->getNodeAggregateId())), $children); + } + } + } + + private function getState(): DocumentUriPathFinder + { + return $this->contentRepository->projectionState(DocumentUriPathFinder::class); + } + + private function isLiveContentStream(ContentStreamId $contentStreamId): bool + { + return $contentStreamId->equals($this->getState()->getLiveContentStreamId()); + } + + private function tryGetNode(\Closure $closure): ?DocumentNodeInfo + { + try { + return $closure(); + } catch (NodeNotFoundException $_) { + /** @noinspection BadExceptionsProcessingInspection */ + return null; + } + } + + protected function getNodeAddress( + ContentStreamId $contentStreamId, + DimensionSpacePoint $dimensionSpacePoint, + NodeAggregateId $nodeAggregateId, + ): NodeAddress { + return new NodeAddress($contentStreamId, $dimensionSpacePoint, $nodeAggregateId, WorkspaceName::forLive()); + } + + private function getContentRepositoryId(): ContentRepositoryId + { + return $this->contentRepositoryRegistry->getContentRepositoryIdByContentRepository($this->contentRepository); + } +} \ No newline at end of file diff --git a/Classes/CatchUpHook/DocumentUriPathProjectionHookFactory.php b/Classes/CatchUpHook/DocumentUriPathProjectionHookFactory.php new file mode 100644 index 0000000..74789c7 --- /dev/null +++ b/Classes/CatchUpHook/DocumentUriPathProjectionHookFactory.php @@ -0,0 +1,27 @@ +contentRepositoryRegistry, + $this->redirectService + ); + } +} \ No newline at end of file diff --git a/Classes/Package.php b/Classes/Package.php deleted file mode 100644 index faf3587..0000000 --- a/Classes/Package.php +++ /dev/null @@ -1,76 +0,0 @@ -getSignalSlotDispatcher(); - - $dispatcher->connect(DocumentUriPathProjection::class, 'afterNodeAggregateWasMoved', function ( - ContentRepositoryId $contentRepositoryId, string $oldUriPath, string $newUriPath, NodeTypeName $nodeTypeName, SiteNodeName $siteNodeName, $_, - ) use ($bootstrap) { - $nodeRedirectService = $bootstrap->getObjectManager()->get(NodeRedirectService::class); - - $contentRepository = $bootstrap->getObjectManager()->get(ContentRepositoryRegistry::class)->get($contentRepositoryId); - $nodeTypeManager = $contentRepository->getNodeTypeManager(); - $nodeType = $nodeTypeManager->getNodeType($nodeTypeName); - - $nodeRedirectService->createRedirect($oldUriPath, $newUriPath, $nodeType, $siteNodeName); - }); - - $dispatcher->connect(DocumentUriPathProjection::class, 'afterNodeAggregateWasRemoved', function ( - ContentRepositoryId $contentRepositoryId, string $oldUriPath, NodeTypeName $nodeTypeName, SiteNodeName $siteNodeName, $_, - ) use ($bootstrap) { - $nodeRedirectService = $bootstrap->getObjectManager()->get(NodeRedirectService::class); - - $contentRepository = $bootstrap->getObjectManager()->get(ContentRepositoryRegistry::class)->get($contentRepositoryId); - $nodeTypeManager = $contentRepository->getNodeTypeManager(); - $nodeType = $nodeTypeManager->getNodeType($nodeTypeName); - - $nodeRedirectService->createRedirect($oldUriPath, null, $nodeType, $siteNodeName); - }); - - $dispatcher->connect(DocumentUriPathProjection::class, 'afterDocumentUriPathChanged', function ( - ContentRepositoryId $contentRepositoryId, string $oldUriPath, string $newUriPath, NodeTypeName $nodeTypeName, SiteNodeName $siteNodeName, $_, EventEnvelope $eventEnvelope, - ) use ($bootstrap) { - $nodeRedirectService = $bootstrap->getObjectManager()->get(NodeRedirectService::class); - - $contentRepository = $bootstrap->getObjectManager()->get(ContentRepositoryRegistry::class)->get($contentRepositoryId); - $nodeTypeManager = $contentRepository->getNodeTypeManager(); - $nodeType = $nodeTypeManager->getNodeType($nodeTypeName); - - $nodeRedirectService->createRedirect($oldUriPath, $newUriPath, $nodeType, $siteNodeName); - }); - - } -} diff --git a/Classes/Service/NodeRedirectService.php b/Classes/Service/NodeRedirectService.php index 6036a8d..5f84f14 100644 --- a/Classes/Service/NodeRedirectService.php +++ b/Classes/Service/NodeRedirectService.php @@ -13,8 +13,6 @@ * source code. */ -use Neos\ContentRepository\Core\Projection\ContentGraph\Node; -use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\Flow\Annotations as Flow; use Neos\Flow\Persistence\PersistenceManagerInterface; use Neos\Neos\Domain\Model\Domain; @@ -23,11 +21,22 @@ use Neos\RedirectHandler\Storage\RedirectStorageInterface; use Psr\Log\LoggerInterface; use Neos\ContentRepository\Core\NodeType\NodeType; +use Neos\Neos\FrontendRouting\NodeUriBuilder; +use Neos\ContentRepository\Core\Factory\ContentRepositoryId; +use GuzzleHttp\Psr7\ServerRequest; +use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionResult; +use Neos\Flow\Mvc\ActionRequest; +use Neos\Neos\FrontendRouting\Projection\DocumentNodeInfo; +use Neos\Neos\FrontendRouting\NodeAddress; +use Neos\ContentRepository\Core\NodeType\NodeTypeName; +use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; +use GuzzleHttp\Psr7\Uri; +use Neos\Flow\Mvc\Exception\NoMatchingRouteException; /** * Service that creates redirects for moved / deleted nodes. * - * Note: This is usually invoked by signals. + * Note: This is usually invoked by a catchup hook. See: Neos\RedirectHandler\NeosAdapter\CatchUpHook\DocumentUriPathProjectionHook * * @Flow\Scope("singleton") */ @@ -36,99 +45,134 @@ class NodeRedirectService const STATUS_CODE_TYPE_REDIRECT = 'redirect'; const STATUS_CODE_TYPE_GONE = 'gone'; - /** - * @Flow\Inject - * @var RedirectStorageInterface - */ - protected $redirectStorage; + private array $affectedNodes = []; + private array $hostnamesRuntimeCache = []; - /** - * @Flow\Inject - * @var PersistenceManagerInterface - */ - protected $persistenceManager; + #[Flow\Inject] + protected RedirectStorageInterface $redirectStorage; - /** - * @Flow\Inject - * @var LoggerInterface - */ - protected $logger; + #[Flow\Inject] + protected PersistenceManagerInterface $persistenceManager; - /** - * @Flow\InjectConfiguration(path="statusCode", package="Neos.RedirectHandler") - * @var array - */ - protected $defaultStatusCode; + #[Flow\Inject] + protected LoggerInterface $logger; - /** - * @Flow\InjectConfiguration(path="enableAutomaticRedirects", package="Neos.RedirectHandler.NeosAdapter") - * @var array - */ - protected $enableAutomaticRedirects; + #[Flow\InjectConfiguration(path: "statusCode", package: "Neos.RedirectHandler")] + protected array $defaultStatusCode; - /** - * @Flow\InjectConfiguration(path="enableRemovedNodeRedirect", package="Neos.RedirectHandler.NeosAdapter") - * @var array - */ - protected $enableRemovedNodeRedirect; + #[Flow\InjectConfiguration(path: "enableAutomaticRedirects", package: "Neos.RedirectHandler.NeosAdapter")] + protected bool $enableAutomaticRedirects; - /** - * @Flow\InjectConfiguration(path="restrictByOldUriPrefix", package="Neos.RedirectHandler.NeosAdapter") - * @var array - */ + #[Flow\InjectConfiguration(path: "enableRemovedNodeRedirect", package: "Neos.RedirectHandler.NeosAdapter")] + protected bool $enableRemovedNodeRedirect; + + #[Flow\InjectConfiguration(path: "restrictByOldUriPrefix", package: "Neos.RedirectHandler.NeosAdapter")] protected $restrictByOldUriPrefix; - /** - * @Flow\InjectConfiguration(path="restrictByNodeType", package="Neos.RedirectHandler.NeosAdapter") - * @var array - */ + #[Flow\InjectConfiguration(path: "restrictByNodeType", package: "Neos.RedirectHandler.NeosAdapter")] protected $restrictByNodeType; - /** - * @Flow\Inject - * @var SiteRepository - */ - protected $siteRepository; + #[Flow\Inject] + protected ContentRepositoryRegistry $contentRepositoryRegistry; + #[Flow\Inject] + protected SiteRepository $siteRepository; - /** - * @param string $oldUriPath - * @param string|null $newUriPath - * @param SiteNodeName $siteNodeName - * @return void - */ - public function createRedirect( - string $oldUriPath, - ?string $newUriPath, - NodeType $nodeType, - SiteNodeName $siteNodeName, - ): void { + public function appendAffectedNode(DocumentNodeInfo $nodeInfo, NodeAddress $nodeAddress, ContentRepositoryId $contentRepositoryId): void + { + try { + $this->affectedNodes[$this->getAffectedNodesKey($nodeInfo, $contentRepositoryId)] = [ + 'node' => $nodeInfo, + 'url' => $this->getNodeUriBuilder($nodeInfo->getSiteNodeName(), $contentRepositoryId)->uriFor($nodeAddress), + ]; + } catch (NoMatchingRouteException $exception) { + } + } + public function createRedirectForAffectedNode(DocumentNodeInfo $nodeInfo, NodeAddress $nodeAddress, ContentRepositoryId $contentRepositoryId): void + { if (!$this->enableAutomaticRedirects) { return; } - if ($this->isRestrictedByNodeType($nodeType) || $this->isRestrictedByOldUri($oldUriPath)) { + $affectedNode = $this->affectedNodes[$this->getAffectedNodesKey($nodeInfo, $contentRepositoryId)] ?? null; + if ($affectedNode === null) { return; } - $oldUriPath = $this->buildUri($oldUriPath); - if ($newUriPath !== null) { - $newUriPath = $this->buildUri($newUriPath); - $this->createRedirectWithNewTarget($oldUriPath, $newUriPath, $siteNodeName); - } else { - $this->createRedirectForRemovedTarget($oldUriPath, $siteNodeName); + unset($this->affectedNodes[$this->getAffectedNodesKey($nodeInfo, $contentRepositoryId)]); + + /** @var Uri $oldUri */ + $oldUri = $affectedNode['url']; + $nodeType = $this->getNodeType($contentRepositoryId, $nodeInfo->getNodeTypeName()); + + if ($this->isRestrictedByNodeType($nodeType) || $this->isRestrictedByOldUri($oldUri->getPath())) { + return; + } + try { + $newUri = $this->getNodeUriBuilder($nodeInfo->getSiteNodeName(), $contentRepositoryId)->uriFor($nodeAddress); + } catch (NoMatchingRouteException $exception) { + return; } + file_put_contents('/var/www/html/Data/Logs/foo.log', $oldUri->getPath() ." => " . $newUri->getPath() . "\n", flags: FILE_APPEND); + file_put_contents('/var/www/html/Data/Logs/foo.log', $affectedNode['node']->getDimensionSpacePointHash() ." => " . $nodeInfo->getDimensionSpacePointHash() . "\n", flags: FILE_APPEND); + $this->createRedirectWithNewTarget($oldUri->getPath(), $newUri->getPath(), $nodeInfo->getSiteNodeName()); $this->persistenceManager->persistAll(); } + public function createRedirectForRemovedAffectedNode(DocumentNodeInfo $nodeInfo, ContentRepositoryId $contentRepositoryId): void + { + if (!$this->enableAutomaticRedirects) { + return; + } + + $affectedNode = $this->affectedNodes[$this->getAffectedNodesKey($nodeInfo, $contentRepositoryId)] ?? null; + if ($affectedNode === null) { + return; + } + unset($this->affectedNodes[$this->getAffectedNodesKey($nodeInfo, $contentRepositoryId)]); + + /** @var Uri $oldUri */ + $oldUri = $affectedNode['url']; + $nodeType = $this->getNodeType($contentRepositoryId, $nodeInfo->getNodeTypeName()); + + if ($this->isRestrictedByNodeType($nodeType) || $this->isRestrictedByOldUri($oldUri->getPath())) { + return; + } + + $this->createRedirectForRemovedTarget($oldUri->getPath(), $nodeInfo->getSiteNodeName()); + + $this->persistenceManager->persistAll(); + } + + protected function getNodeType(ContentRepositoryId $contentRepositoryId, NodeTypeName $nodeTypeName): NodeType + { + return $this->contentRepositoryRegistry->get($contentRepositoryId)->getNodeTypeManager()->getNodeType($nodeTypeName); + } + + private function getAffectedNodesKey(DocumentNodeInfo $nodeInfo, ContentRepositoryId $contentRepositoryId): string + { + return $contentRepositoryId->value . '#' . $nodeInfo->getNodeAggregateId()->value . '#' . $nodeInfo->getDimensionSpacePointHash(); + } + + protected function getNodeUriBuilder(SiteNodeName $siteNodeName, ContentRepositoryId $contentRepositoryId): NodeUriBuilder + { + // Generate a custom request when the current request was triggered from CLI + $baseUri = 'http://localhost'; + + // Prevent `index.php` appearing in generated redirects + putenv('FLOW_REWRITEURLS=1'); + + $httpRequest = new ServerRequest('POST', $baseUri); + + $httpRequest = (SiteDetectionResult::create($siteNodeName, $contentRepositoryId))->storeInRequest($httpRequest); + $actionRequest = ActionRequest::fromHttpRequest($httpRequest); + + return NodeUriBuilder::fromRequest($actionRequest); + } + /** * Adds a redirect for given $oldUriPath to $newUriPath for all domains set up for $siteNode - * - * @param NodeAggregateId $nodeAggregateId - * @param string $oldUriPath - * @param string $newUriPath - * @return bool */ protected function createRedirectWithNewTarget(string $oldUriPath, string $newUriPath, SiteNodeName $siteNodeName): bool { @@ -146,10 +190,6 @@ protected function createRedirectWithNewTarget(string $oldUriPath, string $newUr /** * Removes a redirect - * - * @param string $oldUriPath - * @param SiteNodeName $siteNodeName - * @return bool */ protected function createRedirectForRemovedTarget(string $oldUriPath, SiteNodeName $siteNodeName): bool { @@ -169,9 +209,6 @@ protected function createRedirectForRemovedTarget(string $oldUriPath, SiteNodeNa /** * Check if the current node type is restricted by Settings - * - * @param Node $node - * @return bool */ protected function isRestrictedByNodeType(NodeType $nodeType): bool { @@ -199,9 +236,6 @@ protected function isRestrictedByNodeType(NodeType $nodeType): bool /** * Check if the old URI is restricted by Settings - * - * @param string $oldUriPath - * @return bool */ protected function isRestrictedByOldUri(string $oldUriPath): bool { @@ -230,38 +264,26 @@ protected function isRestrictedByOldUri(string $oldUriPath): bool /** * Collects all hostnames from the Domain entries attached to the current site. - * - * @param SiteNodeName $siteNodeName - * @return array */ protected function getHostnames(SiteNodeName $siteNodeName): array { - // TODO: Caching - $site = $this->siteRepository->findOneByNodeName($siteNodeName); + if (!isset($this->hostnamesRuntimeCache[$siteNodeName->value])) { + $site = $this->siteRepository->findOneByNodeName($siteNodeName); - $domains = []; - if ($site === null) { - return $domains; - } + $domains = []; + if ($site === null) { + return $domains; + } + + foreach ($site->getActiveDomains() as $domain) { + /** @var Domain $domain */ + $domains[] = $domain->getHostname(); + } - foreach ($site->getActiveDomains() as $domain) { - /** @var Domain $domain */ - $domains[] = $domain->getHostname(); + $this->hostnamesRuntimeCache[$siteNodeName->value] = $domains; } - return $domains; + return $this->hostnamesRuntimeCache[$siteNodeName->value]; } - /** - * Creates a (relative) URI for the given $nodeInfo - * - * @param string $uriPath - * @return string the resulting (relative) URI - */ - protected function buildUri(string $uriPath): string - { - // TODO: Add dimension prefix - // TODO: Add uriSuffix - return $uriPath; - } } diff --git a/Configuration/Settings.yaml b/Configuration/Settings.yaml index 42d0436..be82519 100755 --- a/Configuration/Settings.yaml +++ b/Configuration/Settings.yaml @@ -15,3 +15,13 @@ Neos: # in some cases you might need to completely disable the automatic redirects # e.g. on cli, during imports or similar enableAutomaticRedirects: true + + + ContentRepositoryRegistry: + presets: + 'default': + projections: + 'Neos.Neos:DocumentUriPathProjection': + catchUpHooks: + 'Neos.RedirectHandler.NeosAdapter:DocumentUriPathProjectionHook': + factoryObjectName: Neos\RedirectHandler\NeosAdapter\CatchUpHook\DocumentUriPathProjectionHookFactory diff --git a/Tests/Behavior/Features/Bootstrap/FeatureContext.php b/Tests/Behavior/Features/Bootstrap/FeatureContext.php index 3af7b4c..b969163 100644 --- a/Tests/Behavior/Features/Bootstrap/FeatureContext.php +++ b/Tests/Behavior/Features/Bootstrap/FeatureContext.php @@ -21,6 +21,7 @@ use Neos\Flow\Utility\Environment; use Neos\Neos\Tests\Functional\Command\BehatTestHelper; use Neos\RedirectHandler\NeosAdapter\Tests\Behavior\Features\Bootstrap\RedirectOperationTrait; +use Neos\ContentRepositoryRegistry\Factory\ProjectionCatchUpTrigger\CatchUpTriggerWithSynchronousOption; require_once(__DIR__ . '/../../../../../../Neos/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/CurrentSubgraphTrait.php'); require_once(__DIR__ . '/../../../../../../Neos/Neos.ContentRepository.Core/Tests/Behavior/Features/Bootstrap/CurrentUserTrait.php'); @@ -86,6 +87,10 @@ public function __construct() $this->nodeAuthorizationService = $this->objectManager->get(AuthorizationService::class); $this->setupSecurity(); + if (getenv('CATCHUPTRIGGER_ENABLE_SYNCHRONOUS_OPTION')) { + CatchUpTriggerWithSynchronousOption::enableSynchonityForSpeedingUpTesting(); + } + $this->setupEventSourcedTrait(true); } diff --git a/Tests/Behavior/Features/OneDimension.feature b/Tests/Behavior/Features/OneDimension.feature new file mode 100644 index 0000000..6bce948 --- /dev/null +++ b/Tests/Behavior/Features/OneDimension.feature @@ -0,0 +1,272 @@ +@fixtures @contentrepository +Feature: Basic redirect handling with document nodes in one dimension + + Background: + Given I have the following content dimensions: + | Identifier | Values | Generalizations | + | language | de, en, ch | ch->de, en | + And I am user identified by "initiating-user-identifier" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | newContentStreamId | "cs-identifier" | + And the event RootNodeAggregateWithNodeWasCreated was published with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "site-root" | + | nodeTypeName | "Neos.Neos:Sites" | + | coveredDimensionSpacePoints | [{"language": "en"},{"language": "de"},{"language": "ch"}] | + | nodeAggregateClassification | "root" | + And the graph projection is fully up to date + + # site-root + # behat + # company + # service + # about + # imprint + # buy + # mail + And I am in content stream "cs-identifier" and dimension space point {} + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | parentNodeAggregateId | nodeTypeName | initialPropertyValues | originDimensionSpacePoint | nodeName | + | behat | site-root | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "home"} | {"language": "en"} | node1 | + | behat-de | site-root | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "home"} | {"language": "de"} | node1 | + | company | behat | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "company"} | {"language": "en"} | node2 | + | company-de | behat-de | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "company"} | {"language": "de"} | node2-de | + | service | company | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "service"} | {"language": "en"} | node3 | + | service-de | company-de | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "service"} | {"language": "de"} | node3-de | + | about | company | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "about"} | {"language": "en"} | node4 | + | about-de | company-de | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "about"} | {"language": "de"} | node4-de | + | imprint | behat | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "imprint"} | {"language": "en"} | node5 | + | buy | behat | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "buy", "title": "Buy"} | {"language": "en"} | node6 | + | mail | behat | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "mail"} | {"language": "en"} | node7 | + | restricted-by-nodetype | behat | Neos.Neos:Test.Redirect.RestrictedPage | {"uriPathSegment": "restricted-by-nodetype"} | {"language": "en"} | node8 | + + And A site exists for node name "node1" + And the sites configuration is: + """ + Neos: + Neos: + sites: + '*': + contentRepository: default + contentDimensions: + defaultDimensionSpacePoint: + language: en + resolver: + factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\UriPathResolverFactory + options: + segments: + - + dimensionIdentifier: language + dimensionValueMapping: + de: '' + en: en + ch: ch + """ + And The documenturipath projection is up to date + + @fixtures + Scenario: Move a node down into different node and a redirect will be created + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "imprint" | + | dimensionSpacePoint | {"language": "en"} | + | newParentNodeAggregateId | "company" | + | newSucceedingSiblingNodeAggregateId | null | + And The documenturipath projection is up to date + Then I should have a redirect with sourceUri "en/imprint" and targetUri "en/company/imprint" + + Scenario: Move a node up into different node and a redirect will be created + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "service" | + | dimensionSpacePoint | {"language": "en"} | + | newParentNodeAggregateId | "behat" | + | newSucceedingSiblingNodeAggregateId | null | + And The documenturipath projection is up to date + Then I should have a redirect with sourceUri "en/company/service" and targetUri "en/service" + + @fixtures + Scenario: Change the the `uriPathSegment` and a redirect will be created + When the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "company" | + | originDimensionSpacePoint | {"language": "en"} | + | propertyValues | {"uriPathSegment": "evil-corp"} | + And The documenturipath projection is up to date + + Then I should have a redirect with sourceUri "en/company" and targetUri "en/evil-corp" + And I should have a redirect with sourceUri "en/company/service" and targetUri "en/evil-corp/service" + + @fixtures + Scenario: Change the the `uriPathSegment` multiple times and multiple redirects will be created + When the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "company" | + | originDimensionSpacePoint | {"language": "en"} | + | propertyValues | {"uriPathSegment": "evil-corp"} | + And the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "company" | + | originDimensionSpacePoint | {"language": "en"} | + | propertyValues | {"uriPathSegment": "more-evil-corp"} | + And The documenturipath projection is up to date + + Then I should have a redirect with sourceUri "en/company" and targetUri "en/more-evil-corp" + And I should have a redirect with sourceUri "en/company/service" and targetUri "en/more-evil-corp/service" + And I should have a redirect with sourceUri "en/evil-corp" and targetUri "en/more-evil-corp" + And I should have a redirect with sourceUri "en/evil-corp/service" and targetUri "en/more-evil-corp/service" + + + @fixtures + Scenario: Retarget an existing redirect when the source URI matches the source URI of the new redirect + When I have the following redirects: + | sourceuripath | targeturipath | + | company | company-old | + And the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "company" | + | originDimensionSpacePoint | {"language": "en"} | + | propertyValues | {"uriPathSegment": "my-company"} | + And The documenturipath projection is up to date + Then I should have a redirect with sourceUri "en/company" and targetUri "en/my-company" + And I should have no redirect with sourceUri "en/company" and targetUri "en/company-old" + And I should have a redirect with sourceUri "en/company/service" and targetUri "en/my-company/service" + + @fixtures + Scenario: No redirect should be created for an existing node if any non URI related property changes + When the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "buy" | + | originDimensionSpacePoint | {"language": "en"} | + | propertyValues | {"title": "my-buy"} | + And The documenturipath projection is up to date + Then I should have no redirect with sourceUri "en/buy" + + @fixtures + Scenario: No redirect should be created for an restricted node by nodetype + When the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "restricted-by-nodetype" | + | originDimensionSpacePoint | {"language": "en"} | + | propertyValues | {"uriPathSegment": "restricted-by-nodetype-new"} | + And The documenturipath projection is up to date + Then I should have no redirect with sourceUri "en/restricted" + + +# @fixtures +# Scenario: Redirects should always be created in the same dimension the node is in +# When I get a node by path "/sites/behat/imprint" with the following context: +# | Workspace | Language | +# | user-testaccount | fr | +# And I set the node property "uriPathSegment" to "empreinte-nouveau" +# And I publish the node +# Then I should have a redirect with sourceUri "fr/empreinte" and targetUri "fr/empreinte-nouveau" +# +# #fixed in 1.0.3 +# @fixtures +# Scenario: Redirects should aways be created in the same dimension the node is in and not the fallback dimension +# When I get a node by path "/sites/behat/imprint" with the following context: +# | Workspace | Language | +# | user-testaccount | de,en | +# And I set the node property "uriPathSegment" to "impressum-neu" +# And I publish the node +# Then I should have a redirect with sourceUri "de/impressum" and targetUri "de/impressum-neu" +# And I should have no redirect with sourceUri "en/impressum" and targetUri "de/impressum-neu" +# +# #fixed in 1.0.3 +# @fixtures +# Scenario: I have an existing redirect and it should never be overwritten for a node variant from a different dimension +# When I have the following redirects: +# | sourceuripath | targeturipath | +# | important-page-from-the-old-site | en/mail | +# When I get a node by path "/sites/behat/mail" with the following context: +# | Workspace | Language | +# | user-testaccount | de,en | +# And I unhide the node +# And I publish the node +# Then I should have a redirect with sourceUri "important-page-from-the-old-site" and targetUri "en/mail" +# And I should have no redirect with sourceUri "en/mail" and targetUri "de/mail" +# + + @fixtures + Scenario: Redirects should be created for a hidden node + When the command DisableNodeAggregate is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "mail" | + | originDimensionSpacePoint | {"language": "en"} | + | nodeVariantSelectionStrategy | "allVariants" | + And the graph projection is fully up to date + When the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "mail" | + | originDimensionSpacePoint | {"language": "en"} | + | propertyValues | {"uriPathSegment": "not-mail"} | + And The documenturipath projection is up to date + Then I should have a redirect with sourceUri "en/mail" and targetUri "en/not-mail" + +# @fixtures +# Scenario: Create redirects for nodes published in different dimensions +# When I get a node by path "/sites/behat/buy" with the following context: +# | Workspace | +# | user-testaccount | +# And I move the node into the node with path "/sites/behat/company" +# And I publish the node +# When I get a node by path "/sites/behat/company/buy" with the following context: +# | Workspace | Language | +# | user-testaccount | de,en | +# And I publish the node +# Then I should have a redirect with sourceUri "en/buy" and targetUri "en/company/buy" +# And I should have a redirect with sourceUri "de/kaufen" and targetUri "de/company/kaufen" +# +# #fixed in 1.0.4 +# @fixtures +# Scenario: Create redirects for nodes that use the current dimension as fallback +# When I get a node by path "/sites/behat/company" with the following context: +# | Workspace | Language | +# | user-testaccount | en | +# And I move the node into the node with path "/sites/behat/service" +# And I publish the node +# Then I should have a redirect with sourceUri "en/company" and targetUri "en/service/company" +# And I should have a redirect with sourceUri "de/company" and targetUri "de/service/company" + @fixtures + Scenario: Change the the `uriPathSegment` and a redirect will be created also for fallback + When the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "company-de" | + | originDimensionSpacePoint | {"language": "ch"} | + | propertyValues | {"uriPathSegment": "unternehmen"} | + And The documenturipath projection is up to date + + Then I should have a redirect with sourceUri "de/company" and targetUri "de/unternehmen" + And I should have a redirect with sourceUri "de/company/service" and targetUri "de/unternehmen/service" + + + @fixtures + Scenario: A removed node should lead to a GONE response with empty target uri + Given the event NodeAggregateWasRemoved was published with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "company" | + | affectedOccupiedDimensionSpacePoints | [{"language": "en"}] | + | affectedCoveredDimensionSpacePoints | [{"language": "en"}] | + And the graph projection is fully up to date + And The documenturipath projection is up to date + + Then I should have a redirect with sourceUri "en/company" and statusCode "410" + And I should have a redirect with sourceUri "en/company" and targetUri "" + And I should have a redirect with sourceUri "en/company/service" and statusCode "410" + And I should have a redirect with sourceUri "en/company/service" and targetUri "" \ No newline at end of file diff --git a/Tests/Behavior/Features/Redirect.feature b/Tests/Behavior/Features/WithoutDimensions.feature similarity index 72% rename from Tests/Behavior/Features/Redirect.feature rename to Tests/Behavior/Features/WithoutDimensions.feature index ca3f44d..bda6d64 100755 --- a/Tests/Behavior/Features/Redirect.feature +++ b/Tests/Behavior/Features/WithoutDimensions.feature @@ -1,5 +1,5 @@ @fixtures @contentrepository -Feature: Basic redirect handling with document nodes in one dimension +Feature: Basic redirect handling with document nodes without dimensions Background: Given I have no content dimensions @@ -36,7 +36,6 @@ Feature: Basic redirect handling with document nodes in one dimension | buy | behat | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "buy", "title": "Buy"} | node6 | | mail | behat | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "mail"} | node7 | | restricted-by-nodetype | behat | Neos.Neos:Test.Redirect.RestrictedPage | {"uriPathSegment": "restricted-by-nodetype"} | node8 | - | restricted-by-path | behat | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "restricted-by-path"} | node9 | And A site exists for node name "node1" And the sites configuration is: """ @@ -87,7 +86,7 @@ Feature: Basic redirect handling with document nodes in one dimension And I should have a redirect with sourceUri "company/service" and targetUri "evil-corp/service" @fixtures - Scenario: Change the the `uriPathSegment` mutiple times and mutliple redirects will be created + Scenario: Change the the `uriPathSegment` multiple times and multiple redirects will be created When the command SetNodeProperties is executed with payload: | Key | Value | | contentStreamId | "cs-identifier" | @@ -146,53 +145,6 @@ Feature: Basic redirect handling with document nodes in one dimension And The documenturipath projection is up to date Then I should have no redirect with sourceUri "restricted" - - @fixtures - Scenario: No redirect should be created for an restricted node by path - When the command SetNodeProperties is executed with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "restricted-by-path" | - | originDimensionSpacePoint | {} | - | propertyValues | {"uriPathSegment": "restricted-by-path-new"} | - And The documenturipath projection is up to date - Then I should have no redirect with sourceUri "restricted-by-path" - -# @fixtures -# Scenario: Redirects should always be created in the same dimension the node is in -# When I get a node by path "/sites/behat/imprint" with the following context: -# | Workspace | Language | -# | user-testaccount | fr | -# And I set the node property "uriPathSegment" to "empreinte-nouveau" -# And I publish the node -# Then I should have a redirect with sourceUri "fr/empreinte" and targetUri "fr/empreinte-nouveau" -# -# #fixed in 1.0.3 -# @fixtures -# Scenario: Redirects should aways be created in the same dimension the node is in and not the fallback dimension -# When I get a node by path "/sites/behat/imprint" with the following context: -# | Workspace | Language | -# | user-testaccount | de,en | -# And I set the node property "uriPathSegment" to "impressum-neu" -# And I publish the node -# Then I should have a redirect with sourceUri "de/impressum" and targetUri "de/impressum-neu" -# And I should have no redirect with sourceUri "en/impressum" and targetUri "de/impressum-neu" -# -# #fixed in 1.0.3 -# @fixtures -# Scenario: I have an existing redirect and it should never be overwritten for a node variant from a different dimension -# When I have the following redirects: -# | sourceuripath | targeturipath | -# | important-page-from-the-old-site | en/mail | -# When I get a node by path "/sites/behat/mail" with the following context: -# | Workspace | Language | -# | user-testaccount | de,en | -# And I unhide the node -# And I publish the node -# Then I should have a redirect with sourceUri "important-page-from-the-old-site" and targetUri "en/mail" -# And I should have no redirect with sourceUri "en/mail" and targetUri "de/mail" -# - @fixtures Scenario: Redirects should be created for a hidden node When the command DisableNodeAggregate is executed with payload: @@ -211,32 +163,6 @@ Feature: Basic redirect handling with document nodes in one dimension And The documenturipath projection is up to date Then I should have a redirect with sourceUri "mail" and targetUri "not-mail" -# @fixtures -# Scenario: Create redirects for nodes published in different dimensions -# When I get a node by path "/sites/behat/buy" with the following context: -# | Workspace | -# | user-testaccount | -# And I move the node into the node with path "/sites/behat/company" -# And I publish the node -# When I get a node by path "/sites/behat/company/buy" with the following context: -# | Workspace | Language | -# | user-testaccount | de,en | -# And I publish the node -# Then I should have a redirect with sourceUri "en/buy" and targetUri "en/company/buy" -# And I should have a redirect with sourceUri "de/kaufen" and targetUri "de/company/kaufen" -# -# #fixed in 1.0.4 -# @fixtures -# Scenario: Create redirects for nodes that use the current dimension as fallback -# When I get a node by path "/sites/behat/company" with the following context: -# | Workspace | Language | -# | user-testaccount | en | -# And I move the node into the node with path "/sites/behat/service" -# And I publish the node -# Then I should have a redirect with sourceUri "en/company" and targetUri "en/service/company" -# And I should have a redirect with sourceUri "de/company" and targetUri "de/service/company" - - @fixtures Scenario: A removed node should lead to a GONE response with empty target uri Given the event NodeAggregateWasRemoved was published with payload: From bb5d8d8b172ea5e52d64224fa1630b6c7cafc874 Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Tue, 20 Jun 2023 13:35:12 +0200 Subject: [PATCH 06/32] TASK: Fix tests --- .../Features/Bootstrap/FeatureContext.php | 5 +- Tests/Behavior/Features/OneDimension.feature | 167 +++++++++--------- .../Features/WithoutDimensions.feature | 6 +- 3 files changed, 90 insertions(+), 88 deletions(-) diff --git a/Tests/Behavior/Features/Bootstrap/FeatureContext.php b/Tests/Behavior/Features/Bootstrap/FeatureContext.php index b969163..2c5ea6e 100644 --- a/Tests/Behavior/Features/Bootstrap/FeatureContext.php +++ b/Tests/Behavior/Features/Bootstrap/FeatureContext.php @@ -87,9 +87,8 @@ public function __construct() $this->nodeAuthorizationService = $this->objectManager->get(AuthorizationService::class); $this->setupSecurity(); - if (getenv('CATCHUPTRIGGER_ENABLE_SYNCHRONOUS_OPTION')) { - CatchUpTriggerWithSynchronousOption::enableSynchonityForSpeedingUpTesting(); - } + + CatchUpTriggerWithSynchronousOption::enableSynchonityForSpeedingUpTesting(); $this->setupEventSourcedTrait(true); } diff --git a/Tests/Behavior/Features/OneDimension.feature b/Tests/Behavior/Features/OneDimension.feature index 6bce948..1b353ea 100644 --- a/Tests/Behavior/Features/OneDimension.feature +++ b/Tests/Behavior/Features/OneDimension.feature @@ -3,20 +3,18 @@ Feature: Basic redirect handling with document nodes in one dimension Background: Given I have the following content dimensions: - | Identifier | Values | Generalizations | - | language | de, en, ch | ch->de, en | + | Identifier | Values | Generalizations | + | language | de, en, gsw | gsw->de, en | And I am user identified by "initiating-user-identifier" And the command CreateRootWorkspace is executed with payload: | Key | Value | | workspaceName | "live" | | newContentStreamId | "cs-identifier" | - And the event RootNodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "site-root" | - | nodeTypeName | "Neos.Neos:Sites" | - | coveredDimensionSpacePoints | [{"language": "en"},{"language": "de"},{"language": "ch"}] | - | nodeAggregateClassification | "root" | + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "site-root" | + | nodeTypeName | "Neos.Neos:Sites" | + | contentStreamId | "cs-identifier" | And the graph projection is fully up to date # site-root @@ -31,13 +29,9 @@ Feature: Basic redirect handling with document nodes in one dimension And the following CreateNodeAggregateWithNode commands are executed: | nodeAggregateId | parentNodeAggregateId | nodeTypeName | initialPropertyValues | originDimensionSpacePoint | nodeName | | behat | site-root | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "home"} | {"language": "en"} | node1 | - | behat-de | site-root | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "home"} | {"language": "de"} | node1 | | company | behat | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "company"} | {"language": "en"} | node2 | - | company-de | behat-de | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "company"} | {"language": "de"} | node2-de | | service | company | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "service"} | {"language": "en"} | node3 | - | service-de | company-de | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "service"} | {"language": "de"} | node3-de | | about | company | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "about"} | {"language": "en"} | node4 | - | about-de | company-de | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "about"} | {"language": "de"} | node4-de | | imprint | behat | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "imprint"} | {"language": "en"} | node5 | | buy | behat | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "buy", "title": "Buy"} | {"language": "en"} | node6 | | mail | behat | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "mail"} | {"language": "en"} | node7 | @@ -63,8 +57,28 @@ Feature: Basic redirect handling with document nodes in one dimension dimensionValueMapping: de: '' en: en - ch: ch + gsw: ch """ + And the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "behat" | + | sourceOrigin | {"language":"en"} | + | targetOrigin | {"language":"de"} | + And the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "company" | + | sourceOrigin | {"language":"en"} | + | targetOrigin | {"language":"de"} | + And the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "service" | + | sourceOrigin | {"language":"en"} | + | targetOrigin | {"language":"de"} | + And the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "imprint" | + | sourceOrigin | {"language":"en"} | + | targetOrigin | {"language":"de"} | And The documenturipath projection is up to date @fixtures @@ -79,6 +93,20 @@ Feature: Basic redirect handling with document nodes in one dimension And The documenturipath projection is up to date Then I should have a redirect with sourceUri "en/imprint" and targetUri "en/company/imprint" + @fixtures + Scenario: Move a node down into different node and a redirect will be created also for fallback + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "imprint" | + | dimensionSpacePoint | {"language": "de"} | + | newParentNodeAggregateId | "company" | + | newSucceedingSiblingNodeAggregateId | null | + And The documenturipath projection is up to date + Then I should have a redirect with sourceUri "imprint" and targetUri "company/imprint" + Then I should have a redirect with sourceUri "ch/imprint" and targetUri "ch/company/imprint" + + @fixtures Scenario: Move a node up into different node and a redirect will be created When the command MoveNodeAggregate is executed with payload: | Key | Value | @@ -90,6 +118,19 @@ Feature: Basic redirect handling with document nodes in one dimension And The documenturipath projection is up to date Then I should have a redirect with sourceUri "en/company/service" and targetUri "en/service" + @fixtures + Scenario: Move a node up into different node and a redirect will be created also for fallback + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "service" | + | dimensionSpacePoint | {"language": "de"} | + | newParentNodeAggregateId | "behat" | + | newSucceedingSiblingNodeAggregateId | null | + And The documenturipath projection is up to date + Then I should have a redirect with sourceUri "company/service" and targetUri "service" + And I should have a redirect with sourceUri "ch/company/service" and targetUri "ch/service" + @fixtures Scenario: Change the the `uriPathSegment` and a redirect will be created When the command SetNodeProperties is executed with payload: @@ -163,49 +204,13 @@ Feature: Basic redirect handling with document nodes in one dimension And The documenturipath projection is up to date Then I should have no redirect with sourceUri "en/restricted" - -# @fixtures -# Scenario: Redirects should always be created in the same dimension the node is in -# When I get a node by path "/sites/behat/imprint" with the following context: -# | Workspace | Language | -# | user-testaccount | fr | -# And I set the node property "uriPathSegment" to "empreinte-nouveau" -# And I publish the node -# Then I should have a redirect with sourceUri "fr/empreinte" and targetUri "fr/empreinte-nouveau" -# -# #fixed in 1.0.3 -# @fixtures -# Scenario: Redirects should aways be created in the same dimension the node is in and not the fallback dimension -# When I get a node by path "/sites/behat/imprint" with the following context: -# | Workspace | Language | -# | user-testaccount | de,en | -# And I set the node property "uriPathSegment" to "impressum-neu" -# And I publish the node -# Then I should have a redirect with sourceUri "de/impressum" and targetUri "de/impressum-neu" -# And I should have no redirect with sourceUri "en/impressum" and targetUri "de/impressum-neu" -# -# #fixed in 1.0.3 -# @fixtures -# Scenario: I have an existing redirect and it should never be overwritten for a node variant from a different dimension -# When I have the following redirects: -# | sourceuripath | targeturipath | -# | important-page-from-the-old-site | en/mail | -# When I get a node by path "/sites/behat/mail" with the following context: -# | Workspace | Language | -# | user-testaccount | de,en | -# And I unhide the node -# And I publish the node -# Then I should have a redirect with sourceUri "important-page-from-the-old-site" and targetUri "en/mail" -# And I should have no redirect with sourceUri "en/mail" and targetUri "de/mail" -# - @fixtures Scenario: Redirects should be created for a hidden node When the command DisableNodeAggregate is executed with payload: | Key | Value | | contentStreamId | "cs-identifier" | | nodeAggregateId | "mail" | - | originDimensionSpacePoint | {"language": "en"} | + | coveredDimensionSpacePoint | {"language": "en"} | | nodeVariantSelectionStrategy | "allVariants" | And the graph projection is fully up to date When the command SetNodeProperties is executed with payload: @@ -217,42 +222,21 @@ Feature: Basic redirect handling with document nodes in one dimension And The documenturipath projection is up to date Then I should have a redirect with sourceUri "en/mail" and targetUri "en/not-mail" -# @fixtures -# Scenario: Create redirects for nodes published in different dimensions -# When I get a node by path "/sites/behat/buy" with the following context: -# | Workspace | -# | user-testaccount | -# And I move the node into the node with path "/sites/behat/company" -# And I publish the node -# When I get a node by path "/sites/behat/company/buy" with the following context: -# | Workspace | Language | -# | user-testaccount | de,en | -# And I publish the node -# Then I should have a redirect with sourceUri "en/buy" and targetUri "en/company/buy" -# And I should have a redirect with sourceUri "de/kaufen" and targetUri "de/company/kaufen" -# -# #fixed in 1.0.4 -# @fixtures -# Scenario: Create redirects for nodes that use the current dimension as fallback -# When I get a node by path "/sites/behat/company" with the following context: -# | Workspace | Language | -# | user-testaccount | en | -# And I move the node into the node with path "/sites/behat/service" -# And I publish the node -# Then I should have a redirect with sourceUri "en/company" and targetUri "en/service/company" -# And I should have a redirect with sourceUri "de/company" and targetUri "de/service/company" @fixtures Scenario: Change the the `uriPathSegment` and a redirect will be created also for fallback - When the command SetNodeProperties is executed with payload: + + And the command SetNodeProperties is executed with payload: | Key | Value | | contentStreamId | "cs-identifier" | - | nodeAggregateId | "company-de" | - | originDimensionSpacePoint | {"language": "ch"} | + | nodeAggregateId | "company" | + | originDimensionSpacePoint | {"language": "de"} | | propertyValues | {"uriPathSegment": "unternehmen"} | And The documenturipath projection is up to date - Then I should have a redirect with sourceUri "de/company" and targetUri "de/unternehmen" - And I should have a redirect with sourceUri "de/company/service" and targetUri "de/unternehmen/service" + Then I should have a redirect with sourceUri "company" and targetUri "unternehmen" + And I should have a redirect with sourceUri "company/service" and targetUri "unternehmen/service" + And I should have a redirect with sourceUri "ch/company" and targetUri "ch/unternehmen" + And I should have a redirect with sourceUri "ch/company/service" and targetUri "ch/unternehmen/service" @fixtures @@ -269,4 +253,25 @@ Feature: Basic redirect handling with document nodes in one dimension Then I should have a redirect with sourceUri "en/company" and statusCode "410" And I should have a redirect with sourceUri "en/company" and targetUri "" And I should have a redirect with sourceUri "en/company/service" and statusCode "410" - And I should have a redirect with sourceUri "en/company/service" and targetUri "" \ No newline at end of file + And I should have a redirect with sourceUri "en/company/service" and targetUri "" + + @fixtures + Scenario: A removed node should lead to a GONE response with empty target uri also for fallback + Given the event NodeAggregateWasRemoved was published with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "company" | + | affectedOccupiedDimensionSpacePoints | [{"language": "de"}] | + | affectedCoveredDimensionSpacePoints | [{"language": "de"}, {"language": "gsw"}] | + And the graph projection is fully up to date + And The documenturipath projection is up to date + + Then I should have a redirect with sourceUri "company" and statusCode "410" + And I should have a redirect with sourceUri "company" and targetUri "" + And I should have a redirect with sourceUri "company/service" and statusCode "410" + And I should have a redirect with sourceUri "company/service" and targetUri "" + + And I should have a redirect with sourceUri "ch/company" and statusCode "410" + And I should have a redirect with sourceUri "ch/company" and targetUri "" + And I should have a redirect with sourceUri "ch/company/service" and statusCode "410" + And I should have a redirect with sourceUri "ch/company/service" and targetUri "" \ No newline at end of file diff --git a/Tests/Behavior/Features/WithoutDimensions.feature b/Tests/Behavior/Features/WithoutDimensions.feature index bda6d64..06c894c 100755 --- a/Tests/Behavior/Features/WithoutDimensions.feature +++ b/Tests/Behavior/Features/WithoutDimensions.feature @@ -8,13 +8,11 @@ Feature: Basic redirect handling with document nodes without dimensions | Key | Value | | workspaceName | "live" | | newContentStreamId | "cs-identifier" | - And the event RootNodeAggregateWithNodeWasCreated was published with payload: + And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | contentStreamId | "cs-identifier" | | nodeAggregateId | "site-root" | | nodeTypeName | "Neos.Neos:Sites" | - | coveredDimensionSpacePoints | [{}] | - | nodeAggregateClassification | "root" | And the graph projection is fully up to date # site-root @@ -151,7 +149,7 @@ Feature: Basic redirect handling with document nodes without dimensions | Key | Value | | contentStreamId | "cs-identifier" | | nodeAggregateId | "mail" | - | originDimensionSpacePoint | {} | + | coveredDimensionSpacePoint | {} | | nodeVariantSelectionStrategy | "allVariants" | And the graph projection is fully up to date When the command SetNodeProperties is executed with payload: From e69154721f364c29c3499b435fa8dca3ece76601 Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Tue, 20 Jun 2023 18:04:30 +0200 Subject: [PATCH 07/32] FEATURE: Redirect NeosAdapter for new ContentRepository --- .../DocumentUriPathProjectionHook.php | 50 +-- .../DocumentUriPathProjectionHookFactory.php | 2 +- Classes/Service/NodeRedirectService.php | 86 ++-- Configuration/Testing/Behat/Settings.Mvc.yaml | 8 + .../Features/MultipleDimensions.feature | 380 ++++++++++++++++++ Tests/Behavior/Features/OneDimension.feature | 194 +++++---- .../Features/WithoutDimensions.feature | 85 ++-- 7 files changed, 619 insertions(+), 186 deletions(-) create mode 100644 Configuration/Testing/Behat/Settings.Mvc.yaml create mode 100644 Tests/Behavior/Features/MultipleDimensions.feature diff --git a/Classes/CatchUpHook/DocumentUriPathProjectionHook.php b/Classes/CatchUpHook/DocumentUriPathProjectionHook.php index 7d1dc95..8e33b15 100644 --- a/Classes/CatchUpHook/DocumentUriPathProjectionHook.php +++ b/Classes/CatchUpHook/DocumentUriPathProjectionHook.php @@ -25,6 +25,7 @@ final class DocumentUriPathProjectionHook implements CatchUpHookInterface { /** + * Runtime cache to keep DocumentNodeInfos until they get removed. * @var array> */ private array $documentNodeInfosBeforeRemoval; @@ -97,17 +98,17 @@ private function onBeforeNodeAggregateWasRemoved(NodeAggregateWasRemoved $event) ); $this->documentNodeInfosBeforeRemoval[$dimensionSpacePoint->hash][] = $node; - $children = $this->getState()->getAllChildrenOfNode($node); + $descendantsOfNode = $this->getState()->getDescendantsOfNode($node); array_map( - function ($childNode) use ($event, $dimensionSpacePoint) { + function ($descendantOfNode) use ($event, $dimensionSpacePoint) { $this->nodeRedirectService->appendAffectedNode( - $childNode, - $this->getNodeAddress($event->contentStreamId, $dimensionSpacePoint, $childNode->getNodeAggregateId()), + $descendantOfNode, + $this->getNodeAddress($event->contentStreamId, $dimensionSpacePoint, $descendantOfNode->getNodeAggregateId()), $this->getContentRepositoryId() ); - $this->documentNodeInfosBeforeRemoval[$dimensionSpacePoint->hash][] = $childNode; + $this->documentNodeInfosBeforeRemoval[$dimensionSpacePoint->hash][] = $descendantOfNode; }, - $children); + iterator_to_array($descendantsOfNode)); } } @@ -135,9 +136,7 @@ private function onBeforeNodePropertiesWereSet(NodePropertiesWereSet $event): vo { $this->handleNodePropertiesWereSet( $event, - fn( - DocumentNodeInfo $node, NodeAddress $nodeAddress, - ) => $this->nodeRedirectService->appendAffectedNode($node, $nodeAddress, $this->getContentRepositoryId()) + $this->nodeRedirectService->appendAffectedNode(...) ); } @@ -145,9 +144,7 @@ private function onAfterNodePropertiesWereSet(NodePropertiesWereSet $event): voi { $this->handleNodePropertiesWereSet( $event, - fn( - DocumentNodeInfo $node, NodeAddress $nodeAddress, - ) => $this->nodeRedirectService->createRedirectForAffectedNode($node, $nodeAddress, $this->getContentRepositoryId()) + $this->nodeRedirectService->createRedirectForAffectedNode(...) ); } @@ -167,16 +164,19 @@ private function handleNodePropertiesWereSet(NodePropertiesWereSet $event, \Clos $event->nodeAggregateId, $affectedDimensionSpacePoint->hash )); - file_put_contents('/var/www/html/Data/Logs/foo.log', $node->getUriPath() . "\n", flags: FILE_APPEND); if ($node === null) { // probably not a document node continue; } - $closure($node, $this->getNodeAddress($event->contentStreamId, $affectedDimensionSpacePoint, $node->getNodeAggregateId())); + $closure($node, $this->getNodeAddress($event->contentStreamId, $affectedDimensionSpacePoint, $node->getNodeAggregateId()), $this->getContentRepositoryId()); - $children = $this->getState()->getAllChildrenOfNode($node); - array_map(fn($childNode) => $closure($childNode, $this->getNodeAddress($event->contentStreamId, $affectedDimensionSpacePoint, $childNode->getNodeAggregateId())), $children); + $descendantsOfNode = $this->getState()->getDescendantsOfNode($node); + array_map(fn($descendantOfNode) => $closure( + $descendantOfNode, + $this->getNodeAddress($event->contentStreamId, $affectedDimensionSpacePoint, $descendantOfNode->getNodeAggregateId()), + $this->getContentRepositoryId() + ), iterator_to_array($descendantsOfNode)); } } @@ -184,9 +184,7 @@ private function onBeforeNodeAggregateWasMoved(NodeAggregateWasMoved $event): vo { $this->handleNodeWasMoved( $event, - fn( - DocumentNodeInfo $node, NodeAddress $nodeAddress, - ) => $this->nodeRedirectService->appendAffectedNode($node, $nodeAddress, $this->getContentRepositoryId()) + $this->nodeRedirectService->appendAffectedNode(...) ); } @@ -194,9 +192,7 @@ private function onAfterNodeAggregateWasMoved(NodeAggregateWasMoved $event): voi { $this->handleNodeWasMoved( $event, - fn( - DocumentNodeInfo $node, NodeAddress $nodeAddress, - ) => $this->nodeRedirectService->createRedirectForAffectedNode($node, $nodeAddress, $this->getContentRepositoryId()) + $this->nodeRedirectService->createRedirectForAffectedNode(...) ); } @@ -220,10 +216,14 @@ private function handleNodeWasMoved(NodeAggregateWasMoved $event, \Closure $clos continue; } - $closure($node, $this->getNodeAddress($event->contentStreamId, $newLocation->coveredDimensionSpacePoint, $node->getNodeAggregateId())); + $closure($node, $this->getNodeAddress($event->contentStreamId, $newLocation->coveredDimensionSpacePoint, $node->getNodeAggregateId()), $this->getContentRepositoryId()); - $children = $this->getState()->getAllChildrenOfNode($node); - array_map(fn($childNode) => $closure($childNode, $this->getNodeAddress($event->contentStreamId, $newLocation->coveredDimensionSpacePoint, $childNode->getNodeAggregateId())), $children); + $descendantsOfNode = $this->getState()->getDescendantsOfNode($node); + array_map(fn($descendantOfNode) => $closure( + $descendantOfNode, + $this->getNodeAddress($event->contentStreamId, $newLocation->coveredDimensionSpacePoint, $descendantOfNode->getNodeAggregateId()), + $this->getContentRepositoryId() + ), iterator_to_array($descendantsOfNode)); } } } diff --git a/Classes/CatchUpHook/DocumentUriPathProjectionHookFactory.php b/Classes/CatchUpHook/DocumentUriPathProjectionHookFactory.php index 74789c7..682e915 100644 --- a/Classes/CatchUpHook/DocumentUriPathProjectionHookFactory.php +++ b/Classes/CatchUpHook/DocumentUriPathProjectionHookFactory.php @@ -8,7 +8,7 @@ use Neos\RedirectHandler\NeosAdapter\Service\NodeRedirectService; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; -class DocumentUriPathProjectionHookFactory implements CatchUpHookFactoryInterface +final class DocumentUriPathProjectionHookFactory implements CatchUpHookFactoryInterface { public function __construct( protected readonly NodeRedirectService $redirectService, diff --git a/Classes/Service/NodeRedirectService.php b/Classes/Service/NodeRedirectService.php index 5f84f14..b8b9e7e 100644 --- a/Classes/Service/NodeRedirectService.php +++ b/Classes/Service/NodeRedirectService.php @@ -40,7 +40,7 @@ * * @Flow\Scope("singleton") */ -class NodeRedirectService +final class NodeRedirectService { const STATUS_CODE_TYPE_REDIRECT = 'redirect'; const STATUS_CODE_TYPE_GONE = 'gone'; @@ -49,14 +49,11 @@ class NodeRedirectService private array $hostnamesRuntimeCache = []; #[Flow\Inject] - protected RedirectStorageInterface $redirectStorage; - - #[Flow\Inject] - protected PersistenceManagerInterface $persistenceManager; - - #[Flow\Inject] - protected LoggerInterface $logger; + protected ?LoggerInterface $logger = null; + /** + * @var array + */ #[Flow\InjectConfiguration(path: "statusCode", package: "Neos.RedirectHandler")] protected array $defaultStatusCode; @@ -66,22 +63,40 @@ class NodeRedirectService #[Flow\InjectConfiguration(path: "enableRemovedNodeRedirect", package: "Neos.RedirectHandler.NeosAdapter")] protected bool $enableRemovedNodeRedirect; + /** + * @var array + */ #[Flow\InjectConfiguration(path: "restrictByOldUriPrefix", package: "Neos.RedirectHandler.NeosAdapter")] - protected $restrictByOldUriPrefix; + protected array $restrictByOldUriPrefix = []; + /** + * @var array + */ #[Flow\InjectConfiguration(path: "restrictByNodeType", package: "Neos.RedirectHandler.NeosAdapter")] - protected $restrictByNodeType; - - #[Flow\Inject] - protected ContentRepositoryRegistry $contentRepositoryRegistry; - - #[Flow\Inject] - protected SiteRepository $siteRepository; + protected array $restrictByNodeType = []; + + public function __construct( + #[Flow\Inject] + protected RedirectStorageInterface $redirectStorage, + #[Flow\Inject] + protected PersistenceManagerInterface $persistenceManager, + #[Flow\Inject] + protected ContentRepositoryRegistry $contentRepositoryRegistry, + #[Flow\Inject] + protected SiteRepository $siteRepository, + ) { + } + /** + * Collects affected nodes before they got moved or removed. + * + * @throws \Neos\Flow\Http\Exception + * @throws \Neos\Flow\Mvc\Routing\Exception\MissingActionNameException + */ public function appendAffectedNode(DocumentNodeInfo $nodeInfo, NodeAddress $nodeAddress, ContentRepositoryId $contentRepositoryId): void { try { - $this->affectedNodes[$this->getAffectedNodesKey($nodeInfo, $contentRepositoryId)] = [ + $this->affectedNodes[$this->createAffectedNodesKey($nodeInfo, $contentRepositoryId)] = [ 'node' => $nodeInfo, 'url' => $this->getNodeUriBuilder($nodeInfo->getSiteNodeName(), $contentRepositoryId)->uriFor($nodeAddress), ]; @@ -89,17 +104,23 @@ public function appendAffectedNode(DocumentNodeInfo $nodeInfo, NodeAddress $node } } + /** + * Creates redirects for given node and uses the collected affected nodes to determine the source of the new redirect target. + * + * @throws \Neos\Flow\Http\Exception + * @throws \Neos\Flow\Mvc\Routing\Exception\MissingActionNameException + */ public function createRedirectForAffectedNode(DocumentNodeInfo $nodeInfo, NodeAddress $nodeAddress, ContentRepositoryId $contentRepositoryId): void { if (!$this->enableAutomaticRedirects) { return; } - $affectedNode = $this->affectedNodes[$this->getAffectedNodesKey($nodeInfo, $contentRepositoryId)] ?? null; + $affectedNode = $this->affectedNodes[$this->createAffectedNodesKey($nodeInfo, $contentRepositoryId)] ?? null; if ($affectedNode === null) { return; } - unset($this->affectedNodes[$this->getAffectedNodesKey($nodeInfo, $contentRepositoryId)]); + unset($this->affectedNodes[$this->createAffectedNodesKey($nodeInfo, $contentRepositoryId)]); /** @var Uri $oldUri */ $oldUri = $affectedNode['url']; @@ -111,26 +132,28 @@ public function createRedirectForAffectedNode(DocumentNodeInfo $nodeInfo, NodeAd try { $newUri = $this->getNodeUriBuilder($nodeInfo->getSiteNodeName(), $contentRepositoryId)->uriFor($nodeAddress); } catch (NoMatchingRouteException $exception) { + // We can't build an uri for given node, so we can't create any redirect. E.g.: Node is disabled. return; } - file_put_contents('/var/www/html/Data/Logs/foo.log', $oldUri->getPath() ." => " . $newUri->getPath() . "\n", flags: FILE_APPEND); - file_put_contents('/var/www/html/Data/Logs/foo.log', $affectedNode['node']->getDimensionSpacePointHash() ." => " . $nodeInfo->getDimensionSpacePointHash() . "\n", flags: FILE_APPEND); $this->createRedirectWithNewTarget($oldUri->getPath(), $newUri->getPath(), $nodeInfo->getSiteNodeName()); $this->persistenceManager->persistAll(); } + /** + * Creates redirects for given removed node and uses the collected affected nodes to determine the source of the new redirect. + */ public function createRedirectForRemovedAffectedNode(DocumentNodeInfo $nodeInfo, ContentRepositoryId $contentRepositoryId): void { if (!$this->enableAutomaticRedirects) { return; } - $affectedNode = $this->affectedNodes[$this->getAffectedNodesKey($nodeInfo, $contentRepositoryId)] ?? null; + $affectedNode = $this->affectedNodes[$this->createAffectedNodesKey($nodeInfo, $contentRepositoryId)] ?? null; if ($affectedNode === null) { return; } - unset($this->affectedNodes[$this->getAffectedNodesKey($nodeInfo, $contentRepositoryId)]); + unset($this->affectedNodes[$this->createAffectedNodesKey($nodeInfo, $contentRepositoryId)]); /** @var Uri $oldUri */ $oldUri = $affectedNode['url']; @@ -150,7 +173,7 @@ protected function getNodeType(ContentRepositoryId $contentRepositoryId, NodeTyp return $this->contentRepositoryRegistry->get($contentRepositoryId)->getNodeTypeManager()->getNodeType($nodeTypeName); } - private function getAffectedNodesKey(DocumentNodeInfo $nodeInfo, ContentRepositoryId $contentRepositoryId): string + private function createAffectedNodesKey(DocumentNodeInfo $nodeInfo, ContentRepositoryId $contentRepositoryId): string { return $contentRepositoryId->value . '#' . $nodeInfo->getNodeAggregateId()->value . '#' . $nodeInfo->getDimensionSpacePointHash(); } @@ -181,7 +204,7 @@ protected function createRedirectWithNewTarget(string $oldUriPath, string $newUr } $hosts = $this->getHostnames($siteNodeName); - $statusCode = (integer)$this->defaultStatusCode[self::STATUS_CODE_TYPE_REDIRECT]; + $statusCode = $this->defaultStatusCode[self::STATUS_CODE_TYPE_REDIRECT]; $this->redirectStorage->addRedirect($oldUriPath, $newUriPath, $statusCode, $hosts); @@ -189,7 +212,7 @@ protected function createRedirectWithNewTarget(string $oldUriPath, string $newUr } /** - * Removes a redirect + * Adds a redirect for a removed target if enabled. */ protected function createRedirectForRemovedTarget(string $oldUriPath, SiteNodeName $siteNodeName): bool { @@ -198,7 +221,7 @@ protected function createRedirectForRemovedTarget(string $oldUriPath, SiteNodeNa // For example redirect to dedicated landing pages for deleted campaign NodeTypes if ($this->enableRemovedNodeRedirect) { $hosts = $this->getHostnames($siteNodeName); - $statusCode = (integer)$this->defaultStatusCode[self::STATUS_CODE_TYPE_GONE]; + $statusCode = $this->defaultStatusCode[self::STATUS_CODE_TYPE_GONE]; $this->redirectStorage->addRedirect($oldUriPath, '', $statusCode, $hosts); return true; @@ -208,7 +231,7 @@ protected function createRedirectForRemovedTarget(string $oldUriPath, SiteNodeNa } /** - * Check if the current node type is restricted by Settings + * Check if the current node type is restricted by NodeType */ protected function isRestrictedByNodeType(NodeType $nodeType): bool { @@ -222,7 +245,7 @@ protected function isRestrictedByNodeType(NodeType $nodeType): bool } if ($nodeType->isOfType($disabledNodeType)) { - $this->logger->debug(vsprintf('Redirect skipped based on the current node type (%s) for a node because is of type %s', [ + $this->logger?->debug(vsprintf('Redirect skipped based on the current node type (%s) for a node because is of type %s', [ $nodeType->name->value, $disabledNodeType ])); @@ -235,7 +258,7 @@ protected function isRestrictedByNodeType(NodeType $nodeType): bool } /** - * Check if the old URI is restricted by Settings + * Check if the old URI is restricted by old uri */ protected function isRestrictedByOldUri(string $oldUriPath): bool { @@ -250,7 +273,7 @@ protected function isRestrictedByOldUri(string $oldUriPath): bool $uriPrefix = rtrim($uriPrefix, '/') . '/'; $oldUriPath = rtrim($oldUriPath, '/') . '/'; if (mb_strpos($oldUriPath, $uriPrefix) === 0) { - $this->logger->debug(vsprintf('Redirect skipped based on the old URI (%s) because prefix matches %s', [ + $this->logger?->debug(vsprintf('Redirect skipped based on the old URI (%s) because prefix matches %s', [ $oldUriPath, $uriPrefix ])); @@ -264,6 +287,7 @@ protected function isRestrictedByOldUri(string $oldUriPath): bool /** * Collects all hostnames from the Domain entries attached to the current site. + * @return array> */ protected function getHostnames(SiteNodeName $siteNodeName): array { diff --git a/Configuration/Testing/Behat/Settings.Mvc.yaml b/Configuration/Testing/Behat/Settings.Mvc.yaml new file mode 100644 index 0000000..5f6eef5 --- /dev/null +++ b/Configuration/Testing/Behat/Settings.Mvc.yaml @@ -0,0 +1,8 @@ +Neos: + Flow: + mvc: + routes: + 'Neos.Neos': + variables: + # set explicitly for testing as it may be overriden by other packages (e.g. Neos.Demo) + 'defaultUriSuffix': '.html' \ No newline at end of file diff --git a/Tests/Behavior/Features/MultipleDimensions.feature b/Tests/Behavior/Features/MultipleDimensions.feature new file mode 100644 index 0000000..8c5284a --- /dev/null +++ b/Tests/Behavior/Features/MultipleDimensions.feature @@ -0,0 +1,380 @@ +@fixtures @contentrepository +Feature: Basic redirect handling with document nodes in multiple dimensions + + Background: + Given I have the following content dimensions: + | Identifier | Values | Generalizations | + | language | de, en, gsw | gsw->de, en | + | market | DE, CH | CH->DE | + And I am user identified by "initiating-user-identifier" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | newContentStreamId | "cs-identifier" | + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "site-root" | + | nodeTypeName | "Neos.Neos:Sites" | + | contentStreamId | "cs-identifier" | + And the graph projection is fully up to date + + # site-root + # behat + # company + # service + # about + # imprint + # buy + # mail + And I am in content stream "cs-identifier" and dimension space point {} + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | parentNodeAggregateId | nodeTypeName | initialPropertyValues | originDimensionSpacePoint | nodeName | + | behat | site-root | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "home"} | {"language": "en", "market": "DE"} | node1 | + | company | behat | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "company"} | {"language": "en", "market": "DE"} | node2 | + | service | company | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "service"} | {"language": "en", "market": "DE"} | node3 | + | about | company | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "about"} | {"language": "en", "market": "DE"} | node4 | + | imprint | behat | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "imprint"} | {"language": "en", "market": "DE"} | node5 | + | buy | behat | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "buy", "title": "Buy"} | {"language": "en", "market": "DE"} | node6 | + | mail | behat | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "mail"} | {"language": "en", "market": "DE"} | node7 | + | restricted-by-nodetype | behat | Neos.Neos:Test.Redirect.RestrictedPage | {"uriPathSegment": "restricted-by-nodetype"} | {"language": "en", "market": "DE"} | node8 | + + And A site exists for node name "node1" + And the sites configuration is: + """ + Neos: + Neos: + sites: + '*': + contentRepository: default + contentDimensions: + defaultDimensionSpacePoint: + language: en + resolver: + factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\UriPathResolverFactory + options: + segments: + - + dimensionIdentifier: language + dimensionValueMapping: + de: '' + en: en + gsw: ch + - + dimensionIdentifier: market + dimensionValueMapping: + DE: DE + CH: CH + """ + And The documenturipath projection is up to date + And the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "behat" | + | sourceOrigin | {"language": "en", "market": "DE"} | + | targetOrigin | {"language": "de", "market": "DE"} | + And the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "company" | + | sourceOrigin | {"language":"en", "market": "DE"} | + | targetOrigin | {"language":"de", "market": "DE"} | + And the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "service" | + | sourceOrigin | {"language":"en", "market": "DE"} | + | targetOrigin | {"language":"de", "market": "DE"} | + And the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "imprint" | + | sourceOrigin | {"language":"en", "market": "DE"} | + | targetOrigin | {"language":"de", "market": "DE"} | + And the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "imprint" | + | sourceOrigin | {"language":"en", "market": "DE"} | + | targetOrigin | {"language":"gsw", "market": "CH"} | + And the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "company" | + | sourceOrigin | {"language":"en", "market": "DE"} | + | targetOrigin | {"language":"en", "market": "CH"} | + And the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "service" | + | sourceOrigin | {"language":"en", "market": "DE"} | + | targetOrigin | {"language":"en", "market": "CH"} | + + @fixtures + Scenario: Move a node down into different node and a redirect will be created + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "imprint" | + | dimensionSpacePoint | {"language": "en", "market": "DE"} | + | newParentNodeAggregateId | "company" | + | newSucceedingSiblingNodeAggregateId | null | + And The documenturipath projection is up to date + Then I should have a redirect with sourceUri "DE/imprint.html" and targetUri "DE/company/imprint.html" + Then I should have a redirect with sourceUri "CH/imprint.html" and targetUri "CH/company/imprint.html" + Then I should have a redirect with sourceUri "en_DE/imprint.html" and targetUri "en_DE/company/imprint.html" + Then I should have a redirect with sourceUri "en_CH/imprint.html" and targetUri "en_CH/company/imprint.html" + Then I should have a redirect with sourceUri "ch_DE/imprint.html" and targetUri "ch_DE/company/imprint.html" + Then I should have a redirect with sourceUri "ch_CH/imprint.html" and targetUri "ch_CH/company/imprint.html" + + @fixtures + Scenario: Move a node up into different node and a redirect will be created + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "service" | + | dimensionSpacePoint | {"language": "en", "market": "DE"} | + | newParentNodeAggregateId | "behat" | + | newSucceedingSiblingNodeAggregateId | null | + And The documenturipath projection is up to date + + Then I should have a redirect with sourceUri "DE/company/service.html" and targetUri "DE/service.html" + And I should have a redirect with sourceUri "CH/company/service.html" and targetUri "CH/service.html" + And I should have a redirect with sourceUri "en_DE/company/service.html" and targetUri "en_DE/service.html" + And I should have a redirect with sourceUri "en_CH/company/service.html" and targetUri "en_CH/service.html" + And I should have a redirect with sourceUri "ch_DE/company/service.html" and targetUri "ch_DE/service.html" + And I should have a redirect with sourceUri "ch_CH/company/service.html" and targetUri "ch_CH/service.html" + + @fixtures + Scenario: Change the the `uriPathSegment` and a redirect will be created + When the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "company" | + | originDimensionSpacePoint | {"language": "en", "market": "CH"} | + | propertyValues | {"uriPathSegment": "evil-company"} | + And The documenturipath projection is up to date + + Then I should have a redirect with sourceUri "en_CH/company.html" and targetUri "en_CH/evil-company.html" + And I should have a redirect with sourceUri "en_CH/company/service.html" and targetUri "en_CH/evil-company/service.html" + And I should have a redirect with sourceUri "en_CH/company/about.html" and targetUri "en_CH/evil-company/about.html" + + And I should have no redirect with sourceUri "CH/company.html" + And I should have no redirect with sourceUri "DE/company.html" + And I should have no redirect with sourceUri "ch_CH/company.html" + And I should have no redirect with sourceUri "ch_DE/company.html" + + @fixtures + Scenario: Change the the `uriPathSegment` multiple times and multiple redirects will be created + When the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "company" | + | originDimensionSpacePoint | {"language": "en", "market": "CH"} | + | propertyValues | {"uriPathSegment": "evil-corp"} | + And the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "company" | + | originDimensionSpacePoint | {"language": "en", "market": "CH"} | + | propertyValues | {"uriPathSegment": "more-evil-corp"} | + And The documenturipath projection is up to date + + Then I should have a redirect with sourceUri "en_CH/company.html" and targetUri "en_CH/more-evil-corp.html" + And I should have a redirect with sourceUri "en_CH/company/service.html" and targetUri "en_CH/more-evil-corp/service.html" + And I should have a redirect with sourceUri "en_CH/company/about.html" and targetUri "en_CH/more-evil-corp/about.html" + And I should have a redirect with sourceUri "en_CH/evil-corp.html" and targetUri "en_CH/more-evil-corp.html" + And I should have a redirect with sourceUri "en_CH/evil-corp/service.html" and targetUri "en_CH/more-evil-corp/service.html" + And I should have a redirect with sourceUri "en_CH/evil-corp/about.html" and targetUri "en_CH/more-evil-corp/about.html" + + And I should have no redirect with sourceUri "CH/company.html" + And I should have no redirect with sourceUri "DE/company.html" + And I should have no redirect with sourceUri "en_DE/company.html" + + @fixtures + Scenario: Retarget an existing redirect when the source URI matches the source URI of the new redirect + When I have the following redirects: + | sourceuripath | targeturipath | + | en_CH/company.html | en_CH/company-old.html | + And the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "company" | + | originDimensionSpacePoint | {"language": "en", "market": "CH"} | + | propertyValues | {"uriPathSegment": "my-company"} | + And The documenturipath projection is up to date + Then I should have a redirect with sourceUri "en_CH/company.html" and targetUri "en_CH/my-company.html" + And I should have a redirect with sourceUri "en_CH/company/service.html" and targetUri "en_CH/my-company/service.html" + And I should have a redirect with sourceUri "en_CH/company/about.html" and targetUri "en_CH/my-company/about.html" + + And I should have no redirect with sourceUri "en_CH/company.html" and targetUri "en_CH/company-old.html" + + @fixtures + Scenario: No redirect should be created for an existing node if any non URI related property changes + When the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "buy" | + | originDimensionSpacePoint | {"language": "en", "market": "DE"} | + | propertyValues | {"title": "my-buy"} | + And The documenturipath projection is up to date + Then I should have no redirect with sourceUri "en_DE/buy.html" + + @fixtures + Scenario: No redirect should be created for an restricted node by nodetype + When the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "restricted-by-nodetype" | + | originDimensionSpacePoint | {"language": "en", "market": "DE"} | + | propertyValues | {"uriPathSegment": "restricted-by-nodetype-new"} | + And The documenturipath projection is up to date + Then I should have no redirect with sourceUri "en/restricted.html" + +# @fixtures +# Scenario: Redirects should be created for a hidden node +# When the command DisableNodeAggregate is executed with payload: +# | Key | Value | +# | contentStreamId | "cs-identifier" | +# | nodeAggregateId | "mail" | +# | coveredDimensionSpacePoint | {"language": "en", "market": "DE"} | +# | nodeVariantSelectionStrategy | "allVariants" | +# And the graph projection is fully up to date +# When the command SetNodeProperties is executed with payload: +# | Key | Value | +# | contentStreamId | "cs-identifier" | +# | nodeAggregateId | "mail" | +# | originDimensionSpacePoint | {"language": "en", "market": "DE"} | +# | propertyValues | {"uriPathSegment": "not-mail"} | +# And The documenturipath projection is up to date +# Then I should have a redirect with sourceUri "en_DE/mail.html" and targetUri "en_DE/not-mail.html" +# Then I should have a redirect with sourceUri "en_CH/mail.html" and targetUri "en_CH/not-mail.html" + + @fixtures + Scenario: Change the the `uriPathSegment` and a redirect will be created also for fallback + When the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "company" | + | originDimensionSpacePoint | {"language": "de", "market": "DE"} | + | propertyValues | {"uriPathSegment": "evil-company"} | + And The documenturipath projection is up to date + + Then I should have a redirect with sourceUri "DE/company.html" and targetUri "DE/evil-company.html" + And I should have a redirect with sourceUri "DE/company/service.html" and targetUri "DE/evil-company/service.html" + And I should have a redirect with sourceUri "CH/company.html" and targetUri "CH/evil-company.html" + And I should have a redirect with sourceUri "CH/company/service.html" and targetUri "CH/evil-company/service.html" + And I should have a redirect with sourceUri "ch_DE/company.html" and targetUri "ch_DE/evil-company.html" + And I should have a redirect with sourceUri "ch_DE/company/service.html" and targetUri "ch_DE/evil-company/service.html" + And I should have a redirect with sourceUri "ch_CH/company.html" and targetUri "ch_CH/evil-company.html" + And I should have a redirect with sourceUri "ch_CH/company/service.html" and targetUri "ch_CH/evil-company/service.html" + + And I should have no redirect with sourceUri "en_DE/company.html" + And I should have no redirect with sourceUri "en_DE/company/service.html" + And I should have no redirect with sourceUri "en_CH/company.html" + And I should have no redirect with sourceUri "en_CH/company/service.html" + + @fixtures + Scenario: A removed node should lead to a GONE response with empty target uri (allSpecializations) + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "company" | + | coveredDimensionSpacePoint | {"language": "en", "market": "CH"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + And The documenturipath projection is up to date + + Then I should have a redirect with sourceUri "en_CH/company.html" and statusCode "410" + And I should have a redirect with sourceUri "en_CH/company.html" and targetUri "" + And I should have a redirect with sourceUri "en_CH/company/service.html" and statusCode "410" + And I should have a redirect with sourceUri "en_CH/company/service.html" and targetUri "" + And I should have a redirect with sourceUri "en_CH/company/about.html" and statusCode "410" + And I should have a redirect with sourceUri "en_CH/company/about.html" and targetUri "" + + And I should have no redirect with sourceUri "DE/company.html" + And I should have no redirect with sourceUri "DE/company/service.html" + And I should have no redirect with sourceUri "DE/company/about.html" + And I should have no redirect with sourceUri "CH/company.html" + And I should have no redirect with sourceUri "CH/company/service.html" + And I should have no redirect with sourceUri "CH/company/about.html" + And I should have no redirect with sourceUri "ch_DE/company.html" + And I should have no redirect with sourceUri "ch_DE/company/service.html" + And I should have no redirect with sourceUri "ch_DE/company/about.html" + And I should have no redirect with sourceUri "en_DE/company.html" + And I should have no redirect with sourceUri "en_DE/company/service.html" + And I should have no redirect with sourceUri "en_DE/company/about.html" + + @fixtures + Scenario: A removed node should lead to a GONE response with empty target uri (allVariants) + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "company" | + | coveredDimensionSpacePoint | {"language": "de", "market": "CH"} | + | nodeVariantSelectionStrategy | "allVariants" | + And the graph projection is fully up to date + And The documenturipath projection is up to date + + Then I should have a redirect with sourceUri "DE/company.html" and statusCode "410" + And I should have a redirect with sourceUri "DE/company.html" and targetUri "" + And I should have a redirect with sourceUri "DE/company/service.html" and statusCode "410" + And I should have a redirect with sourceUri "DE/company/service.html" and targetUri "" + + And I should have a redirect with sourceUri "CH/company.html" and statusCode "410" + And I should have a redirect with sourceUri "CH/company.html" and targetUri "" + And I should have a redirect with sourceUri "CH/company/service.html" and statusCode "410" + And I should have a redirect with sourceUri "CH/company/service.html" and targetUri "" + + And I should have a redirect with sourceUri "ch_CH/company.html" and statusCode "410" + And I should have a redirect with sourceUri "ch_CH/company.html" and targetUri "" + And I should have a redirect with sourceUri "ch_CH/company/service.html" and statusCode "410" + And I should have a redirect with sourceUri "ch_CH/company/service.html" and targetUri "" + + And I should have a redirect with sourceUri "ch_DE/company.html" and statusCode "410" + And I should have a redirect with sourceUri "ch_DE/company.html" and targetUri "" + And I should have a redirect with sourceUri "ch_DE/company/service.html" and statusCode "410" + And I should have a redirect with sourceUri "ch_DE/company/service.html" and targetUri "" + + And I should have a redirect with sourceUri "en_CH/company.html" and statusCode "410" + And I should have a redirect with sourceUri "en_CH/company.html" and targetUri "" + And I should have a redirect with sourceUri "en_CH/company/service.html" and statusCode "410" + And I should have a redirect with sourceUri "en_CH/company/service.html" and targetUri "" + And I should have a redirect with sourceUri "en_CH/company/about.html" and statusCode "410" + And I should have a redirect with sourceUri "en_CH/company/about.html" and targetUri "" + + And I should have a redirect with sourceUri "en_DE/company.html" and statusCode "410" + And I should have a redirect with sourceUri "en_DE/company.html" and targetUri "" + And I should have a redirect with sourceUri "en_DE/company/service.html" and statusCode "410" + And I should have a redirect with sourceUri "en_DE/company/service.html" and targetUri "" + And I should have a redirect with sourceUri "en_DE/company/about.html" and statusCode "410" + And I should have a redirect with sourceUri "en_DE/company/about.html" and targetUri "" + + + @fixtures + Scenario: A removed node should lead to a GONE response with empty target uri also for fallback (allSpecializations) + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "company" | + | nodeVariantSelectionStrategy | "allSpecializations" | + | coveredDimensionSpacePoint | {"language": "de", "market" : "DE"} | + And the graph projection is fully up to date + And The documenturipath projection is up to date + + Then I should have a redirect with sourceUri "DE/company.html" and statusCode "410" + And I should have a redirect with sourceUri "DE/company.html" and targetUri "" + And I should have a redirect with sourceUri "DE/company/service.html" and statusCode "410" + And I should have a redirect with sourceUri "DE/company/service.html" and targetUri "" + + And I should have a redirect with sourceUri "CH/company.html" and statusCode "410" + And I should have a redirect with sourceUri "CH/company.html" and targetUri "" + And I should have a redirect with sourceUri "CH/company/service.html" and statusCode "410" + And I should have a redirect with sourceUri "CH/company/service.html" and targetUri "" + + And I should have a redirect with sourceUri "ch_DE/company.html" and statusCode "410" + And I should have a redirect with sourceUri "ch_DE/company.html" and targetUri "" + And I should have a redirect with sourceUri "ch_DE/company/service.html" and statusCode "410" + And I should have a redirect with sourceUri "ch_DE/company/service.html" and targetUri "" + + And I should have a redirect with sourceUri "ch_CH/company.html" and statusCode "410" + And I should have a redirect with sourceUri "ch_CH/company.html" and targetUri "" + And I should have a redirect with sourceUri "ch_CH/company/service.html" and statusCode "410" + And I should have a redirect with sourceUri "ch_CH/company/service.html" and targetUri "" + + And I should have no redirect with sourceUri "en_DE/company.html" + And I should have no redirect with sourceUri "en_DE/company/service.html" + And I should have no redirect with sourceUri "en_CH/company.html" + And I should have no redirect with sourceUri "en_CH/company/service.html" \ No newline at end of file diff --git a/Tests/Behavior/Features/OneDimension.feature b/Tests/Behavior/Features/OneDimension.feature index 1b353ea..776294f 100644 --- a/Tests/Behavior/Features/OneDimension.feature +++ b/Tests/Behavior/Features/OneDimension.feature @@ -91,20 +91,9 @@ Feature: Basic redirect handling with document nodes in one dimension | newParentNodeAggregateId | "company" | | newSucceedingSiblingNodeAggregateId | null | And The documenturipath projection is up to date - Then I should have a redirect with sourceUri "en/imprint" and targetUri "en/company/imprint" - - @fixtures - Scenario: Move a node down into different node and a redirect will be created also for fallback - When the command MoveNodeAggregate is executed with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "imprint" | - | dimensionSpacePoint | {"language": "de"} | - | newParentNodeAggregateId | "company" | - | newSucceedingSiblingNodeAggregateId | null | - And The documenturipath projection is up to date - Then I should have a redirect with sourceUri "imprint" and targetUri "company/imprint" - Then I should have a redirect with sourceUri "ch/imprint" and targetUri "ch/company/imprint" + Then I should have a redirect with sourceUri "en/imprint.html" and targetUri "en/company/imprint.html" + And I should have a redirect with sourceUri "imprint.html" and targetUri "company/imprint.html" + And I should have a redirect with sourceUri "ch/imprint.html" and targetUri "ch/company/imprint.html" @fixtures Scenario: Move a node up into different node and a redirect will be created @@ -116,20 +105,9 @@ Feature: Basic redirect handling with document nodes in one dimension | newParentNodeAggregateId | "behat" | | newSucceedingSiblingNodeAggregateId | null | And The documenturipath projection is up to date - Then I should have a redirect with sourceUri "en/company/service" and targetUri "en/service" - - @fixtures - Scenario: Move a node up into different node and a redirect will be created also for fallback - When the command MoveNodeAggregate is executed with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "service" | - | dimensionSpacePoint | {"language": "de"} | - | newParentNodeAggregateId | "behat" | - | newSucceedingSiblingNodeAggregateId | null | - And The documenturipath projection is up to date - Then I should have a redirect with sourceUri "company/service" and targetUri "service" - And I should have a redirect with sourceUri "ch/company/service" and targetUri "ch/service" + Then I should have a redirect with sourceUri "en/company/service.html" and targetUri "en/service.html" + And I should have a redirect with sourceUri "company/service.html" and targetUri "service.html" + And I should have a redirect with sourceUri "ch/company/service.html" and targetUri "ch/service.html" @fixtures Scenario: Change the the `uriPathSegment` and a redirect will be created @@ -141,8 +119,18 @@ Feature: Basic redirect handling with document nodes in one dimension | propertyValues | {"uriPathSegment": "evil-corp"} | And The documenturipath projection is up to date - Then I should have a redirect with sourceUri "en/company" and targetUri "en/evil-corp" - And I should have a redirect with sourceUri "en/company/service" and targetUri "en/evil-corp/service" + Then I should have a redirect with sourceUri "en/company.html" and targetUri "en/evil-corp.html" + And I should have a redirect with sourceUri "en/company/about.html" and targetUri "en/evil-corp/about.html" + And I should have a redirect with sourceUri "en/company/service.html" and targetUri "en/evil-corp/service.html" + + And I should have no redirect with sourceUri "company.html" + And I should have no redirect with sourceUri "company/about.html" + And I should have no redirect with sourceUri "company/service.html" + + And I should have no redirect with sourceUri "ch/company.html" + And I should have no redirect with sourceUri "ch/company/about.html" + And I should have no redirect with sourceUri "ch/company/service.html" + @fixtures Scenario: Change the the `uriPathSegment` multiple times and multiple redirects will be created @@ -160,10 +148,10 @@ Feature: Basic redirect handling with document nodes in one dimension | propertyValues | {"uriPathSegment": "more-evil-corp"} | And The documenturipath projection is up to date - Then I should have a redirect with sourceUri "en/company" and targetUri "en/more-evil-corp" - And I should have a redirect with sourceUri "en/company/service" and targetUri "en/more-evil-corp/service" - And I should have a redirect with sourceUri "en/evil-corp" and targetUri "en/more-evil-corp" - And I should have a redirect with sourceUri "en/evil-corp/service" and targetUri "en/more-evil-corp/service" + Then I should have a redirect with sourceUri "en/company.html" and targetUri "en/more-evil-corp.html" + And I should have a redirect with sourceUri "en/company/service.html" and targetUri "en/more-evil-corp/service.html" + And I should have a redirect with sourceUri "en/evil-corp.html" and targetUri "en/more-evil-corp.html" + And I should have a redirect with sourceUri "en/evil-corp/service.html" and targetUri "en/more-evil-corp/service.html" @fixtures @@ -178,9 +166,9 @@ Feature: Basic redirect handling with document nodes in one dimension | originDimensionSpacePoint | {"language": "en"} | | propertyValues | {"uriPathSegment": "my-company"} | And The documenturipath projection is up to date - Then I should have a redirect with sourceUri "en/company" and targetUri "en/my-company" - And I should have no redirect with sourceUri "en/company" and targetUri "en/company-old" - And I should have a redirect with sourceUri "en/company/service" and targetUri "en/my-company/service" + Then I should have a redirect with sourceUri "en/company.html" and targetUri "en/my-company.html" + And I should have no redirect with sourceUri "en/company.html" and targetUri "en/company-old.html" + And I should have a redirect with sourceUri "en/company/service.html" and targetUri "en/my-company/service.html" @fixtures Scenario: No redirect should be created for an existing node if any non URI related property changes @@ -191,7 +179,7 @@ Feature: Basic redirect handling with document nodes in one dimension | originDimensionSpacePoint | {"language": "en"} | | propertyValues | {"title": "my-buy"} | And The documenturipath projection is up to date - Then I should have no redirect with sourceUri "en/buy" + Then I should have no redirect with sourceUri "en/buy.html" @fixtures Scenario: No redirect should be created for an restricted node by nodetype @@ -202,25 +190,25 @@ Feature: Basic redirect handling with document nodes in one dimension | originDimensionSpacePoint | {"language": "en"} | | propertyValues | {"uriPathSegment": "restricted-by-nodetype-new"} | And The documenturipath projection is up to date - Then I should have no redirect with sourceUri "en/restricted" + Then I should have no redirect with sourceUri "en/restricted.html" - @fixtures - Scenario: Redirects should be created for a hidden node - When the command DisableNodeAggregate is executed with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "mail" | - | coveredDimensionSpacePoint | {"language": "en"} | - | nodeVariantSelectionStrategy | "allVariants" | - And the graph projection is fully up to date - When the command SetNodeProperties is executed with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "mail" | - | originDimensionSpacePoint | {"language": "en"} | - | propertyValues | {"uriPathSegment": "not-mail"} | - And The documenturipath projection is up to date - Then I should have a redirect with sourceUri "en/mail" and targetUri "en/not-mail" +# @fixtures +# Scenario: Redirects should be created for a hidden node +# When the command DisableNodeAggregate is executed with payload: +# | Key | Value | +# | contentStreamId | "cs-identifier" | +# | nodeAggregateId | "mail" | +# | coveredDimensionSpacePoint | {"language": "en"} | +# | nodeVariantSelectionStrategy | "allVariants" | +# And the graph projection is fully up to date +# When the command SetNodeProperties is executed with payload: +# | Key | Value | +# | contentStreamId | "cs-identifier" | +# | nodeAggregateId | "mail" | +# | originDimensionSpacePoint | {"language": "en"} | +# | propertyValues | {"uriPathSegment": "not-mail"} | +# And The documenturipath projection is up to date +# Then I should have a redirect with sourceUri "en/mail.html" and targetUri "en/not-mail.html" @fixtures Scenario: Change the the `uriPathSegment` and a redirect will be created also for fallback @@ -233,45 +221,79 @@ Feature: Basic redirect handling with document nodes in one dimension | propertyValues | {"uriPathSegment": "unternehmen"} | And The documenturipath projection is up to date - Then I should have a redirect with sourceUri "company" and targetUri "unternehmen" - And I should have a redirect with sourceUri "company/service" and targetUri "unternehmen/service" - And I should have a redirect with sourceUri "ch/company" and targetUri "ch/unternehmen" - And I should have a redirect with sourceUri "ch/company/service" and targetUri "ch/unternehmen/service" + Then I should have a redirect with sourceUri "company.html" and targetUri "unternehmen.html" + And I should have a redirect with sourceUri "company/service.html" and targetUri "unternehmen/service.html" + And I should have a redirect with sourceUri "ch/company.html" and targetUri "ch/unternehmen.html" + And I should have a redirect with sourceUri "ch/company/service.html" and targetUri "ch/unternehmen/service.html" + + + @fixtures + Scenario: A removed node should lead to a GONE response with empty target uri (allSpecializations) + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "company" | + | coveredDimensionSpacePoint | {"language": "en"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + And The documenturipath projection is up to date + + Then I should have a redirect with sourceUri "en/company.html" and statusCode "410" + And I should have a redirect with sourceUri "en/company.html" and targetUri "" + And I should have a redirect with sourceUri "en/company/service.html" and statusCode "410" + And I should have a redirect with sourceUri "en/company/service.html" and targetUri "" + And I should have no redirect with sourceUri "company.html" + And I should have no redirect with sourceUri "company/service.html" + And I should have no redirect with sourceUri "ch/company.html" + And I should have no redirect with sourceUri "ch/company/service.html" @fixtures - Scenario: A removed node should lead to a GONE response with empty target uri - Given the event NodeAggregateWasRemoved was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "company" | - | affectedOccupiedDimensionSpacePoints | [{"language": "en"}] | - | affectedCoveredDimensionSpacePoints | [{"language": "en"}] | + Scenario: A removed node should lead to a GONE response with empty target uri (allVariants) + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "company" | + | coveredDimensionSpacePoint | {"language": "de"} | + | nodeVariantSelectionStrategy | "allVariants" | And the graph projection is fully up to date And The documenturipath projection is up to date - Then I should have a redirect with sourceUri "en/company" and statusCode "410" - And I should have a redirect with sourceUri "en/company" and targetUri "" - And I should have a redirect with sourceUri "en/company/service" and statusCode "410" - And I should have a redirect with sourceUri "en/company/service" and targetUri "" + Then I should have a redirect with sourceUri "en/company.html" and statusCode "410" + And I should have a redirect with sourceUri "en/company.html" and targetUri "" + And I should have a redirect with sourceUri "en/company/service.html" and statusCode "410" + And I should have a redirect with sourceUri "en/company/service.html" and targetUri "" + + And I should have a redirect with sourceUri "company.html" and statusCode "410" + And I should have a redirect with sourceUri "company.html" and targetUri "" + And I should have a redirect with sourceUri "company/service.html" and statusCode "410" + And I should have a redirect with sourceUri "company/service.html" and targetUri "" + + And I should have a redirect with sourceUri "ch/company.html" and statusCode "410" + And I should have a redirect with sourceUri "ch/company.html" and targetUri "" + And I should have a redirect with sourceUri "ch/company/service.html" and statusCode "410" + And I should have a redirect with sourceUri "ch/company/service.html" and targetUri "" @fixtures - Scenario: A removed node should lead to a GONE response with empty target uri also for fallback - Given the event NodeAggregateWasRemoved was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "company" | - | affectedOccupiedDimensionSpacePoints | [{"language": "de"}] | - | affectedCoveredDimensionSpacePoints | [{"language": "de"}, {"language": "gsw"}] | + Scenario: A removed node should lead to a GONE response with empty target uri also for fallback (allSpecializations) + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "company" | + | nodeVariantSelectionStrategy | "allSpecializations" | + | coveredDimensionSpacePoint | {"language": "de"} | And the graph projection is fully up to date And The documenturipath projection is up to date - Then I should have a redirect with sourceUri "company" and statusCode "410" - And I should have a redirect with sourceUri "company" and targetUri "" - And I should have a redirect with sourceUri "company/service" and statusCode "410" - And I should have a redirect with sourceUri "company/service" and targetUri "" + Then I should have a redirect with sourceUri "company.html" and statusCode "410" + And I should have a redirect with sourceUri "company.html" and targetUri "" + And I should have a redirect with sourceUri "company/service.html" and statusCode "410" + And I should have a redirect with sourceUri "company/service.html" and targetUri "" + + And I should have a redirect with sourceUri "ch/company.html" and statusCode "410" + And I should have a redirect with sourceUri "ch/company.html" and targetUri "" + And I should have a redirect with sourceUri "ch/company/service.html" and statusCode "410" + And I should have a redirect with sourceUri "ch/company/service.html" and targetUri "" - And I should have a redirect with sourceUri "ch/company" and statusCode "410" - And I should have a redirect with sourceUri "ch/company" and targetUri "" - And I should have a redirect with sourceUri "ch/company/service" and statusCode "410" - And I should have a redirect with sourceUri "ch/company/service" and targetUri "" \ No newline at end of file + And I should have no redirect with sourceUri "en/company.html" + And I should have no redirect with sourceUri "en/company/service.html" \ No newline at end of file diff --git a/Tests/Behavior/Features/WithoutDimensions.feature b/Tests/Behavior/Features/WithoutDimensions.feature index 06c894c..bcef41b 100755 --- a/Tests/Behavior/Features/WithoutDimensions.feature +++ b/Tests/Behavior/Features/WithoutDimensions.feature @@ -9,10 +9,10 @@ Feature: Basic redirect handling with document nodes without dimensions | workspaceName | "live" | | newContentStreamId | "cs-identifier" | And the command CreateRootNodeAggregateWithNode is executed with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "site-root" | - | nodeTypeName | "Neos.Neos:Sites" | + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "site-root" | + | nodeTypeName | "Neos.Neos:Sites" | And the graph projection is fully up to date # site-root @@ -58,7 +58,7 @@ Feature: Basic redirect handling with document nodes without dimensions | newParentNodeAggregateId | "company" | | newSucceedingSiblingNodeAggregateId | null | And The documenturipath projection is up to date - Then I should have a redirect with sourceUri "imprint" and targetUri "company/imprint" + Then I should have a redirect with sourceUri "imprint.html" and targetUri "company/imprint.html" Scenario: Move a node up into different node and a redirect will be created When the command MoveNodeAggregate is executed with payload: @@ -69,7 +69,7 @@ Feature: Basic redirect handling with document nodes without dimensions | newParentNodeAggregateId | "behat" | | newSucceedingSiblingNodeAggregateId | null | And The documenturipath projection is up to date - Then I should have a redirect with sourceUri "company/service" and targetUri "service" + Then I should have a redirect with sourceUri "company/service.html" and targetUri "service.html" @fixtures Scenario: Change the the `uriPathSegment` and a redirect will be created @@ -80,8 +80,8 @@ Feature: Basic redirect handling with document nodes without dimensions | originDimensionSpacePoint | {} | | propertyValues | {"uriPathSegment": "evil-corp"} | And The documenturipath projection is up to date - Then I should have a redirect with sourceUri "company" and targetUri "evil-corp" - And I should have a redirect with sourceUri "company/service" and targetUri "evil-corp/service" + Then I should have a redirect with sourceUri "company.html" and targetUri "evil-corp.html" + And I should have a redirect with sourceUri "company/service.html" and targetUri "evil-corp/service.html" @fixtures Scenario: Change the the `uriPathSegment` multiple times and multiple redirects will be created @@ -99,10 +99,10 @@ Feature: Basic redirect handling with document nodes without dimensions | propertyValues | {"uriPathSegment": "more-evil-corp"} | And The documenturipath projection is up to date - Then I should have a redirect with sourceUri "company" and targetUri "more-evil-corp" - And I should have a redirect with sourceUri "company/service" and targetUri "more-evil-corp/service" - And I should have a redirect with sourceUri "evil-corp" and targetUri "more-evil-corp" - And I should have a redirect with sourceUri "evil-corp/service" and targetUri "more-evil-corp/service" + Then I should have a redirect with sourceUri "company.html" and targetUri "more-evil-corp.html" + And I should have a redirect with sourceUri "company/service.html" and targetUri "more-evil-corp/service.html" + And I should have a redirect with sourceUri "evil-corp.html" and targetUri "more-evil-corp.html" + And I should have a redirect with sourceUri "evil-corp/service.html" and targetUri "more-evil-corp/service.html" @fixtures @@ -117,9 +117,9 @@ Feature: Basic redirect handling with document nodes without dimensions | originDimensionSpacePoint | {} | | propertyValues | {"uriPathSegment": "my-company"} | And The documenturipath projection is up to date - Then I should have a redirect with sourceUri "company" and targetUri "my-company" - And I should have no redirect with sourceUri "company" and targetUri "company-old" - And I should have a redirect with sourceUri "company/service" and targetUri "my-company/service" + Then I should have a redirect with sourceUri "company.html" and targetUri "my-company.html" + And I should have no redirect with sourceUri "company.html" and targetUri "company-old.html" + And I should have a redirect with sourceUri "company/service.html" and targetUri "my-company/service.html" @fixtures Scenario: No redirect should be created for an existing node if any non URI related property changes @@ -130,7 +130,7 @@ Feature: Basic redirect handling with document nodes without dimensions | originDimensionSpacePoint | {} | | propertyValues | {"title": "my-buy"} | And The documenturipath projection is up to date - Then I should have no redirect with sourceUri "buy" + Then I should have no redirect with sourceUri "buy.html" @fixtures Scenario: No redirect should be created for an restricted node by nodetype @@ -141,38 +141,37 @@ Feature: Basic redirect handling with document nodes without dimensions | originDimensionSpacePoint | {} | | propertyValues | {"uriPathSegment": "restricted-by-nodetype-new"} | And The documenturipath projection is up to date - Then I should have no redirect with sourceUri "restricted" + Then I should have no redirect with sourceUri "restricted.html" + +# @fixtures +# Scenario: Redirects should be created for a hidden node +# When the command DisableNodeAggregate is executed with payload: +# | Key | Value | +# | contentStreamId | "cs-identifier" | +# | nodeAggregateId | "mail" | +# | coveredDimensionSpacePoint | {} | +# | nodeVariantSelectionStrategy | "allVariants" | +# And the graph projection is fully up to date +# When the command SetNodeProperties is executed with payload: +# | Key | Value | +# | contentStreamId | "cs-identifier" | +# | nodeAggregateId | "mail" | +# | originDimensionSpacePoint | {} | +# | propertyValues | {"uriPathSegment": "not-mail"} | +# And The documenturipath projection is up to date +# Then I should have a redirect with sourceUri "mail.html" and targetUri "not-mail.html" @fixtures - Scenario: Redirects should be created for a hidden node - When the command DisableNodeAggregate is executed with payload: + Scenario: A removed node should lead to a GONE response with empty target uri + Given the command RemoveNodeAggregate is executed with payload: | Key | Value | | contentStreamId | "cs-identifier" | - | nodeAggregateId | "mail" | - | coveredDimensionSpacePoint | {} | + | nodeAggregateId | "company" | | nodeVariantSelectionStrategy | "allVariants" | And the graph projection is fully up to date - When the command SetNodeProperties is executed with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "mail" | - | originDimensionSpacePoint | {} | - | propertyValues | {"uriPathSegment": "not-mail"} | - And The documenturipath projection is up to date - Then I should have a redirect with sourceUri "mail" and targetUri "not-mail" - - @fixtures - Scenario: A removed node should lead to a GONE response with empty target uri - Given the event NodeAggregateWasRemoved was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "company" | - | affectedOccupiedDimensionSpacePoints | [] | - | affectedCoveredDimensionSpacePoints | [[]] | - And the graph projection is fully up to date And The documenturipath projection is up to date - Then I should have a redirect with sourceUri "company" and statusCode "410" - And I should have a redirect with sourceUri "company" and targetUri "" - And I should have a redirect with sourceUri "company/service" and statusCode "410" - And I should have a redirect with sourceUri "company/service" and targetUri "" \ No newline at end of file + Then I should have a redirect with sourceUri "company.html" and statusCode "410" + And I should have a redirect with sourceUri "company.html" and targetUri "" + And I should have a redirect with sourceUri "company/service.html" and statusCode "410" + And I should have a redirect with sourceUri "company/service.html" and targetUri "" \ No newline at end of file From 74c705c3a411ffcf44e00bed1925710bf59c4830 Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Tue, 20 Jun 2023 20:25:06 +0200 Subject: [PATCH 08/32] FEATURE: Redirect NeosAdapter for new ContentRepository --- Configuration/Testing/Behat/NodeTypes.Test.Redirect.yaml | 2 +- Configuration/Testing/Behat/Settings.Mvc.yaml | 2 +- Configuration/Testing/Behat/Settings.Restictions.yaml | 1 - Tests/Behavior/Features/MultipleDimensions.feature | 2 +- Tests/Behavior/Features/OneDimension.feature | 2 +- Tests/Behavior/Features/WithoutDimensions.feature | 2 +- 6 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Configuration/Testing/Behat/NodeTypes.Test.Redirect.yaml b/Configuration/Testing/Behat/NodeTypes.Test.Redirect.yaml index 682fd71..42ac6b1 100644 --- a/Configuration/Testing/Behat/NodeTypes.Test.Redirect.yaml +++ b/Configuration/Testing/Behat/NodeTypes.Test.Redirect.yaml @@ -14,4 +14,4 @@ constraints: nodeTypes: '*': true - 'Neos.Neos:Test.Redirect.Page': true \ No newline at end of file + 'Neos.Neos:Test.Redirect.Page': true diff --git a/Configuration/Testing/Behat/Settings.Mvc.yaml b/Configuration/Testing/Behat/Settings.Mvc.yaml index 5f6eef5..4624aac 100644 --- a/Configuration/Testing/Behat/Settings.Mvc.yaml +++ b/Configuration/Testing/Behat/Settings.Mvc.yaml @@ -5,4 +5,4 @@ Neos: 'Neos.Neos': variables: # set explicitly for testing as it may be overriden by other packages (e.g. Neos.Demo) - 'defaultUriSuffix': '.html' \ No newline at end of file + 'defaultUriSuffix': '.html' diff --git a/Configuration/Testing/Behat/Settings.Restictions.yaml b/Configuration/Testing/Behat/Settings.Restictions.yaml index 742ea15..9239268 100644 --- a/Configuration/Testing/Behat/Settings.Restictions.yaml +++ b/Configuration/Testing/Behat/Settings.Restictions.yaml @@ -7,4 +7,3 @@ Neos: 'Neos.Neos:Test.Redirect.RestrictedPage': true restrictByOldUriPrefix: 'restricted-by-path': true - diff --git a/Tests/Behavior/Features/MultipleDimensions.feature b/Tests/Behavior/Features/MultipleDimensions.feature index 8c5284a..4d735dd 100644 --- a/Tests/Behavior/Features/MultipleDimensions.feature +++ b/Tests/Behavior/Features/MultipleDimensions.feature @@ -377,4 +377,4 @@ Feature: Basic redirect handling with document nodes in multiple dimensions And I should have no redirect with sourceUri "en_DE/company.html" And I should have no redirect with sourceUri "en_DE/company/service.html" And I should have no redirect with sourceUri "en_CH/company.html" - And I should have no redirect with sourceUri "en_CH/company/service.html" \ No newline at end of file + And I should have no redirect with sourceUri "en_CH/company/service.html" diff --git a/Tests/Behavior/Features/OneDimension.feature b/Tests/Behavior/Features/OneDimension.feature index 776294f..d58d63f 100644 --- a/Tests/Behavior/Features/OneDimension.feature +++ b/Tests/Behavior/Features/OneDimension.feature @@ -296,4 +296,4 @@ Feature: Basic redirect handling with document nodes in one dimension And I should have a redirect with sourceUri "ch/company/service.html" and targetUri "" And I should have no redirect with sourceUri "en/company.html" - And I should have no redirect with sourceUri "en/company/service.html" \ No newline at end of file + And I should have no redirect with sourceUri "en/company/service.html" diff --git a/Tests/Behavior/Features/WithoutDimensions.feature b/Tests/Behavior/Features/WithoutDimensions.feature index bcef41b..efc452b 100755 --- a/Tests/Behavior/Features/WithoutDimensions.feature +++ b/Tests/Behavior/Features/WithoutDimensions.feature @@ -174,4 +174,4 @@ Feature: Basic redirect handling with document nodes without dimensions Then I should have a redirect with sourceUri "company.html" and statusCode "410" And I should have a redirect with sourceUri "company.html" and targetUri "" And I should have a redirect with sourceUri "company/service.html" and statusCode "410" - And I should have a redirect with sourceUri "company/service.html" and targetUri "" \ No newline at end of file + And I should have a redirect with sourceUri "company/service.html" and targetUri "" From 256b494115c13ecdd60af15491ff87fec1dcd49b Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Tue, 20 Jun 2023 20:28:42 +0200 Subject: [PATCH 09/32] FEATURE: Redirect NeosAdapter for new ContentRepository --- Classes/CatchUpHook/DocumentUriPathProjectionHook.php | 2 +- Classes/CatchUpHook/DocumentUriPathProjectionHookFactory.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Classes/CatchUpHook/DocumentUriPathProjectionHook.php b/Classes/CatchUpHook/DocumentUriPathProjectionHook.php index 8e33b15..2828c74 100644 --- a/Classes/CatchUpHook/DocumentUriPathProjectionHook.php +++ b/Classes/CatchUpHook/DocumentUriPathProjectionHook.php @@ -260,4 +260,4 @@ private function getContentRepositoryId(): ContentRepositoryId { return $this->contentRepositoryRegistry->getContentRepositoryIdByContentRepository($this->contentRepository); } -} \ No newline at end of file +} diff --git a/Classes/CatchUpHook/DocumentUriPathProjectionHookFactory.php b/Classes/CatchUpHook/DocumentUriPathProjectionHookFactory.php index 682e915..f06c100 100644 --- a/Classes/CatchUpHook/DocumentUriPathProjectionHookFactory.php +++ b/Classes/CatchUpHook/DocumentUriPathProjectionHookFactory.php @@ -24,4 +24,4 @@ public function build(ContentRepository $contentRepository): CatchUpHookInterfac $this->redirectService ); } -} \ No newline at end of file +} From ed6ae0c1a1f270f2d04ea7b184c5c72af4a45420 Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Wed, 28 Jun 2023 00:05:49 +0200 Subject: [PATCH 10/32] FEATURE: Allow route resolving of disabled nodes --- .github/workflows/tests.yml | 83 +++++- .../Features/WithoutDimensions.feature | 34 +-- .../Service/NodeRedirectServiceTest.php | 263 ------------------ composer.json | 1 + 4 files changed, 86 insertions(+), 295 deletions(-) delete mode 100644 Tests/Functional/Service/NodeRedirectServiceTest.php diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ac0e366..1387129 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,19 +9,40 @@ on: jobs: build: env: - FLOW_TARGET_VERSION: 6.3 - FLOW_CONTEXT: Testing - FLOW_FOLDER: ../flow-base-distribution + FLOW_CONTEXT: Testing/Behat + NEOS_TARGET_VERSION: 9.0 + NEOS_BASE_FOLDER: neos-base-distribution + PACKAGE_FOLDER: redirect-neosadapter runs-on: ubuntu-latest strategy: fail-fast: false matrix: - php-versions: ['7.4'] + php-versions: ['8.2'] + + services: + mariadb: + # see https://mariadb.com/kb/en/mariadb-server-release-dates/ + # this should be a current release, e.g. the LTS version + image: mariadb:10.8 + env: + MYSQL_USER: neos + MYSQL_PASSWORD: neos + MYSQL_DATABASE: neos_functional_testing + MYSQL_ROOT_PASSWORD: neos + ports: + - "3306:3306" + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 steps: - uses: actions/checkout@v2 + with: + path: ${{ env.PACKAGE_FOLDER }} + + - name: Set package branch name + run: echo "PACKAGE_TARGET_VERSION=${GITHUB_BASE_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_ENV + working-directory: . - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -48,21 +69,53 @@ jobs: path: ~/.composer/cache key: dependencies-composer-${{ hashFiles('composer.json') }} - - name: Prepare Flow distribution + - name: Checkout development distribution + uses: actions/checkout@v2 + with: + repository: neos/neos-development-distribution + ref: ${{ env.NEOS_TARGET_VERSION }} + path: ${{ env.NEOS_BASE_FOLDER }} + + - name: Prepare external packages for development distribution run: | - git clone https://github.com/neos/flow-base-distribution.git -b ${FLOW_TARGET_VERSION} ${FLOW_FOLDER} - cd ${FLOW_FOLDER} - composer require --no-update --no-interaction neos/redirecthandler-databasestorage:~4.0 - composer require --no-update --no-interaction neos/redirecthandler-neosadapter:~4.0 + cd ${NEOS_BASE_FOLDER} + composer config repositories.package '{ "type": "path", "url": "../${{ env.PACKAGE_FOLDER }}", "options": { "symlink": false } }' + composer require --no-update --no-interaction neos/redirecthandler:"dev-flow-9-compatibility as dev-master" + composer require --no-update --no-interaction neos/redirecthandler-databasestorage:"dev-flow-9-compatibility as dev-main" + composer require --no-update --no-interaction neos/redirecthandler-neosadapter:"dev-${PACKAGE_TARGET_VERSION}" - - name: Install distribution + - name: Composer Install run: | - cd ${FLOW_FOLDER} + cd ${NEOS_BASE_FOLDER} composer install --no-interaction --no-progress - rm -rf Packages/Application/Neos.RedirectHandler.NeosAdapter - cp -r ../redirecthandler-neosadapter Packages/Application/Neos.RedirectHandler.NeosAdapter + + - name: Setup Flow configuration + run: | + cd ${NEOS_BASE_FOLDER} + mkdir -p Configuration/Testing/Behat + rm -f Configuration/Testing/Behat/Settings.yaml + cat <> Configuration/Testing/Behat/Settings.yaml + Neos: + Flow: + persistence: + backendOptions: + host: '127.0.0.1' + driver: pdo_mysql + user: 'neos' + password: 'neos' + dbname: 'neos_functional_testing' + EOF + + - name: Install behat + run: | + cd ${NEOS_BASE_FOLDER} + cp -R Packages/Neos/Neos.ContentRepository.BehavioralTests/DistributionBehatTemplate/ Build/Behat + pushd Build/Behat/ + rm composer.lock || true + composer install + popd - name: Run Functional tests run: | - cd ${FLOW_FOLDER} - bin/phpunit --colors -c Build/BuildEssentials/PhpUnit/FunctionalTests.xml Packages/Application/Neos.RedirectHandler.NeosAdapter/Tests/Functional/* + cd ${NEOS_BASE_FOLDER} + bin/behat -c Packages/Application/Neos.RedirectHandler.NeosAdapter/Tests/Behavior/behat.yml.dist diff --git a/Tests/Behavior/Features/WithoutDimensions.feature b/Tests/Behavior/Features/WithoutDimensions.feature index efc452b..b1eaa58 100755 --- a/Tests/Behavior/Features/WithoutDimensions.feature +++ b/Tests/Behavior/Features/WithoutDimensions.feature @@ -143,23 +143,23 @@ Feature: Basic redirect handling with document nodes without dimensions And The documenturipath projection is up to date Then I should have no redirect with sourceUri "restricted.html" -# @fixtures -# Scenario: Redirects should be created for a hidden node -# When the command DisableNodeAggregate is executed with payload: -# | Key | Value | -# | contentStreamId | "cs-identifier" | -# | nodeAggregateId | "mail" | -# | coveredDimensionSpacePoint | {} | -# | nodeVariantSelectionStrategy | "allVariants" | -# And the graph projection is fully up to date -# When the command SetNodeProperties is executed with payload: -# | Key | Value | -# | contentStreamId | "cs-identifier" | -# | nodeAggregateId | "mail" | -# | originDimensionSpacePoint | {} | -# | propertyValues | {"uriPathSegment": "not-mail"} | -# And The documenturipath projection is up to date -# Then I should have a redirect with sourceUri "mail.html" and targetUri "not-mail.html" + @fixtures + Scenario: Redirects should be created for a hidden node + When the command DisableNodeAggregate is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "mail" | + | coveredDimensionSpacePoint | {} | + | nodeVariantSelectionStrategy | "allVariants" | + And the graph projection is fully up to date + When the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "mail" | + | originDimensionSpacePoint | {} | + | propertyValues | {"uriPathSegment": "not-mail"} | + And The documenturipath projection is up to date + Then I should have a redirect with sourceUri "mail.html" and targetUri "not-mail.html" @fixtures Scenario: A removed node should lead to a GONE response with empty target uri diff --git a/Tests/Functional/Service/NodeRedirectServiceTest.php b/Tests/Functional/Service/NodeRedirectServiceTest.php deleted file mode 100644 index 12433d3..0000000 --- a/Tests/Functional/Service/NodeRedirectServiceTest.php +++ /dev/null @@ -1,263 +0,0 @@ -nodeRedirectService = $this->objectManager->get(NodeRedirectService::class); - $this->publishingService = $this->objectManager->get(PublishingService::class); - $this->nodeDataRepository = $this->objectManager->get(NodeDataRepository::class); - $this->siteRepository = $this->objectManager->get(SiteRepository::class); - $this->mockRedirectStorage = $this->getMockBuilder(RedirectStorageInterface::class)->getMock(); - $this->inject($this->nodeRedirectService, 'redirectStorage', $this->mockRedirectStorage); - $this->contentContextFactory = $this->objectManager->get(ContentContextFactory::class); - $this->nodeTypeManager = $this->objectManager->get(\Neos\ContentRepository\Core\NodeType\NodeTypeManager::class); - $this->workspaceRepository = $this->objectManager->get(WorkspaceRepository::class); - $this->liveWorkspace = new \Neos\ContentRepository\Core\Projection\Workspace\Workspace('live'); - $this->userWorkspace = new \Neos\ContentRepository\Core\Projection\Workspace\Workspace('user-me', $this->liveWorkspace); - $this->workspaceRepository->add($this->liveWorkspace); - $this->workspaceRepository->add($this->userWorkspace); - $liveContext = $this->contentContextFactory->create([ - 'workspaceName' => 'live' - ]); - $this->userContext = $this->contentContextFactory->create([ - 'workspaceName' => 'user-me' - ]); - - $sites = $liveContext->getRootNode()->createNode('sites'); - $this->site = $sites->createNode('site', $this->nodeTypeManager->getNodeType('Neos.Neos:Document'), 'site'); - $site = new Site('site'); - $site->setSiteResourcesPackageKey('My.Package'); - $site->setState(Site::STATE_ONLINE); - $this->siteRepository->add($site); - } - - /** - * @return void - */ - public function tearDown(): void - { - parent::tearDown(); - $this->inject($this->contentContextFactory, 'contextInstances', array()); - } - - /** - * @test - * @throws NodeExistsException - * @throws NodeTypeNotFoundException - */ - public function createRedirectsForPublishedNodeCreatesRedirectFromPreviousUriWhenMovingDocumentDown(): void - { - $documentNodeType = $this->nodeTypeManager->getNodeType('Neos.Neos:Document'); - - $count = 0; - $this->mockRedirectStorage - ->method('addRedirect') - ->willReturnCallback(function ($sourceUri, $targetUri, $statusCode, $hosts) use (&$count) { - if ($sourceUri === '/en/document.html') { - self::assertSame('/en/outer/document.html', $targetUri); - self::assertSame(301, $statusCode); - self::assertSame([], $hosts); - $count++; - } - return []; - }); - - $outerDocument = $this->site->createNode('outer', $documentNodeType); - $outerDocument->setProperty('uriPathSegment', 'outer'); - $document = $this->site->createNode('document', $documentNodeType, 'document'); - $document->setProperty('uriPathSegment', 'document'); - - $documentToBeMoved = $this->userContext->adoptNode($document); - $documentToBeMoved->moveInto($outerDocument); - - $this->publishingService->publishNode($documentToBeMoved); - $this->persistenceManager->persistAll(); - - self::assertSame(1, $count, 'The primary redirect should have been created'); - } - - /** - * @test - * @throws NodeExistsException - * @throws NodeTypeNotFoundException - */ - public function createRedirectsForPublishedNodeCreatesRedirectFromPreviousUriWhenMovingDocumentUp(): void - { - $documentNodeType = $this->nodeTypeManager->getNodeType('Neos.Neos:Document'); - - $count = 0; - $this->mockRedirectStorage - ->method('addRedirect') - ->willReturnCallback(function ($sourceUri, $targetUri, $statusCode, $hosts) use (&$count) { - if ($sourceUri === '/en/outer/document.html') { - self::assertSame('/en/document.html', $targetUri); - self::assertSame(301, $statusCode); - self::assertSame([], $hosts); - $count++; - } - return []; - }); - - $outerDocument = $this->site->createNode('outer', $documentNodeType); - $outerDocument->setProperty('uriPathSegment', 'outer'); - $document = $outerDocument->createNode('document', $documentNodeType, 'document'); - $document->setProperty('uriPathSegment', 'document'); - - $documentToBeMoved = $this->userContext->adoptNode($document); - $documentToBeMoved->moveInto($this->site); - - $this->publishingService->publishNode($documentToBeMoved); - $this->persistenceManager->persistAll(); - - self::assertSame(1, $count, 'The primary redirect should have been created'); - } - - /** - * @test - * @throws NodeExistsException - * @throws NodeTypeNotFoundException - */ - public function createRedirectsForPublishedNodeLeavesUpwardRedirectWhenMovingDocumentDownAndUp(): void - { - $documentNodeType = $this->nodeTypeManager->getNodeType('Neos.Neos:Document'); - - $countA = 0; - $countB = 0; - $this->mockRedirectStorage - ->method('addRedirect') - ->willReturnCallback(function ($sourceUri, $targetUri, $statusCode, $hosts) use (&$countA, &$countB) { - if ($sourceUri === '/en/outer/document.html') { - self::assertSame('/en/document.html', $targetUri); - self::assertSame(301, $statusCode); - self::assertSame([], $hosts); - $countA++; - } elseif ($sourceUri === '/en/document.html') { - self::assertSame('/en/outer/document.html', $targetUri); - self::assertSame(301, $statusCode); - self::assertSame([], $hosts); - $countB++; - } - return []; - }); - - $outerDocument = $this->site->createNode('outer', $documentNodeType, 'outer'); - $outerDocument->setProperty('uriPathSegment', 'outer'); - $document = $this->site->createNode('document', $documentNodeType, 'document'); - $document->setProperty('uriPathSegment', 'document'); - - $documentToBeMoved = $this->userContext->adoptNode($document); - $documentToBeMoved->moveInto($this->userContext->getNodeByIdentifier('outer')); - $this->publishingService->publishNode($documentToBeMoved); - $this->persistenceManager->persistAll(); - - $documentToBeMoved = $this->userContext->adoptNode($outerDocument->getNode('document')); - $documentToBeMoved->moveInto($this->userContext->getNodeByIdentifier('site')); - - $this->publishingService->publishNode($documentToBeMoved); - $this->persistenceManager->persistAll(); - - self::assertSame(1, $countA, 'The primary redirect should have been created'); - self::assertSame(1, $countB, 'The secondary redirect should have been created'); - } -} diff --git a/composer.json b/composer.json index 8377f1e..660d5e8 100644 --- a/composer.json +++ b/composer.json @@ -4,6 +4,7 @@ "description": "Neos Redirect Handler", "license": "GPL-3.0-or-later", "require": { + "php": ">=8.2", "neos/redirecthandler": "~6.0 || dev-master", "neos/neos": "^9.0", "neos/flow": "^9.0" From 8c6a64765cfdcd808b026fef303a02fde89c1884 Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Wed, 28 Jun 2023 00:10:27 +0200 Subject: [PATCH 11/32] FEATURE: Redirect NeosAdapter for new ContentRepository --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1387129..7cf135b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,7 +10,7 @@ jobs: build: env: FLOW_CONTEXT: Testing/Behat - NEOS_TARGET_VERSION: 9.0 + NEOS_TARGET_VERSION: '9.0' NEOS_BASE_FOLDER: neos-base-distribution PACKAGE_FOLDER: redirect-neosadapter From ce84b69f87237b13b1bdb54a04f51d82abb8bc5c Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Wed, 28 Jun 2023 09:51:15 +0200 Subject: [PATCH 12/32] FEATURE: Redirect NeosAdapter for new ContentRepository --- .github/workflows/tests.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7cf135b..a193093 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -79,10 +79,12 @@ jobs: - name: Prepare external packages for development distribution run: | cd ${NEOS_BASE_FOLDER} - composer config repositories.package '{ "type": "path", "url": "../${{ env.PACKAGE_FOLDER }}", "options": { "symlink": false } }' composer require --no-update --no-interaction neos/redirecthandler:"dev-flow-9-compatibility as dev-master" composer require --no-update --no-interaction neos/redirecthandler-databasestorage:"dev-flow-9-compatibility as dev-main" - composer require --no-update --no-interaction neos/redirecthandler-neosadapter:"dev-${PACKAGE_TARGET_VERSION}" + + git -C ../${{ env.PACKAGE_FOLDER }} checkout -b build + composer config repositories.package '{ "type": "path", "url": "../${{ env.PACKAGE_FOLDER }}", "options": { "symlink": false } }' + composer require --no-update --no-interaction neos/redirecthandler-neosadapter:"dev-build as dev-${PACKAGE_TARGET_VERSION}" - name: Composer Install run: | From 3d3af9f2e9e777388fc64295294acd00d2f330d3 Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Wed, 28 Jun 2023 15:23:24 +0200 Subject: [PATCH 13/32] FEATURE: Redirect NeosAdapter for new ContentRepository --- .github/workflows/tests.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a193093..464d25c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -121,3 +121,9 @@ jobs: run: | cd ${NEOS_BASE_FOLDER} bin/behat -c Packages/Application/Neos.RedirectHandler.NeosAdapter/Tests/Behavior/behat.yml.dist + + - name: Show log on failure + if: ${{ failure() }} + run: | + cd ${NEOS_BASE_FOLDER} + cat Data/Logs/System_Testing.log From 19beb62e2f183246ee2e62c0e902691ab2961ede Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Fri, 7 Jul 2023 12:22:41 +0200 Subject: [PATCH 14/32] Update Classes/CatchUpHook/DocumentUriPathProjectionHook.php Co-authored-by: Bastian Waidelich --- Classes/CatchUpHook/DocumentUriPathProjectionHook.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Classes/CatchUpHook/DocumentUriPathProjectionHook.php b/Classes/CatchUpHook/DocumentUriPathProjectionHook.php index 2828c74..6ea28a5 100644 --- a/Classes/CatchUpHook/DocumentUriPathProjectionHook.php +++ b/Classes/CatchUpHook/DocumentUriPathProjectionHook.php @@ -119,6 +119,9 @@ private function onAfterNodeAggregateWasRemoved(NodeAggregateWasRemoved $event): } foreach ($event->affectedCoveredDimensionSpacePoints as $dimensionSpacePoint) { + if (!array_key_exists($dimensionSpacePoint->hash, $this->documentNodeInfosBeforeRemoval)) { + continue; + } $documentNodeInfosBeforeRemoval = $this->documentNodeInfosBeforeRemoval[$dimensionSpacePoint->hash]; unset($this->documentNodeInfosBeforeRemoval[$dimensionSpacePoint->hash]); From 980ce1be92ab4d0a36cdd996addc327f272a1f84 Mon Sep 17 00:00:00 2001 From: bwaidelich Date: Fri, 7 Jul 2023 19:55:34 +0200 Subject: [PATCH 15/32] Mini refactoring as discussed --- .../DocumentUriPathProjectionHook.php | 23 +++++-------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/Classes/CatchUpHook/DocumentUriPathProjectionHook.php b/Classes/CatchUpHook/DocumentUriPathProjectionHook.php index 2828c74..3062bfb 100644 --- a/Classes/CatchUpHook/DocumentUriPathProjectionHook.php +++ b/Classes/CatchUpHook/DocumentUriPathProjectionHook.php @@ -82,10 +82,7 @@ private function onBeforeNodeAggregateWasRemoved(NodeAggregateWasRemoved $event) $this->documentNodeInfosBeforeRemoval = []; foreach ($event->affectedCoveredDimensionSpacePoints as $dimensionSpacePoint) { - $node = $this->tryGetNode(fn() => $this->getState()->getByIdAndDimensionSpacePointHash( - $event->nodeAggregateId, - $dimensionSpacePoint->hash - )); + $node = $this->findNodeByIdAndDimensionSpacePointHash($event->nodeAggregateId, $dimensionSpacePoint->hash); if ($node === null) { // Probably not a document node continue; @@ -160,10 +157,7 @@ private function handleNodePropertiesWereSet(NodePropertiesWereSet $event, \Clos } foreach ($event->affectedDimensionSpacePoints as $affectedDimensionSpacePoint) { - $node = $this->tryGetNode(fn() => $this->getState()->getByIdAndDimensionSpacePointHash( - $event->nodeAggregateId, - $affectedDimensionSpacePoint->hash - )); + $node = $this->findNodeByIdAndDimensionSpacePointHash($event->nodeAggregateId, $affectedDimensionSpacePoint->hash); if ($node === null) { // probably not a document node continue; @@ -206,12 +200,8 @@ private function handleNodeWasMoved(NodeAggregateWasMoved $event, \Closure $clos /* @var \Neos\ContentRepository\Core\Feature\NodeMove\Dto\OriginNodeMoveMapping $moveMapping */ foreach ($moveMapping->newLocations as $newLocation) { /* @var $newLocation CoverageNodeMoveMapping */ - $node = $this->tryGetNode(fn() => $this->getState()->getByIdAndDimensionSpacePointHash( - $event->nodeAggregateId, - $newLocation->coveredDimensionSpacePoint->hash - )); - - if (!$node) { + $node = $this->findNodeByIdAndDimensionSpacePointHash($event->nodeAggregateId, $newLocation->coveredDimensionSpacePoint->hash); + if ($node === null) { // node probably no document node, skip continue; } @@ -238,12 +228,11 @@ private function isLiveContentStream(ContentStreamId $contentStreamId): bool return $contentStreamId->equals($this->getState()->getLiveContentStreamId()); } - private function tryGetNode(\Closure $closure): ?DocumentNodeInfo + private function findNodeByIdAndDimensionSpacePointHash(NodeAggregateId $nodeAggregateId, string $dimensionSpacePointHash): ?DocumentNodeInfo { try { - return $closure(); + return $this->getState()->getByIdAndDimensionSpacePointHash($nodeAggregateId, $dimensionSpacePointHash); } catch (NodeNotFoundException $_) { - /** @noinspection BadExceptionsProcessingInspection */ return null; } } From ea8c997ac11d3639bd71c2cd060f1c358d31c5fc Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Thu, 24 Aug 2023 17:45:08 +0200 Subject: [PATCH 16/32] TASK: Use NodeAddressFactory for NodeAddress creation --- .../DocumentUriPathProjectionHook.php | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/Classes/CatchUpHook/DocumentUriPathProjectionHook.php b/Classes/CatchUpHook/DocumentUriPathProjectionHook.php index b7730b0..5ad970e 100644 --- a/Classes/CatchUpHook/DocumentUriPathProjectionHook.php +++ b/Classes/CatchUpHook/DocumentUriPathProjectionHook.php @@ -16,11 +16,10 @@ use Neos\Neos\FrontendRouting\Projection\DocumentNodeInfo; use Neos\Neos\FrontendRouting\Exception\NodeNotFoundException; use Neos\Neos\FrontendRouting\NodeAddress; -use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; -use Neos\ContentRepository\Core\Factory\ContentRepositoryId; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; +use Neos\Neos\FrontendRouting\NodeAddressFactory; final class DocumentUriPathProjectionHook implements CatchUpHookInterface { @@ -91,7 +90,7 @@ private function onBeforeNodeAggregateWasRemoved(NodeAggregateWasRemoved $event) $this->nodeRedirectService->appendAffectedNode( $node, $this->getNodeAddress($event->contentStreamId, $dimensionSpacePoint, $node->getNodeAggregateId()), - $this->getContentRepositoryId() + $this->contentRepository->id ); $this->documentNodeInfosBeforeRemoval[$dimensionSpacePoint->hash][] = $node; @@ -101,7 +100,7 @@ function ($descendantOfNode) use ($event, $dimensionSpacePoint) { $this->nodeRedirectService->appendAffectedNode( $descendantOfNode, $this->getNodeAddress($event->contentStreamId, $dimensionSpacePoint, $descendantOfNode->getNodeAggregateId()), - $this->getContentRepositoryId() + $this->contentRepository->id ); $this->documentNodeInfosBeforeRemoval[$dimensionSpacePoint->hash][] = $descendantOfNode; }, @@ -123,9 +122,9 @@ private function onAfterNodeAggregateWasRemoved(NodeAggregateWasRemoved $event): unset($this->documentNodeInfosBeforeRemoval[$dimensionSpacePoint->hash]); array_map( - fn($node) => $this->nodeRedirectService->createRedirectForRemovedAffectedNode( + fn(DocumentNodeInfo $node) => $this->nodeRedirectService->createRedirectForRemovedAffectedNode( $node, - $this->getContentRepositoryId() + $this->contentRepository->id ), $documentNodeInfosBeforeRemoval ); @@ -166,13 +165,13 @@ private function handleNodePropertiesWereSet(NodePropertiesWereSet $event, \Clos continue; } - $closure($node, $this->getNodeAddress($event->contentStreamId, $affectedDimensionSpacePoint, $node->getNodeAggregateId()), $this->getContentRepositoryId()); + $closure($node, $this->getNodeAddress($event->contentStreamId, $affectedDimensionSpacePoint, $node->getNodeAggregateId()), $this->contentRepository->id); $descendantsOfNode = $this->getState()->getDescendantsOfNode($node); - array_map(fn($descendantOfNode) => $closure( + array_map(fn(DocumentNodeInfo $descendantOfNode) => $closure( $descendantOfNode, $this->getNodeAddress($event->contentStreamId, $affectedDimensionSpacePoint, $descendantOfNode->getNodeAggregateId()), - $this->getContentRepositoryId() + $this->contentRepository->id ), iterator_to_array($descendantsOfNode)); } } @@ -209,13 +208,13 @@ private function handleNodeWasMoved(NodeAggregateWasMoved $event, \Closure $clos continue; } - $closure($node, $this->getNodeAddress($event->contentStreamId, $newLocation->coveredDimensionSpacePoint, $node->getNodeAggregateId()), $this->getContentRepositoryId()); + $closure($node, $this->getNodeAddress($event->contentStreamId, $newLocation->coveredDimensionSpacePoint, $node->getNodeAggregateId()), $this->contentRepository->id); $descendantsOfNode = $this->getState()->getDescendantsOfNode($node); - array_map(fn($descendantOfNode) => $closure( + array_map(fn(DocumentNodeInfo $descendantOfNode) => $closure( $descendantOfNode, $this->getNodeAddress($event->contentStreamId, $newLocation->coveredDimensionSpacePoint, $descendantOfNode->getNodeAggregateId()), - $this->getContentRepositoryId() + $this->contentRepository->id ), iterator_to_array($descendantsOfNode)); } } @@ -245,11 +244,8 @@ protected function getNodeAddress( DimensionSpacePoint $dimensionSpacePoint, NodeAggregateId $nodeAggregateId, ): NodeAddress { - return new NodeAddress($contentStreamId, $dimensionSpacePoint, $nodeAggregateId, WorkspaceName::forLive()); - } - - private function getContentRepositoryId(): ContentRepositoryId - { - return $this->contentRepositoryRegistry->getContentRepositoryIdByContentRepository($this->contentRepository); + return NodeAddressFactory::create($this->contentRepository)->createFromContentStreamIdAndDimensionSpacePointAndNodeAggregateId( + $contentStreamId, $dimensionSpacePoint, $nodeAggregateId + ); } } From 4bf4b386f644a4afb5c4d7c3de230094582c23f9 Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Thu, 24 Aug 2023 17:45:32 +0200 Subject: [PATCH 17/32] TASK: Remove test configuration --- Configuration/Testing/Behat/Settings.Mvc.yaml | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 Configuration/Testing/Behat/Settings.Mvc.yaml diff --git a/Configuration/Testing/Behat/Settings.Mvc.yaml b/Configuration/Testing/Behat/Settings.Mvc.yaml deleted file mode 100644 index 4624aac..0000000 --- a/Configuration/Testing/Behat/Settings.Mvc.yaml +++ /dev/null @@ -1,8 +0,0 @@ -Neos: - Flow: - mvc: - routes: - 'Neos.Neos': - variables: - # set explicitly for testing as it may be overriden by other packages (e.g. Neos.Demo) - 'defaultUriSuffix': '.html' From fd781f72a910499e50cc051075fcbdc2a7f2dfdc Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Thu, 24 Aug 2023 17:46:33 +0200 Subject: [PATCH 18/32] TASK: Use buildService method of CR registry --- Tests/Behavior/Features/Bootstrap/FeatureContext.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Tests/Behavior/Features/Bootstrap/FeatureContext.php b/Tests/Behavior/Features/Bootstrap/FeatureContext.php index 2c5ea6e..45512c7 100644 --- a/Tests/Behavior/Features/Bootstrap/FeatureContext.php +++ b/Tests/Behavior/Features/Bootstrap/FeatureContext.php @@ -11,7 +11,6 @@ use Neos\ContentRepository\Core\Tests\Behavior\Features\Bootstrap\Helpers\FakeUserIdProviderFactory; use Neos\ContentRepository\Core\Tests\Behavior\Features\Bootstrap\NodeOperationsTrait; use Neos\ContentRepository\Core\Tests\Behavior\Features\Helper\ContentGraphs; -use Neos\ContentRepository\Security\Service\AuthorizationService; use Neos\ContentRepository\Core\Tests\Behavior\Features\Bootstrap\EventSourcedTrait; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Configuration\ConfigurationManager; @@ -85,10 +84,11 @@ public function __construct() $this->objectManager = self::$bootstrap->getObjectManager(); $this->environment = $this->objectManager->get(Environment::class); - $this->nodeAuthorizationService = $this->objectManager->get(AuthorizationService::class); $this->setupSecurity(); - CatchUpTriggerWithSynchronousOption::enableSynchonityForSpeedingUpTesting(); +// if (getenv('CATCHUPTRIGGER_ENABLE_SYNCHRONOUS_OPTION')) { +// CatchUpTriggerWithSynchronousOption::enableSynchonityForSpeedingUpTesting(); +// } $this->setupEventSourcedTrait(true); } @@ -103,7 +103,7 @@ protected function getContentRepositoryRegistry(): ContentRepositoryRegistry protected function getContentRepositoryService(ContentRepositoryId $contentRepositoryId, ContentRepositoryServiceFactoryInterface $factory): ContentRepositoryServiceInterface { - return $this->getContentRepositoryRegistry()->getService($contentRepositoryId, $factory); + return $this->getContentRepositoryRegistry()->buildService($contentRepositoryId, $factory); } /** @@ -144,7 +144,7 @@ protected function initCleanContentRepository(array $adapterKeys): void $this->contentRepository->setUp(); self::$wasContentRepositorySetupCalled = true; } - $this->contentRepositoryInternals = $this->contentRepositoryRegistry->getService($this->contentRepositoryId, new ContentRepositoryInternalsFactory()); + $this->contentRepositoryInternals = $this->contentRepositoryRegistry->buildService($this->contentRepositoryId, new ContentRepositoryInternalsFactory()); $availableContentGraphs = []; $availableContentGraphs['DoctrineDBAL'] = $this->contentRepository->getContentGraph(); From 8ff93f73e8507013a4cff933ec9e84904dab1e1d Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Fri, 29 Sep 2023 13:53:56 +0200 Subject: [PATCH 19/32] Task: Adopt behat tests to latest refactoring in flow and neos behat testing suite --- .../Features/Bootstrap/FeatureContext.php | 146 ++++-------------- .../Bootstrap/RedirectOperationTrait.php | 1 - .../Features/MultipleDimensions.feature | 36 ++++- Tests/Behavior/Features/OneDimension.feature | 38 ++++- .../Features/WithoutDimensions.feature | 46 +++++- 5 files changed, 143 insertions(+), 124 deletions(-) diff --git a/Tests/Behavior/Features/Bootstrap/FeatureContext.php b/Tests/Behavior/Features/Bootstrap/FeatureContext.php index 45512c7..4fd0a20 100644 --- a/Tests/Behavior/Features/Bootstrap/FeatureContext.php +++ b/Tests/Behavior/Features/Bootstrap/FeatureContext.php @@ -1,64 +1,32 @@ initializeFlow(); } $this->objectManager = self::$bootstrap->getObjectManager(); - $this->environment = $this->objectManager->get(Environment::class); - - $this->setupSecurity(); - -// if (getenv('CATCHUPTRIGGER_ENABLE_SYNCHRONOUS_OPTION')) { -// CatchUpTriggerWithSynchronousOption::enableSynchonityForSpeedingUpTesting(); -// } - - $this->setupEventSourcedTrait(true); - } - - protected function getContentRepositoryRegistry(): ContentRepositoryRegistry - { - /** @var ContentRepositoryRegistry $contentRepositoryRegistry */ - $contentRepositoryRegistry = $this->objectManager->get(ContentRepositoryRegistry::class); + $this->contentRepositoryRegistry = $this->objectManager->get(ContentRepositoryRegistry::class); - return $contentRepositoryRegistry; + $this->setupCRTestSuiteTrait(); } - protected function getContentRepositoryService(ContentRepositoryId $contentRepositoryId, ContentRepositoryServiceFactoryInterface $factory): ContentRepositoryServiceInterface - { - return $this->getContentRepositoryRegistry()->buildService($contentRepositoryId, $factory); - } - - /** - * @param array $adapterKeys "DoctrineDBAL" if - * @return void - */ - protected function initCleanContentRepository(array $adapterKeys): void - { - $this->logToRaceConditionTracker(['msg' => 'initCleanContentRepository']); - - $configurationManager = $this->getObjectManager()->get(ConfigurationManager::class); - $registrySettings = $configurationManager->getConfiguration( - ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, - 'Neos.ContentRepositoryRegistry' - ); - - if (!in_array('Postgres', $adapterKeys)) { - // in case we do not have tests annotated with @adapters=Postgres, we - // REMOVE the Postgres projection from the Registry settings. This way, we won't trigger - // Postgres projection catchup for tests which are not yet postgres-aware. - // - // This is to make the testcases more stable and deterministic. We can remove this workaround - // once the Postgres adapter is fully ready. - unset($registrySettings['presets'][$this->contentRepositoryId->value]['projections']['Neos.ContentGraph.PostgreSQLAdapter:Hypergraph']); - } - $registrySettings['presets'][$this->contentRepositoryId->value]['userIdProvider']['factoryObjectName'] = FakeUserIdProviderFactory::class; - $registrySettings['presets'][$this->contentRepositoryId->value]['clock']['factoryObjectName'] = FakeClockFactory::class; - - $this->contentRepositoryRegistry = new ContentRepositoryRegistry( - $registrySettings, - $this->getObjectManager() + protected function getContentRepositoryService( + ContentRepositoryServiceFactoryInterface $factory + ): ContentRepositoryServiceInterface { + return $this->contentRepositoryRegistry->buildService( + $this->currentContentRepository->id, + $factory ); + } + protected function createContentRepository( + ContentRepositoryId $contentRepositoryId + ): ContentRepository { + $this->contentRepositoryRegistry->resetFactoryInstance($contentRepositoryId); + $contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId); + GherkinTableNodeBasedContentDimensionSourceFactory::reset(); + GherkinPyStringNodeBasedNodeTypeManagerFactory::reset(); - $this->contentRepository = $this->contentRepositoryRegistry->get($this->contentRepositoryId); - // Big performance optimization: only run the setup once - DRAMATICALLY reduces test time - if ($this->alwaysRunContentRepositorySetup || !self::$wasContentRepositorySetupCalled) { - $this->contentRepository->setUp(); - self::$wasContentRepositorySetupCalled = true; - } - $this->contentRepositoryInternals = $this->contentRepositoryRegistry->buildService($this->contentRepositoryId, new ContentRepositoryInternalsFactory()); - - $availableContentGraphs = []; - $availableContentGraphs['DoctrineDBAL'] = $this->contentRepository->getContentGraph(); - // NOTE: to disable a content graph (do not run the tests for it), you can use "null" as value. - if (in_array('Postgres', $adapterKeys)) { - $availableContentGraphs['Postgres'] = $this->contentRepository->projectionState(ContentHypergraph::class); - } - - if (count($availableContentGraphs) === 0) { - throw new \RuntimeException('No content graph active during testing. Please set one in settings in activeContentGraphs'); - } - $this->availableContentGraphs = new ContentGraphs($availableContentGraphs); + return $contentRepository; } } diff --git a/Tests/Behavior/Features/Bootstrap/RedirectOperationTrait.php b/Tests/Behavior/Features/Bootstrap/RedirectOperationTrait.php index f28410e..cc6c820 100755 --- a/Tests/Behavior/Features/Bootstrap/RedirectOperationTrait.php +++ b/Tests/Behavior/Features/Bootstrap/RedirectOperationTrait.php @@ -1,5 +1,4 @@ de, en | | market | DE, CH | CH->DE | + And using the following node types: + """yaml + 'Neos.ContentRepository:Root': [] + + 'Neos.Neos:Sites': + superTypes: + 'Neos.ContentRepository:Root': true + 'Neos.Neos:Document': + properties: + uriPathSegment: + type: string + + 'Neos.Neos:Test.Redirect.Page': + superTypes: + 'Neos.Neos:Document': true + constraints: + nodeTypes: + '*': true + 'Neos.Neos:Test.Redirect.Page': true + properties: + title: + type: string + + 'Neos.Neos:Test.Redirect.RestrictedPage': + superTypes: + 'Neos.Neos:Document': true + constraints: + nodeTypes: + '*': true + 'Neos.Neos:Test.Redirect.Page': true + """ + And using identifier "default", I define a content repository + And I am in content repository "default" And I am user identified by "initiating-user-identifier" And the command CreateRootWorkspace is executed with payload: | Key | Value | @@ -45,6 +78,7 @@ Feature: Basic redirect handling with document nodes in multiple dimensions Neos: sites: '*': + uriPathSuffix: '.html' contentRepository: default contentDimensions: defaultDimensionSpacePoint: diff --git a/Tests/Behavior/Features/OneDimension.feature b/Tests/Behavior/Features/OneDimension.feature index d58d63f..2c420d0 100644 --- a/Tests/Behavior/Features/OneDimension.feature +++ b/Tests/Behavior/Features/OneDimension.feature @@ -2,9 +2,42 @@ Feature: Basic redirect handling with document nodes in one dimension Background: - Given I have the following content dimensions: + Given using the following content dimensions: | Identifier | Values | Generalizations | | language | de, en, gsw | gsw->de, en | + And using the following node types: + """yaml + 'Neos.ContentRepository:Root': [] + + 'Neos.Neos:Sites': + superTypes: + 'Neos.ContentRepository:Root': true + 'Neos.Neos:Document': + properties: + uriPathSegment: + type: string + + 'Neos.Neos:Test.Redirect.Page': + superTypes: + 'Neos.Neos:Document': true + constraints: + nodeTypes: + '*': true + 'Neos.Neos:Test.Redirect.Page': true + properties: + title: + type: string + + 'Neos.Neos:Test.Redirect.RestrictedPage': + superTypes: + 'Neos.Neos:Document': true + constraints: + nodeTypes: + '*': true + 'Neos.Neos:Test.Redirect.Page': true + """ + And using identifier "default", I define a content repository + And I am in content repository "default" And I am user identified by "initiating-user-identifier" And the command CreateRootWorkspace is executed with payload: | Key | Value | @@ -39,11 +72,12 @@ Feature: Basic redirect handling with document nodes in one dimension And A site exists for node name "node1" And the sites configuration is: - """ + """yaml Neos: Neos: sites: '*': + uriPathSuffix: '.html' contentRepository: default contentDimensions: defaultDimensionSpacePoint: diff --git a/Tests/Behavior/Features/WithoutDimensions.feature b/Tests/Behavior/Features/WithoutDimensions.feature index b1eaa58..bfeec77 100755 --- a/Tests/Behavior/Features/WithoutDimensions.feature +++ b/Tests/Behavior/Features/WithoutDimensions.feature @@ -2,12 +2,47 @@ Feature: Basic redirect handling with document nodes without dimensions Background: - Given I have no content dimensions + Given using no content dimensions + And using the following node types: + """yaml + 'Neos.ContentRepository:Root': [] + + 'Neos.Neos:Sites': + superTypes: + 'Neos.ContentRepository:Root': true + 'Neos.Neos:Document': + properties: + uriPathSegment: + type: string + + 'Neos.Neos:Test.Redirect.Page': + superTypes: + 'Neos.Neos:Document': true + constraints: + nodeTypes: + '*': true + 'Neos.Neos:Test.Redirect.Page': true + properties: + title: + type: string + + 'Neos.Neos:Test.Redirect.RestrictedPage': + superTypes: + 'Neos.Neos:Document': true + constraints: + nodeTypes: + '*': true + 'Neos.Neos:Test.Redirect.Page': true + """ + And using identifier "default", I define a content repository + And I am in content repository "default" And I am user identified by "initiating-user-identifier" And the command CreateRootWorkspace is executed with payload: - | Key | Value | - | workspaceName | "live" | - | newContentStreamId | "cs-identifier" | + | Key | Value | + | workspaceName | "live" | + | workspaceTitle | "Live" | + | workspaceDescription | "The live workspace" | + | newContentStreamId | "cs-identifier" | And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | contentStreamId | "cs-identifier" | @@ -36,11 +71,12 @@ Feature: Basic redirect handling with document nodes without dimensions | restricted-by-nodetype | behat | Neos.Neos:Test.Redirect.RestrictedPage | {"uriPathSegment": "restricted-by-nodetype"} | node8 | And A site exists for node name "node1" And the sites configuration is: - """ + """yaml Neos: Neos: sites: '*': + uriPathSuffix: '.html' contentRepository: default contentDimensions: resolver: From 8c6501eab5b56630000977ed6623fa46091129fa Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Fri, 29 Sep 2023 14:01:48 +0200 Subject: [PATCH 20/32] Task: Composer update in pipeline --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 464d25c..b088ff1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -89,7 +89,7 @@ jobs: - name: Composer Install run: | cd ${NEOS_BASE_FOLDER} - composer install --no-interaction --no-progress + composer update --no-interaction --no-progress - name: Setup Flow configuration run: | From 2c79ffc29570a0afe71bdd2936f6b66b2b3de728 Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Sat, 30 Sep 2023 10:03:13 +0200 Subject: [PATCH 21/32] TASK: Behat testing in Pipeline --- .github/workflows/tests.yml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b088ff1..4033b81 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -108,15 +108,6 @@ jobs: dbname: 'neos_functional_testing' EOF - - name: Install behat - run: | - cd ${NEOS_BASE_FOLDER} - cp -R Packages/Neos/Neos.ContentRepository.BehavioralTests/DistributionBehatTemplate/ Build/Behat - pushd Build/Behat/ - rm composer.lock || true - composer install - popd - - name: Run Functional tests run: | cd ${NEOS_BASE_FOLDER} From 1368a09b11168e969a9b70e55b4a129b3684a940 Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Sat, 30 Sep 2023 10:10:19 +0200 Subject: [PATCH 22/32] TASK: Add workaround for broken behat autoloading --- Configuration/Settings.yaml | 9 +++++++++ Tests/Behavior/Features/Bootstrap/FeatureContext.php | 1 + 2 files changed, 10 insertions(+) diff --git a/Configuration/Settings.yaml b/Configuration/Settings.yaml index be82519..4115099 100755 --- a/Configuration/Settings.yaml +++ b/Configuration/Settings.yaml @@ -25,3 +25,12 @@ Neos: catchUpHooks: 'Neos.RedirectHandler.NeosAdapter:DocumentUriPathProjectionHook': factoryObjectName: Neos\RedirectHandler\NeosAdapter\CatchUpHook\DocumentUriPathProjectionHookFactory + + Flow: + # TODO remove this temporary hack once neos is fixed. + object: + includeClasses: + "Neos.ContentRepository.TestSuite": + - "(*FAIL)" + "Neos.ContentRepositoryRegistry": + - "Neos\\\\ContentRepositoryRegistry\\\\(?!TestSuite\\\\Behavior\\\\CRRegistrySubjectProvider)" \ No newline at end of file diff --git a/Tests/Behavior/Features/Bootstrap/FeatureContext.php b/Tests/Behavior/Features/Bootstrap/FeatureContext.php index 4fd0a20..c28e3dc 100644 --- a/Tests/Behavior/Features/Bootstrap/FeatureContext.php +++ b/Tests/Behavior/Features/Bootstrap/FeatureContext.php @@ -15,6 +15,7 @@ use Neos\ContentRepository\BehavioralTests\TestSuite\Behavior\GherkinTableNodeBasedContentDimensionSourceFactory; use Neos\ContentRepository\BehavioralTests\TestSuite\Behavior\GherkinPyStringNodeBasedNodeTypeManagerFactory; +require_once(__DIR__ . '/../../../../../../Application/Neos.Behat/Tests/Behat/FlowContextTrait.php'); require_once(__DIR__ . '/../../../../../../Neos/Neos.Neos/Tests/Behavior/Features/Bootstrap/RoutingTrait.php'); /** From e503178bbec0a9b9cb11aefcfecf692279488bc4 Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Sat, 30 Sep 2023 10:10:19 +0200 Subject: [PATCH 23/32] TASK: Run tests synchronous --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4033b81..6e1a112 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -111,7 +111,7 @@ jobs: - name: Run Functional tests run: | cd ${NEOS_BASE_FOLDER} - bin/behat -c Packages/Application/Neos.RedirectHandler.NeosAdapter/Tests/Behavior/behat.yml.dist + CATCHUPTRIGGER_ENABLE_SYNCHRONOUS_OPTION=1 bin/behat -c Packages/Application/Neos.RedirectHandler.NeosAdapter/Tests/Behavior/behat.yml.dist - name: Show log on failure if: ${{ failure() }} From 539a5bf720ae3b839c5e7b63483d957df42e6b89 Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Sat, 30 Sep 2023 10:42:08 +0200 Subject: [PATCH 24/32] TASK: Apply doctrine migrations before running tests --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6e1a112..73b1af8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -111,6 +111,7 @@ jobs: - name: Run Functional tests run: | cd ${NEOS_BASE_FOLDER} + FLOW_CONTEXT=Testing ./flow doctrine:migrate --quiet CATCHUPTRIGGER_ENABLE_SYNCHRONOUS_OPTION=1 bin/behat -c Packages/Application/Neos.RedirectHandler.NeosAdapter/Tests/Behavior/behat.yml.dist - name: Show log on failure From 97e441ecd056b56702de0d443710c59aeb19bdb7 Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Sat, 30 Sep 2023 10:48:02 +0200 Subject: [PATCH 25/32] TASK: Apply doctrine migrations before running tests --- .github/workflows/tests.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 73b1af8..3b6b845 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -111,7 +111,8 @@ jobs: - name: Run Functional tests run: | cd ${NEOS_BASE_FOLDER} - FLOW_CONTEXT=Testing ./flow doctrine:migrate --quiet + ./flow doctrine:migrate + ./flow cr:setup CATCHUPTRIGGER_ENABLE_SYNCHRONOUS_OPTION=1 bin/behat -c Packages/Application/Neos.RedirectHandler.NeosAdapter/Tests/Behavior/behat.yml.dist - name: Show log on failure From e3cd14e6f82fb1a14eba3e3eec2d14b17c7e32e6 Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Sat, 30 Sep 2023 10:55:47 +0200 Subject: [PATCH 26/32] TASK: Apply doctrine migrations before running tests --- .github/workflows/tests.yml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3b6b845..c3e6337 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -108,13 +108,21 @@ jobs: dbname: 'neos_functional_testing' EOF - - name: Run Functional tests + - name: Setup database schema + run: | + cd ${NEOS_BASE_FOLDER} + ./flow doctrine:migrate --quite + + - name: Setup CR run: | cd ${NEOS_BASE_FOLDER} - ./flow doctrine:migrate ./flow cr:setup + + - name: Run Functional tests + run: | + cd ${NEOS_BASE_FOLDER} CATCHUPTRIGGER_ENABLE_SYNCHRONOUS_OPTION=1 bin/behat -c Packages/Application/Neos.RedirectHandler.NeosAdapter/Tests/Behavior/behat.yml.dist - + - name: Show log on failure if: ${{ failure() }} run: | From b2ef5afb50776c1d43d05c8277fac820f8434f2c Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Sat, 30 Sep 2023 11:08:28 +0200 Subject: [PATCH 27/32] TASK: Check configuration before setup --- .github/workflows/tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c3e6337..28d435d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -116,6 +116,8 @@ jobs: - name: Setup CR run: | cd ${NEOS_BASE_FOLDER} + ./flow + ./flow configuration:show --path Neos.ContentRepositoryRegistry ./flow cr:setup - name: Run Functional tests From c0bbf17be69f94cdc1256850bacd074ed693c220 Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Sat, 30 Sep 2023 11:11:59 +0200 Subject: [PATCH 28/32] TASK: Change flow context to testing --- .github/workflows/tests.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 28d435d..84e6ffd 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,7 +9,7 @@ on: jobs: build: env: - FLOW_CONTEXT: Testing/Behat + FLOW_CONTEXT: Testing NEOS_TARGET_VERSION: '9.0' NEOS_BASE_FOLDER: neos-base-distribution PACKAGE_FOLDER: redirect-neosadapter @@ -94,9 +94,9 @@ jobs: - name: Setup Flow configuration run: | cd ${NEOS_BASE_FOLDER} - mkdir -p Configuration/Testing/Behat - rm -f Configuration/Testing/Behat/Settings.yaml - cat <> Configuration/Testing/Behat/Settings.yaml + mkdir -p Configuration/Testing + rm -f Configuration/Testing/Settings.yaml + cat <> Configuration/Testing/Settings.yaml Neos: Flow: persistence: From 4fed2da04ed2d066ec46bcaf67397941750023ea Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Sat, 30 Sep 2023 11:21:24 +0200 Subject: [PATCH 29/32] TASK: Change flow context to testing --- .github/workflows/tests.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 84e6ffd..41391e8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -116,8 +116,6 @@ jobs: - name: Setup CR run: | cd ${NEOS_BASE_FOLDER} - ./flow - ./flow configuration:show --path Neos.ContentRepositoryRegistry ./flow cr:setup - name: Run Functional tests From 22aad7b48b2bf9faebeb9a091692b75799748e66 Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Sat, 30 Sep 2023 11:25:12 +0200 Subject: [PATCH 30/32] TASK: Fix styles --- .../CatchUpHook/DocumentUriPathProjectionHook.php | 13 ++++++++----- Classes/Service/NodeRedirectService.php | 1 - 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Classes/CatchUpHook/DocumentUriPathProjectionHook.php b/Classes/CatchUpHook/DocumentUriPathProjectionHook.php index 5ad970e..d736a54 100644 --- a/Classes/CatchUpHook/DocumentUriPathProjectionHook.php +++ b/Classes/CatchUpHook/DocumentUriPathProjectionHook.php @@ -104,7 +104,8 @@ function ($descendantOfNode) use ($event, $dimensionSpacePoint) { ); $this->documentNodeInfosBeforeRemoval[$dimensionSpacePoint->hash][] = $descendantOfNode; }, - iterator_to_array($descendantsOfNode)); + iterator_to_array($descendantsOfNode) + ); } } @@ -122,7 +123,7 @@ private function onAfterNodeAggregateWasRemoved(NodeAggregateWasRemoved $event): unset($this->documentNodeInfosBeforeRemoval[$dimensionSpacePoint->hash]); array_map( - fn(DocumentNodeInfo $node) => $this->nodeRedirectService->createRedirectForRemovedAffectedNode( + fn (DocumentNodeInfo $node) => $this->nodeRedirectService->createRedirectForRemovedAffectedNode( $node, $this->contentRepository->id ), @@ -168,7 +169,7 @@ private function handleNodePropertiesWereSet(NodePropertiesWereSet $event, \Clos $closure($node, $this->getNodeAddress($event->contentStreamId, $affectedDimensionSpacePoint, $node->getNodeAggregateId()), $this->contentRepository->id); $descendantsOfNode = $this->getState()->getDescendantsOfNode($node); - array_map(fn(DocumentNodeInfo $descendantOfNode) => $closure( + array_map(fn (DocumentNodeInfo $descendantOfNode) => $closure( $descendantOfNode, $this->getNodeAddress($event->contentStreamId, $affectedDimensionSpacePoint, $descendantOfNode->getNodeAggregateId()), $this->contentRepository->id @@ -211,7 +212,7 @@ private function handleNodeWasMoved(NodeAggregateWasMoved $event, \Closure $clos $closure($node, $this->getNodeAddress($event->contentStreamId, $newLocation->coveredDimensionSpacePoint, $node->getNodeAggregateId()), $this->contentRepository->id); $descendantsOfNode = $this->getState()->getDescendantsOfNode($node); - array_map(fn(DocumentNodeInfo $descendantOfNode) => $closure( + array_map(fn (DocumentNodeInfo $descendantOfNode) => $closure( $descendantOfNode, $this->getNodeAddress($event->contentStreamId, $newLocation->coveredDimensionSpacePoint, $descendantOfNode->getNodeAggregateId()), $this->contentRepository->id @@ -245,7 +246,9 @@ protected function getNodeAddress( NodeAggregateId $nodeAggregateId, ): NodeAddress { return NodeAddressFactory::create($this->contentRepository)->createFromContentStreamIdAndDimensionSpacePointAndNodeAggregateId( - $contentStreamId, $dimensionSpacePoint, $nodeAggregateId + $contentStreamId, + $dimensionSpacePoint, + $nodeAggregateId ); } } diff --git a/Classes/Service/NodeRedirectService.php b/Classes/Service/NodeRedirectService.php index b8b9e7e..10ae274 100644 --- a/Classes/Service/NodeRedirectService.php +++ b/Classes/Service/NodeRedirectService.php @@ -309,5 +309,4 @@ protected function getHostnames(SiteNodeName $siteNodeName): array return $this->hostnamesRuntimeCache[$siteNodeName->value]; } - } From e04f362c7e3e29cd2047887fe6fe4f7fccdd599d Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Sat, 30 Sep 2023 11:26:48 +0200 Subject: [PATCH 31/32] TASK: Fix styles --- Classes/CatchUpHook/DocumentUriPathProjectionHook.php | 1 - 1 file changed, 1 deletion(-) diff --git a/Classes/CatchUpHook/DocumentUriPathProjectionHook.php b/Classes/CatchUpHook/DocumentUriPathProjectionHook.php index d736a54..b08949b 100644 --- a/Classes/CatchUpHook/DocumentUriPathProjectionHook.php +++ b/Classes/CatchUpHook/DocumentUriPathProjectionHook.php @@ -49,7 +49,6 @@ public function onBeforeEvent(EventInterface $eventInstance, EventEnvelope $even NodeAggregateWasMoved::class => $this->onBeforeNodeAggregateWasMoved($eventInstance), default => null }; - } public function onAfterEvent(EventInterface $eventInstance, EventEnvelope $eventEnvelope): void From 892f8a4e6b618d935c709103cd6ebdf5253eb504 Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Tue, 10 Oct 2023 09:25:33 +0200 Subject: [PATCH 32/32] TASK: Require proper packages for ci testing --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index af40249..2bb1227 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -79,8 +79,8 @@ jobs: - name: Prepare external packages for development distribution run: | cd ${NEOS_BASE_FOLDER} - composer require --no-update --no-interaction neos/redirecthandler:"dev-flow-9-compatibility as dev-master" - composer require --no-update --no-interaction neos/redirecthandler-databasestorage:"dev-flow-9-compatibility as dev-main" + composer require --no-update --no-interaction neos/redirecthandler:"^6.0" + composer require --no-update --no-interaction neos/redirecthandler-databasestorage:"^6.0" git -C ../${{ env.PACKAGE_FOLDER }} checkout -b build composer config repositories.package '{ "type": "path", "url": "../${{ env.PACKAGE_FOLDER }}", "options": { "symlink": false } }'