[TASK] Add frontend functional tests for pages having slugs 15/58015/17
authorOliver Hader <oliver@typo3.org>
Fri, 31 Aug 2018 07:39:35 +0000 (09:39 +0200)
committerFrans Saris <franssaris@gmail.com>
Fri, 31 Aug 2018 12:21:02 +0000 (14:21 +0200)
Functional tests for pages having site configuration and using slugs
are introduced. Existing tests (not relying on slugs) are still kept
in addition to the new tests in order to still have tests for legacy
"index.php?id=123" behavior.

Resolves: #85962
Releases: master
Change-Id: I7902bfd028f9c8a1d41f3573d72a2a7e96148b43
Reviewed-on: https://review.typo3.org/58015
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Benni Mack <benni@typo3.org>
Tested-by: Benni Mack <benni@typo3.org>
Reviewed-by: Frans Saris <franssaris@gmail.com>
Tested-by: Frans Saris <franssaris@gmail.com>
typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/SlugScenario.yaml [new file with mode: 0644]
typo3/sysext/frontend/Tests/Functional/SiteHandling/SlugLinkGeneratorTest.php [new file with mode: 0644]
typo3/sysext/frontend/Tests/Functional/SiteHandling/SlugSiteRequestTest.php [new file with mode: 0644]

