[TEST] Extend site handling link generation tests 23/57923/10
authorOliver Hader <oliver@typo3.org>
Sat, 18 Aug 2018 21:14:57 +0000 (23:14 +0200)
committerBenni Mack <benni@typo3.org>
Wed, 22 Aug 2018 12:59:48 +0000 (14:59 +0200)
Current side handling functional test cases only support link
resolving and rendering. The opposite part of generating links
has been introduced with this change.

Besides that, existing rendering tests have been extended for
frontend restricted page rendering.

Resolves: #85922
Releases: master
Change-Id: Ie12c2883463e5560dd03e18dab3dc3a277076815
Reviewed-on: https://review.typo3.org/57923
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Benni Mack <benni@typo3.org>
Tested-by: Benni Mack <benni@typo3.org>
14 files changed:
composer.json
composer.lock
typo3/sysext/core/Tests/Functional/Fixtures/Frontend/AdditionalConfiguration.php
typo3/sysext/core/composer.json
typo3/sysext/frontend/Tests/Functional/SiteHandling/AbstractRequestTest.php [deleted file]
typo3/sysext/frontend/Tests/Functional/SiteHandling/AbstractTestCase.php [new file with mode: 0644]
typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/FluidError.html
typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/FluidJson.html [new file with mode: 0644]
typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/LinkGenerator.typoscript [new file with mode: 0644]
typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/LinkGeneratorController.php [new file with mode: 0644]
typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/scenario.yaml
typo3/sysext/frontend/Tests/Functional/SiteHandling/LinkGeneratorTest.php [new file with mode: 0644]
typo3/sysext/frontend/Tests/Functional/SiteHandling/PlainRequestTest.php
typo3/sysext/frontend/Tests/Functional/SiteHandling/SiteRequestTest.php

index 80fa7bc..35c4f60 100644 (file)
@@ -68,7 +68,7 @@
                "fiunchinho/phpunit-randomizer": "^4.0",
                "friendsofphp/php-cs-fixer": "^2.12.2",
                "typo3/cms-styleguide": "~9.2.0",
