diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9a75f19..2bb1227 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 + 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,62 @@ 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: | + cd ${NEOS_BASE_FOLDER} + 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 } }' + composer require --no-update --no-interaction neos/redirecthandler-neosadapter:"dev-build as dev-${PACKAGE_TARGET_VERSION}" + + - name: Composer Install 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 update --no-interaction --no-progress - - name: Install distribution + - name: Setup Flow configuration run: | - cd ${FLOW_FOLDER} - composer install --no-interaction --no-progress - rm -rf Packages/Application/Neos.RedirectHandler.NeosAdapter - cp -r ../redirecthandler-neosadapter Packages/Application/Neos.RedirectHandler.NeosAdapter + cd ${NEOS_BASE_FOLDER} + mkdir -p Configuration/Testing + rm -f Configuration/Testing/Settings.yaml + cat <> Configuration/Testing/Settings.yaml + Neos: + Flow: + persistence: + backendOptions: + host: '127.0.0.1' + driver: pdo_mysql + user: 'neos' + password: 'neos' + dbname: 'neos_functional_testing' + EOF + + - name: Setup database schema + run: | + cd ${NEOS_BASE_FOLDER} + ./flow doctrine:migrate --quite + + - name: Setup CR + run: | + cd ${NEOS_BASE_FOLDER} + ./flow cr:setup - 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} + 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: | + cd ${NEOS_BASE_FOLDER} + cat Data/Logs/System_Testing.log diff --git a/Classes/CatchUpHook/DocumentUriPathProjectionHook.php b/Classes/CatchUpHook/DocumentUriPathProjectionHook.php new file mode 100644 index 0000000..b08949b --- /dev/null +++ b/Classes/CatchUpHook/DocumentUriPathProjectionHook.php @@ -0,0 +1,253 @@ +> + */ + 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->findNodeByIdAndDimensionSpacePointHash($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->contentRepository->id + ); + $this->documentNodeInfosBeforeRemoval[$dimensionSpacePoint->hash][] = $node; + + $descendantsOfNode = $this->getState()->getDescendantsOfNode($node); + array_map( + function ($descendantOfNode) use ($event, $dimensionSpacePoint) { + $this->nodeRedirectService->appendAffectedNode( + $descendantOfNode, + $this->getNodeAddress($event->contentStreamId, $dimensionSpacePoint, $descendantOfNode->getNodeAggregateId()), + $this->contentRepository->id + ); + $this->documentNodeInfosBeforeRemoval[$dimensionSpacePoint->hash][] = $descendantOfNode; + }, + iterator_to_array($descendantsOfNode) + ); + } + } + + private function onAfterNodeAggregateWasRemoved(NodeAggregateWasRemoved $event): void + { + if (!$this->isLiveContentStream($event->contentStreamId)) { + return; + } + + foreach ($event->affectedCoveredDimensionSpacePoints as $dimensionSpacePoint) { + if (!array_key_exists($dimensionSpacePoint->hash, $this->documentNodeInfosBeforeRemoval)) { + continue; + } + $documentNodeInfosBeforeRemoval = $this->documentNodeInfosBeforeRemoval[$dimensionSpacePoint->hash]; + unset($this->documentNodeInfosBeforeRemoval[$dimensionSpacePoint->hash]); + + array_map( + fn (DocumentNodeInfo $node) => $this->nodeRedirectService->createRedirectForRemovedAffectedNode( + $node, + $this->contentRepository->id + ), + $documentNodeInfosBeforeRemoval + ); + } + } + + private function onBeforeNodePropertiesWereSet(NodePropertiesWereSet $event): void + { + $this->handleNodePropertiesWereSet( + $event, + $this->nodeRedirectService->appendAffectedNode(...) + ); + } + + private function onAfterNodePropertiesWereSet(NodePropertiesWereSet $event): void + { + $this->handleNodePropertiesWereSet( + $event, + $this->nodeRedirectService->createRedirectForAffectedNode(...) + ); + } + + 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->findNodeByIdAndDimensionSpacePointHash($event->nodeAggregateId, $affectedDimensionSpacePoint->hash); + if ($node === null) { + // probably not a document node + continue; + } + + $closure($node, $this->getNodeAddress($event->contentStreamId, $affectedDimensionSpacePoint, $node->getNodeAggregateId()), $this->contentRepository->id); + + $descendantsOfNode = $this->getState()->getDescendantsOfNode($node); + array_map(fn (DocumentNodeInfo $descendantOfNode) => $closure( + $descendantOfNode, + $this->getNodeAddress($event->contentStreamId, $affectedDimensionSpacePoint, $descendantOfNode->getNodeAggregateId()), + $this->contentRepository->id + ), iterator_to_array($descendantsOfNode)); + } + } + + private function onBeforeNodeAggregateWasMoved(NodeAggregateWasMoved $event): void + { + $this->handleNodeWasMoved( + $event, + $this->nodeRedirectService->appendAffectedNode(...) + ); + } + + private function onAfterNodeAggregateWasMoved(NodeAggregateWasMoved $event): void + { + $this->handleNodeWasMoved( + $event, + $this->nodeRedirectService->createRedirectForAffectedNode(...) + ); + } + + 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->findNodeByIdAndDimensionSpacePointHash($event->nodeAggregateId, $newLocation->coveredDimensionSpacePoint->hash); + if ($node === null) { + // node probably no document node, skip + continue; + } + + $closure($node, $this->getNodeAddress($event->contentStreamId, $newLocation->coveredDimensionSpacePoint, $node->getNodeAggregateId()), $this->contentRepository->id); + + $descendantsOfNode = $this->getState()->getDescendantsOfNode($node); + array_map(fn (DocumentNodeInfo $descendantOfNode) => $closure( + $descendantOfNode, + $this->getNodeAddress($event->contentStreamId, $newLocation->coveredDimensionSpacePoint, $descendantOfNode->getNodeAggregateId()), + $this->contentRepository->id + ), iterator_to_array($descendantsOfNode)); + } + } + } + + private function getState(): DocumentUriPathFinder + { + return $this->contentRepository->projectionState(DocumentUriPathFinder::class); + } + + private function isLiveContentStream(ContentStreamId $contentStreamId): bool + { + return $contentStreamId->equals($this->getState()->getLiveContentStreamId()); + } + + private function findNodeByIdAndDimensionSpacePointHash(NodeAggregateId $nodeAggregateId, string $dimensionSpacePointHash): ?DocumentNodeInfo + { + try { + return $this->getState()->getByIdAndDimensionSpacePointHash($nodeAggregateId, $dimensionSpacePointHash); + } catch (NodeNotFoundException $_) { + return null; + } + } + + protected function getNodeAddress( + ContentStreamId $contentStreamId, + DimensionSpacePoint $dimensionSpacePoint, + NodeAggregateId $nodeAggregateId, + ): NodeAddress { + return NodeAddressFactory::create($this->contentRepository)->createFromContentStreamIdAndDimensionSpacePointAndNodeAggregateId( + $contentStreamId, + $dimensionSpacePoint, + $nodeAggregateId + ); + } +} diff --git a/Classes/CatchUpHook/DocumentUriPathProjectionHookFactory.php b/Classes/CatchUpHook/DocumentUriPathProjectionHookFactory.php new file mode 100644 index 0000000..f06c100 --- /dev/null +++ b/Classes/CatchUpHook/DocumentUriPathProjectionHookFactory.php @@ -0,0 +1,27 @@ +contentRepositoryRegistry, + $this->redirectService + ); + } +} diff --git a/Classes/Package.php b/Classes/Package.php deleted file mode 100644 index 3d0b686..0000000 --- a/Classes/Package.php +++ /dev/null @@ -1,38 +0,0 @@ -getSignalSlotDispatcher(); - - $dispatcher->connect(Workspace::class, 'beforeNodePublishing', NodeRedirectService::class, 'collectPossibleRedirects'); - $dispatcher->connect(PersistenceManager::class, 'allObjectsPersisted', NodeRedirectService::class, 'createPendingRedirects'); - } -} diff --git a/Classes/Service/NodeRedirectService.php b/Classes/Service/NodeRedirectService.php index 0856ab4..10ae274 100644 --- a/Classes/Service/NodeRedirectService.php +++ b/Classes/Service/NodeRedirectService.php @@ -13,393 +13,216 @@ * 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\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; +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") */ -class NodeRedirectService +final class NodeRedirectService { - use CreateContentContextTrait; + const STATUS_CODE_TYPE_REDIRECT = 'redirect'; + const STATUS_CODE_TYPE_GONE = 'gone'; - /** - * @var UriBuilder - */ - protected $uriBuilder; + private array $affectedNodes = []; + private array $hostnamesRuntimeCache = []; - /** - * @Flow\Inject - * @var RedirectStorageInterface - */ - protected $redirectStorage; + #[Flow\Inject] + protected ?LoggerInterface $logger = null; /** - * @Flow\Inject - * @var RouterCachingService + * @var array */ - protected $routerCachingService; + #[Flow\InjectConfiguration(path: "statusCode", package: "Neos.RedirectHandler")] + protected array $defaultStatusCode; - /** - * @Flow\Inject - * @var PersistenceManagerInterface - */ - protected $persistenceManager; + #[Flow\InjectConfiguration(path: "enableAutomaticRedirects", package: "Neos.RedirectHandler.NeosAdapter")] + protected bool $enableAutomaticRedirects; - /** - * @Flow\Inject - * @var ContextFactoryInterface - */ - protected $contextFactory; + #[Flow\InjectConfiguration(path: "enableRemovedNodeRedirect", package: "Neos.RedirectHandler.NeosAdapter")] + protected bool $enableRemovedNodeRedirect; /** - * @Flow\Inject - * @var NodeFactory + * @var array */ - protected $nodeFactory; + #[Flow\InjectConfiguration(path: "restrictByOldUriPrefix", package: "Neos.RedirectHandler.NeosAdapter")] + protected array $restrictByOldUriPrefix = []; /** - * @Flow\Inject - * @var LoggerInterface + * @var array */ - protected $logger; + #[Flow\InjectConfiguration(path: "restrictByNodeType", package: "Neos.RedirectHandler.NeosAdapter")] + protected array $restrictByNodeType = []; - /** - * @var Bootstrap - * @Flow\Inject - */ - protected $bootstrap; - - /** - * @Flow\InjectConfiguration(path="statusCode", package="Neos.RedirectHandler") - * @var array - */ - protected $defaultStatusCode; - - /** - * @Flow\Inject - * @var ContentDimensionCombinator - */ - protected $contentDimensionCombinator; - - /** - * @Flow\InjectConfiguration(path="enableRemovedNodeRedirect", package="Neos.RedirectHandler.NeosAdapter") - * @var array - */ - protected $enableRemovedNodeRedirect; - - /** - * @Flow\InjectConfiguration(path="restrictByPathPrefix", package="Neos.RedirectHandler.NeosAdapter") - * @var array - */ - protected $restrictByPathPrefix; - - /** - * @Flow\InjectConfiguration(path="restrictByOldUriPrefix", package="Neos.RedirectHandler.NeosAdapter") - * @var array - */ - protected $restrictByOldUriPrefix; - - /** - * @Flow\InjectConfiguration(path="restrictByNodeType", package="Neos.RedirectHandler.NeosAdapter") - * @var array - */ - protected $restrictByNodeType; - - /** - * @Flow\InjectConfiguration(path="enableAutomaticRedirects", package="Neos.RedirectHandler.NeosAdapter") - * @var array - */ - protected $enableAutomaticRedirects; - - /** - * @Flow\InjectConfiguration(path="http.baseUri", package="Neos.Flow") - * @var string - */ - protected $baseUri; - - /** - * @var array - */ - protected $pendingRedirects = []; - - /** - * @var ActionRequest - */ - protected $actionRequestForUriBuilder; + public function __construct( + #[Flow\Inject] + protected RedirectStorageInterface $redirectStorage, + #[Flow\Inject] + protected PersistenceManagerInterface $persistenceManager, + #[Flow\Inject] + protected ContentRepositoryRegistry $contentRepositoryRegistry, + #[Flow\Inject] + protected SiteRepository $siteRepository, + ) { + } /** - * Collects the node for redirection if it is a 'Neos.Neos:Document' node and its URI has changed + * Collects affected nodes before they got moved or removed. * - * @param NodeInterface $node The node that is about to be published - * @param Workspace $targetWorkspace - * @return void - * @throws MissingActionNameException + * @throws \Neos\Flow\Http\Exception + * @throws \Neos\Flow\Mvc\Routing\Exception\MissingActionNameException */ - public function collectPossibleRedirects(NodeInterface $node, Workspace $targetWorkspace): void + public function appendAffectedNode(DocumentNodeInfo $nodeInfo, NodeAddress $nodeAddress, ContentRepositoryId $contentRepositoryId): void { - if (!$this->enableAutomaticRedirects) { - return; - } - - $nodeType = $node->getNodeType(); - if ($targetWorkspace->isPublicWorkspace() === false || $nodeType->isOfType('Neos.Neos:Document') === false) { - return; - } - - if ($this->hasNodeUriChanged($node, $targetWorkspace)) { - $this->appendNodeAndChildrenDocumentsToPendingRedirects($node, $targetWorkspace); + try { + $this->affectedNodes[$this->createAffectedNodesKey($nodeInfo, $contentRepositoryId)] = [ + 'node' => $nodeInfo, + 'url' => $this->getNodeUriBuilder($nodeInfo->getSiteNodeName(), $contentRepositoryId)->uriFor($nodeAddress), + ]; + } catch (NoMatchingRouteException $exception) { } } /** - * Returns the current http request or a generated http request - * based on a configured baseUri to allow redirect generation - * for CLI requests. + * Creates redirects for given node and uses the collected affected nodes to determine the source of the new redirect target. * - * @return ActionRequest + * @throws \Neos\Flow\Http\Exception + * @throws \Neos\Flow\Mvc\Routing\Exception\MissingActionNameException */ - protected function getActionRequestForUriBuilder(): ?ActionRequest + public function createRedirectForAffectedNode(DocumentNodeInfo $nodeInfo, NodeAddress $nodeAddress, ContentRepositoryId $contentRepositoryId): void { - if ($this->actionRequestForUriBuilder) { - return $this->actionRequestForUriBuilder; + if (!$this->enableAutomaticRedirects) { + return; } - /** @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'; + $affectedNode = $this->affectedNodes[$this->createAffectedNodesKey($nodeInfo, $contentRepositoryId)] ?? null; + if ($affectedNode === null) { + return; + } + unset($this->affectedNodes[$this->createAffectedNodesKey($nodeInfo, $contentRepositoryId)]); - // Prevent `index.php` appearing in generated redirects - putenv('FLOW_REWRITEURLS=1'); + /** @var Uri $oldUri */ + $oldUri = $affectedNode['url']; + $nodeType = $this->getNodeType($contentRepositoryId, $nodeInfo->getNodeTypeName()); - $httpRequest = new ServerRequest('POST', $baseUri); - } else { - $httpRequest = $requestHandler->getHttpRequest(); + if ($this->isRestrictedByNodeType($nodeType) || $this->isRestrictedByOldUri($oldUri->getPath())) { + return; } - - 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); + 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; } + $this->createRedirectWithNewTarget($oldUri->getPath(), $newUri->getPath(), $nodeInfo->getSiteNodeName()); - return $this->actionRequestForUriBuilder; + $this->persistenceManager->persistAll(); } /** - * Creates the queued redirects provided we can find the node. - * - * @return void - * @throws MissingActionNameException + * Creates redirects for given removed node and uses the collected affected nodes to determine the source of the new redirect. */ - public function createPendingRedirects(): void + public function createRedirectForRemovedAffectedNode(DocumentNodeInfo $nodeInfo, ContentRepositoryId $contentRepositoryId): void { if (!$this->enableAutomaticRedirects) { return; } - $this->nodeFactory->reset(); - foreach ($this->pendingRedirects as $nodeIdentifierAndWorkspace => $oldUriPerDimensionCombination) { - [$nodeIdentifier, $workspaceName] = explode('@', $nodeIdentifierAndWorkspace); - $this->buildRedirects($nodeIdentifier, $workspaceName, $oldUriPerDimensionCombination); + $affectedNode = $this->affectedNodes[$this->createAffectedNodesKey($nodeInfo, $contentRepositoryId)] ?? null; + if ($affectedNode === null) { + return; } - $this->pendingRedirects = []; + unset($this->affectedNodes[$this->createAffectedNodesKey($nodeInfo, $contentRepositoryId)]); - $this->persistenceManager->persistAll(); - } + /** @var Uri $oldUri */ + $oldUri = $affectedNode['url']; + $nodeType = $this->getNodeType($contentRepositoryId, $nodeInfo->getNodeTypeName()); - /** - * @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])) { + if ($this->isRestrictedByNodeType($nodeType) || $this->isRestrictedByOldUri($oldUri->getPath())) { return; } - $this->pendingRedirects[$identifierAndWorkspaceKey] = $this->createUriPathsAcrossDimensionsForNode($node->getIdentifier(), $targetWorkspace); + $this->createRedirectForRemovedTarget($oldUri->getPath(), $nodeInfo->getSiteNodeName()); - foreach ($node->getChildNodes('Neos.Neos:Document') as $childNode) { - $this->appendNodeAndChildrenDocumentsToPendingRedirects($childNode, $targetWorkspace); - } + $this->persistenceManager->persistAll(); } - /** - * @param string $nodeIdentifier - * @param Workspace $targetWorkspace - * @return array - * @throws MissingActionNameException - */ - protected function createUriPathsAcrossDimensionsForNode(string $nodeIdentifier, Workspace $targetWorkspace): array + protected function getNodeType(ContentRepositoryId $contentRepositoryId, NodeTypeName $nodeTypeName): NodeType { - $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; + return $this->contentRepositoryRegistry->get($contentRepositoryId)->getNodeTypeManager()->getNodeType($nodeTypeName); } - /** - * Has the Uri changed at all. - * - * @param NodeInterface $node - * @param Workspace $targetWorkspace - * @return bool - * @throws MissingActionNameException - */ - protected function hasNodeUriChanged(NodeInterface $node, Workspace $targetWorkspace): bool + private function createAffectedNodesKey(DocumentNodeInfo $nodeInfo, ContentRepositoryId $contentRepositoryId): string { - $nodeInTargetWorkspace = $this->getNodeInWorkspace($node, $targetWorkspace); + return $contentRepositoryId->value . '#' . $nodeInfo->getNodeAggregateId()->value . '#' . $nodeInfo->getDimensionSpacePointHash(); + } - if (!$nodeInTargetWorkspace) { - return false; - } + protected function getNodeUriBuilder(SiteNodeName $siteNodeName, ContentRepositoryId $contentRepositoryId): NodeUriBuilder + { + // Generate a custom request when the current request was triggered from CLI + $baseUri = 'http://localhost'; - if ($node->getProperty('uriPathSegment') !== $nodeInTargetWorkspace->getProperty('uriPathSegment')) { - return true; - } + // Prevent `index.php` appearing in generated redirects + putenv('FLOW_REWRITEURLS=1'); - if ($node->getParentPath() !== $nodeInTargetWorkspace->getParentPath()) { - return true; - } + $httpRequest = new ServerRequest('POST', $baseUri); - return false; - } + $httpRequest = (SiteDetectionResult::create($siteNodeName, $contentRepositoryId))->storeInRequest($httpRequest); + $actionRequest = ActionRequest::fromHttpRequest($httpRequest); - /** - * 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); - } + return NodeUriBuilder::fromRequest($actionRequest); } /** - * 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 + * Adds a redirect for given $oldUriPath to $newUriPath for all domains set up for $siteNode */ - protected function createRedirectFrom(string $oldUri, string $nodeIdentifer, string $workspaceName, array $dimensionCombination): bool + protected function createRedirectWithNewTarget(string $oldUriPath, string $newUriPath, SiteNodeName $siteNodeName): bool { - $node = $this->getNodeInWorkspaceAndDimensions($nodeIdentifer, $workspaceName, $dimensionCombination); - if ($node === null) { + if ($oldUriPath === $newUriPath) { return false; } - if ($this->isRestrictedByNodeType($node) || $this->isRestrictedByPath($node) || $this->isRestrictedByOldUri($oldUri, $node)) { - return false; - } + $hosts = $this->getHostnames($siteNodeName); + $statusCode = $this->defaultStatusCode[self::STATUS_CODE_TYPE_REDIRECT]; - 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; } /** - * Removes a redirect - * - * @param NodeInterface $node - * @param string $newUri - * @return bool + * Adds a redirect for a removed target if enabled. */ - 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 = $this->defaultStatusCode[self::STATUS_CODE_TYPE_GONE]; + $this->redirectStorage->addRedirect($oldUriPath, '', $statusCode, $hosts); return true; } @@ -408,24 +231,9 @@ protected function removeNodeRedirectIfNeeded(NodeInterface $node, string $newUr } /** - * Removes any context information appended to a node Uri. - * - * @param string $relativeNodeUri - * @return string + * Check if the current node type is restricted by NodeType */ - 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 - * @return bool - */ - protected function isRestrictedByNodeType(NodeInterface $node): bool + protected function isRestrictedByNodeType(NodeType $nodeType): bool { if (!isset($this->restrictByNodeType)) { return false; @@ -435,42 +243,11 @@ protected function isRestrictedByNodeType(NodeInterface $node): bool if ($status !== true) { continue; } - if ($node->getNodeType()->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(), - $disabledNodeType - ])); - - return true; - } - } - - 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 + 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 ])); return true; @@ -481,13 +258,9 @@ protected function isRestrictedByPath(NodeInterface $node): bool } /** - * Check if the old URI is restricted by Settings - * - * @param string $oldUri - * @param NodeInterface $node - * @return bool + * Check if the old URI is restricted by old uri */ - protected function isRestrictedByOldUri(string $oldUri, NodeInterface $node): bool + protected function isRestrictedByOldUri(string $oldUriPath): bool { if (!isset($this->restrictByOldUriPrefix)) { return false; @@ -498,10 +271,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 ])); @@ -514,101 +287,26 @@ protected function isRestrictedByOldUri(string $oldUri, NodeInterface $node): bo /** * Collects all hostnames from the Domain entries attached to the current site. - * - * @param NodeInterface $node - * @return array + * @return array> */ - protected function getHostnames(NodeInterface $node): array + protected function getHostnames(SiteNodeName $siteNodeName): array { - $contentContext = $this->createContextMatchingNodeData($node->getNodeData()); - $domains = []; - $site = $contentContext->getCurrentSite(); - if ($site === null) { - return $domains; - } + if (!isset($this->hostnamesRuntimeCache[$siteNodeName->value])) { + $site = $this->siteRepository->findOneByNodeName($siteNodeName); - foreach ($site->getActiveDomains() as $domain) { - /** @var Domain $domain */ - $domains[] = $domain->getHostname(); - } - - return $domains; - } - - /** - * 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); - } + $domains = []; + if ($site === null) { + return $domains; + } - /** - * Creates a (relative) URI for the given $nodeContextPath removing the "@workspace-name" from the result - * - * @param NodeInterface $node - * @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'); - } + foreach ($site->getActiveDomains() as $domain) { + /** @var Domain $domain */ + $domains[] = $domain->getHostname(); + } - /** - * Creates an UriBuilder instance for the current request - * - * @return UriBuilder - */ - protected function getUriBuilder(): UriBuilder - { - if ($this->uriBuilder !== null) { - return $this->uriBuilder; + $this->hostnamesRuntimeCache[$siteNodeName->value] = $domains; } - $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); + return $this->hostnamesRuntimeCache[$siteNodeName->value]; } } diff --git a/Configuration/Settings.yaml b/Configuration/Settings.yaml index bb9f2fb..4115099 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 @@ -18,3 +15,22 @@ 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 + + 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/Configuration/Testing/Behat/NodeTypes.Test.Redirect.yaml b/Configuration/Testing/Behat/NodeTypes.Test.Redirect.yaml new file mode 100644 index 0000000..42ac6b1 --- /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 diff --git a/Configuration/Testing/Behat/Settings.Restictions.yaml b/Configuration/Testing/Behat/Settings.Restictions.yaml new file mode 100644 index 0000000..9239268 --- /dev/null +++ b/Configuration/Testing/Behat/Settings.Restictions.yaml @@ -0,0 +1,9 @@ +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..c28e3dc 100644 --- a/Tests/Behavior/Features/Bootstrap/FeatureContext.php +++ b/Tests/Behavior/Features/Bootstrap/FeatureContext.php @@ -1,45 +1,78 @@ initializeFlow(); } $this->objectManager = self::$bootstrap->getObjectManager(); - $this->nodeAuthorizationService = $this->objectManager->get(AuthorizationService::class); - $this->nodeTypeManager = $this->objectManager->get(NodeTypeManager::class); - $this->setupSecurity(); + $this->contentRepositoryRegistry = $this->objectManager->get(ContentRepositoryRegistry::class); + + $this->setupCRTestSuiteTrait(); + } + + 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(); + + return $contentRepository; } } diff --git a/Tests/Behavior/Features/Bootstrap/RedirectOperationTrait.php b/Tests/Behavior/Features/Bootstrap/RedirectOperationTrait.php index abb3b78..cc6c820 100755 --- a/Tests/Behavior/Features/Bootstrap/RedirectOperationTrait.php +++ b/Tests/Behavior/Features/Bootstrap/RedirectOperationTrait.php @@ -1,5 +1,4 @@ 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 +68,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 +89,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/MultipleDimensions.feature b/Tests/Behavior/Features/MultipleDimensions.feature new file mode 100644 index 0000000..8bec5f0 --- /dev/null +++ b/Tests/Behavior/Features/MultipleDimensions.feature @@ -0,0 +1,414 @@ +@fixtures @contentrepository +Feature: Basic redirect handling with document nodes in multiple dimensions + + Background: + Given using the following content dimensions: + | Identifier | Values | Generalizations | + | language | de, en, gsw | gsw->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 | + | 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: + '*': + uriPathSuffix: '.html' + 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" diff --git a/Tests/Behavior/Features/OneDimension.feature b/Tests/Behavior/Features/OneDimension.feature new file mode 100644 index 0000000..2c420d0 --- /dev/null +++ b/Tests/Behavior/Features/OneDimension.feature @@ -0,0 +1,333 @@ +@fixtures @contentrepository +Feature: Basic redirect handling with document nodes in one dimension + + Background: + 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 | + | 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"} | node1 | + | company | behat | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "company"} | {"language": "en"} | node2 | + | service | company | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "service"} | {"language": "en"} | node3 | + | about | company | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "about"} | {"language": "en"} | node4 | + | 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: + """yaml + Neos: + Neos: + sites: + '*': + uriPathSuffix: '.html' + contentRepository: default + contentDimensions: + defaultDimensionSpacePoint: + language: en + resolver: + factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\UriPathResolverFactory + options: + segments: + - + dimensionIdentifier: language + dimensionValueMapping: + de: '' + en: en + 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 + 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.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 + 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.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 + 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.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 + 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.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 + 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.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 + 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.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"} | + | 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"} | +# | 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 + + And the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "company" | + | originDimensionSpacePoint | {"language": "de"} | + | propertyValues | {"uriPathSegment": "unternehmen"} | + And The documenturipath projection is up to date + + 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 (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.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 (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.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 no redirect with sourceUri "en/company.html" + And I should have no redirect with sourceUri "en/company/service.html" diff --git a/Tests/Behavior/Features/Redirect.feature b/Tests/Behavior/Features/Redirect.feature deleted file mode 100755 index d14265f..0000000 --- a/Tests/Behavior/Features/Redirect.feature +++ /dev/null @@ -1,129 +0,0 @@ -Feature: Redirects are created automatically when the URI of an existing node is changed - 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 | - - @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" - - @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" - - @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 - @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.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" - - @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" - - @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" - - @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 - @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" diff --git a/Tests/Behavior/Features/WithoutDimensions.feature b/Tests/Behavior/Features/WithoutDimensions.feature new file mode 100755 index 0000000..bfeec77 --- /dev/null +++ b/Tests/Behavior/Features/WithoutDimensions.feature @@ -0,0 +1,213 @@ +@fixtures @contentrepository +Feature: Basic redirect handling with document nodes without dimensions + + Background: + 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" | + | workspaceTitle | "Live" | + | workspaceDescription | "The live workspace" | + | newContentStreamId | "cs-identifier" | + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "site-root" | + | nodeTypeName | "Neos.Neos:Sites" | + 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 | + And A site exists for node name "node1" + And the sites configuration is: + """yaml + Neos: + Neos: + sites: + '*': + uriPathSuffix: '.html' + contentRepository: default + contentDimensions: + resolver: + factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\NoopResolverFactory + """ + 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 | {} | + | newParentNodeAggregateId | "company" | + | newSucceedingSiblingNodeAggregateId | null | + And The documenturipath projection is up to date + 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: + | 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.html" and targetUri "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 | {} | + | propertyValues | {"uriPathSegment": "evil-corp"} | + And The documenturipath projection is up to date + 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 + 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.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 + 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 | {} | + | propertyValues | {"uriPathSegment": "my-company"} | + And The documenturipath projection is up to date + 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 + 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.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 | {} | + | propertyValues | {"uriPathSegment": "restricted-by-nodetype-new"} | + 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: 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 | "company" | + | 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 "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 "" 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 deleted file mode 100644 index 360e5bc..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(NodeTypeManager::class); - $this->workspaceRepository = $this->objectManager->get(WorkspaceRepository::class); - $this->liveWorkspace = new Workspace('live'); - $this->userWorkspace = new 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 87391e8..a1bd313 100644 --- a/composer.json +++ b/composer.json @@ -4,8 +4,10 @@ "description": "Neos Redirect Handler", "license": "GPL-3.0-or-later", "require": { - "neos/redirecthandler": "~3.0 || ~4.0 || ~5.0 || dev-main", - "neos/neos": "~4.3 || ~5.0 || ~7.0 || ~8.0 || dev-master" + "php": ">=8.2", + "neos/redirecthandler": "~6.0 || dev-main", + "neos/neos": "^9.0", + "neos/flow": "^9.0" }, "autoload": { "psr-4": {