diff --git a/typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/SlugScenario.yaml b/typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/SlugScenario.yaml
new file mode 100644 (file)
index 0000000..e7fb802
--- /dev/null
@@ -0,0 +1,139 @@
+__variables:
+  - &pageStandard 0
+  - &pageShortcut 4
+  - &pageMount 7
+  - &pageFolder 254
+  - &contentText 'text'
+  - &idAcmeRootPage 1000
+  - &idAcmeFirstPage 1100
+
+entitySettings:
+  '*':
+    nodeColumnName: 'pid'
+    columnNames: {id: 'uid', language: 'sys_language_uid'}
+    defaultValues: {pid: 0}
+  page:
+    isNode: true
+    tableName: 'pages'
+    parentColumnName: 'pid'
+    languageColumnNames: ['l10n_parent', 'l10n_source']
+    columnNames: {type: 'doktype', root: 'is_siteroot', mount: 'mount_pid', visitorGroups: 'fe_group'}
+    defaultValues: {hidden: 0, doktype: *pageStandard}
+    valueInstructions:
+      shortcut:
+        first: {shortcut: 0, shortcut_mode: 1}
+  content:
+    tableName: 'tt_content'
+    languageColumnNames: ['l18n_parent', 'l10n_source']
+    columnNames: {title: 'header', type: 'CType'}
+  domain:
+    tableName: 'sys_domain'
+  workspace:
+    tableName: 'sys_workspace'
+  language:
+    tableName: 'sys_language'
+    columnNames: {code: 'language_isocode'}
+  visitorGroup:
+    tableName: 'fe_groups'
+  visitor:
+    tableName: 'fe_users'
+    columnNames: {groups: 'usergroup'}
+  typoscript:
+    tableName: 'sys_template'
+    valueInstructions:
+      type:
+        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'}
+  page:
+    - self: {id: *idAcmeRootPage, title: 'ACME Inc', type: *pageShortcut, shortcut: 'first', root: true, alias: 'acme-root', slug: '/'}
+      children:
+        - self: {id: *idAcmeFirstPage, title: 'EN: Welcome', alias: 'acme-first', slug: '/welcome'}
+          languageVariants:
+            - self: {id: 1101, title: 'FR: Welcome', language: 1, slug: '/bienvenue'}
+            - self: {id: 1102, title: 'FR-CA: Welcome', language: 2, slug: '/bienvenue'}
+          versionVariants:
+            - version: {title: 'EN: Welcome to ACME Inc', workspace: 1, slug: '/welcome'}
+          entities:
+            content:
+              - self: {title: 'EN: Content Element #1', type: *contentText}
+                # @todo does not work due to a bug in DataHandler's remap stack for l10n_source
+                languageVariants:
+                  - self: {title: 'FR: Content Element #1', type: *contentText, language: 1}
+                    languageVariants:
+                      - self: {title: 'FR-CA: Content Element #1', type: *contentText, language: 2}
+              - self: {title: 'EN: Content Element #2', type: *contentText}
+        - self: {id: 1200, title: 'EN: Features', slug: '/features'}
+          children:
+            - self: {id: 1210, title: 'EN: Frontend Editing', slug: '/features/frontend-editing'}
+        - self: {id: 1300, title: 'EN: Products', root: true, slug: '/products'}
+          children:
+            - self: {id: 1310, title: 'EN: Planets', slug: '/products/planets'}
+            - self: {id: 1320, title: 'EN: Spaceships', slug: '/products/spaceships'}
+            - self: {id: 1330, title: 'EN: Dark Matter', slug: '/products/dark-matter'}
+        - self: {id: 1500, title: 'Internal', slug: '/my-acme'}
+          children:
+            - self: {id: 1510, title: 'Whitepapers', visitorGroups: -2, extendToSubpages: true, slug: '/my-acme/whitepapers'}
+              children:
+                - self: {id: 1511, title: 'Products', slug: '/my-acme/whitepapers/products'}
+                - self: {id: 1512, title: 'Solutions', visitorGroups: 10, slug: '/my-acme/whitepapers/solutions'}
+                - self: {id: 1515, title: 'Research', visitorGroups: 20, slug: '/my-acme/whitepapers/research'}
+            - self: {id: 1520, title: 'Forecasts', visitorGroups: 20, extendToSubpages: true, slug: '/my-acme/forecasts'}
+              children:
+                - self: {id: 1521, title: 'Current Year', slug: '/my-acme/forecasts/current-year'}
+                - self: {id: 1522, title: 'Next Year', slug: '/my-acme/forecasts/next-year'}
+                - self: {id: 1523, title: 'Five Years', slug: '/my-acme/forecasts/five-years'}
+        - self: {id: 1600, title: 'About us', slug: '/about'}
+        - self: {id: 1700, title: 'Announcements & News', type: *pageMount, mount: 7100, slug: '/news'}
+        - self: {id: 404, title: 'Page not found', slug: '/404'}
+          entities:
+            content:
+              - self: {title: 'EN: Page not found', type: *contentText}
+        - self: {id: 1930, title: 'Our Blog', type: *pageShortcut, shortcut: 2000, slug: '/blog'}
+        - version: {id: 1950, title: 'EN: Goodbye', workspace: 1, slug: '/bye'}
+        - self: {id: 1990, title: 'Storage', type: *pageFolder}
+          entities:
+            visitorGroup:
+              - self: {id: 10, title: 'Customers'}
+              - self: {id: 20, title: 'Partners'}
+            visitor:
+              - self: {id: 1, username: 'john@doe.local', groups: '10'}
+              - self: {id: 2, username: 'manager@other-inc.local', groups: '20'}
+              - self: {id: 3, username: 'big-boss@acme-inc.local', groups: '10,20'}
+    - self: {id: 2000, title: 'ACME Blog', type: *pageShortcut, shortcut: 'first', root: true, alias: 'blog-root', slug: '/'}
+      children:
+        - self: {id: 2100, title: 'Authors', slug: '/authors'}
+          children:
+            - self: {id: 2110, title: 'John Doe', slug: '/john'}
+              children:
+                - self: {id: 2111, title: 'About', slug: '/about-john'}
+            - self: {id: 2120, title: 'Jane Doe', slug: '/jane'}
+              children:
+                - self: {id: 2121, title: 'About', slug: '/about-jane'}
+        - self: {id: 2700, title: 'Announcements & News', type: *pageMount, mount: 7100, slug: '/news'}
+        - self: {id: 2930, title: 'ACME Inc', type: *pageShortcut, shortcut: 1000, slug: '/acme'}
+    - self: {id: 3000, title: 'ACME Archive', type: *pageShortcut, shortcut: 'first', root: true}
+      children:
+        - self: {id: 3100, title: 'EN: Statistics'}
+          languageVariants:
+            - self: {id: 3101, title: 'FR: Statistics', language: 1}
+            - self: {id: 3102, title: 'FR-CA: Statistics', language: 2}
+          children:
+            - self: {id: 3110, title: 'EN: Markets'}
+            - self: {id: 3120, title: 'EN: Products'}
+            - self: {id: 3130, title: 'EN: Partners'}
+      entities:
+        domain:
+          - self: {domainName: 'archive.acme.com'}
+    - self: {id: 7000, title: 'Common Collection', type: *pageFolder}
+      children:
+        - self: {id: 7100, title: 'Announcements & News', slug: '/common/news'}
+          children:
+            - self: {id: 7110, title: 'Markets', slug: '/common/markets'}
+            - self: {id: 7120, title: 'Products', slug: '/common/products'}
+            - self: {id: 7130, title: 'Partners', slug: '/common/partners'}
diff --git a/typo3/sysext/frontend/Tests/Functional/SiteHandling/SlugLinkGeneratorTest.php b/typo3/sysext/frontend/Tests/Functional/SiteHandling/SlugLinkGeneratorTest.php
new file mode 100644 (file)
index 0000000..af44304
--- /dev/null
@@ -0,0 +1,823 @@
+<?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 SlugLinkGeneratorTest extends AbstractTestCase
+{
+    /**
+     * @var string
+     */
+    private $siteTitle = 'A Company that Manufactures Everything Inc';
+
+    /**
+     * @var InternalRequestContext
+     */
+    private $internalRequestContext;
+
+    public static function setUpBeforeClass()
+    {
+        parent::setUpBeforeClass();
+        static::initializeDatabaseSnapshot();
+    }
+
+    public static function tearDownAfterClass()
+    {
+        static::destroyDatabaseSnapshot();
+        parent::tearDownAfterClass();
+    }
+
+    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]);
+
+        $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/')
+        );
+
+        $this->withDatabaseSnapshot(function () {
+            $this->setUpDatabase();
+        });
+    }
+
+    protected function setUpDatabase()
+    {
+        $backendUser = $this->setUpBackendUserFromFixture(1);
+        Bootstrap::initializeLanguageObject();
+
+        $scenarioFile = __DIR__ . '/Fixtures/SlugScenario.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 Blog',
+                'sitetitle' => $this->siteTitle,
+            ]
+        );
+    }
+
+    protected function tearDown()
+    {
+        unset($this->internalRequestContext);
+        parent::tearDown();
+    }
+
+    /**
+     * @return array
+     */
+    public function linkIsGeneratedDataProvider(): array
+    {
+        $instructions = [
+            // acme.com -> acme.com (same site)
+            ['https://acme.us/', 1100, 1000, '/'],
+            ['https://acme.us/', 1100, 1100, '/welcome'],
+            ['https://acme.us/', 1100, 1200, '/features'],
+            ['https://acme.us/', 1100, 1210, '/features/frontend-editing'],
+            ['https://acme.us/', 1100, 404, '/404'],
+            // acme.com -> products.acme.com (nested sub-site)
+            ['https://acme.us/', 1100, 1300, 'https://products.acme.com/products'],
+            ['https://acme.us/', 1100, 1310, 'https://products.acme.com/products/planets'],
+            // acme.com -> blog.acme.com (different site)
+            ['https://acme.us/', 1100, 2000, 'https://blog.acme.com/'],
+            ['https://acme.us/', 1100, 2100, 'https://blog.acme.com/authors'],
+            ['https://acme.us/', 1100, 2110, 'https://blog.acme.com/john/john'],
+            ['https://acme.us/', 1100, 2111, 'https://blog.acme.com/john/about-john'],
+            // blog.acme.com -> acme.com (different site)
+            ['https://blog.acme.com/', 2100, 1000, 'https://acme.us/'],
+            ['https://blog.acme.com/', 2100, 1100, 'https://acme.us/welcome'],
+            ['https://blog.acme.com/', 2100, 1200, 'https://acme.us/features'],
+            ['https://blog.acme.com/', 2100, 1210, 'https://acme.us/features/frontend-editing'],
+            ['https://blog.acme.com/', 2100, 404, 'https://acme.us/404'],
+            // blog.acme.com -> products.acme.com (different sub-site)
+            ['https://blog.acme.com/', 2100, 1300, 'https://products.acme.com/products'],
+            ['https://blog.acme.com/', 2100, 1310, 'https://products.acme.com/products/planets'],
+        ];
+
+        return $this->keysFromTemplate(
+            $instructions,
+            '%2$d->%3$d'
+        );
+    }
+
+    /**
+     * @param string $hostPrefix
+     * @param int $sourcePageId
+     * @param int $targetPageId
+     * @param string $expectation
+     *
+     * @test
+     * @dataProvider linkIsGeneratedDataProvider
+     */
+    public function linkIsGenerated(string $hostPrefix, int $sourcePageId, int $targetPageId, string $expectation)
+    {
+        $response = $this->executeFrontendRequest(
+            (new InternalRequest($hostPrefix))
+                ->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)
+            ['https://acme.us/', [7100, 1700], 7110, 1000, '/'],
+            ['https://acme.us/', [7100, 1700], 7110, 1100, '/welcome'],
+            ['https://acme.us/', [7100, 1700], 7110, 1200, '/features'],
+            ['https://acme.us/', [7100, 1700], 7110, 1210, '/features/frontend-editing'],
+            ['https://acme.us/', [7100, 1700], 7110, 404, '/404'],
+            // acme.com -> products.acme.com (nested sub-site)
+            ['https://acme.us/', [7100, 1700], 7110, 1300, 'https://products.acme.com/products'],
+            ['https://acme.us/', [7100, 1700], 7110, 1310, 'https://products.acme.com/products/planets'],
+            // acme.com -> blog.acme.com (different site)
+            ['https://acme.us/', [7100, 1700], 7110, 2000, 'https://blog.acme.com/'],
+            ['https://acme.us/', [7100, 1700], 7110, 2100, 'https://blog.acme.com/authors'],
+            ['https://acme.us/', [7100, 1700], 7110, 2110, 'https://blog.acme.com/john/john'],
+            ['https://acme.us/', [7100, 1700], 7110, 2111, 'https://blog.acme.com/john/about-john'],
+            // blog.acme.com -> acme.com (different site)
+            ['https://blog.acme.com/', [7100, 2700], 7110, 1000, 'https://acme.us/'],
+            ['https://blog.acme.com/', [7100, 2700], 7110, 1100, 'https://acme.us/welcome'],
+            ['https://blog.acme.com/', [7100, 2700], 7110, 1200, 'https://acme.us/features'],
+            ['https://blog.acme.com/', [7100, 2700], 7110, 1210, 'https://acme.us/features/frontend-editing'],
+            ['https://blog.acme.com/', [7100, 2700], 7110, 404, 'https://acme.us/404'],
+            // blog.acme.com -> products.acme.com (different sub-site)
+            ['https://blog.acme.com/', [7100, 2700], 7110, 1300, 'https://products.acme.com/products'],
+            ['https://blog.acme.com/', [7100, 2700], 7110, 1310, 'https://products.acme.com/products/planets'],
+        ];
+
+        return $this->keysFromTemplate(
+            $instructions,
+            '%3$d->%4$d (mount:%2$s)',
+            function (array $items) {
+                array_splice(
+                    $items,
+                    1,
+                    1,
+                    [implode('->', $items[1])]
+                );
+                return $items;
+            }
+        );
+    }
+
+    /**
+     * @param string $hostPrefix
+     * @param array $pageMount
+     * @param int $sourcePageId
+     * @param int $targetPageId
+     * @param string $expectation
+     *
+     * @test
+     * @dataProvider linkIsGeneratedFromMountPointDataProvider
+     */
+    public function linkIsGeneratedFromMountPoint(string $hostPrefix, array $pageMount, int $sourcePageId, int $targetPageId, string $expectation)
+    {
+        $response = $this->executeFrontendRequest(
+            (new InternalRequest($hostPrefix))
+                ->withMountPoint(...$pageMount)
+                ->withPageId($sourcePageId)
+                ->withInstructions([
+                    $this->createTypoLinkUrlInstruction([
+                        'parameter' => $targetPageId,
+                    ])
+                ]),
+            $this->internalRequestContext
+        );
+
+        static::assertSame($expectation, (string)$response->getBody());
+    }
+
+    /**
+     * @return array
+     */
+    public function linkIsGeneratedForLanguageDataProvider(): array
+    {
+        // @todo localized pages are not applied
+        $instructions = [
+            // acme.com -> acme.com (same site)
+            ['https://acme.us/', 1100, 1100, 0, '/welcome'],
+            ['https://acme.us/', 1100, 1100, 1, 'https://acme.fr/bienvenue'],
+            ['https://acme.us/', 1100, 1100, 2, 'https://acme.ca/bienvenue'],
+            ['https://acme.us/', 1100, 1101, 0, 'https://acme.fr/bienvenue'],
+            ['https://acme.us/', 1100, 1102, 0, 'https://acme.ca/bienvenue'],
+            // acme.com -> products.acme.com (nested sub-site)
+            ['https://acme.us/', 1100, 1300, 0, 'https://products.acme.com/products'],
+            ['https://acme.us/', 1100, 1310, 0, 'https://products.acme.com/products/planets'],
+            // acme.com -> archive (outside site)
+            ['https://acme.us/', 1100, 3100, 0, '/index.php?id=3100&L=0'],
+            ['https://acme.us/', 1100, 3100, 1, '/index.php?id=3100&L=1'],
+            ['https://acme.us/', 1100, 3100, 2, '/index.php?id=3100&L=2'],
+            ['https://acme.us/', 1100, 3101, 0, '/index.php?id=3100&L=1'],
+            ['https://acme.us/', 1100, 3102, 0, '/index.php?id=3100&L=2'],
+            // blog.acme.com -> acme.com (different site)
+            ['https://blog.acme.com/', 2100, 1100, 0, 'https://acme.us/welcome'],
+            ['https://blog.acme.com/', 2100, 1100, 1, 'https://acme.fr/bienvenue'],
+            ['https://blog.acme.com/', 2100, 1100, 2, 'https://acme.ca/bienvenue'],
+            ['https://blog.acme.com/', 2100, 1101, 0, 'https://acme.fr/bienvenue'],
+            ['https://blog.acme.com/', 2100, 1102, 0, 'https://acme.ca/bienvenue'],
+            // blog.acme.com -> archive (outside site)
+            ['https://blog.acme.com/', 2100, 3100, 0, '/index.php?id=3100&L=0'],
+            ['https://blog.acme.com/', 2100, 3100, 1, '/index.php?id=3100&L=1'],
+            ['https://blog.acme.com/', 2100, 3100, 2, '/index.php?id=3100&L=2'],
+            ['https://blog.acme.com/', 2100, 3101, 0, '/index.php?id=3100&L=1'],
+            ['https://blog.acme.com/', 2100, 3102, 0, '/index.php?id=3100&L=2'],
+            // blog.acme.com -> products.acme.com (different sub-site)
+            ['https://blog.acme.com/', 2100, 1300, 0, 'https://products.acme.com/products'],
+            ['https://blog.acme.com/', 2100, 1310, 0, 'https://products.acme.com/products/planets'],
+        ];
+
+        return $this->keysFromTemplate(
+            $instructions,
+            '%2$d->%3$d (lang:%4$d)'
+        );
+    }
+
+    /**
+     * @param string $hostPrefix
+     * @param int $sourcePageId
+     * @param int $targetPageId
+     * @param int $targetLanguageId
+     * @param string $expectation
+     *
+     * @test
+     * @dataProvider linkIsGeneratedForLanguageDataProvider
+     */
+    public function linkIsGeneratedForLanguage(string $hostPrefix, int $sourcePageId, int $targetPageId, int $targetLanguageId, string $expectation)
+    {
+        $response = $this->executeFrontendRequest(
+            (new InternalRequest($hostPrefix))
+                ->withPageId($sourcePageId)
+                ->withInstructions([
+                    $this->createTypoLinkUrlInstruction([
+                        'parameter' => $targetPageId,
+                        'language' => $targetLanguageId,
+                    ])
+                ]),
+            $this->internalRequestContext
+        );
+
+        static::assertSame($expectation, (string)$response->getBody());
+    }
+
+    /**
+     * @return array
+     */
+    public function linkIsGeneratedWithQueryParametersDataProvider(): array
+    {
+        $instructions = [
+            // acme.com -> acme.com (same site)
+            ['https://acme.us/', 1100, 1000, '/?testing%5Bvalue%5D=1&cHash=7d1f13fa91159dac7feb3c824936b39d'],
+            ['https://acme.us/', 1100, 1100, '/welcome?testing%5Bvalue%5D=1&cHash=f42b850e435f0cedd366f5db749fc1af'],
+            ['https://acme.us/', 1100, 1200, '/features?testing%5Bvalue%5D=1&cHash=784e11c50ea1a13fd7d969df4ec53ea3'],
+            ['https://acme.us/', 1100, 1210, '/features/frontend-editing?testing%5Bvalue%5D=1&cHash=ccb7067022b9835ebfd8f720722bc708'],
+            ['https://acme.us/', 1100, 404, '/404?testing%5Bvalue%5D=1&cHash=864e96f586a78a53452f3bf0f4d24591'],
+            // acme.com -> products.acme.com (nested sub-site)
+            ['https://acme.us/', 1100, 1300, 'https://products.acme.com/products?testing%5Bvalue%5D=1&cHash=dbd6597d72ed5098cce3d03eac1eeefe'],
+            ['https://acme.us/', 1100, 1310, 'https://products.acme.com/products/planets?testing%5Bvalue%5D=1&cHash=e64bfc7ab7dd6b70d161e4d556be9726'],
+            // acme.com -> blog.acme.com (different site)
+            ['https://acme.us/', 1100, 2000, 'https://blog.acme.com/?testing%5Bvalue%5D=1&cHash=a14da633e46dba71640cb85226cd12c5'],
+            ['https://acme.us/', 1100, 2100, 'https://blog.acme.com/authors?testing%5Bvalue%5D=1&cHash=d23d74cb50383f8788a9930ec8ba679f'],
+            ['https://acme.us/', 1100, 2110, 'https://blog.acme.com/john/john?testing%5Bvalue%5D=1&cHash=bf25eea89f44a9a79dabdca98f38a432'],
+            ['https://acme.us/', 1100, 2111, 'https://blog.acme.com/john/about-john?testing%5Bvalue%5D=1&cHash=42dbaeb9172b6b1ca23b49941e194db2'],
+            // blog.acme.com -> acme.com (different site)
+            ['https://blog.acme.com/', 2100, 1000, 'https://acme.us/?testing%5Bvalue%5D=1&cHash=7d1f13fa91159dac7feb3c824936b39d'],
+            ['https://blog.acme.com/', 2100, 1100, 'https://acme.us/welcome?testing%5Bvalue%5D=1&cHash=f42b850e435f0cedd366f5db749fc1af'],
+            ['https://blog.acme.com/', 2100, 1200, 'https://acme.us/features?testing%5Bvalue%5D=1&cHash=784e11c50ea1a13fd7d969df4ec53ea3'],
+            ['https://blog.acme.com/', 2100, 1210, 'https://acme.us/features/frontend-editing?testing%5Bvalue%5D=1&cHash=ccb7067022b9835ebfd8f720722bc708'],
+            ['https://blog.acme.com/', 2100, 404, 'https://acme.us/404?testing%5Bvalue%5D=1&cHash=864e96f586a78a53452f3bf0f4d24591'],
+            // blog.acme.com -> products.acme.com (different sub-site)
+            ['https://blog.acme.com/', 2100, 1300, 'https://products.acme.com/products?testing%5Bvalue%5D=1&cHash=dbd6597d72ed5098cce3d03eac1eeefe'],
+            ['https://blog.acme.com/', 2100, 1310, 'https://products.acme.com/products/planets?testing%5Bvalue%5D=1&cHash=e64bfc7ab7dd6b70d161e4d556be9726'],
+        ];
+
+        return $this->keysFromTemplate(
+            $instructions,
+            '%2$d->%3$d'
+        );
+    }
+
+    /**
+     * @param string $hostPrefix
+     * @param int $sourcePageId
+     * @param int $targetPageId
+     * @param string $expectation
+     *
+     * @test
+     * @dataProvider linkIsGeneratedWithQueryParametersDataProvider
+     */
+    public function linkIsGeneratedWithQueryParameters(string $hostPrefix, int $sourcePageId, int $targetPageId, string $expectation)
+    {
+        $response = $this->executeFrontendRequest(
+            (new InternalRequest($hostPrefix))
+                ->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 = [
+            ['https://acme.us/', 1100, 1510, 0, ''],
+            // ['https://acme.us/', 1100, 1511, 0, ''], // @todo Fails, not expanded to sub-pages
+            ['https://acme.us/', 1100, 1512, 0, ''],
+            ['https://acme.us/', 1100, 1515, 0, ''],
+            ['https://acme.us/', 1100, 1520, 0, ''],
+            // ['https://acme.us/', 1100, 1521, 0, ''], // @todo Fails, not expanded to sub-pages
+            //
+            ['https://acme.us/', 1100, 1510, 1, '/my-acme/whitepapers'],
+            ['https://acme.us/', 1100, 1511, 1, '/my-acme/whitepapers/products'],
+            ['https://acme.us/', 1100, 1512, 1, '/my-acme/whitepapers/solutions'],
+            ['https://acme.us/', 1100, 1515, 1, ''],
+            ['https://acme.us/', 1100, 1520, 1, ''],
+            // ['https://acme.us/', 1100, 1521, 1, ''], // @todo Fails, not expanded to sub-pages
+            //
+            ['https://acme.us/', 1100, 1510, 2, '/my-acme/whitepapers'],
+            ['https://acme.us/', 1100, 1511, 2, '/my-acme/whitepapers/products'],
+            ['https://acme.us/', 1100, 1512, 2, ''],
+            ['https://acme.us/', 1100, 1515, 2, '/my-acme/whitepapers/research'],
+            ['https://acme.us/', 1100, 1520, 2, '/my-acme/forecasts'],
+            ['https://acme.us/', 1100, 1521, 2, '/my-acme/forecasts/current-year'],
+            //
+            ['https://acme.us/', 1100, 1510, 3, '/my-acme/whitepapers'],
+            ['https://acme.us/', 1100, 1511, 3, '/my-acme/whitepapers/products'],
+            ['https://acme.us/', 1100, 1512, 3, '/my-acme/whitepapers/solutions'],
+            ['https://acme.us/', 1100, 1515, 3, '/my-acme/whitepapers/research'],
+            ['https://acme.us/', 1100, 1520, 3, '/my-acme/forecasts'],
+            ['https://acme.us/', 1100, 1521, 3, '/my-acme/forecasts/current-year'],
+        ];
+
+        return $this->keysFromTemplate(
+            $instructions,
+            '%2$d->%3$d (user:%4$d)'
+        );
+    }
+
+    /**
+     * @param string $hostPrefix
+     * @param int $sourcePageId
+     * @param int $targetPageId
+     * @param int $frontendUserId
+     * @param string $expectation
+     *
+     * @test
+     * @dataProvider linkIsGeneratedForRestrictedPageDataProvider
+     */
+    public function linkIsGeneratedForRestrictedPage(string $hostPrefix, int $sourcePageId, int $targetPageId, int $frontendUserId, string $expectation)
+    {
+        $response = $this->executeFrontendRequest(
+            (new InternalRequest($hostPrefix))
+                ->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
+            ['https://acme.us/', 1100, 1510, 1500, 0, '/my-acme?pageId=1510'],
+            // ['https://acme.us/', 1100, 1511, 1500, 0, '/my-acme?pageId=1511'], // @todo Fails, not expanded to sub-pages
+            ['https://acme.us/', 1100, 1512, 1500, 0, '/my-acme?pageId=1512'],
+            ['https://acme.us/', 1100, 1515, 1500, 0, '/my-acme?pageId=1515'],
+            ['https://acme.us/', 1100, 1520, 1500, 0, '/my-acme?pageId=1520'],
+            // ['https://acme.us/', 1100, 1521, 1500, 0, '/my-acme?pageId=1521'], // @todo Fails, not expanded to sub-pages
+            // frontend user 1
+            ['https://acme.us/', 1100, 1510, 1500, 1, '/my-acme/whitepapers'],
+            ['https://acme.us/', 1100, 1511, 1500, 1, '/my-acme/whitepapers/products'],
+            ['https://acme.us/', 1100, 1512, 1500, 1, '/my-acme/whitepapers/solutions'],
+            ['https://acme.us/', 1100, 1515, 1500, 1, '/my-acme?pageId=1515'],
+            ['https://acme.us/', 1100, 1520, 1500, 1, '/my-acme?pageId=1520'],
+            // ['https://acme.us/', 1100, 1521, 1500, 1, '/my-acme?pageId=1521'], // @todo Fails, not expanded to sub-pages
+            // frontend user 2
+            ['https://acme.us/', 1100, 1510, 1500, 2, '/my-acme/whitepapers'],
+            ['https://acme.us/', 1100, 1511, 1500, 2, '/my-acme/whitepapers/products'],
+            ['https://acme.us/', 1100, 1512, 1500, 2, '/my-acme?pageId=1512'],
+            ['https://acme.us/', 1100, 1515, 1500, 2, '/my-acme/whitepapers/research'],
+            ['https://acme.us/', 1100, 1520, 1500, 2, '/my-acme/forecasts'],
+            ['https://acme.us/', 1100, 1521, 1500, 2, '/my-acme/forecasts/current-year'],
+            // frontend user 3
+            ['https://acme.us/', 1100, 1510, 1500, 3, '/my-acme/whitepapers'],
+            ['https://acme.us/', 1100, 1511, 1500, 3, '/my-acme/whitepapers/products'],
+            ['https://acme.us/', 1100, 1512, 1500, 3, '/my-acme/whitepapers/solutions'],
+            ['https://acme.us/', 1100, 1515, 1500, 3, '/my-acme/whitepapers/research'],
+            ['https://acme.us/', 1100, 1520, 1500, 3, '/my-acme/forecasts'],
+            ['https://acme.us/', 1100, 1521, 1500, 3, '/my-acme/forecasts/current-year'],
+        ];
+
+        return $this->keysFromTemplate(
+            $instructions,
+            '%2$d->%3$d (via: %4$d, user:%5$d)'
+        );
+    }
+
+    /**
+     * @param string $hostPrefix
+     * @param int $sourcePageId
+     * @param int $targetPageId
+     * @param int $loginPageId
+     * @param int $frontendUserId
+     * @param string $expectation
+     *
+     * @test
+     * @dataProvider linkIsGeneratedForRestrictedPageUsingLoginPageDataProvider
+     */
+    public function linkIsGeneratedForRestrictedPageUsingLoginPage(string $hostPrefix, int $sourcePageId, int $targetPageId, int $loginPageId, int $frontendUserId, string $expectation)
+    {
+        $response = $this->executeFrontendRequest(
+            (new InternalRequest($hostPrefix))
+                ->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
+    {
+        // -> most probably since pid=-1 is not correctly resolved
+        $instructions = [
+            // acme.com -> acme.com (same site)
+            ['https://acme.us/', 1100, 1100, false, '/welcome'],
+            ['https://acme.us/', 1100, 1100, true, '/index.php?id=acme-first'], // @todo Alias not removed, yet
+            // ['https://acme.us/', 1100, 1950, false, '/?id=1950'], // @todo Not generated for new-placeholder
+            ['https://acme.us/', 1100, 1950, true, '/index.php?id={targetPageId}'],
+            // blog.acme.com -> acme.com (different site)
+            ['https://blog.acme.com/', 2100, 1100, false, 'https://acme.us/welcome'],
+            ['https://blog.acme.com/', 2100, 1100, true, '/index.php?id=acme-first'], // @todo Alias not removed, yet, domain missing
+            // ['https://blog.acme.com/', 2100, 1950, false, '/?id=1950'], // @todo Not generated for new-placeholder
+            ['https://blog.acme.com/', 2100, 1950, true, '/index.php?id={targetPageId}'], // @todo Domain missing
+        ];
+
+        return $this->keysFromTemplate(
+            $instructions,
+            '%2$d->%3$d (resolve:%4$d)'
+        );
+    }
+
+    /**
+     * @param string $hostPrefix
+     * @param int $sourcePageId
+     * @param int $targetPageId
+     * @param bool $resolveVersion
+     * @param string $expectation
+     *
+     * @test
+     * @dataProvider linkIsGeneratedForPageVersionDataProvider
+     */
+    public function linkIsGeneratedForPageVersion(string $hostPrefix, 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($hostPrefix))
+                ->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' => [
+                'https://acme.us/',
+                1100,
+                [
+                    ['title' => 'EN: Welcome', 'link' => '/welcome'],
+                    [
+                        'title' => 'EN: Features',
+                        'link' => '/features',
+                        'children' => [
+                            [
+                                'title' => 'EN: Frontend Editing',
+                                'link' => '/features/frontend-editing',
+                            ],
+                        ],
+                    ],
+                    [
+                        'title' => 'EN: Products',
+                        'link' => 'https://products.acme.com/products',
+                        'children' => [
+                            [
+                                'title' => 'EN: Planets',
+                                'link' => 'https://products.acme.com/products/planets',
+                            ],
+                            [
+                                'title' => 'EN: Spaceships',
+                                'link' => 'https://products.acme.com/products/spaceships',
+                            ],
+                            [
+                                'title' => 'EN: Dark Matter',
+                                'link' => 'https://products.acme.com/products/dark-matter',
+                            ],
+                        ],
+                    ],
+                    ['title' => 'Internal', 'link' => '/my-acme'],
+                    ['title' => 'About us', 'link' => '/about'],
+                    [
+                        'title' => 'Announcements & News',
+                        'link' => '/news',
+                        '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' => '/404'],
+                    ['title' => 'Our Blog', 'link' => 'https://blog.acme.com/authors'],
+                ]
+            ],
+            'ACME Blog' => [
+                'https://blog.acme.com/',
+                2100,
+                [
+                    [
+                        'title' => 'Authors',
+                        'link' => '/authors',
+                        'children' => [
+                            [
+                                'title' => 'John Doe',
+                                'link' => 'https://blog.acme.com/john/john',
+                            ],
+                            [
+                                'title' => 'Jane Doe',
+                                'link' => 'https://blog.acme.com/jane/jane',
+                            ],
+                        ],
+                    ],
+                    1 =>
+                        [
+                            'title' => 'Announcements & News',
+                            'link' => '/news',
+                            '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',
+                                ],
+                            ],
+                        ],
+                    ['title' => 'ACME Inc', 'link' => 'https://acme.us/welcome'],
+                ]
+            ]
+        ];
+    }
+
+    /**
+     * @param string $hostPrefix
+     * @param int $sourcePageId
+     * @param array $expectation
+     *
+     * @test
+     * @dataProvider menuIsGeneratedDataProvider
+     */
+    public function menuIsGenerated(string $hostPrefix, int $sourcePageId, array $expectation)
+    {
+        $response = $this->executeFrontendRequest(
+            (new InternalRequest($hostPrefix))
+                ->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
+        );
+    }
+}
diff --git a/typo3/sysext/frontend/Tests/Functional/SiteHandling/SlugSiteRequestTest.php b/typo3/sysext/frontend/Tests/Functional/SiteHandling/SlugSiteRequestTest.php
new file mode 100644 (file)
index 0000000..ff91765
--- /dev/null
@@ -0,0 +1,912 @@
+<?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\Core\Bootstrap;
+use TYPO3\CMS\Core\Error\Http\PageNotFoundException;
+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;
+
+/**
+ * Test case for frontend requests having site handling configured
+ */
+class SlugSiteRequestTest extends AbstractTestCase
+{
+    /**
+     * @var string
+     */
+    private $siteTitle = 'A Company that Manufactures Everything Inc';
+
+    /**
+     * @var InternalRequestContext
+     */
+    private $internalRequestContext;
+
+    public static function setUpBeforeClass()
+    {
+        parent::setUpBeforeClass();
+        static::initializeDatabaseSnapshot();
+    }
+
+    public static function tearDownAfterClass()
+    {
+        static::destroyDatabaseSnapshot();
+        parent::tearDownAfterClass();
+    }
+
+    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]);
+
+        $this->withDatabaseSnapshot(function () {
+            $this->setUpDatabase();
+        });
+    }
+
+    protected function setUpDatabase()
+    {
+        $backendUser = $this->setUpBackendUserFromFixture(1);
+        Bootstrap::initializeLanguageObject();
+
+        $scenarioFile = __DIR__ . '/Fixtures/SlugScenario.yaml';
+        $factory = DataHandlerFactory::fromYamlFile($scenarioFile);
+        $writer = DataHandlerWriter::withBackendUser($backendUser);
+        $writer->invokeFactory($factory);
+        static::failIfArrayIsNotEmpty(
+            $writer->getErrors()
+        );
+
+        $this->setUpFrontendRootPage(
+            1000,
+            [
+                'typo3/sysext/core/Tests/Functional/Fixtures/Frontend/JsonRenderer.typoscript',
+                'typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/JsonRenderer.typoscript',
+            ],
+            [
+                'title' => 'ACME Root',
+                'sitetitle' => $this->siteTitle,
+            ]
+        );
+    }
+
+    protected function tearDown()
+    {
+        unset($this->internalRequestContext);
+        parent::tearDown();
+    }
+
+    /**
+     * @return array
+     */
+    public function requestsAreRedirectedWithoutHavingDefaultSiteLanguageDataProvider(): array
+    {
+        $domainPaths = [
+            'https://website.local/',
+            'https://website.local/?',
+        ];
+
+        return $this->wrapInArray(
+            $this->keysFromValues($domainPaths)
+        );
+    }
+
+    /**
+     * @param string $uri
+     *
+     * @test
+     * @dataProvider requestsAreRedirectedWithoutHavingDefaultSiteLanguageDataProvider
+     */
+    public function requestsAreRedirectedWithoutHavingDefaultSiteLanguage(string $uri)
+    {
+        $this->writeSiteConfiguration(
+            'website-local',
+            $this->buildSiteConfiguration(1000, 'https://website.local/')
+        );
+
+        $expectedStatusCode = 307;
+        $expectedHeaders = ['location' => ['https://website.local/welcome']];
+
+        $response = $this->executeFrontendRequest(
+            new InternalRequest($uri),
+            $this->internalRequestContext
+        );
+        static::assertSame($expectedStatusCode, $response->getStatusCode());
+        static::assertSame($expectedHeaders, $response->getHeaders());
+    }
+
+    /**
+     * @return array
+     */
+    public function shortcutsAreRedirectedDataProvider(): array
+    {
+        $domainPaths = [
+            'https://website.local/',
+            'https://website.local/?',
+            'https://website.local/welcome',
+        ];
+
+        return $this->wrapInArray(
+            $this->keysFromValues($domainPaths)
+        );
+    }
+
+    /**
+     * @param string $uri
+     *
+     * @test
+     * @dataProvider shortcutsAreRedirectedDataProvider
+     */
+    public function shortcutsAreRedirectedToDefaultSiteLanguage(string $uri)
+    {
+        $this->writeSiteConfiguration(
+            'website-local',
+            $this->buildSiteConfiguration(1000, 'https://website.local/'),
+            [
+                $this->buildDefaultLanguageConfiguration('EN', '/en-en/'),
+            ]
+        );
+
+        $expectedStatusCode = 307;
+        $expectedHeaders = [
+            'location' => ['https://website.local/en-en/'],
+        ];
+
+        $response = $this->executeFrontendRequest(
+            new InternalRequest($uri),
+            $this->internalRequestContext
+        );
+        static::assertSame($expectedStatusCode, $response->getStatusCode());
+        static::assertSame($expectedHeaders, $response->getHeaders());
+    }
+
+    /**
+     * @param string $uri
+     *
+     * @test
+     * @dataProvider shortcutsAreRedirectedDataProvider
+     */
+    public function shortcutsAreRedirectedAndRenderFirstSubPage(string $uri)
+    {
+        $this->writeSiteConfiguration(
+            'website-local',
+            $this->buildSiteConfiguration(1000, 'https://website.local/'),
+            [
+                $this->buildDefaultLanguageConfiguration('EN', '/en-en/'),
+            ]
+        );
+
+        $expectedStatusCode = 200;
+        $expectedPageTitle = 'EN: Welcome';
+
+        $response = $this->executeFrontendRequest(
+            new InternalRequest($uri),
+            $this->internalRequestContext,
+            true
+        );
+        $responseStructure = ResponseContent::fromString(
+            (string)$response->getBody()
+        );
+
+        static::assertSame(
+            $expectedStatusCode,
+            $response->getStatusCode()
+        );
+        static::assertSame(
+            $this->siteTitle,
+            $responseStructure->getScopePath('template/sitetitle')
+        );
+        static::assertSame(
+            $expectedPageTitle,
+            $responseStructure->getScopePath('page/title')
+        );
+    }
+
+    /**
+     * @test
+     */
+    public function invalidSiteResultsInNotFoundResponse()
+    {
+        $this->writeSiteConfiguration(
+            'website-local',
+            $this->buildSiteConfiguration(1000, 'https://website.local/'),
+            [
+                $this->buildDefaultLanguageConfiguration('EN', '/en-en/'),
+            ],
+            $this->buildErrorHandlingConfiguration('Fluid', [404])
+        );
+
+        // @todo Expected page not found response (404) instead
+        $this->expectException(\TYPO3\CMS\Core\Error\Http\ServiceUnavailableException::class);
+        $this->expectExceptionCode(1294587218);
+
+        $uri = 'https://website.other/any/invalid/slug';
+        $this->executeFrontendRequest(
+            new InternalRequest($uri),
+            $this->internalRequestContext
+        );
+    }
+
+    /**
+     * @test
+     */
+    public function invalidSlugOutsideSiteLanguageResultsInNotFoundResponse()
+    {
+        $this->writeSiteConfiguration(
+            'website-local',
+            $this->buildSiteConfiguration(1000, 'https://website.local/'),
+            [
+                $this->buildDefaultLanguageConfiguration('EN', '/en-en/')
+            ],
+            $this->buildErrorHandlingConfiguration('Fluid', [404])
+        );
+
+        $expectedStatusCode = 307;
+        $expectedHeaders = ['location' => ['https://website.local/en-en/']];
+
+        $uri = 'https://website.local/any/invalid/slug';
+        $response = $this->executeFrontendRequest(
+            new InternalRequest($uri),
+            $this->internalRequestContext
+        );
+
+        static::assertSame(
+            $expectedStatusCode,
+            $response->getStatusCode()
+        );
+        static::assertSame(
+            $expectedHeaders,
+            $response->getHeaders()
+        );
+        // @todo Expected page not found response (404) instead
+        // static::assertContains(
+        //     'message: The requested page does not exist',
+        //    (string)$response->getBody()
+        // );
+    }
+    /**
+     * @test
+     */
+    public function invalidSlugInsideSiteLanguageResultsInNotFoundResponse()
+    {
+        $this->writeSiteConfiguration(
+            'website-local',
+            $this->buildSiteConfiguration(1000, 'https://website.local/'),
+            [
+                $this->buildDefaultLanguageConfiguration('EN', '/en-en/')
+            ],
+            $this->buildErrorHandlingConfiguration('Fluid', [404])
+        );
+
+        $uri = 'https://website.local/en-en/any/invalid/slug';
+        $response = $this->executeFrontendRequest(
+            new InternalRequest($uri),
+            $this->internalRequestContext
+        );
+
+        static::assertSame(
+            404,
+            $response->getStatusCode()
+        );
+        static::assertContains(
+            'message: The requested page does not exist',
+            (string)$response->getBody()
+        );
+    }
+
+    /**
+     * @return array
+     */
+    public function pageIsRenderedWithPathsDataProvider(): array
+    {
+        $domainPaths = [
+            'https://website.local/en-en/welcome',
+            'https://website.local/fr-fr/bienvenue',
+            'https://website.local/fr-ca/bienvenue',
+        ];
+
+        return array_map(
+            function (string $uri) {
+                if (strpos($uri, '/fr-fr/') !== false) {
+                    $expectedPageTitle = 'FR: Welcome';
+                } elseif (strpos($uri, '/fr-ca/') !== false) {
+                    $expectedPageTitle = 'FR-CA: Welcome';
+                } else {
+                    $expectedPageTitle = 'EN: Welcome';
+                }
+                return [$uri, $expectedPageTitle];
+            },
+            $this->keysFromValues($domainPaths)
+        );
+    }
+
+    /**
+     * @param string $uri
+     * @param string $expectedPageTitle
+     *
+     * @test
+     * @dataProvider pageIsRenderedWithPathsDataProvider
+     */
+    public function pageIsRenderedWithPaths(string $uri, string $expectedPageTitle)
+    {
+        $this->writeSiteConfiguration(
+            'website-local',
+            $this->buildSiteConfiguration(1000, 'https://website.local/'),
+            [
+                $this->buildDefaultLanguageConfiguration('EN', '/en-en/'),
+                $this->buildLanguageConfiguration('FR', '/fr-fr/', ['EN']),
+                $this->buildLanguageConfiguration('FR-CA', '/fr-ca/', ['FR', 'EN']),
+            ]
+        );
+
+        $response = $this->executeFrontendRequest(
+            new InternalRequest($uri),
+            $this->internalRequestContext
+        );
+        $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 pageIsRenderedWithDomainsDataProvider(): array
+    {
+        $domainPaths = [
+            'https://website.us/welcome',
+            'https://website.fr/bienvenue',
+            'https://website.ca/bienvenue',
+        ];
+
+        return array_map(
+            function (string $uri) {
+                if (strpos($uri, '.fr/') !== false) {
+                    $expectedPageTitle = 'FR: Welcome';
+                } elseif (strpos($uri, '.ca/') !== false) {
+                    $expectedPageTitle = 'FR-CA: Welcome';
+                } else {
+                    $expectedPageTitle = 'EN: Welcome';
+                }
+                return [$uri, $expectedPageTitle];
+            },
+            $this->keysFromValues($domainPaths)
+        );
+    }
+
+    /**
+     * @param string $uri
+     * @param string $expectedPageTitle
+     *
+     * @test
+     * @dataProvider pageIsRenderedWithDomainsDataProvider
+     */
+    public function pageIsRenderedWithDomains(string $uri, string $expectedPageTitle)
+    {
+        $this->writeSiteConfiguration(
+            'website-local',
+            $this->buildSiteConfiguration(1000, 'https://website.local/'),
+            [
+                $this->buildDefaultLanguageConfiguration('EN', 'https://website.us/'),
+                $this->buildLanguageConfiguration('FR', 'https://website.fr/', ['EN']),
+                $this->buildLanguageConfiguration('FR-CA', 'https://website.ca/', ['FR', 'EN']),
+            ]
+        );
+
+        $response = $this->executeFrontendRequest(
+            new InternalRequest($uri),
+            $this->internalRequestContext
+        );
+        $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 restrictedPageIsRenderedDataProvider(): array
+    {
+        $instructions = [
+            // frontend user 1
+            ['https://website.local/my-acme/whitepapers', 1, 'Whitepapers'],
+            ['https://website.local/my-acme/whitepapers/products', 1, 'Products'],
+            ['https://website.local/my-acme/whitepapers/solutions', 1, 'Solutions'],
+            // frontend user 2
+            ['https://website.local/my-acme/whitepapers', 2, 'Whitepapers'],
+            ['https://website.local/my-acme/whitepapers/products', 2, 'Products'],
+            ['https://website.local/my-acme/whitepapers/research', 2, 'Research'],
+            ['https://website.local/my-acme/forecasts', 2, 'Forecasts'],
+            ['https://website.local/my-acme/forecasts/current-year', 2, 'Current Year'],
+            // frontend user 3
+            ['https://website.local/my-acme/whitepapers', 3, 'Whitepapers'],
+            ['https://website.local/my-acme/whitepapers/products', 3, 'Products'],
+            ['https://website.local/my-acme/whitepapers/solutions', 3, 'Solutions'],
+            ['https://website.local/my-acme/whitepapers/research', 3, 'Research'],
+            ['https://website.local/my-acme/forecasts', 3, 'Forecasts'],
+            ['https://website.local/my-acme/forecasts/current-year', 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/my-acme/whitepapers', 0],
+            // ['https://website.local/my-acme/whitepapers/products', 0], // @todo extendToSubpages currently missing
+            ['https://website.local/my-acme/whitepapers/solutions', 0],
+            ['https://website.local/my-acme/whitepapers/research', 0],
+            ['https://website.local/my-acme/forecasts', 0],
+            // ['https://website.local/my-acme/forecasts/current-year', 0], // @todo extendToSubpages currently missing
+            // frontend user 1
+            ['https://website.local/my-acme/whitepapers/research', 1],
+            ['https://website.local/my-acme/forecasts', 1],
+            // ['https://website.local/my-acme/forecasts/current-year', 1], // @todo extendToSubpages currently missing
+            // frontend user 2
+            ['https://website.local/my-acme/whitepapers/solutions', 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->markTestSkipped('Skipped until PageContentErrorHandler::handlePageError does not use HTTP anymore');
+
+        $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 = [
+            'https://website.local/',
+        ];
+
+        $queries = [
+            '',
+            'welcome',
+        ];
+
+        $customQueries = [
+            '?testing[value]=1',
+            '?testing[value]=1&cHash=',
+            '?testing[value]=1&cHash=WRONG',
+        ];
+
+        return $this->wrapInArray(
+            $this->keysFromValues(
+                $this->meltStrings([$domainPaths, $queries, $customQueries])
+            )
+        );
+    }
+
+    /**
+     * @param string $uri
+     *
+     * @test
+     * @dataProvider pageRenderingStopsWithInvalidCacheHashDataProvider
+     */
+    public function pageRequestThrowsExceptionWithInvalidCacheHashWithoutHavingErrorHandling(string $uri)
+    {
+        $this->writeSiteConfiguration(
+            'website-local',
+            $this->buildSiteConfiguration(1000, 'https://website.local/')
+        );
+
+        $this->expectExceptionCode(1518472189);
+        $this->expectException(PageNotFoundException::class);
+
+        $this->executeFrontendRequest(
+            new InternalRequest($uri),
+            $this->internalRequestContext
+        );
+    }
+
+    /**
+     * @param string $uri
+     *
+     * @test
+     * @dataProvider pageRenderingStopsWithInvalidCacheHashDataProvider
+     */
+    public function pageRequestSendsNotFoundResponseWithInvalidCacheHash(string $uri)
+    {
+        $response = $this->executeFrontendRequest(
+            new InternalRequest($uri),
+            $this->internalRequestContext->withMergedGlobalSettings([
+                'TYPO3_CONF_VARS' => [
+                    'FE' => [
+                        'pageNotFound_handling' => 'READFILE:typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/PageError.txt',
+                    ]
+                ]
+            ])
+        );
+
+        static::assertSame(
+            404,
+            $response->getStatusCode()
+        );
+        static::assertThat(
+            (string)$response->getBody(),
+            static::logicalOr(
+                static::stringContains('reason: Request parameters could not be validated (&amp;cHash empty)'),
+                static::stringContains('reason: Request parameters could not be validated (&amp;cHash comparison failed)')
+            )
+        );
+    }
+
+    /**
+     * @param string $uri
+     *
+     * @test
+     * @dataProvider pageRenderingStopsWithInvalidCacheHashDataProvider
+     */
+    public function pageRequestSendsNotFoundResponseWithInvalidCacheHashWithHavingFluidErrorHandling(string $uri)
+    {
+        $this->writeSiteConfiguration(
+            'website-local',
+            $this->buildSiteConfiguration(1000, 'https://website.local/'),
+            [],
+            $this->buildErrorHandlingConfiguration('Fluid', [404])
+        );
+
+        $response = $this->executeFrontendRequest(
+            new InternalRequest($uri),
+            $this->internalRequestContext
+        );
+
+        static::assertSame(
+            404,
+            $response->getStatusCode()
+        );
+        static::assertThat(
+            (string)$response->getBody(),
+            static::logicalOr(
+                static::stringContains('message: Request parameters could not be validated (&amp;cHash empty)'),
+                static::stringContains('message: Request parameters could not be validated (&amp;cHash comparison failed)')
+            )
+        );
+    }
+
+    /**
+     * @param string $uri
+     *
+     * @test
+     * @dataProvider pageRenderingStopsWithInvalidCacheHashDataProvider
+     * @todo Response body cannot be asserted since PageContentErrorHandler::handlePageError executes request via HTTP (not internally)
+     */
+    public function pageRequestSendsNotFoundResponseWithInvalidCacheHashWithHavingPageErrorHandling(string $uri)
+    {
+        $this->markTestSkipped('Skipped until PageContentErrorHandler::handlePageError does not use HTTP anymore');
+
+        $this->writeSiteConfiguration(
+            'website-local',
+            $this->buildSiteConfiguration(1000, 'https://website.local/'),
+            [],
+            $this->buildErrorHandlingConfiguration('Page', [404])
+        );
+
+        $response = $this->executeFrontendRequest(
+            new InternalRequest($uri),
+            $this->internalRequestContext
+        );
+
+        static::assertSame(
+            404,
+            $response->getStatusCode()
+        );
+    }
+
+    /**
+     * @param string $uri
+     *
+     * @test
+     * @dataProvider pageRenderingStopsWithInvalidCacheHashDataProvider
+     */
+    public function pageRequestSendsNotFoundResponseWithInvalidCacheHashWithHavingPhpErrorHandling(string $uri)
+    {
+        $this->writeSiteConfiguration(
+            'website-local',
+            $this->buildSiteConfiguration(1000, 'https://website.local/'),
+            [],
+            $this->buildErrorHandlingConfiguration('PHP', [404])
+        );
+
+        $response = $this->executeFrontendRequest(
+            new InternalRequest($uri),
+            $this->internalRequestContext
+        );
+        $json = json_decode((string)$response->getBody(), true);
+
+        static::assertSame(
+            404,
+            $response->getStatusCode()
+        );
+        static::assertThat(
+            $json['message'] ?? null,
+            static::logicalOr(
+                static::identicalTo('Request parameters could not be validated (&cHash empty)'),
+                static::identicalTo('Request parameters could not be validated (&cHash comparison failed)')
+            )
+        );
+    }
+
+    /**
+     * @return array
+     */
+    public function pageIsRenderedWithValidCacheHashDataProvider(): array
+    {
+        $domainPaths = [
+            'https://website.local/',
+        ];
+
+        // cHash has been calculated with encryption key set to
+        // '4408d27a916d51e624b69af3554f516dbab61037a9f7b9fd6f81b4d3bedeccb6'
+        $queries = [
+            // @todo Currently fails since cHash is verified after(!) redirect to page 1100
+            // '?cHash=7d1f13fa91159dac7feb3c824936b39d',
+            // '?cHash=7d1f13fa91159dac7feb3c824936b39d',
+            'welcome?cHash=f42b850e435f0cedd366f5db749fc1af',
+        ];
+
+        $customQueries = [
+            '&testing[value]=1',
+        ];
+
+        $dataSet = $this->wrapInArray(
+            $this->keysFromValues(
+                $this->meltStrings([$domainPaths, $queries, $customQueries])
+            )
+        );
+
+        return $dataSet;
+    }
+
+    /**
+     * @param string $uri
+     *
+     * @test
+     * @dataProvider pageIsRenderedWithValidCacheHashDataProvider
+     */
+    public function pageIsRenderedWithValidCacheHash($uri)
+    {
+        $this->writeSiteConfiguration(
+            'website-local',
+            $this->buildSiteConfiguration(1000, 'https://website.local/')
+        );
+
+        $response = $this->executeFrontendRequest(
+            new InternalRequest($uri),
+            $this->internalRequestContext
+        );
+        $responseStructure = ResponseContent::fromString(
+            (string)$response->getBody()
+        );
+        static::assertSame(
+            '1',
+            $responseStructure->getScopePath('getpost/testing.value')
+        );
+    }
+}