From 0ecaf94ad3edb883afcf9f754bac494c0ad1e95b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Stefan=20B=C3=BCrk?= <stefan@buerk.tech>
Date: Thu, 3 Mar 2022 00:55:04 +0100
Subject: [PATCH] [TASK] Add request tests for DELETE,PATCH,PUT AND POST with
 data

The testing-framework with FE requests using sub requests
is now able to handle DELETE,PATCH,PUT and POST requests.

The situation in testing-framework is not the final solution,
though. The patch adds a test to verify if testing-framework
request details are properly received in the FE application.

This not only adds a use case to the core for these kind
of requests, but also ensures that we don't break this
detail again when testing-framework internal handling is
further refactored.

Also needs a testing-framework bugfix in v11:
> composer req --dev typo3/testing-framework:^6.16.2

Resolves: #97084
Releases: main, 11.5
Change-Id: I8268625d4b439f1657168d6b9c9a3878b36477bd
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/73768
Tested-by: core-ci <typo3@b13.com>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
---
 composer.json                                 |   5 +-
 composer.lock                                 |  14 +-
 .../Classes/Middleware/RequestMirror.php      |  50 ++++
 .../Configuration/RequestMiddlewares.php      |  16 ++
 .../test_request_mirror/ext_emconf.php        |  22 ++
 .../InternalRequestDataMappingTest.php        | 247 ++++++++++++++++++
 6 files changed, 345 insertions(+), 9 deletions(-)
 create mode 100644 typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_request_mirror/Classes/Middleware/RequestMirror.php
 create mode 100644 typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_request_mirror/Configuration/RequestMiddlewares.php
 create mode 100644 typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_request_mirror/ext_emconf.php
 create mode 100644 typo3/sysext/frontend/Tests/Functional/Request/InternalRequestDataMappingTest.php

diff --git a/composer.json b/composer.json
index fe15a6daac70..b0be852da770 100644
--- a/composer.json
+++ b/composer.json
@@ -121,7 +121,7 @@
 		"phpstan/phpstan-phpunit": "^1.0",
 		"phpunit/phpunit": "^9.5.10",
 		"typo3/cms-styleguide": "~11.5.4",