-               "typo3/testing-framework": "~4.4.1"
+               "typo3/testing-framework": "~4.6.0"
        },
        "suggest": {
                "ext-gd": "GDlib/Freetype is required for building images with text (GIFBUILDER) and can also be used to scale images",
index c159a6f..b85ef21 100644 (file)
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "05058f1ae500b8d7826e1b616caaf3ed",
+    "content-hash": "a31065c1e94f6c986083f4986f01d285",
     "packages": [
         {
             "name": "cogpowered/finediff",
         },
         {
             "name": "typo3/testing-framework",
-            "version": "4.4.1",
+            "version": "4.6.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/TYPO3/testing-framework.git",
-                "reference": "b0a8b49939817cbbd14e67e98b1c6c9fc34a68da"
+                "reference": "5ff512494724b6700ec7309e8205dedde175716e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/TYPO3/testing-framework/zipball/b0a8b49939817cbbd14e67e98b1c6c9fc34a68da",
-                "reference": "b0a8b49939817cbbd14e67e98b1c6c9fc34a68da",
+                "url": "https://api.github.com/repos/TYPO3/testing-framework/zipball/5ff512494724b6700ec7309e8205dedde175716e",
+                "reference": "5ff512494724b6700ec7309e8205dedde175716e",
                 "shasum": ""
             },
             "require": {
                 "tests",
                 "typo3"
             ],
-            "time": "2018-08-20T11:20:22+00:00"
+            "time": "2018-08-22T09:29:31+00:00"
         },
         {
             "name": "webmozart/assert",
index ced90bb..bcd6a16 100644 (file)
@@ -13,4 +13,6 @@ if (TYPO3_MODE === 'FE') {
         \TYPO3\TestingFramework\Core\Functional\Framework\Frontend\Hook\FrontendUserHandler::class . '->initialize';
     $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/index_ts.php']['postBeUser']['FunctionalTest'] =
         \TYPO3\TestingFramework\Core\Functional\Framework\Frontend\Hook\BackendUserHandler::class . '->initialize';
+    $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['Core/TypoScript/TemplateService']['runThroughTemplatesPostProcessing']['FunctionalTest'] =
+        \TYPO3\TestingFramework\Core\Functional\Framework\Frontend\Hook\TypoScriptInstructionModifier::class . '->apply';
 }
index 1578a14..4a991aa 100644 (file)
@@ -50,7 +50,7 @@
                "fiunchinho/phpunit-randomizer": "^4.0",
                "friendsofphp/php-cs-fixer": "^2.12.2",
                "typo3/cms-styleguide": "~9.2.0",
-               "typo3/testing-framework": "~4.4.1"
+               "typo3/testing-framework": "~4.6.0"
        },
        "suggest": {
                "ext-fileinfo": "Used for proper file type detection in the file abstraction layer",
diff --git a/typo3/sysext/frontend/Tests/Functional/SiteHandling/AbstractRequestTest.php b/typo3/sysext/frontend/Tests/Functional/SiteHandling/AbstractRequestTest.php
deleted file mode 100644 (file)
index 1d86ccb..0000000
+++ /dev/null
@@ -1,312 +0,0 @@
-<?php
-declare(strict_types = 1);
-namespace TYPO3\CMS\Frontend\Tests\Functional\SiteHandling;
-
-/*
- * This file is part of the TYPO3 CMS project.
- *
- * It is free software; you can redistribute it and/or modify it under
- * the terms of the GNU General Public License, either version 2
- * of the License, or any later version.
- *
- * For the full copyright and license information, please read the
- * LICENSE.txt file that was distributed with this source code.
- *
- * The TYPO3 project - inspiring people to share!
- */
-
-use TYPO3\CMS\Core\Configuration\SiteConfiguration;
-use TYPO3\CMS\Core\Utility\ArrayUtility;
-use TYPO3\CMS\Frontend\Page\CacheHashCalculator;
-use TYPO3\CMS\Frontend\Tests\Functional\SiteHandling\Fixtures\PhpError;
-use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
-
-/**
- * Abstract test case for frontend requests
- */
-abstract class AbstractRequestTest extends FunctionalTestCase
-{
-    protected const ENCRYPTION_KEY = '4408d27a916d51e624b69af3554f516dbab61037a9f7b9fd6f81b4d3bedeccb6';
-
-    protected const TYPO3_CONF_VARS = [
-        'SYS' => [
-            'encryptionKey' => self::ENCRYPTION_KEY,
-        ],
-        'FE' => [
-            'cacheHash' => [
-                'requireCacheHashPresenceParameters' => ['testing[value]']
-            ],
-        ]
-    ];
-
-    protected const LANGUAGE_PRESETS = [
-        'EN' => ['id' => 0, 'title' => 'English', 'locale' => 'en_US.UTF8', 'iso' => 'en', 'hrefLang' => 'en-US', 'direction' => ''],
-        'FR' => ['id' => 1, 'title' => 'French', 'locale' => 'fr_FR.UTF8', 'iso' => 'fr', 'hrefLang' => 'fr-FR', 'direction' => ''],
-        'FR-CA' => ['id' => 2, 'title' => 'Franco-Canadian', 'locale' => 'fr_CA.UTF8', 'iso' => 'fr', 'hrefLang' => 'fr-CA', 'direction' => ''],
-    ];
-
-    /**
-     * @var array
-     */
-    protected $coreExtensionsToLoad = ['frontend'];
-
-    /**
-     * Combines string values of multiple array as cross-product into flat items.
-     *
-     * Example:
-     * + meltStrings(['a','b'], ['c','e'], ['f','g'])
-     * + results into ['acf', 'acg', 'aef', 'aeg', 'bcf', 'bcg', 'bef', 'beg']
-     *
-     * @param array $arrays Distinct array that should be melted
-     * @param callable $finalCallback Callback being executed on last multiplier
-     * @param string $prefix Prefix containing concatenated previous values
-     * @return array
-     */
-    protected function meltStrings(array $arrays, callable $finalCallback = null, string $prefix = ''): array
-    {
-        $results = [];
-        $array = array_shift($arrays);
-        foreach ($array as $item) {
-            $resultItem = $prefix . $item;
-            if (count($arrays) > 0) {
-                $results = array_merge(
-                    $results,
-                    $this->meltStrings($arrays, $finalCallback, $resultItem)
-                );
-                continue;
-            }
-            if ($finalCallback !== null) {
-                $resultItem = call_user_func($finalCallback, $resultItem);
-            }
-            $results[] = $resultItem;
-        }
-        return $results;
-    }
-
-    /**
-     * @param array $array
-     * @return array
-     */
-    protected function wrapInArray(array $array): array
-    {
-        return array_map(
-            function ($item) {
-                return [$item];
-            },
-            $array
-        );
-    }
-
-    /**
-     * @param array $array
-     * @return array
-     */
-    protected function keysFromValues(array $array): array
-    {
-        return array_combine($array, $array);
-    }
-
-    /**
-     * @param array $items
-     */
-    protected static function failIfArrayIsNotEmpty(array $items): void
-    {
-        if (empty($items)) {
-            return;
-        }
-
-        static::fail(
-            'Array was not empty as expected, but contained these items:' . LF
-            . '* ' . implode(LF . '* ', $items)
-        );
-    }
-
-    /**
-     * @param string $identifier
-     * @param array $site
-     * @param array $languages
-     * @param array $errorHandling
-     */
-    protected function writeSiteConfiguration(
-        string $identifier,
-        array $site = [],
-        array $languages = [],
-        array $errorHandling = []
-    ) {
-        $configuration = [
-            'site' => $site,
-        ];
-        if (!empty($languages)) {
-            $configuration['site']['languages'] = $languages;
-        }
-        if (!empty($errorHandling)) {
-            $configuration['site']['errorHandling'] = $errorHandling;
-        }
-
-        $siteConfiguration = new SiteConfiguration(
-            $this->instancePath . '/typo3conf/sites/'
-        );
-
-        try {
-            $siteConfiguration->write($identifier, $configuration);
-        } catch (\Exception $exception) {
-            $this->markTestSkipped($exception->getMessage());
-        }
-    }
-
-    /**
-     * @param int $rootPageId
-     * @param string $base
-     * @return array
-     */
-    protected function buildSiteConfiguration(
-        int $rootPageId,
-        string $base = ''
-    ): array {
-        return [
-            'rootPageId' => $rootPageId,
-            'base' => $base,
-        ];
-    }
-
-    /**
-     * @param string $identifier
-     * @param string $base
-     * @return array
-     */
-    protected function buildDefaultLanguageConfiguration(
-        string $identifier,
-        string $base
-    ): array {
-        $configuration = $this->buildLanguageConfiguration($identifier, $base);
-        $configuration['typo3Language'] = 'default';
-        $configuration['flag'] = 'global';
-        unset($configuration['fallbackType']);
-        return $configuration;
-    }
-
-    /**
-     * @param string $identifier
-     * @param string $base
-     * @param array $fallbackIdentifiers
-     * @return array
-     */
-    protected function buildLanguageConfiguration(
-        string $identifier,
-        string $base,
-        array $fallbackIdentifiers = []
-    ): array {
-        $preset = $this->resolveLanguagePreset($identifier);
-
-        $configuration = [
-            'languageId' => $preset['id'],
-            'title' => $preset['title'],
-            'navigationTitle' => $preset['title'],
-            'base' => $base,
-            'locale' => $preset['locale'],
-            'iso-639-1' => $preset['iso'],
-            'hreflang' => $preset['hrefLang'],
-            'direction' => $preset['direction'],
-            'typo3Language' => $preset['iso'],
-            'flag' => $preset['iso'],
-            'fallbackType' => 'strict',
-        ];
-
-        if (!empty($fallbackIdentifiers)) {
-            $fallbackIds = array_map(
-                function (string $fallbackIdentifier) {
-                    $preset = $this->resolveLanguagePreset($fallbackIdentifier);
-                    return $preset['id'];
-                },
-                $fallbackIdentifiers
-            );
-            $configuration['fallbackType'] = 'fallback';
-            $configuration['fallbackType'] = implode(',', $fallbackIds);
-        }
-
-        return $configuration;
-    }
-
-    /**
-     * @param string $handler
-     * @param array $codes
-     * @return array
-     */
-    protected function buildErrorHandlingConfiguration(
-        string $handler,
-        array $codes
-    ): array {
-        if ($handler === 'Page') {
-            $baseConfiguration = [
-                'errorContentSource' => '404',
-            ];
-        } elseif ($handler === 'Fluid') {
-            $baseConfiguration = [
-                'errorFluidTemplate' => 'typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/FluidError.html',
-                'errorFluidTemplatesRootPath' => '',
-                'errorFluidLayoutsRootPath' => '',
-                'errorFluidPartialsRootPath' => '',
-            ];
-        } elseif ($handler === 'PHP') {
-            $baseConfiguration = [
-                'errorPhpClassFQCN' => PhpError::class,
-            ];
-        } else {
-            throw new \LogicException(
-                sprintf('Invalid handler "%s"', $handler),
-                1533894782
-            );
-        }
-
-        $baseConfiguration['errorHandler'] = $handler;
-
-        return array_map(
-            function (int $code) use ($baseConfiguration) {
-                $baseConfiguration['errorCode'] = $code;
-                return $baseConfiguration;
-            },
-            $codes
-        );
-    }
-
-    /**
-     * @param string $identifier
-     * @return mixed
-     */
-    protected function resolveLanguagePreset(string $identifier)
-    {
-        if (!isset(static::LANGUAGE_PRESETS[$identifier])) {
-            throw new \LogicException(
-                sprintf('Undefined preset identifier "%s"', $identifier),
-                1533893665
-            );
-        }
-        return static::LANGUAGE_PRESETS[$identifier];
-    }
-
-    /**
-     * @param string $uri
-     * @return string
-     */
-    protected static function generateCacheHash(string $uri): string
-    {
-        if (!isset($GLOBALS['TYPO3_CONF_VARS'])) {
-            $GLOBALS['TYPO3_CONF_VARS'] = [];
-        }
-
-        $configuration = $GLOBALS['TYPO3_CONF_VARS'];
-        ArrayUtility::mergeRecursiveWithOverrule(
-            $GLOBALS['TYPO3_CONF_VARS'],
-            static::TYPO3_CONF_VARS
-        );
-
-        $calculator = new CacheHashCalculator();
-        $parameters = $calculator->getRelevantParameters(
-            parse_url($uri, PHP_URL_QUERY)
-        );
-        $cacheHash = $calculator->calculateCacheHash($parameters);
-
-        $GLOBALS['TYPO3_CONF_VARS'] = $configuration;
-        return $cacheHash;
-    }
-}
diff --git a/typo3/sysext/frontend/Tests/Functional/SiteHandling/AbstractTestCase.php b/typo3/sysext/frontend/Tests/Functional/SiteHandling/AbstractTestCase.php
new file mode 100644 (file)
index 0000000..c1b713a
--- /dev/null
@@ -0,0 +1,358 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Frontend\Tests\Functional\SiteHandling;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Configuration\SiteConfiguration;
+use TYPO3\CMS\Core\Utility\ArrayUtility;
+use TYPO3\CMS\Frontend\Page\CacheHashCalculator;
+use TYPO3\CMS\Frontend\Tests\Functional\SiteHandling\Fixtures\PhpError;
+use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
+
+/**
+ * Abstract test case for frontend requests
+ */
+abstract class AbstractTestCase extends FunctionalTestCase
+{
+    protected const ENCRYPTION_KEY = '4408d27a916d51e624b69af3554f516dbab61037a9f7b9fd6f81b4d3bedeccb6';
+
+    protected const TYPO3_CONF_VARS = [
+        'SYS' => [
+            'encryptionKey' => self::ENCRYPTION_KEY,
+        ],
+        'FE' => [
+            'cacheHash' => [
+                'requireCacheHashPresenceParameters' => ['testing[value]']
+            ],
+        ]
+    ];
+
+    protected const LANGUAGE_PRESETS = [
+        'EN' => ['id' => 0, 'title' => 'English', 'locale' => 'en_US.UTF8', 'iso' => 'en', 'hrefLang' => 'en-US', 'direction' => ''],
+        'FR' => ['id' => 1, 'title' => 'French', 'locale' => 'fr_FR.UTF8', 'iso' => 'fr', 'hrefLang' => 'fr-FR', 'direction' => ''],
+        'FR-CA' => ['id' => 2, 'title' => 'Franco-Canadian', 'locale' => 'fr_CA.UTF8', 'iso' => 'fr', 'hrefLang' => 'fr-CA', 'direction' => ''],
+    ];
+
+    /**
+     * @var string[]
+     */
+    protected $coreExtensionsToLoad = ['frontend', 'workspaces'];
+
+    /**
+     * @var string[]
+     */
+    protected $pathsToLinkInTestInstance = [
+        'typo3/sysext/core/Tests/Functional/Fixtures/Frontend/AdditionalConfiguration.php' => 'typo3conf/AdditionalConfiguration.php',
+    ];
+
+    /**
+     * Combines string values of multiple array as cross-product into flat items.
+     *
+     * Example:
+     * + meltStrings(['a','b'], ['c','e'], ['f','g'])
+     * + results into ['acf', 'acg', 'aef', 'aeg', 'bcf', 'bcg', 'bef', 'beg']
+     *
+     * @param array $arrays Distinct array that should be melted
+     * @param callable $finalCallback Callback being executed on last multiplier
+     * @param string $prefix Prefix containing concatenated previous values
+     * @return array
+     */
+    protected function meltStrings(array $arrays, callable $finalCallback = null, string $prefix = ''): array
+    {
+        $results = [];
+        $array = array_shift($arrays);
+        foreach ($array as $item) {
+            $resultItem = $prefix . $item;
+            if (count($arrays) > 0) {
+                $results = array_merge(
+                    $results,
+                    $this->meltStrings($arrays, $finalCallback, $resultItem)
+                );
+                continue;
+            }
+            if ($finalCallback !== null) {
+                $resultItem = call_user_func($finalCallback, $resultItem);
+            }
+            $results[] = $resultItem;
+        }
+        return $results;
+    }
+
+    /**
+     * @param array $array
+     * @return array
+     */
+    protected function wrapInArray(array $array): array
+    {
+        return array_map(
+            function ($item) {
+                return [$item];
+            },
+            $array
+        );
+    }
+
+    /**
+     * @param string[] $array
+     * @return array
+     */
+    protected function keysFromValues(array $array): array
+    {
+        return array_combine($array, $array);
+    }
+
+    /**
+     * Generates key names based on a template and array items as arguments.
+     *
+     * + keysFromTemplate([[1, 2, 3], [11, 22, 33]], '%1$d->%2$d (user:%3$d)')
+     * + returns the following array with generated keys
+     *   [
+     *     '1->2 (user:3)'    => [1, 2, 3],
+     *     '11->22 (user:33)' => [11, 22, 33],
+     *   ]
+     *
+     * @param array $array
+     * @param string $template
+     * @param callable|null $callback
+     * @return array
+     */
+    protected function keysFromTemplate(array $array, string $template, callable $callback = null): array
+    {
+        $keys = array_unique(
+            array_map(
+                function (array $values) use ($template, $callback) {
+                    if ($callback !== null) {
+                        $values = call_user_func($callback, $values);
+                    }
+                    return vsprintf($template, $values);
+                },
+                $array
+            )
+        );
+
+        if (count($keys) !== count($array)) {
+            throw new \LogicException(
+                'Amount of generated keys does not match to item count.',
+                1534682840
+            );
+        }
+
+        return array_combine($keys, $array);
+    }
+
+    /**
+     * @param array $items
+     */
+    protected static function failIfArrayIsNotEmpty(array $items): void
+    {
+        if (empty($items)) {
+            return;
+        }
+
+        static::fail(
+            'Array was not empty as expected, but contained these items:' . LF
+            . '* ' . implode(LF . '* ', $items)
+        );
+    }
+
+    /**
+     * @param string $identifier
+     * @param array $site
+     * @param array $languages
+     * @param array $errorHandling
+     */
+    protected function writeSiteConfiguration(
+        string $identifier,
+        array $site = [],
+        array $languages = [],
+        array $errorHandling = []
+    ) {
+        $configuration = [
+            'site' => $site,
+        ];
+        if (!empty($languages)) {
+            $configuration['site']['languages'] = $languages;
+        }
+        if (!empty($errorHandling)) {
+            $configuration['site']['errorHandling'] = $errorHandling;
+        }
+
+        $siteConfiguration = new SiteConfiguration(
+            $this->instancePath . '/typo3conf/sites/'
+        );
+
+        try {
+            $siteConfiguration->write($identifier, $configuration);
+        } catch (\Exception $exception) {
+            $this->markTestSkipped($exception->getMessage());
+        }
+    }
+
+    /**
+     * @param int $rootPageId
+     * @param string $base
+     * @return array
+     */
+    protected function buildSiteConfiguration(
+        int $rootPageId,
+        string $base = ''
+    ): array {
+        return [
+            'rootPageId' => $rootPageId,
+            'base' => $base,
+        ];
+    }
+
+    /**
+     * @param string $identifier
+     * @param string $base
+     * @return array
+     */
+    protected function buildDefaultLanguageConfiguration(
+        string $identifier,
+        string $base
+    ): array {
+        $configuration = $this->buildLanguageConfiguration($identifier, $base);
+        $configuration['typo3Language'] = 'default';
+        $configuration['flag'] = 'global';
+        unset($configuration['fallbackType']);
+        return $configuration;
+    }
+
+    /**
+     * @param string $identifier
+     * @param string $base
+     * @param array $fallbackIdentifiers
+     * @return array
+     */
+    protected function buildLanguageConfiguration(
+        string $identifier,
+        string $base,
+        array $fallbackIdentifiers = []
+    ): array {
+        $preset = $this->resolveLanguagePreset($identifier);
+
+        $configuration = [
+            'languageId' => $preset['id'],
+            'title' => $preset['title'],
+            'navigationTitle' => $preset['title'],
+            'base' => $base,
+            'locale' => $preset['locale'],
+            'iso-639-1' => $preset['iso'],
+            'hreflang' => $preset['hrefLang'],
+            'direction' => $preset['direction'],
+            'typo3Language' => $preset['iso'],
+            'flag' => $preset['iso'],
+            'fallbackType' => 'strict',
+        ];
+
+        if (!empty($fallbackIdentifiers)) {
+            $fallbackIds = array_map(
+                function (string $fallbackIdentifier) {
+                    $preset = $this->resolveLanguagePreset($fallbackIdentifier);
+                    return $preset['id'];
+                },
+                $fallbackIdentifiers
+            );
+            $configuration['fallbackType'] = 'fallback';
+            $configuration['fallbackType'] = implode(',', $fallbackIds);
+        }
+
+        return $configuration;
+    }
+
+    /**
+     * @param string $handler
+     * @param array $codes
+     * @return array
+     */
+    protected function buildErrorHandlingConfiguration(
+        string $handler,
+        array $codes
+    ): array {
+        if ($handler === 'Page') {
+            $baseConfiguration = [
+                'errorContentSource' => '404',
+            ];
+        } elseif ($handler === 'Fluid') {
+            $baseConfiguration = [
+                'errorFluidTemplate' => 'typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/FluidError.html',
+                'errorFluidTemplatesRootPath' => '',
+                'errorFluidLayoutsRootPath' => '',
+                'errorFluidPartialsRootPath' => '',
+            ];
+        } elseif ($handler === 'PHP') {
+            $baseConfiguration = [
+                'errorPhpClassFQCN' => PhpError::class,
+            ];
+        } else {
+            throw new \LogicException(
+                sprintf('Invalid handler "%s"', $handler),
+                1533894782
+            );
+        }
+
+        $baseConfiguration['errorHandler'] = $handler;
+
+        return array_map(
+            function (int $code) use ($baseConfiguration) {
+                $baseConfiguration['errorCode'] = $code;
+                return $baseConfiguration;
+            },
+            $codes
+        );
+    }
+
+    /**
+     * @param string $identifier
+     * @return mixed
+     */
+    protected function resolveLanguagePreset(string $identifier)
+    {
+        if (!isset(static::LANGUAGE_PRESETS[$identifier])) {
+            throw new \LogicException(
+                sprintf('Undefined preset identifier "%s"', $identifier),
+                1533893665
+            );
+        }
+        return static::LANGUAGE_PRESETS[$identifier];
+    }
+
+    /**
+     * @param string $uri
+     * @return string
+     */
+    protected static function generateCacheHash(string $uri): string
+    {
+        if (!isset($GLOBALS['TYPO3_CONF_VARS'])) {
+            $GLOBALS['TYPO3_CONF_VARS'] = [];
+        }
+
+        $configuration = $GLOBALS['TYPO3_CONF_VARS'];
+        ArrayUtility::mergeRecursiveWithOverrule(
+            $GLOBALS['TYPO3_CONF_VARS'],
+            static::TYPO3_CONF_VARS
+        );
+
+        $calculator = new CacheHashCalculator();
+        $parameters = $calculator->getRelevantParameters(
+            parse_url($uri, PHP_URL_QUERY)
+        );
+        $cacheHash = $calculator->calculateCacheHash($parameters);
+
+        $GLOBALS['TYPO3_CONF_VARS'] = $configuration;
+        return $cacheHash;
+    }
+}
index af0f60a..4ed80f2 100644 (file)
@@ -1,5 +1,3 @@
 uri: {request.uri}
 message: {message}
-<f:for each="{reasons}" as="reason">
-       reason: {reason}
-</f:for>
+reasons: <f:for each="{reasons}" as="reason" key="key">{key},</f:for>
diff --git a/typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/FluidJson.html b/typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/FluidJson.html
new file mode 100644 (file)
index 0000000..34e988d
--- /dev/null
@@ -0,0 +1 @@
+{results -> f:format.json() -> f:format.raw()}
\ No newline at end of file
diff --git a/typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/LinkGenerator.typoscript b/typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/LinkGenerator.typoscript
new file mode 100644 (file)
index 0000000..743527a
--- /dev/null
@@ -0,0 +1,19 @@
+config {
+  no_cache = 1
+  debug = 0
+  xhtml_cleaning = 0
+  admPanel = 0
+  disableAllHeaderCode = 1
+  sendCacheHeaders = 0
+  sys_language_uid = 0
+  sys_language_mode = ignore
+  sys_language_overlay = 1
+  additionalHeaders.10.header = Content-Type: application/json; charset=utf-8
+  additionalHeaders.10.replace = 1
+}
+
+page = PAGE
+page {
+  10 = USER
+  10.userFunc = TYPO3\CMS\Frontend\Tests\Functional\SiteHandling\Fixtures\LinkGeneratorController->mainAction
+}
diff --git a/typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/LinkGeneratorController.php b/typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/LinkGeneratorController.php
new file mode 100644 (file)
index 0000000..33d1d90
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Frontend\Tests\Functional\SiteHandling\Fixtures;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
+use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\Internal\ArrayValueInstruction;
+use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\RequestBootstrap;
+
+/**
+ * Test case for frontend requests having site handling configured
+ */
+class LinkGeneratorController
+{
+    /**
+     * @var ContentObjectRenderer
+     */
+    public $cObj;
+
+    public function mainAction(): string
+    {
+        $instruction = RequestBootstrap::getInternalRequest()
+            ->getInstruction(LinkGeneratorController::class);
+        if (!$instruction instanceof ArrayValueInstruction) {
+            return '';
+        }
+        return $this->cObj->cObjGet($instruction->getArray());
+    }
+}
index b26f63f..867672b 100644 (file)
@@ -28,6 +28,8 @@ entitySettings:
     columnNames: {title: 'header', type: 'CType'}
   domain:
     tableName: 'sys_domain'
+  workspace:
+    tableName: 'sys_workspace'
   language:
     tableName: 'sys_language'
     columnNames: {code: 'language_isocode'}
@@ -43,6 +45,8 @@ entitySettings:
         site: {root: 1, clear: 1}
 
 entities:
+  workspace:
+    - self: {id: 1, title: 'Workspace'}
   language:
     - self: {id: 1, title: 'French', code: 'fr'}
     - self: {id: 2, title: 'Franco-Canadian', code: 'fr'}
@@ -53,6 +57,8 @@ entities:
           languageVariants:
             - self: {id: 1101, title: 'FR: Welcome', language: 1}
             - self: {id: 1102, title: 'FR-CA: Welcome', language: 2}
+          versionVariants:
+            - version: {title: 'EN: Welcome to ACME Inc', workspace: 1}
           entities:
             content:
               - self: {title: 'EN: Content Element #1', type: *contentText}
@@ -64,7 +70,7 @@ entities:
               - self: {title: 'EN: Content Element #2', type: *contentText}
         - self: {id: 1200, title: 'EN: Features'}
           children:
-            - self: {id: 1121, title: 'EN: Frontend Editing'}
+            - self: {id: 1210, title: 'EN: Frontend Editing'}
         - self: {id: 1300, title: 'EN: Products', root: true}
           children:
             - self: {id: 1310, title: 'EN: Planets'}
@@ -75,7 +81,7 @@ entities:
             - self: {id: 1510, title: 'Whitepapers', visitorGroups: -2, extendToSubpages: true}
               children:
                 - self: {id: 1511, title: 'Products'}
-                - self: {id: 1512, title: 'Solutions'}
+                - self: {id: 1512, title: 'Solutions', visitorGroups: 10}
                 - self: {id: 1515, title: 'Research', visitorGroups: 20}
             - self: {id: 1520, title: 'Forecasts', visitorGroups: 20, extendToSubpages: true}
               children:
@@ -84,10 +90,12 @@ entities:
                 - self: {id: 1523, title: 'Five Years'}
         - self: {id: 1600, title: 'About us'}
         - self: {id: 1700, title: 'Announcements & News', type: *pageMount, mount: 7100}
-        - self: {id: 404, title: 'Page not found'}
           entities:
             content:
               - self: {title: 'EN: Page not found', type: *contentText}
+        - self: {id: 404, title: 'Page not found'}
+        - self: {id: 1930, title: 'Our Blog', type: *pageShortcut, shortcut: 2000}
+        - version: {id: 1950, title: 'EN: Goodbye', workspace: 1}
         - self: {id: 1990, title: 'Storage', type: *pageFolder}
           entities:
             visitorGroup:
@@ -108,6 +116,7 @@ entities:
               children:
                 - self: {id: 2121, title: 'About'}
         - self: {id: 2700, title: 'Announcements & News', type: *pageMount, mount: 7100}
+        - self: {id: 2930, title: 'ACME Inc', type: *pageShortcut, shortcut: 1000}
     - self: {id: 3000, title: 'ACME Archive', type: *pageShortcut, shortcut: 'first', root: true}
       children:
         - self: {id: 3100, title: 'EN: Statistics'}
diff --git a/typo3/sysext/frontend/Tests/Functional/SiteHandling/LinkGeneratorTest.php b/typo3/sysext/frontend/Tests/Functional/SiteHandling/LinkGeneratorTest.php
new file mode 100644 (file)
index 0000000..39ef48c
--- /dev/null
@@ -0,0 +1,807 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Frontend\Tests\Functional\SiteHandling;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Backend\Utility\BackendUtility;
+use TYPO3\CMS\Core\Core\Bootstrap;
+use TYPO3\CMS\Core\TypoScript\TemplateService;
+use TYPO3\CMS\Frontend\Tests\Functional\SiteHandling\Fixtures\LinkGeneratorController;
+use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\Scenario\DataHandlerFactory;
+use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\Scenario\DataHandlerWriter;
+use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\Internal\ArrayValueInstruction;
+use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\Internal\TypoScriptInstruction;
+use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest;
+use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequestContext;
+
+/**
+ * Test case for frontend requests having site handling configured
+ */
+class LinkGeneratorTest extends AbstractTestCase
+{
+    /**
+     * @var string
+     */
+    private $siteTitle = 'A Company that Manufactures Everything Inc';
+
+    /**
+     * @var InternalRequestContext
+     */
+    private $internalRequestContext;
+
+    protected function setUp()
+    {
+        parent::setUp();
+
+        // these settings are forwarded to the frontend sub-request as well
+        $this->internalRequestContext = (new InternalRequestContext())
+            ->withGlobalSettings(['TYPO3_CONF_VARS' => static::TYPO3_CONF_VARS]);
+
+        $backendUser = $this->setUpBackendUserFromFixture(1);
+        Bootstrap::initializeLanguageObject();
+
+        $scenarioFile = __DIR__ . '/Fixtures/scenario.yaml';
+        $factory = DataHandlerFactory::fromYamlFile($scenarioFile);
+        $writer = DataHandlerWriter::withBackendUser($backendUser);
+        $writer->invokeFactory($factory);
+        static::failIfArrayIsNotEmpty(
+            $writer->getErrors()
+        );
+
+        $this->setUpFrontendRootPage(
+            1000,
+            [
+                'typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/LinkGenerator.typoscript',
+            ],
+            [
+                'title' => 'ACME Root',
+                'sitetitle' => $this->siteTitle,
+            ]
+        );
+        $this->setUpFrontendRootPage(
+            2000,
+            [
+                'typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/LinkGenerator.typoscript',
+            ],
+            [
+                'title' => 'ACME Root',
+                'sitetitle' => $this->siteTitle,
+            ]
+        );
+
+        $this->writeSiteConfiguration(
+            'acme-com',
+            $this->buildSiteConfiguration(1000, 'https://acme.com/'),
+            [
+                $this->buildDefaultLanguageConfiguration('EN', 'https://acme.us/'),
+                $this->buildLanguageConfiguration('FR', 'https://acme.fr/', ['EN']),
+                $this->buildLanguageConfiguration('FR-CA', 'https://acme.ca/', ['FR', 'EN']),
+            ]
+        );
+        $this->writeSiteConfiguration(
+            'products-acme-com',
+            $this->buildSiteConfiguration(1300, 'https://products.acme.com/')
+        );
+        $this->writeSiteConfiguration(
+            'blog-acme-com',
+            $this->buildSiteConfiguration(2000, 'https://blog.acme.com/')
+        );
+        $this->writeSiteConfiguration(
+            'john-blog-acme-com',
+            $this->buildSiteConfiguration(2110, 'https://blog.acme.com/john/')
+        );
+        $this->writeSiteConfiguration(
+            'jane-blog-acme-com',
+            $this->buildSiteConfiguration(2120, 'https://blog.acme.com/jane/')
+        );
+    }
+
+    protected function tearDown()
+    {
+        unset($this->internalRequestContext);
+        parent::tearDown();
+    }
+
+    /**
+     * @return array
+     */
+    public function linkIsGeneratedDataProvider(): array
+    {
+        $instructions = [
+            // acme.com -> acme.com (same site)
+            [1100, 1000, '/?id=acme-root'],
+            [1100, 1100, '/?id=acme-first'],
+            [1100, 1200, '/?id=1200'],
+            [1100, 1210, '/?id=1210'],
+            [1100, 404, '/?id=404'],
+            // acme.com -> products.acme.com (nested sub-site)
+            [1100, 1300, '/?id=1300'],
+            [1100, 1310, '/?id=1310'],
+            // acme.com -> blog.acme.com (different site)
+            // @todo https://blog.acme.com/ not prefixed
+            [1100, 2000, '/?id=blog-root'],
+            [1100, 2100, '/?id=2100'],
+            [1100, 2110, '/john/?id=2110'],
+            [1100, 2111, '/john/?id=2111'],
+            // blog.acme.com -> acme.com (different site)
+            // @todo https://acme.com/ not prefixed
+            [2100, 1000, '/?id=acme-root'],
+            [2100, 1100, '/?id=acme-first'],
+            [2100, 1200, '/?id=1200'],
+            [2100, 1210, '/?id=1210'],
+            [2100, 404, '/?id=404'],
+            // blog.acme.com -> products.acme.com (different sub-site)
+            [2100, 1300, '/?id=1300'],
+            [2100, 1310, '/?id=1310'],
+        ];
+
+        return $this->keysFromTemplate(
+            $instructions,
+            '%1$d->%2$d'
+        );
+    }
+
+    /**
+     * @param int $sourcePageId
+     * @param int $targetPageId
+     * @param string $expectation
+     *
+     * @test
+     * @dataProvider linkIsGeneratedDataProvider
+     */
+    public function linkIsGenerated(int $sourcePageId, int $targetPageId, string $expectation)
+    {
+        $response = $this->executeFrontendRequest(
+            (new InternalRequest())
+                ->withPageId($sourcePageId)
+                ->withInstructions([
+                    $this->createTypoLinkUrlInstruction([
+                        'parameter' => $targetPageId,
+                    ])
+                ]),
+            $this->internalRequestContext
+        );
+
+        static::assertSame($expectation, (string)$response->getBody());
+    }
+
+    /**
+     * @return array
+     */
+    public function linkIsGeneratedFromMountPointDataProvider(): array
+    {
+        $instructions = [
+            // acme.com -> acme.com (same site)
+            [[7100, 1700], 7110, 1000, '/?id=acme-root'],
+            [[7100, 1700], 7110, 1100, '/?id=acme-first'],
+            [[7100, 1700], 7110, 1200, '/?id=1200'],
+            [[7100, 1700], 7110, 1210, '/?id=1210'],
+            [[7100, 1700], 7110, 404, '/?id=404'],
+            // acme.com -> products.acme.com (nested sub-site)
+            [[7100, 1700], 7110, 1300, '/?id=1300'],
+            [[7100, 1700], 7110, 1310, '/?id=1310'],
+            // acme.com -> blog.acme.com (different site)
+            // @todo https://blog.acme.com/ not prefixed
+            [[7100, 1700], 7110, 2000, '/?id=blog-root'],
+            [[7100, 1700], 7110, 2100, '/?id=2100'],
+            [[7100, 1700], 7110, 2110, '/john/?id=2110'],
+            [[7100, 1700], 7110, 2111, '/john/?id=2111'],
+            // blog.acme.com -> acme.com (different site)
+            // @todo https://acme.com/ not prefixed
+            [[7100, 2700], 7110, 1000, '/?id=acme-root'],
+            [[7100, 2700], 7110, 1100, '/?id=acme-first'],
+            [[7100, 2700], 7110, 1200, '/?id=1200'],
+            [[7100, 2700], 7110, 1210, '/?id=1210'],
+            [[7100, 2700], 7110, 404, '/?id=404'],
+            // blog.acme.com -> products.acme.com (different sub-site)
+            [[7100, 2700], 7110, 1300, '/?id=1300'],
+            [[7100, 2700], 7110, 1310, '/?id=1310'],
+        ];
+
+        return $this->keysFromTemplate(
+            $instructions,
+            '%2$d->%3$d (mount:%1$s)',
+            function (array $items) {
+                array_splice(
+                    $items,
+                    0,
+                    1,
+                    [implode('->', $items[0])]
+                );
+                return $items;
+            }
+        );
+    }
+
+    /**
+     * @param array $pageMount
+     * @param int $sourcePageId
+     * @param int $targetPageId
+     * @param string $expectation
+     *
+     * @test
+     * @dataProvider linkIsGeneratedFromMountPointDataProvider
+     */
+    public function linkIsGeneratedFromMountPoint(array $pageMount, int $sourcePageId, int $targetPageId, string $expectation)
+    {
+        $response = $this->executeFrontendRequest(
+            (new InternalRequest())
+                ->withMountPoint(...$pageMount)
+                ->withPageId($sourcePageId)
+                ->withInstructions([
+                    $this->createTypoLinkUrlInstruction([
+                        'parameter' => $targetPageId,
+                    ])
+                ]),
+            $this->internalRequestContext
+        );
+
+        static::assertSame($expectation, (string)$response->getBody());
+    }
+
+    /**
+     * @return array
+     */
+    public function linkIsGeneratedForLanguageDataProvider(): array
+    {
+        // @todo L-parameter is not applied
+        $instructions = [
+            // acme.com -> acme.com (same site)
+            [1100, 1100, 0, '/?id=acme-first'],
+            [1100, 1100, 1, '/?id=acme-first'],
+            [1100, 1100, 2, '/?id=acme-first'],
+            // @todo Configuration bug on duplicating alias names and uniqueness
+            [1100, 1101, 0, '/?id=acme-first0'],
+            [1100, 1102, 0, '/?id=acme-first1'],
+            // acme.com -> products.acme.com (nested sub-site)
+            [1100, 1300, 0, '/?id=1300'],
+            [1100, 1310, 0, '/?id=1310'],
+            // acme.com -> archive (outside site)
+            [1100, 3100, 0, 'index.php?id=3100&L=0'],
+            [1100, 3100, 1, 'index.php?id=3100&L=1'],
+            [1100, 3100, 2, 'index.php?id=3100&L=2'],
+            [1100, 3101, 0, 'index.php?id=3101&L=0'],
+            [1100, 3102, 0, 'index.php?id=3102&L=0'],
+            // blog.acme.com -> acme.com (different site)
+            // @todo https://acme.com/ not prefixed
+            [2100, 1100, 0, '/?id=acme-first'],
+            [2100, 1100, 1, '/?id=acme-first'],
+            [2100, 1100, 2, '/?id=acme-first'],
+            // @todo Configuration bug on duplicating alias names and uniqueness
+            [2100, 1101, 0, '/?id=acme-first0'],
+            [2100, 1102, 0, '/?id=acme-first1'],
+            // blog.acme.com -> archive (outside site)
+            [2100, 3100, 0, 'index.php?id=3100&L=0'],
+            [2100, 3100, 1, 'index.php?id=3100&L=1'],
+            [2100, 3100, 2, 'index.php?id=3100&L=2'],
+            [2100, 3101, 0, 'index.php?id=3101&L=0'],
+            [2100, 3102, 0, 'index.php?id=3102&L=0'],
+            // blog.acme.com -> products.acme.com (different sub-site)
+            [2100, 1300, 0, '/?id=1300'],
+            [2100, 1310, 0, '/?id=1310'],
+        ];
+
+        return $this->keysFromTemplate(
+            $instructions,
+            '%1$d->%2$d (lang:%3$d)'
+        );
+    }
+
+    /**
+     * @param int $sourcePageId
+     * @param int $targetPageId
+     * @param int $targetLanguageId
+     * @param string $expectation
+     *
+     * @test
+     * @dataProvider linkIsGeneratedForLanguageDataProvider
+     */
+    public function linkIsGeneratedForLanguage(int $sourcePageId, int $targetPageId, int $targetLanguageId, string $expectation)
+    {
+        $response = $this->executeFrontendRequest(
+            (new InternalRequest())
+                ->withPageId($sourcePageId)
+                ->withInstructions([
+                    $this->createTypoLinkUrlInstruction([
+                        'parameter' => $targetPageId,
+                        'additionalParams' => '&L=' . $targetLanguageId,
+                    ])
+                ]),
+            $this->internalRequestContext
+        );
+
+        static::assertSame($expectation, (string)$response->getBody());
+    }
+
+    /**
+     * @return array
+     */
+    public function linkIsGeneratedWithQueryParametersDataProvider(): array
+    {
+        $instructions = [
+            // acme.com -> acme.com (same site)
+            [1100, 1000, '/?id=acme-root&testing%5Bvalue%5D=1&cHash=7d1f13fa91159dac7feb3c824936b39d'],
+            [1100, 1100, '/?id=acme-first&testing%5Bvalue%5D=1&cHash=f42b850e435f0cedd366f5db749fc1af'],
+            [1100, 1200, '/?id=1200&testing%5Bvalue%5D=1&cHash=784e11c50ea1a13fd7d969df4ec53ea3'],
+            [1100, 1210, '/?id=1210&testing%5Bvalue%5D=1&cHash=ccb7067022b9835ebfd8f720722bc708'],
+            [1100, 404, '/?id=404&testing%5Bvalue%5D=1&cHash=864e96f586a78a53452f3bf0f4d24591'],
+            // acme.com -> products.acme.com (nested sub-site)
+            [1100, 1300, '/?id=1300&testing%5Bvalue%5D=1&cHash=dbd6597d72ed5098cce3d03eac1eeefe'],
+            [1100, 1310, '/?id=1310&testing%5Bvalue%5D=1&cHash=e64bfc7ab7dd6b70d161e4d556be9726'],
+            // acme.com -> blog.acme.com (different site)
+            // @todo https://blog.acme.com/ not prefixed
+            [1100, 2000, '/?id=blog-root&testing%5Bvalue%5D=1&cHash=a14da633e46dba71640cb85226cd12c5'],
+            [1100, 2100, '/?id=2100&testing%5Bvalue%5D=1&cHash=d23d74cb50383f8788a9930ec8ba679f'],
+            [1100, 2110, '/john/?id=2110&testing%5Bvalue%5D=1&cHash=bf25eea89f44a9a79dabdca98f38a432'],
+            [1100, 2111, '/john/?id=2111&testing%5Bvalue%5D=1&cHash=42dbaeb9172b6b1ca23b49941e194db2'],
+            // blog.acme.com -> acme.com (different site)
+            // @todo https://acme.com/ not prefixed
+            [2100, 1000, '/?id=acme-root&testing%5Bvalue%5D=1&cHash=7d1f13fa91159dac7feb3c824936b39d'],
+            [2100, 1100, '/?id=acme-first&testing%5Bvalue%5D=1&cHash=f42b850e435f0cedd366f5db749fc1af'],
+            [2100, 1200, '/?id=1200&testing%5Bvalue%5D=1&cHash=784e11c50ea1a13fd7d969df4ec53ea3'],
+            [2100, 1210, '/?id=1210&testing%5Bvalue%5D=1&cHash=ccb7067022b9835ebfd8f720722bc708'],
+            [2100, 404, '/?id=404&testing%5Bvalue%5D=1&cHash=864e96f586a78a53452f3bf0f4d24591'],
+            // blog.acme.com -> products.acme.com (different sub-site)
+            [2100, 1300, '/?id=1300&testing%5Bvalue%5D=1&cHash=dbd6597d72ed5098cce3d03eac1eeefe'],
+            [2100, 1310, '/?id=1310&testing%5Bvalue%5D=1&cHash=e64bfc7ab7dd6b70d161e4d556be9726'],
+        ];
+
+        return $this->keysFromTemplate(
+            $instructions,
+            '%1$d->%2$d'
+        );
+    }
+
+    /**
+     * @param int $sourcePageId
+     * @param int $targetPageId
+     * @param string $expectation
+     *
+     * @test
+     * @dataProvider linkIsGeneratedWithQueryParametersDataProvider
+     */
+    public function linkIsGeneratedWithQueryParameters(int $sourcePageId, int $targetPageId, string $expectation)
+    {
+        $response = $this->executeFrontendRequest(
+            (new InternalRequest())
+                ->withPageId($sourcePageId)
+                ->withInstructions([
+                    $this->createTypoLinkUrlInstruction([
+                        'parameter' => $targetPageId,
+                        'additionalParams' => '&testing[value]=1',
+                        'useCacheHash' => 1,
+                    ])
+                ]),
+            $this->internalRequestContext
+        );
+
+        static::assertSame($expectation, (string)$response->getBody());
+    }
+
+    /**
+     * @return array
+     */
+    public function linkIsGeneratedForRestrictedPageDataProvider(): array
+    {
+        $instructions = [
+            [1100, 1510, 0, ''],
+            // [1100, 1511, 0, ''], // @todo Fails, not expanded to sub-pages
+            [1100, 1512, 0, ''],
+            [1100, 1515, 0, ''],
+            [1100, 1520, 0, ''],
+            // [1100, 1521, 0, ''], // @todo Fails, not expanded to sub-pages
+            //
+            [1100, 1510, 1, '/?id=1510'],
+            [1100, 1511, 1, '/?id=1511'],
+            [1100, 1512, 1, '/?id=1512'],
+            [1100, 1515, 1, ''],
+            [1100, 1520, 1, ''],
+            // [1100, 1521, 1, ''], // @todo Fails, not expanded to sub-pages
+            //
+            [1100, 1510, 2, '/?id=1510'],
+            [1100, 1511, 2, '/?id=1511'],
+            [1100, 1512, 2, ''],
+            [1100, 1515, 2, '/?id=1515'],
+            [1100, 1520, 2, '/?id=1520'],
+            [1100, 1521, 2, '/?id=1521'],
+            //
+            [1100, 1510, 3, '/?id=1510'],
+            [1100, 1511, 3, '/?id=1511'],
+            [1100, 1512, 3, '/?id=1512'],
+            [1100, 1515, 3, '/?id=1515'],
+            [1100, 1520, 3, '/?id=1520'],
+            [1100, 1521, 3, '/?id=1521'],
+        ];
+
+        return $this->keysFromTemplate(
+            $instructions,
+            '%1$d->%2$d (user:%3$d)'
+        );
+    }
+
+    /**
+     * @param int $sourcePageId
+     * @param int $targetPageId
+     * @param int $frontendUserId
+     * @param string $expectation
+     *
+     * @test
+     * @dataProvider linkIsGeneratedForRestrictedPageDataProvider
+     */
+    public function linkIsGeneratedForRestrictedPage(int $sourcePageId, int $targetPageId, int $frontendUserId, string $expectation)
+    {
+        $response = $this->executeFrontendRequest(
+            (new InternalRequest())
+                ->withPageId($sourcePageId)
+                ->withInstructions([
+                    $this->createTypoLinkUrlInstruction([
+                        'parameter' => $targetPageId,
+                    ])
+                ]),
+            $this->internalRequestContext
+                ->withFrontendUserId($frontendUserId)
+        );
+
+        static::assertSame($expectation, (string)$response->getBody());
+    }
+
+    /**
+     * @return array
+     */
+    public function linkIsGeneratedForRestrictedPageUsingLoginPageDataProvider(): array
+    {
+        $instructions = [
+            // no frontend user given
+            [1100, 1510, 1500, 0, '/?id=1500&pageId=1510'],
+            // [1100, 1511, 1500, 0, '/?id=1500&pageId=1511'], // @todo Fails, not expanded to sub-pages
+            [1100, 1512, 1500, 0, '/?id=1500&pageId=1512'],
+            [1100, 1515, 1500, 0, '/?id=1500&pageId=1515'],
+            [1100, 1520, 1500, 0, '/?id=1500&pageId=1520'],
+            // [1100, 1521, 1500, 0, '/?id=1500&pageId=1521'], // @todo Fails, not expanded to sub-pages
+            // frontend user 1
+            [1100, 1510, 1500, 1, '/?id=1510'],
+            [1100, 1511, 1500, 1, '/?id=1511'],
+            [1100, 1512, 1500, 1, '/?id=1512'],
+            [1100, 1515, 1500, 1, '/?id=1500&pageId=1515'],
+            [1100, 1520, 1500, 1, '/?id=1500&pageId=1520'],
+            // [1100, 1521, 1500, 1, '/?id=1500&pageId=1521'], // @todo Fails, not expanded to sub-pages
+            // frontend user 2
+            [1100, 1510, 1500, 2, '/?id=1510'],
+            [1100, 1511, 1500, 2, '/?id=1511'],
+            [1100, 1512, 1500, 2, '/?id=1500&pageId=1512'],
+            [1100, 1515, 1500, 2, '/?id=1515'],
+            [1100, 1520, 1500, 2, '/?id=1520'],
+            [1100, 1521, 1500, 2, '/?id=1521'],
+            // frontend user 3
+            [1100, 1510, 1500, 3, '/?id=1510'],
+            [1100, 1511, 1500, 3, '/?id=1511'],
+            [1100, 1512, 1500, 3, '/?id=1512'],
+            [1100, 1515, 1500, 3, '/?id=1515'],
+            [1100, 1520, 1500, 3, '/?id=1520'],
+            [1100, 1521, 1500, 3, '/?id=1521'],
+        ];
+
+        return $this->keysFromTemplate(
+            $instructions,
+            '%1$d->%2$d (via: %3$d, user:%4$d)'
+        );
+    }
+
+    /**
+     * @param int $sourcePageId
+     * @param int $targetPageId
+     * @param int $loginPageId
+     * @param int $frontendUserId
+     * @param string $expectation
+     *
+     * @test
+     * @dataProvider linkIsGeneratedForRestrictedPageUsingLoginPageDataProvider
+     */
+    public function linkIsGeneratedForRestrictedPageUsingLoginPage(int $sourcePageId, int $targetPageId, int $loginPageId, int $frontendUserId, string $expectation)
+    {
+        $response = $this->executeFrontendRequest(
+            (new InternalRequest())
+                ->withPageId($sourcePageId)
+                ->withInstructions([
+                    (new TypoScriptInstruction(TemplateService::class))
+                        ->withTypoScript([
+                            'config.' => [
+                                'typolinkLinkAccessRestrictedPages' => $loginPageId,
+                                'typolinkLinkAccessRestrictedPages_addParams' => '&pageId=###PAGE_ID###'
+                            ],
+                        ]),
+                    $this->createTypoLinkUrlInstruction([
+                        'parameter' => $targetPageId,
+                    ])
+                ]),
+            $this->internalRequestContext
+                ->withFrontendUserId($frontendUserId)
+        );
+
+        static::assertSame($expectation, (string)$response->getBody());
+    }
+
+    public function linkIsGeneratedForPageVersionDataProvider(): array
+    {
+        // @todo Generation is not consistent "?id=" vs "index.php?id="
+        // -> most probably since pid=-1 is not correctly resolved
+        $instructions = [
+            // acme.com -> acme.com (same site)
+            [1100, 1100, false, '/?id=acme-first'],
+            [1100, 1100, true, 'index.php?id=acme-first'],
+            // [1100, 1950, false, '/?id=1950'], // @todo Not generated for new-placeholder
+            [1100, 1950, true, 'index.php?id={targetPageId}'],
+            // blog.acme.com -> acme.com (different site)
+            // @todo https://acme.com/ not prefixed
+            [2100, 1100, false, '/?id=acme-first'],
+            [2100, 1100, true, 'index.php?id=acme-first'],
+            // [2100, 1950, false, '/?id=1950'], // @todo Not generated for new-placeholder
+            [2100, 1950, true, 'index.php?id={targetPageId}'],
+        ];
+
+        return $this->keysFromTemplate(
+            $instructions,
+            '%1$d->%2$d (resolve:%3$d)'
+        );
+    }
+
+    /**
+     * @param int $sourcePageId
+     * @param int $targetPageId
+     * @param bool $resolveVersion
+     * @param string $expectation
+     *
+     * @test
+     * @dataProvider linkIsGeneratedForPageVersionDataProvider
+     */
+    public function linkIsGeneratedForPageVersion(int $sourcePageId, int $targetPageId, bool $resolveVersion, string $expectation)
+    {
+        $workspaceId = 1;
+        if ($resolveVersion) {
+            $targetPageId = BackendUtility::getWorkspaceVersionOfRecord(
+                $workspaceId,
+                'pages',
+                $targetPageId,
+                'uid'
+            )['uid'] ?? null;
+        }
+
+        $response = $this->executeFrontendRequest(
+            (new InternalRequest())
+                ->withPageId($sourcePageId)
+                ->withInstructions([
+                    $this->createTypoLinkUrlInstruction([
+                        'parameter' => $targetPageId,
+                    ])
+                ]),
+            $this->internalRequestContext
+                ->withWorkspaceId($workspaceId)
+        );
+
+        $expectation = str_replace(
+            ['{targetPageId}'],
+            [$targetPageId],
+            $expectation
+        );
+
+        static::assertSame($expectation, (string)$response->getBody());
+    }
+
+    public function menuIsGeneratedDataProvider(): array
+    {
+        return [
+            'ACME Inc' => [
+                1100,
+                [
+                    ['title' => 'EN: Welcome', 'link' => '/?id=acme-first'],
+                    [
+                        'title' => 'EN: Features',
+                        'link' => '/?id=1200',
+                        'children' => [
+                            [
+                                'title' => 'EN: Frontend Editing',
+                                'link' => '/?id=1210',
+                            ],
+                        ],
+                    ],
+                    [
+                        'title' => 'EN: Products',
+                        'link' => '/?id=1300',
+                        'children' => [
+                            [
+                                'title' => 'EN: Planets',
+                                'link' => '/?id=1310',
+                            ],
+                            [
+                                'title' => 'EN: Spaceships',
+                                'link' => '/?id=1320',
+                            ],
+                            [
+                                'title' => 'EN: Dark Matter',
+                                'link' => '/?id=1330',
+                            ],
+                        ],
+                    ],
+                    ['title' => 'Internal', 'link' => '/?id=1500'],
+                    ['title' => 'About us', 'link' => '/?id=1600'],
+                    [
+                        'title' => 'Announcements & News',
+                        'link' => '/?id=1700',
+                        'children' => [
+                            [
+                                'title' => 'Markets',
+                                'link' => 'index.php?id=7110&MP=7100-1700',
+                            ],
+                            [
+                                'title' => 'Products',
+                                'link' => 'index.php?id=7120&MP=7100-1700',
+                            ],
+                            [
+                                'title' => 'Partners',
+                                'link' => 'index.php?id=7130&MP=7100-1700',
+                            ],
+                        ],
+                    ],
+                    ['title' => 'Page not found', 'link' => '/?id=404'],
+                    // @todo Link should be prefixed with different site
+                    ['title' => 'Our Blog', 'link' => '/?id=2100'],
+                ]
+            ],
+            'ACME Blog' => [
+                2100,
+                [
+                    [
+                        'title' => 'Authors',
+                        'link' => '/?id=2100',
+                        'children' => [
+                            [
+                                'title' => 'John Doe',
+                                'link' => '/john/?id=2110',
+                            ],
+                            [
+                                'title' => 'Jane Doe',
+                                'link' => '/jane/?id=2120',
+                            ],
+                        ],
+                    ],
+                    1 =>
+                        [
+                            'title' => 'Announcements & News',
+                            'link' => '/?id=2700',
+                            'children' => [
+                                [
+                                    'title' => 'Markets',
+                                    'link' => 'index.php?id=7110&MP=7100-2700',
+                                ],
+                                [
+                                    'title' => 'Products',
+                                    'link' => 'index.php?id=7120&MP=7100-2700',
+                                ],
+                                [
+                                    'title' => 'Partners',
+                                    'link' => 'index.php?id=7130&MP=7100-2700',
+                                ],
+                            ],
+                        ],
+                    // @todo Link should be prefixed with different site
+                    ['title' => 'ACME Inc', 'link' => '/?id=acme-first'],
+                ]
+            ]
+        ];
+    }
+
+    /**
+     * @param int $sourcePageId
+     * @param array $expectation
+     *
+     * @test
+     * @dataProvider menuIsGeneratedDataProvider
+     */
+    public function menuIsGenerated(int $sourcePageId, array $expectation)
+    {
+        $response = $this->executeFrontendRequest(
+            (new InternalRequest())
+                ->withPageId($sourcePageId)
+                ->withInstructions([
+                    $this->createMenuProcessorInstruction([
+                        'levels' => 2,
+                        'entryLevel' => 0,
+                        'expandAll' => 1,
+                        'includeSpacer' => 1,
+                        'titleField' => 'title',
+                        'as' => 'results',
+                    ])
+                ]),
+            $this->internalRequestContext
+        );
+
+        $json = json_decode((string)$response->getBody(), true);
+        $json = $this->filterMenu($json);
+
+        static::assertSame($expectation, $json);
+    }
+
+    /**
+     * @param array $typoScript
+     * @return ArrayValueInstruction
+     */
+    private function createTypoLinkUrlInstruction(array $typoScript): ArrayValueInstruction
+    {
+        return (new ArrayValueInstruction(LinkGeneratorController::class))
+            ->withArray([
+                '10' => 'TEXT',
+                '10.' => [
+                    'typolink.' => array_merge(
+                        $typoScript,
+                        ['returnLast' => 'url']
+                    )
+                ]
+            ]);
+    }
+
+    /**
+     * @param array $typoScript
+     * @return ArrayValueInstruction
+     */
+    private function createMenuProcessorInstruction(array $typoScript): ArrayValueInstruction
+    {
+        return (new ArrayValueInstruction(LinkGeneratorController::class))
+            ->withArray([
+                '10' => 'FLUIDTEMPLATE',
+                '10.' => [
+                    'file' => 'typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/FluidJson.html',
+                    'dataProcessing.' => [
+                        '1' => 'TYPO3\\CMS\\Frontend\\DataProcessing\\MenuProcessor',
+                        '1.' => $typoScript
+                    ],
+                ],
+            ]);
+    }
+
+    /**
+     * Filters and keeps only desired names.
+     *
+     * @param array $menu
+     * @param array $keepNames
+     * @return array
+     */
+    private function filterMenu(
+        array $menu,
+        array $keepNames = ['title', 'link']
+    ): array {
+        if (!in_array('children', $keepNames)) {
+            $keepNames[] = 'children';
+        }
+        return array_map(
+            function (array $menuItem) use ($keepNames) {
+                $menuItem = array_filter(
+                    $menuItem,
+                    function (string $name) use ($keepNames) {
+                        return in_array($name, $keepNames);
+                    },
+                    ARRAY_FILTER_USE_KEY
+                );
+                if (is_array($menuItem['children'] ?? null)) {
+                    $menuItem['children'] = $this->filterMenu(
+                        $menuItem['children'],
+                        $keepNames
+                    );
+                }
+                return $menuItem;
+            },
+            $menu
+        );
+    }
+}
index 2c09120..70239a4 100644 (file)
@@ -17,8 +17,8 @@ namespace TYPO3\CMS\Frontend\Tests\Functional\SiteHandling;
 
 use TYPO3\CMS\Core\Core\Bootstrap;
 use TYPO3\CMS\Core\Error\Http\PageNotFoundException;
-use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\ActionService;
-use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\Scenario\DataMapFactory;
+use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\Scenario\DataHandlerFactory;
+use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\Scenario\DataHandlerWriter;
 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest;
 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequestContext;
 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\ResponseContent;
@@ -26,7 +26,7 @@ use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\ResponseContent;
 /**
  * Test case for frontend requests without having site handling configured
  */
-class PlainRequestTest extends AbstractRequestTest
+class PlainRequestTest extends AbstractTestCase
 {
     /**
      * @var string
@@ -38,11 +38,6 @@ class PlainRequestTest extends AbstractRequestTest
      */
     private $internalRequestContext;
 
-    /**
-     * @var ActionService
-     */
-    private $actionService;
-
     protected function setUp()
     {
         parent::setUp();
@@ -51,19 +46,15 @@ class PlainRequestTest extends AbstractRequestTest
         $this->internalRequestContext = (new InternalRequestContext())
             ->withGlobalSettings(['TYPO3_CONF_VARS' => static::TYPO3_CONF_VARS]);
 
-        $this->setUpBackendUserFromFixture(1);
-        $this->actionService = new ActionService();
+        $backendUser = $this->setUpBackendUserFromFixture(1);
         Bootstrap::initializeLanguageObject();
 
         $scenarioFile = __DIR__ . '/Fixtures/scenario.yaml';
-        $factory = DataMapFactory::fromYamlFile($scenarioFile);
-        $this->actionService->invoke(
-            $factory->getDataMap(),
-            [],
-            $factory->getSuggestedIds()
-        );
+        $factory = DataHandlerFactory::fromYamlFile($scenarioFile);
+        $writer = DataHandlerWriter::withBackendUser($backendUser);
+        $writer->invokeFactory($factory);
         static::failIfArrayIsNotEmpty(
-            $this->actionService->getDataHandler()->errorLog
+            $writer->getErrors()
         );
 
         $this->setUpFrontendRootPage(
@@ -92,10 +83,7 @@ class PlainRequestTest extends AbstractRequestTest
 
     protected function tearDown()
     {
-        unset(
-            $this->actionService,
-            $this->internalRequestContext
-        );
+        unset($this->internalRequestContext);
         parent::tearDown();
     }
 
@@ -222,18 +210,14 @@ class PlainRequestTest extends AbstractRequestTest
      */
     public function pageIsRenderedWithDomainsDataProvider(): array
     {
-        $uris = [
-            'https://archive.acme.com/?id=3100' => 'EN: Statistics',
-            'https://archive.acme.com/?id=3110' => 'EN: Markets',
-            'https://archive.acme.com/?id=3120' => 'EN: Products',
-            'https://archive.acme.com/?id=3130' => 'EN: Partners',
+        $instructions = [
+            ['https://archive.acme.com/?id=3100', 'EN: Statistics'],
+            ['https://archive.acme.com/?id=3110', 'EN: Markets'],
+            ['https://archive.acme.com/?id=3120', 'EN: Products'],
+            ['https://archive.acme.com/?id=3130', 'EN: Partners'],
         ];
 
-        $data = [];
-        foreach ($uris as $uri => $expectation) {
-            $data[$uri] = [$uri, $expectation];
-        }
-        return $data;
+        return $this->keysFromTemplate($instructions, '%1$s');
     }
 
     /**
@@ -270,6 +254,154 @@ class PlainRequestTest extends AbstractRequestTest
     /**
      * @return array
      */
+    public function restrictedPageIsRenderedDataProvider(): array
+    {
+        $instructions = [
+            // frontend user 1
+            ['https://website.local/?id=1510', 1, 'Whitepapers'],
+            ['https://website.local/?id=1511', 1, 'Products'],
+            ['https://website.local/?id=1512', 1, 'Solutions'],
+            // frontend user 2
+            ['https://website.local/?id=1510', 2, 'Whitepapers'],
+            ['https://website.local/?id=1511', 2, 'Products'],
+            ['https://website.local/?id=1515', 2, 'Research'],
+            ['https://website.local/?id=1520', 2, 'Forecasts'],
+            ['https://website.local/?id=1521', 2, 'Current Year'],
+            // frontend user 3
+            ['https://website.local/?id=1510', 3, 'Whitepapers'],
+            ['https://website.local/?id=1511', 3, 'Products'],
+            ['https://website.local/?id=1512', 3, 'Solutions'],
+            ['https://website.local/?id=1515', 3, 'Research'],
+            ['https://website.local/?id=1520', 3, 'Forecasts'],
+            ['https://website.local/?id=1521', 3, 'Current Year'],
+        ];
+
+        return $this->keysFromTemplate($instructions, '%1$s (user:%2$s)');
+    }
+
+    /**
+     * @param string $uri
+     * @param int $frontendUserId
+     * @param string $expectedPageTitle
+     *
+     * @test
+     * @dataProvider restrictedPageIsRenderedDataProvider
+     */
+    public function restrictedPageIsRendered(string $uri, int $frontendUserId, string $expectedPageTitle)
+    {
+        $response = $this->executeFrontendRequest(
+            new InternalRequest($uri),
+            $this->internalRequestContext
+                ->withFrontendUserId($frontendUserId)
+        );
+        $responseStructure = ResponseContent::fromString(
+            (string)$response->getBody()
+        );
+
+        static::assertSame(
+            200,
+            $response->getStatusCode()
+        );
+        static::assertSame(
+            $this->siteTitle,
+            $responseStructure->getScopePath('template/sitetitle')
+        );
+        static::assertSame(
+            $expectedPageTitle,
+            $responseStructure->getScopePath('page/title')
+        );
+    }
+
+    /**
+     * @return array
+     */
+    public function restrictedPageSendsForbiddenResponseWithUnauthorizedVisitorDataProvider(): array
+    {
+        $instructions = [
+            // no frontend user given
+            ['https://website.local/?id=1510', 0],
+            ['https://website.local/?id=1511', 0],
+            ['https://website.local/?id=1512', 0],
+            ['https://website.local/?id=1515', 0],
+            ['https://website.local/?id=1520', 0],
+            ['https://website.local/?id=1521', 0],
+            // frontend user 1
+            ['https://website.local/?id=1515', 1],
+            ['https://website.local/?id=1520', 1],
+            ['https://website.local/?id=1521', 1],
+            // frontend user 2
+            ['https://website.local/?id=1512', 2],
+        ];
+
+        return $this->keysFromTemplate($instructions, '%1$s (user:%2$s)');
+    }
+
+    /**
+     * @param string $uri
+     * @param int $frontendUserId
+     *
+     * @test
+     * @dataProvider restrictedPageSendsForbiddenResponseWithUnauthorizedVisitorDataProvider
+     */
+    public function restrictedPageSendsForbiddenResponseWithUnauthorizedVisitorUsingDefaultErrorHandling(string $uri, int $frontendUserId)
+    {
+        $response = $this->executeFrontendRequest(
+            new InternalRequest($uri),
+            $this->internalRequestContext
+                ->withFrontendUserId($frontendUserId)
+        );
+
+        static::assertSame(
+            403,
+            $response->getStatusCode()
+        );
+        static::assertThat(
+            (string)$response->getBody(),
+            static::logicalOr(
+                static::stringContains('Reason: ID was not an accessible page'),
+                static::stringContains('Reason: Subsection was found and not accessible')
+            )
+        );
+    }
+
+    /**
+     * @param string $uri
+     * @param int $frontendUserId
+     *
+     * @test
+     * @dataProvider restrictedPageSendsForbiddenResponseWithUnauthorizedVisitorDataProvider
+     */
+    public function restrictedPageSendsForbiddenResponseWithUnauthorizedVisitorUsingCustomErrorHandling(string $uri, int $frontendUserId)
+    {
+        $response = $this->executeFrontendRequest(
+            new InternalRequest($uri),
+            $this->internalRequestContext
+                ->withFrontendUserId($frontendUserId)
+                ->withMergedGlobalSettings([
+                    'TYPO3_CONF_VARS' => [
+                        'FE' => [
+                            'pageNotFound_handling' => 'READFILE:typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/PageError.txt',
+                        ]
+                    ]
+                ])
+        );
+
+        static::assertSame(
+            403,
+            $response->getStatusCode()
+        );
+        static::assertThat(
+            (string)$response->getBody(),
+            static::logicalOr(
+                static::stringContains('reason: ID was not an accessible page'),
+                static::stringContains('reason: Subsection was found and not accessible')
+            )
+        );
+    }
+
+    /**
+     * @return array
+     */
     public function pageRenderingStopsWithInvalidCacheHashDataProvider(): array
     {
         $domainPaths = [
index cfe86a8..c6292fc 100644 (file)
@@ -17,8 +17,8 @@ namespace TYPO3\CMS\Frontend\Tests\Functional\SiteHandling;
 
 use TYPO3\CMS\Core\Core\Bootstrap;
 use TYPO3\CMS\Core\Error\Http\PageNotFoundException;
-use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\ActionService;
-use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\Scenario\DataMapFactory;
+use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\Scenario\DataHandlerFactory;
+use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\Scenario\DataHandlerWriter;
 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest;
 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequestContext;
 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\ResponseContent;
@@ -26,7 +26,7 @@ use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\ResponseContent;
 /**
  * Test case for frontend requests having site handling configured
  */
-class SiteRequestTest extends AbstractRequestTest
+class SiteRequestTest extends AbstractTestCase
 {
     /**
      * @var string
@@ -38,11 +38,6 @@ class SiteRequestTest extends AbstractRequestTest
      */
     private $internalRequestContext;
 
-    /**
-     * @var ActionService
-     */
-    private $actionService;
-
     protected function setUp()
     {
         parent::setUp();
@@ -51,19 +46,15 @@ class SiteRequestTest extends AbstractRequestTest
         $this->internalRequestContext = (new InternalRequestContext())
             ->withGlobalSettings(['TYPO3_CONF_VARS' => static::TYPO3_CONF_VARS]);
 
-        $this->setUpBackendUserFromFixture(1);
-        $this->actionService = new ActionService();
+        $backendUser = $this->setUpBackendUserFromFixture(1);
         Bootstrap::initializeLanguageObject();
 
         $scenarioFile = __DIR__ . '/Fixtures/scenario.yaml';
-        $factory = DataMapFactory::fromYamlFile($scenarioFile);
-        $this->actionService->invoke(
-            $factory->getDataMap(),
-            [],
-            $factory->getSuggestedIds()
-        );
+        $factory = DataHandlerFactory::fromYamlFile($scenarioFile);
+        $writer = DataHandlerWriter::withBackendUser($backendUser);
+        $writer->invokeFactory($factory);
         static::failIfArrayIsNotEmpty(
-            $this->actionService->getDataHandler()->errorLog
+            $writer->getErrors()
         );
 
         $this->setUpFrontendRootPage(
@@ -81,10 +72,7 @@ class SiteRequestTest extends AbstractRequestTest
 
     protected function tearDown()
     {
-        unset(
-            $this->actionService,
-            $this->internalRequestContext
-        );
+        unset($this->internalRequestContext);
         parent::tearDown();
     }
 
@@ -296,6 +284,233 @@ class SiteRequestTest extends AbstractRequestTest
     /**
      * @return array
      */
+    public function restrictedPageIsRenderedDataProvider(): array
+    {
+        $instructions = [
+            // frontend user 1
+            ['https://website.local/?id=1510', 1, 'Whitepapers'],
+            ['https://website.local/?id=1511', 1, 'Products'],
+            ['https://website.local/?id=1512', 1, 'Solutions'],
+            // frontend user 2
+            ['https://website.local/?id=1510', 2, 'Whitepapers'],
+            ['https://website.local/?id=1511', 2, 'Products'],
+            ['https://website.local/?id=1515', 2, 'Research'],
+            ['https://website.local/?id=1520', 2, 'Forecasts'],
+            ['https://website.local/?id=1521', 2, 'Current Year'],
+            // frontend user 3
+            ['https://website.local/?id=1510', 3, 'Whitepapers'],
+            ['https://website.local/?id=1511', 3, 'Products'],
+            ['https://website.local/?id=1512', 3, 'Solutions'],
+            ['https://website.local/?id=1515', 3, 'Research'],
+            ['https://website.local/?id=1520', 3, 'Forecasts'],
+            ['https://website.local/?id=1521', 3, 'Current Year'],
+        ];
+
+        return $this->keysFromTemplate($instructions, '%1$s (user:%2$s)');
+    }
+
+    /**
+     * @param string $uri
+     * @param int $frontendUserId
+     * @param string $expectedPageTitle
+     *
+     * @test
+     * @dataProvider restrictedPageIsRenderedDataProvider
+     */
+    public function restrictedPageIsRendered(string $uri, int $frontendUserId, string $expectedPageTitle)
+    {
+        $this->writeSiteConfiguration(
+            'website-local',
+            $this->buildSiteConfiguration(1000, 'https://website.local/')
+        );
+
+        $response = $this->executeFrontendRequest(
+            new InternalRequest($uri),
+            $this->internalRequestContext
+                ->withFrontendUserId($frontendUserId)
+        );
+        $responseStructure = ResponseContent::fromString(
+            (string)$response->getBody()
+        );
+
+        static::assertSame(
+            200,
+            $response->getStatusCode()
+        );
+        static::assertSame(
+            $this->siteTitle,
+            $responseStructure->getScopePath('template/sitetitle')
+        );
+        static::assertSame(
+            $expectedPageTitle,
+            $responseStructure->getScopePath('page/title')
+        );
+    }
+
+    /**
+     * @return array
+     */
+    public function restrictedPageSendsForbiddenResponseWithUnauthorizedVisitorDataProvider(): array
+    {
+        $instructions = [
+            // no frontend user given
+            ['https://website.local/?id=1510', 0],
+            ['https://website.local/?id=1511', 0],
+            ['https://website.local/?id=1512', 0],
+            ['https://website.local/?id=1515', 0],
+            ['https://website.local/?id=1520', 0],
+            ['https://website.local/?id=1521', 0],
+            // frontend user 1
+            ['https://website.local/?id=1515', 1],
+            ['https://website.local/?id=1520', 1],
+            ['https://website.local/?id=1521', 1],
+            // frontend user 2
+            ['https://website.local/?id=1512', 2],
+        ];
+
+        return $this->keysFromTemplate($instructions, '%1$s (user:%2$s)');
+    }
+
+    /**
+     * @param string $uri
+     * @param int $frontendUserId
+     *
+     * @test
+     * @dataProvider restrictedPageSendsForbiddenResponseWithUnauthorizedVisitorDataProvider
+     */
+    public function restrictedPageSendsForbiddenResponseWithUnauthorizedVisitorWithoutHavingErrorHandling(string $uri, int $frontendUserId)
+    {
+        $this->writeSiteConfiguration(
+            'website-local',
+            $this->buildSiteConfiguration(1000, 'https://website.local/')
+        );
+
+        $response = $this->executeFrontendRequest(
+            new InternalRequest($uri),
+            $this->internalRequestContext
+                ->withFrontendUserId($frontendUserId)
+        );
+
+        static::assertSame(
+            403,
+            $response->getStatusCode()
+        );
+        static::assertThat(
+            (string)$response->getBody(),
+            static::logicalOr(
+                static::stringContains('Reason: ID was not an accessible page'),
+                static::stringContains('Reason: Subsection was found and not accessible')
+            )
+        );
+    }
+
+    /**
+     * @param string $uri
+     * @param int $frontendUserId
+     *
+     * @test
+     * @dataProvider restrictedPageSendsForbiddenResponseWithUnauthorizedVisitorDataProvider
+     */
+    public function restrictedPageSendsForbiddenResponseWithUnauthorizedVisitorWithHavingFluidErrorHandling(string $uri, int $frontendUserId)
+    {
+        $this->writeSiteConfiguration(
+            'website-local',
+            $this->buildSiteConfiguration(1000, 'https://website.local/'),
+            [],
+            $this->buildErrorHandlingConfiguration('Fluid', [403])
+        );
+
+        $response = $this->executeFrontendRequest(
+            new InternalRequest($uri),
+            $this->internalRequestContext
+                ->withFrontendUserId($frontendUserId)
+        );
+
+        static::assertSame(
+            403,
+            $response->getStatusCode()
+        );
+        static::assertContains(
+            'reasons: code,fe_group',
+            (string)$response->getBody()
+        );
+        static::assertThat(
+            (string)$response->getBody(),
+            static::logicalOr(
+                static::stringContains('message: ID was not an accessible page'),
+                static::stringContains('message: Subsection was found and not accessible')
+            )
+        );
+    }
+
+    /**
+     * @param string $uri
+     * @param int $frontendUserId
+     *
+     * @test
+     * @dataProvider restrictedPageSendsForbiddenResponseWithUnauthorizedVisitorDataProvider
+     * @todo Response body cannot be asserted since PageContentErrorHandler::handlePageError executes request via HTTP (not internally)
+     */
+    public function restrictedPageSendsForbiddenResponseWithUnauthorizedVisitorWithHavingPageErrorHandling(string $uri, int $frontendUserId)
+    {
+        $this->writeSiteConfiguration(
+            'website-local',
+            $this->buildSiteConfiguration(1000, 'https://website.local/'),
+            [],
+            $this->buildErrorHandlingConfiguration('Page', [403])
+        );
+
+        $response = $this->executeFrontendRequest(
+            new InternalRequest($uri),
+            $this->internalRequestContext
+                ->withFrontendUserId($frontendUserId)
+        );
+
+        static::assertSame(
+            403,
+            $response->getStatusCode()
+        );
+    }
+
+    /**
+     * @param string $uri
+     * @param int $frontendUserId
+     *
+     * @test
+     * @dataProvider restrictedPageSendsForbiddenResponseWithUnauthorizedVisitorDataProvider
+     */
+    public function restrictedPageSendsForbiddenResponseWithUnauthorizedVisitorWithHavingPhpErrorHandling(string $uri, int $frontendUserId)
+    {
+        $this->writeSiteConfiguration(
+            'website-local',
+            $this->buildSiteConfiguration(1000, 'https://website.local/'),
+            [],
+            $this->buildErrorHandlingConfiguration('PHP', [403])
+        );
+
+        $response = $this->executeFrontendRequest(
+            new InternalRequest($uri),
+            $this->internalRequestContext
+                ->withFrontendUserId($frontendUserId)
+        );
+        $json = json_decode((string)$response->getBody(), true);
+
+        static::assertSame(
+            403,
+            $response->getStatusCode()
+        );
+        static::assertThat(
+            $json['message'] ?? null,
+            static::logicalOr(
+                static::identicalTo('ID was not an accessible page'),
+                static::identicalTo('Subsection was found and not accessible')
+            )
+        );
+    }
+
+    /**
+     * @return array
+     */
     public function pageRenderingStopsWithInvalidCacheHashDataProvider(): array
     {
         $domainPaths = [