[TASK] Correctly initialize environment for DataHandler in tests
[Packages/TYPO3.CMS.git] / typo3 / sysext / frontend / Tests / Functional / SiteHandling / SiteRequestTest.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Frontend\Tests\Functional\SiteHandling;
4
5 /*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18 use TYPO3\CMS\Core\Configuration\SiteConfiguration;
19 use TYPO3\CMS\Core\Core\Bootstrap;
20 use TYPO3\CMS\Frontend\Tests\Functional\SiteHandling\Fixtures\PhpError;
21 use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\ActionService;
22 use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\Scenario\DataMapFactory;
23 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest;
24 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequestContext;
25 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\ResponseContent;
26
27 /**
28 * Test case for frontend requests having site handling configured
29 */
30 class SiteRequestTest extends AbstractRequestTest
31 {
32 protected const LANGUAGE_PRESETS = [
33 'EN' => ['id' => 0, 'title' => 'English', 'locale' => 'en_US.UTF8', 'iso' => 'en', 'hrefLang' => 'en-US', 'direction' => ''],
34 'FR' => ['id' => 1, 'title' => 'French', 'locale' => 'fr_FR.UTF8', 'iso' => 'fr', 'hrefLang' => 'fr-FR', 'direction' => ''],
35 'FR-CA' => ['id' => 2, 'title' => 'Franco-Canadian', 'locale' => 'fr_CA.UTF8', 'iso' => 'fr', 'hrefLang' => 'fr-CA', 'direction' => ''],
36 ];
37
38 /**
39 * @var string
40 */
41 private $siteTitle = 'A Company that Manufactures Everything Inc';
42
43 /**
44 * @var InternalRequestContext
45 */
46 private $internalRequestContext;
47
48 /**
49 * @var ActionService
50 */
51 private $actionService;
52
53 protected function setUp()
54 {
55 parent::setUp();
56
57 // these settings are forwarded to the frontend sub-request as well
58 $this->internalRequestContext = (new InternalRequestContext())
59 ->withGlobalSettings(['TYPO3_CONF_VARS' => static::TYPO3_CONF_VARS]);
60
61 $this->setUpBackendUserFromFixture(1);
62 $this->actionService = new ActionService();
63 Bootstrap::initializeLanguageObject();
64
65 $scenarioFile = __DIR__ . '/Fixtures/scenario.yaml';
66 $factory = DataMapFactory::fromYamlFile($scenarioFile);
67 $this->actionService->invoke(
68 $factory->getDataMap(),
69 [],
70 $factory->getSuggestedIds()
71 );
72 static::failIfArrayIsNotEmpty(
73 $this->actionService->getDataHandler()->errorLog
74 );
75
76 $this->setUpFrontendRootPage(
77 101,
78 [
79 'typo3/sysext/core/Tests/Functional/Fixtures/Frontend/JsonRenderer.typoscript',
80 'typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/JsonRenderer.typoscript',
81 ],
82 [
83 'title' => 'ACME Root',
84 'sitetitle' => $this->siteTitle,
85 ]
86 );
87 }
88
89 protected function tearDown()
90 {
91 unset(
92 $this->actionService,
93 $this->internalRequestContext
94 );
95 parent::tearDown();
96 }
97
98 /**
99 * @return array
100 */
101 public function requestsAreRedirectedDataProvider(): array
102 {
103 $domainPaths = [
104 '/',
105 'https://localhost/',
106 'https://website.local/',
107 ];
108
109 $queries = [
110 '?',
111 '?id=101',
112 '?id=acme-root'
113 ];
114
115 return $this->wrapInArray(
116 $this->keysFromValues(
117 $this->meltStrings([$domainPaths, $queries])
118 )
119 );
120 }
121
122 /**
123 * @param string $uri
124 *
125 * @test
126 * @dataProvider requestsAreRedirectedDataProvider
127 */
128 public function requestsAreRedirected(string $uri)
129 {
130 $this->writeSiteConfiguration(
131 'website-local',
132 $this->buildSiteConfiguration(101, 'https://website.local/')
133 );
134
135 $expectedStatusCode = 307;
136 $expectedHeaders = ['location' => ['/?id=acme-first']];
137
138 $response = $this->executeFrontendRequest(
139 new InternalRequest($uri),
140 $this->internalRequestContext
141 );
142 static::assertSame($expectedStatusCode, $response->getStatusCode());
143 static::assertSame($expectedHeaders, $response->getHeaders());
144 }
145
146 /**
147 * @return array
148 */
149 public function pageIsRenderedWithPathsDataProvider(): array
150 {
151 $domainPaths = [
152 // @todo currently base needs to be defined with domain
153 // '/',
154 'https://website.local/',
155 ];
156
157 $languagePaths = [
158 '',
159 'en-en/',
160 'fr-fr/',
161 'fr-ca/',
162 ];
163
164 $queries = [
165 '?id=102',
166 '?id=acme-first',
167 ];
168
169 return array_map(
170 function (string $uri) {
171 if (strpos($uri, '/fr-fr/') !== false) {
172 $expectedPageTitle = 'FR: Welcome';
173 } elseif (strpos($uri, '/fr-ca/') !== false) {
174 $expectedPageTitle = 'FR-CA: Welcome';
175 } else {
176 $expectedPageTitle = 'EN: Welcome';
177 }
178 return [$uri, $expectedPageTitle];
179 },
180 $this->keysFromValues(
181 $this->meltStrings([$domainPaths, $languagePaths, $queries])
182 )
183 );
184 }
185
186 /**
187 * @param string $uri
188 * @param string $expectedPageTitle
189 *
190 * @test
191 * @dataProvider pageIsRenderedWithPathsDataProvider
192 */
193 public function pageIsRenderedWithPaths(string $uri, string $expectedPageTitle)
194 {
195 $this->writeSiteConfiguration(
196 'website-local',
197 $this->buildSiteConfiguration(101, 'https://website.local/'),
198 [
199 $this->buildDefaultLanguageConfiguration('EN', '/en-en/'),
200 $this->buildLanguageConfiguration('FR', '/fr-fr/', ['EN']),
201 $this->buildLanguageConfiguration('FR-CA', '/fr-ca/', ['FR', 'EN']),
202 ]
203 );
204
205 $response = $this->executeFrontendRequest(
206 new InternalRequest($uri),
207 $this->internalRequestContext
208 );
209 $responseStructure = ResponseContent::fromString(
210 (string)$response->getBody()
211 );
212
213 static::assertSame(
214 200,
215 $response->getStatusCode()
216 );
217 static::assertSame(
218 $this->siteTitle,
219 $responseStructure->getScopePath('template/sitetitle')
220 );
221 static::assertSame(
222 $expectedPageTitle,
223 $responseStructure->getScopePath('page/title')
224 );
225 }
226
227 /**
228 * @return array
229 */
230 public function pageIsRenderedWithDomainsDataProvider(): array
231 {
232 $domainPaths = [
233 'https://website.local/',
234 'https://website.us/',
235 'https://website.fr/',
236 'https://website.ca/',
237 'https://website.other/',
238 ];
239
240 $queries = [
241 '?id=102',
242 '?id=acme-first',
243 ];
244
245 return array_map(
246 function (string $uri) {
247 if (strpos($uri, '.fr/') !== false) {
248 $expectedPageTitle = 'FR: Welcome';
249 } elseif (strpos($uri, '.ca/') !== false) {
250 $expectedPageTitle = 'FR-CA: Welcome';
251 } else {
252 $expectedPageTitle = 'EN: Welcome';
253 }
254 return [$uri, $expectedPageTitle];
255 },
256 $this->keysFromValues(
257 $this->meltStrings([$domainPaths, $queries])
258 )
259 );
260 }
261
262 /**
263 * @param string $uri
264 * @param string $expectedPageTitle
265 *
266 * @test
267 * @dataProvider pageIsRenderedWithDomainsDataProvider
268 */
269 public function pageIsRenderedWithDomains(string $uri, string $expectedPageTitle)
270 {
271 $this->writeSiteConfiguration(
272 'website-local',
273 $this->buildSiteConfiguration(101, 'https://website.local/'),
274 [
275 $this->buildDefaultLanguageConfiguration('EN', 'https://website.us/'),
276 $this->buildLanguageConfiguration('FR', 'https://website.fr/', ['EN']),
277 $this->buildLanguageConfiguration('FR-CA', 'https://website.ca/', ['FR', 'EN']),
278 ]
279 );
280
281 $response = $this->executeFrontendRequest(
282 new InternalRequest($uri),
283 $this->internalRequestContext
284 );
285 $responseStructure = ResponseContent::fromString(
286 (string)$response->getBody()
287 );
288
289 static::assertSame(
290 200,
291 $response->getStatusCode()
292 );
293 static::assertSame(
294 $this->siteTitle,
295 $responseStructure->getScopePath('template/sitetitle')
296 );
297 static::assertSame(
298 $expectedPageTitle,
299 $responseStructure->getScopePath('page/title')
300 );
301 }
302
303 /**
304 * @return array
305 */
306 public function pageRenderingStopsWithInvalidCacheHashDataProvider(): array
307 {
308 $domainPaths = [
309 'https://website.local/',
310 ];
311
312 $queries = [
313 '?',
314 '?id=101',
315 '?id=acme-root',
316 '?id=102',
317 '?id=acme-first',
318 ];
319
320 $customQueries = [
321 '&testing[value]=1',
322 '&testing[value]=1&cHash=',
323 '&testing[value]=1&cHash=WRONG',
324 ];
325
326 return $this->wrapInArray(
327 $this->keysFromValues(
328 $this->meltStrings([$domainPaths, $queries, $customQueries])
329 )
330 );
331 }
332
333 /**
334 * @param string $uri
335 *
336 * @test
337 * @dataProvider pageRenderingStopsWithInvalidCacheHashDataProvider
338 * @todo In case no error handler is defined, default handler should be used
339 * @see PlainRequestTest::pageRequestSendsNotFoundResponseWithInvalidCacheHash
340 */
341 public function pageRequestThrowsExceptionWithInvalidCacheHashWithoutHavingErrorHandling(string $uri)
342 {
343 $this->writeSiteConfiguration(
344 'website-local',
345 $this->buildSiteConfiguration(101, 'https://website.local/')
346 );
347
348 $this->expectExceptionCode(1522495914);
349 $this->expectException(\RuntimeException::class);
350
351 $this->executeFrontendRequest(
352 new InternalRequest($uri),
353 $this->internalRequestContext
354 );
355 }
356
357 /**
358 * @param string $uri
359 *
360 * @test
361 * @dataProvider pageRenderingStopsWithInvalidCacheHashDataProvider
362 */
363 public function pageRequestSendsNotFoundResponseWithInvalidCacheHashWithHavingFluidErrorHandling(string $uri)
364 {
365 $this->writeSiteConfiguration(
366 'website-local',
367 $this->buildSiteConfiguration(101, 'https://website.local/'),
368 [],
369 $this->buildErrorHandlingConfiguration('Fluid', [404])
370 );
371
372 $response = $this->executeFrontendRequest(
373 new InternalRequest($uri),
374 $this->internalRequestContext
375 );
376
377 static::assertSame(
378 404,
379 $response->getStatusCode()
380 );
381 static::assertThat(
382 (string)$response->getBody(),
383 static::logicalOr(
384 static::stringContains('message: Request parameters could not be validated (&amp;cHash empty)'),
385 static::stringContains('message: Request parameters could not be validated (&amp;cHash comparison failed)')
386 )
387 );
388 }
389
390 /**
391 * @param string $uri
392 *
393 * @test
394 * @dataProvider pageRenderingStopsWithInvalidCacheHashDataProvider
395 * @todo Response body cannot be asserted since PageContentErrorHandler::handlePageError executes request via HTTP (not internally)
396 */
397 public function pageRequestSendsNotFoundResponseWithInvalidCacheHashWithHavingPageErrorHandling(string $uri)
398 {
399 $this->writeSiteConfiguration(
400 'website-local',
401 $this->buildSiteConfiguration(101, 'https://website.local/'),
402 [],
403 $this->buildErrorHandlingConfiguration('Page', [404])
404 );
405
406 $response = $this->executeFrontendRequest(
407 new InternalRequest($uri),
408 $this->internalRequestContext
409 );
410
411 static::assertSame(
412 404,
413 $response->getStatusCode()
414 );
415 }
416
417 /**
418 * @param string $uri
419 *
420 * @test
421 * @dataProvider pageRenderingStopsWithInvalidCacheHashDataProvider
422 */
423 public function pageRequestSendsNotFoundResponseWithInvalidCacheHashWithHavingPhpErrorHandling(string $uri)
424 {
425 $this->writeSiteConfiguration(
426 'website-local',
427 $this->buildSiteConfiguration(101, 'https://website.local/'),
428 [],
429 $this->buildErrorHandlingConfiguration('PHP', [404])
430 );
431
432 $response = $this->executeFrontendRequest(
433 new InternalRequest($uri),
434 $this->internalRequestContext
435 );
436 $json = json_decode((string)$response->getBody(), true);
437
438 static::assertSame(
439 404,
440 $response->getStatusCode()
441 );
442 static::assertThat(
443 $json['message'] ?? null,
444 static::logicalOr(
445 static::identicalTo('Request parameters could not be validated (&cHash empty)'),
446 static::identicalTo('Request parameters could not be validated (&cHash comparison failed)')
447 )
448 );
449 }
450
451 /**
452 * @return array
453 */
454 public function pageIsRenderedWithValidCacheHashDataProvider(): array
455 {
456 $domainPaths = [
457 '/',
458 'https://localhost/',
459 'https://website.local/',
460 ];
461
462 // cHash has been calculated with encryption key set to
463 // '4408d27a916d51e624b69af3554f516dbab61037a9f7b9fd6f81b4d3bedeccb6'
464 $queries = [
465 // @todo Currently fails since cHash is verified after(!) redirect to page 102
466 // '?&cHash=814ea11ad629c7e24cfd031cea2779f4&id=101',
467 // '?&cHash=814ea11ad629c7e24cfd031cea2779f4id=acme-root',
468 '?&cHash=126d2980c12f4759fed1bb7429db2dff&id=102',
469 '?&cHash=126d2980c12f4759fed1bb7429db2dff&id=acme-first',
470 ];
471
472 $customQueries = [
473 '&testing[value]=1',
474 ];
475
476 $dataSet = $this->wrapInArray(
477 $this->keysFromValues(
478 $this->meltStrings([$domainPaths, $queries, $customQueries])
479 )
480 );
481
482 return $dataSet;
483 }
484
485 /**
486 * @param string $uri
487 *
488 * @test
489 * @dataProvider pageIsRenderedWithValidCacheHashDataProvider
490 */
491 public function pageIsRenderedWithValidCacheHash($uri)
492 {
493 $this->writeSiteConfiguration(
494 'website-local',
495 $this->buildSiteConfiguration(101, 'https://website.local/')
496 );
497
498 $response = $this->executeFrontendRequest(
499 new InternalRequest($uri),
500 $this->internalRequestContext
501 );
502 $responseStructure = ResponseContent::fromString(
503 (string)$response->getBody()
504 );
505 static::assertSame(
506 '1',
507 $responseStructure->getScopePath('getpost/testing.value')
508 );
509 }
510
511 /**
512 * @param string $identifier
513 * @param array $site
514 * @param array $languages
515 * @param array $errorHandling
516 */
517 private function writeSiteConfiguration(
518 string $identifier,
519 array $site = [],
520 array $languages = [],
521 array $errorHandling = []
522 ) {
523 $configuration = [
524 'site' => $site,
525 ];
526 if (!empty($languages)) {
527 $configuration['site']['languages'] = $languages;
528 }
529 if (!empty($errorHandling)) {
530 $configuration['site']['errorHandling'] = $errorHandling;
531 }
532
533 $siteConfiguration = new SiteConfiguration(
534 $this->instancePath . '/typo3conf/sites/'
535 );
536
537 try {
538 $siteConfiguration->write($identifier, $configuration);
539 } catch (\Exception $exception) {
540 $this->markTestSkipped($exception->getMessage());
541 }
542 }
543
544 /**
545 * @param int $rootPageId
546 * @param string $base
547 * @return array
548 */
549 private function buildSiteConfiguration(
550 int $rootPageId,
551 string $base = ''
552 ): array {
553 return [
554 'rootPageId' => $rootPageId,
555 'base' => $base,
556 ];
557 }
558
559 /**
560 * @param string $identifier
561 * @param string $base
562 * @return array
563 */
564 private function buildDefaultLanguageConfiguration(
565 string $identifier,
566 string $base
567 ): array {
568 $configuration = $this->buildLanguageConfiguration($identifier, $base);
569 $configuration['typo3Language'] = 'default';
570 $configuration['flag'] = 'global';
571 unset($configuration['fallbackType']);
572 return $configuration;
573 }
574
575 /**
576 * @param string $identifier
577 * @param string $base
578 * @param array $fallbackIdentifiers
579 * @return array
580 */
581 private function buildLanguageConfiguration(
582 string $identifier,
583 string $base,
584 array $fallbackIdentifiers = []
585 ): array {
586 $preset = $this->resolveLanguagePreset($identifier);
587
588 $configuration = [
589 'languageId' => $preset['id'],
590 'title' => $preset['title'],
591 'navigationTitle' => $preset['title'],
592 'base' => $base,
593 'locale' => $preset['locale'],
594 'iso-639-1' => $preset['iso'],
595 'hreflang' => $preset['hrefLang'],
596 'direction' => $preset['direction'],
597 'typo3Language' => $preset['iso'],
598 'flag' => $preset['iso'],
599 'fallbackType' => 'strict',
600 ];
601
602 if (!empty($fallbackIdentifiers)) {
603 $fallbackIds = array_map(
604 function (string $fallbackIdentifier) {
605 $preset = $this->resolveLanguagePreset($fallbackIdentifier);
606 return $preset['id'];
607 },
608 $fallbackIdentifiers
609 );
610 $configuration['fallbackType'] = 'fallback';
611 $configuration['fallbackType'] = implode(',', $fallbackIds);
612 }
613
614 return $configuration;
615 }
616
617 /**
618 * @param string $handler
619 * @param array $codes
620 * @return array
621 */
622 private function buildErrorHandlingConfiguration(
623 string $handler,
624 array $codes
625 ): array {
626 if ($handler === 'Page') {
627 $baseConfiguration = [
628 'errorContentSource' => '404',
629 ];
630 } elseif ($handler === 'Fluid') {
631 $baseConfiguration = [
632 'errorFluidTemplate' => 'typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/FluidError.html',
633 'errorFluidTemplatesRootPath' => '',
634 'errorFluidLayoutsRootPath' => '',
635 'errorFluidPartialsRootPath' => '',
636 ];
637 } elseif ($handler === 'PHP') {
638 $baseConfiguration = [
639 'errorPhpClassFQCN' => PhpError::class,
640 ];
641 } else {
642 throw new \LogicException(
643 sprintf('Invalid handler "%s"', $handler),
644 1533894782
645 );
646 }
647
648 $baseConfiguration['errorHandler'] = $handler;
649
650 return array_map(
651 function (int $code) use ($baseConfiguration) {
652 $baseConfiguration['errorCode'] = $code;
653 return $baseConfiguration;
654 },
655 $codes
656 );
657 }
658
659 /**
660 * @param string $identifier
661 * @return mixed
662 */
663 private function resolveLanguagePreset(string $identifier)
664 {
665 if (!isset(static::LANGUAGE_PRESETS[$identifier])) {
666 throw new \LogicException(
667 sprintf('Undefined preset identifier "%s"', $identifier),
668 1533893665
669 );
670 }
671 return static::LANGUAGE_PRESETS[$identifier];
672 }
673 }