-		"typo3/testing-framework": "^6.16.1"
+		"typo3/testing-framework": "^6.16.2"
 	},
 	"suggest": {
 		"ext-gd": "GDlib/Freetype is required for building images with text (GIFBUILDER) and can also be used to scale images",
@@ -290,7 +290,8 @@
 			"typo3/sysext/extbase/Tests/UnitDeprecated/Object/Container/Fixtures/",
 			"typo3/sysext/extbase/Tests/Functional/Fixtures/",
 			"typo3/sysext/extbase/Tests/Functional/Mvc/Controller/Fixture/",
-			"typo3/sysext/fluid/Tests/Functional/Fixtures/Extensions/fluid_test/Classes/"
+			"typo3/sysext/fluid/Tests/Functional/Fixtures/Extensions/fluid_test/Classes/",
+			"typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_request_mirror/Classes/"
 		],
 		"files": [
 			"typo3/sysext/extbase/Tests/Fixture/TxClassWithGettersAndSetters.php"
diff --git a/composer.lock b/composer.lock
index f7d360962a38..ec1cb29d48fe 100644
--- a/composer.lock
+++ b/composer.lock
@@ -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": "ffb1f9b636cecefa553166f2b6d7f5a5",
+    "content-hash": "e7e9c0414caf6cf8ccbfae3557b28b3c",
     "packages": [
         {
             "name": "bacon/bacon-qr-code",
@@ -9008,16 +9008,16 @@
         },
         {
             "name": "typo3/testing-framework",
-            "version": "6.16.1",
+            "version": "6.16.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/TYPO3/testing-framework.git",
-                "reference": "774d2123c907261b79f554c98f25f93ed2b0ee8c"
+                "reference": "54ac7b82c48bc70a9dbb43fbc3f46a59ea28583a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/TYPO3/testing-framework/zipball/774d2123c907261b79f554c98f25f93ed2b0ee8c",
-                "reference": "774d2123c907261b79f554c98f25f93ed2b0ee8c",
+                "url": "https://api.github.com/repos/TYPO3/testing-framework/zipball/54ac7b82c48bc70a9dbb43fbc3f46a59ea28583a",
+                "reference": "54ac7b82c48bc70a9dbb43fbc3f46a59ea28583a",
                 "shasum": ""
             },
             "require": {
@@ -9076,9 +9076,9 @@
             "support": {
                 "general": "https://typo3.org/support/",
                 "issues": "https://github.com/TYPO3/testing-framework/issues",
-                "source": "https://github.com/TYPO3/testing-framework/tree/6.16.1"
+                "source": "https://github.com/TYPO3/testing-framework/tree/6.16.2"
             },
-            "time": "2022-03-04T16:41:08+00:00"
+            "time": "2022-03-04T20:20:41+00:00"
         }
     ],
     "aliases": [],
diff --git a/typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_request_mirror/Classes/Middleware/RequestMirror.php b/typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_request_mirror/Classes/Middleware/RequestMirror.php
new file mode 100644
index 000000000000..a1120402ebda
--- /dev/null
+++ b/typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_request_mirror/Classes/Middleware/RequestMirror.php
@@ -0,0 +1,50 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * 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!
+ */
+
+namespace TYPO3\RequestMirror\Middleware;
+
+use Psr\Http\Message\ResponseFactoryInterface;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Message\StreamFactoryInterface;
+use Psr\Http\Server\MiddlewareInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+class RequestMirror implements MiddlewareInterface
+{
+    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+    {
+        if ($request->getUri()->getPath() !== '/request-mirror') {
+            return $handler->handle($request);
+        }
+        return GeneralUtility::makeInstance(ResponseFactoryInterface::class)
+            ->createResponse(200, '')
+            ->withHeader('Content-type', 'application/json')
+            ->withBody(GeneralUtility::makeInstance(StreamFactoryInterface::class)->createStream(\json_encode(
+                [
+                    'uri' => $request->getUri(),
+                    'method' => $request->getMethod(),
+                    'headers' => $request->getHeaders(),
+                    'queryParams' => $request->getQueryParams(),
+                    'parsedBody' => $request->getParsedBody(),
+                    'body' => (string)$request->getBody(),
+                ],
+                JSON_THROW_ON_ERROR
+            )));
+    }
+}
diff --git a/typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_request_mirror/Configuration/RequestMiddlewares.php b/typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_request_mirror/Configuration/RequestMiddlewares.php
new file mode 100644
index 000000000000..49b801a49722
--- /dev/null
+++ b/typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_request_mirror/Configuration/RequestMiddlewares.php
@@ -0,0 +1,16 @@
+<?php
+
+return [
+    'frontend' => [
+        'typo3/request-mirror' => [
+            'target' => \TYPO3\RequestMirror\Middleware\RequestMirror::class,
+            'after' => [
+                'typo3/cms-core/normalized-params-attribute',
+            ],
+            'before' => [
+                'typo3/cms-frontend/site',
+                'typo3/cms-frontend/eid',
+            ],
+        ],
+    ],
+];
diff --git a/typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_request_mirror/ext_emconf.php b/typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_request_mirror/ext_emconf.php
new file mode 100644
index 000000000000..c5215fb1d2f2
--- /dev/null
+++ b/typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_request_mirror/ext_emconf.php
@@ -0,0 +1,22 @@
+<?php
+
+declare(strict_types=1);
+
+$EM_CONF[$_EXTKEY] = [
+    'title' => 'RequestMirror',
+    'description' => 'RequestMirror',
+    'category' => 'example',
+    'version' => '11.0.0',
+    'state' => 'beta',
+    'author' => 'Stefan Bürk',
+    'author_email' => 'stefan@buerk.tech',
+    'author_company' => '',
+    'constraints' => [
+        'depends' => [
+            'typo3' => '11.0.0-11.99.99',
+            'frontend' => '11.0.0-11.99.99',
+        ],
+        'conflicts' => [],
+        'suggests' => [],
+    ],
+];
diff --git a/typo3/sysext/frontend/Tests/Functional/Request/InternalRequestDataMappingTest.php b/typo3/sysext/frontend/Tests/Functional/Request/InternalRequestDataMappingTest.php
new file mode 100644
index 000000000000..28711fb2e56f
--- /dev/null
+++ b/typo3/sysext/frontend/Tests/Functional/Request/InternalRequestDataMappingTest.php
@@ -0,0 +1,247 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * 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!
+ */
+
+namespace TYPO3\CMS\Frontend\Tests\Functional\Request;
+
+use Psr\Http\Message\StreamFactoryInterface;
+use TYPO3\CMS\Core\Http\StreamFactory;
+use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest;
+use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
+
+class InternalRequestDataMappingTest extends FunctionalTestCase
+{
+    protected $testExtensionsToLoad = [
+        'typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_request_mirror',
+    ];
+
+    public function ensureRequestMappingWorksDataProvider(): \Generator
+    {
+        yield 'POST parsedBody(_POST) as parsedBody' => [
+            'uri' => 'https://acme.com/request-mirror',
+            'method' => 'POST',
+            'parsedBody' => ['param1' => 'value1'],
+            'headers' => [
+                'Content-type' => 'application/x-www-form-urlencoded',
+            ],
+            'body' => null,
+            'expectedJsonKeyValues' => [
+                'method' => 'POST',
+                'parsedBody' => ['param1' => 'value1'],
+                'queryParams' => [],
+                'body' => '',
+                'headers' => [
+                    'Content-type' => [
+                        'application/x-www-form-urlencoded',
+                    ],
+                    'host' => [
+                        'acme.com',
+                    ],
+                ],
+            ],
+        ];
+        yield 'PATCH body as parsedBody' => [
+            'uri' => 'https://acme.com/request-mirror',
+            'method' => 'PATCH',
+            'parsedBody' => null,
+            'headers' => [
+                'Content-type' => 'application/x-www-form-urlencoded',
+            ],
+            'body' => \GuzzleHttp\Psr7\Query::build(['param1' => 'value1']),
+            'expectedJsonKeyValues' => [
+                'method' => 'PATCH',
+                'parsedBody' => ['param1' => 'value1'],
+                'queryParams' => [],
+                'body' => \GuzzleHttp\Psr7\Query::build(['param1' => 'value1']),
+                'headers' => [
+                    'Content-type' => [
+                        'application/x-www-form-urlencoded',
+                    ],
+                    'host' => [
+                        'acme.com',
+                    ],
+                ],
+            ],
+        ];
+        yield 'PUT body as parsedBody' => [
+            'uri' => 'https://acme.com/request-mirror',
+            'method' => 'PUT',
+            'parsedBody' => null,
+            'headers' => [
+                'Content-type' => 'application/x-www-form-urlencoded',
+            ],
+            'body' => \GuzzleHttp\Psr7\Query::build(['param1' => 'value1']),
+            'expectedJsonKeyValues' => [
+                'method' => 'PUT',
+                'parsedBody' => ['param1' => 'value1'],
+                'queryParams' => [],
+                'body' => \GuzzleHttp\Psr7\Query::build(['param1' => 'value1']),
+                'headers' => [
+                    'Content-type' => [
+                        'application/x-www-form-urlencoded',
+                    ],
+                    'host' => [
+                        'acme.com',
+                    ],
+                ],
+            ],
+        ];
+        yield 'DELETE body as parsedBody' => [
+            'uri' => 'https://acme.com/request-mirror',
+            'method' => 'DELETE',
+            'parsedBody' => null,
+            'headers' => [
+                'Content-type' => 'application/x-www-form-urlencoded',
+            ],
+            'body' => \GuzzleHttp\Psr7\Query::build(['param1' => 'value1']),
+            'expectedJsonKeyValues' => [
+                'method' => 'DELETE',
+                'parsedBody' => ['param1' => 'value1'],
+                'queryParams' => [],
+                'body' => \GuzzleHttp\Psr7\Query::build(['param1' => 'value1']),
+                'headers' => [
+                    'Content-type' => [
+                        'application/x-www-form-urlencoded',
+                    ],
+                    'host' => [
+                        'acme.com',
+                    ],
+                ],
+            ],
+        ];
+        yield 'POST parsedBody(_POST) as parsedBody and queryParams' => [
+            'uri' => 'https://acme.com/request-mirror?queryParam1=queryValue1',
+            'method' => 'POST',
+            'parsedBody' => ['param1' => 'value1'],
+            'headers' => [
+                'Content-type' => 'application/x-www-form-urlencoded',
+            ],
+            'body' => null,
+            'expectedJsonKeyValues' => [
+                'method' => 'POST',
+                'parsedBody' => ['param1' => 'value1'],
+                'queryParams' => ['queryParam1' => 'queryValue1'],
+                'body' => '',
+                'headers' => [
+                    'Content-type' => [
+                        'application/x-www-form-urlencoded',
+                    ],
+                    'host' => [
+                        'acme.com',
+                    ],
+                ],
+            ],
+        ];
+        yield 'PATCH body as parsedBody and queryParams' => [
+            'uri' => 'https://acme.com/request-mirror?queryParam1=queryValue1',
+            'method' => 'PATCH',
+            'parsedBody' => null,
+            'headers' => [
+                'Content-type' => 'application/x-www-form-urlencoded',
+            ],
+            'body' => \GuzzleHttp\Psr7\Query::build(['param1' => 'value1']),
+            'expectedJsonKeyValues' => [
+                'method' => 'PATCH',
+                'parsedBody' => ['param1' => 'value1'],
+                'queryParams' => ['queryParam1' => 'queryValue1'],
+                'body' => \GuzzleHttp\Psr7\Query::build(['param1' => 'value1']),
+                'headers' => [
+                    'Content-type' => [
+                        'application/x-www-form-urlencoded',
+                    ],
+                    'host' => [
+                        'acme.com',
+                    ],
+                ],
+            ],
+        ];
+        yield 'PUT body as parsedBody and queryParams' => [
+            'uri' => 'https://acme.com/request-mirror?queryParam1=queryValue1',
+            'method' => 'PUT',
+            'parsedBody' => null,
+            'headers' => [
+                'Content-type' => 'application/x-www-form-urlencoded',
+            ],
+            'body' => \GuzzleHttp\Psr7\Query::build(['param1' => 'value1']),
+            'expectedJsonKeyValues' => [
+                'method' => 'PUT',
+                'parsedBody' => ['param1' => 'value1'],
+                'queryParams' => ['queryParam1' => 'queryValue1'],
+                'body' => \GuzzleHttp\Psr7\Query::build(['param1' => 'value1']),
+                'headers' => [
+                    'Content-type' => [
+                        'application/x-www-form-urlencoded',
+                    ],
+                    'host' => [
+                        'acme.com',
+                    ],
+                ],
+            ],
+        ];
+        yield 'DELETE body as parsedBody and queryParams' => [
+            'uri' => 'https://acme.com/request-mirror?queryParam1=queryValue1',
+            'method' => 'DELETE',
+            'parsedBody' => null,
+            'headers' => [
+                'Content-type' => 'application/x-www-form-urlencoded',
+            ],
+            'body' => \GuzzleHttp\Psr7\Query::build(['param1' => 'value1']),
+            'expectedJsonKeyValues' => [
+                'method' => 'DELETE',
+                'parsedBody' => ['param1' => 'value1'],
+                'queryParams' => ['queryParam1' => 'queryValue1'],
+                'body' => \GuzzleHttp\Psr7\Query::build(['param1' => 'value1']),
+                'headers' => [
+                    'Content-type' => [
+                        'application/x-www-form-urlencoded',
+                    ],
+                    'host' => [
+                        'acme.com',
+                    ],
+                ],
+            ],
+        ];
+    }
+
+    /**
+     * Verify testing-framework request details are properly received
+     * in the application by adding an extension with a middleware.
+     *
+     * @test
+     * @dataProvider ensureRequestMappingWorksDataProvider
+     */
+    public function ensureRequestMappingWorks(string $uri, string $method, ?array $parsedBody, array $headers, ?string $body, array $expectedJsonKeyValues): void
+    {
+        $request = (new InternalRequest($uri))
+            ->withMethod($method)
+            ->withParsedBody($parsedBody);
+        foreach ($headers as $headerName => $headerValue) {
+            $request = $request->withAddedHeader($headerName, $headerValue);
+        }
+        if ($body) {
+            /** @var StreamFactory $streamFactory */
+            $streamFactory = $this->get(StreamFactoryInterface::class);
+            $request = $request->withBody($streamFactory->createStream($body));
+        }
+
+        $response = $this->executeFrontendSubRequest($request);
+        self::assertSame(200, $response->getStatusCode());
+        $json = json_decode((string)$response->getBody(), true);
+        foreach ($expectedJsonKeyValues as $expectedKey => $expectedValue) {
+            self::assertSame($expectedValue, $json[$expectedKey] ?? null);
+        }
+    }
+}
-- 
GitLab