[TASK] Speed up functional tests using database snapshots
[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\Core\Bootstrap;
19 use TYPO3\CMS\Core\Error\Http\PageNotFoundException;
20 use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\Scenario\DataHandlerFactory;
21 use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\Scenario\DataHandlerWriter;
22 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest;
23 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequestContext;
24 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\ResponseContent;
25
26 /**
27 * Test case for frontend requests having site handling configured
28 */
29 class SiteRequestTest extends AbstractTestCase
30 {
31 /**
32 * @var string
33 */
34 private $siteTitle = 'A Company that Manufactures Everything Inc';
35
36 /**
37 * @var InternalRequestContext
38 */
39 private $internalRequestContext;
40
41 public static function setUpBeforeClass()
42 {
43 parent::setUpBeforeClass();
44 static::initializeDatabaseSnapshot();
45 }
46
47 public static function tearDownAfterClass()
48 {
49 static::destroyDatabaseSnapshot();
50 parent::tearDownAfterClass();
51 }
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->withDatabaseSnapshot(function () {
62 $this->setUpDatabase();
63 });
64 }
65
66 protected function setUpDatabase()
67 {
68 $backendUser = $this->setUpBackendUserFromFixture(1);
69 Bootstrap::initializeLanguageObject();
70
71 $scenarioFile = __DIR__ . '/Fixtures/scenario.yaml';
72 $factory = DataHandlerFactory::fromYamlFile($scenarioFile);
73 $writer = DataHandlerWriter::withBackendUser($backendUser);
74 $writer->invokeFactory($factory);
75 static::failIfArrayIsNotEmpty(
76 $writer->getErrors()
77 );
78
79 $this->setUpFrontendRootPage(
80 1000,
81 [
82 'typo3/sysext/core/Tests/Functional/Fixtures/Frontend/JsonRenderer.typoscript',
83 'typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/JsonRenderer.typoscript',
84 ],
85 [
86 'title' => 'ACME Root',
87 'sitetitle' => $this->siteTitle,
88 ]
89 );
90 }
91
92 protected function tearDown()
93 {
94 unset($this->internalRequestContext);
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=1000',
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(1000, '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 'en-en/',
159 'fr-fr/',
160 'fr-ca/',
161 ];
162
163 $queries = [
164 '?id=1100',
165 '?id=acme-first',
166 ];
167
168 return array_map(
169 function (string $uri) {
170 if (strpos($uri, '/fr-fr/') !== false) {
171 $expectedPageTitle = 'FR: Welcome';
172 } elseif (strpos($uri, '/fr-ca/') !== false) {
173 $expectedPageTitle = 'FR-CA: Welcome';
174 } else {
175 $expectedPageTitle = 'EN: Welcome';
176 }
177 return [$uri, $expectedPageTitle];
178 },
179 $this->keysFromValues(
180 $this->meltStrings([$domainPaths, $languagePaths, $queries])
181 )
182 );
183 }
184
185 /**
186 * @param string $uri
187 * @param string $expectedPageTitle
188 *
189 * @test
190 * @dataProvider pageIsRenderedWithPathsDataProvider
191 */
192 public function pageIsRenderedWithPaths(string $uri, string $expectedPageTitle)
193 {
194 $this->writeSiteConfiguration(
195 'website-local',
196 $this->buildSiteConfiguration(1000, 'https://website.local/'),
197 [
198 $this->buildDefaultLanguageConfiguration('EN', '/en-en/'),
199 $this->buildLanguageConfiguration('FR', '/fr-fr/', ['EN']),
200 $this->buildLanguageConfiguration('FR-CA', '/fr-ca/', ['FR', 'EN']),
201 ]
202 );
203
204 $response = $this->executeFrontendRequest(
205 new InternalRequest($uri),
206 $this->internalRequestContext
207 );
208 $responseStructure = ResponseContent::fromString(
209 (string)$response->getBody()
210 );
211
212 static::assertSame(
213 200,
214 $response->getStatusCode()
215 );
216 static::assertSame(
217 $this->siteTitle,
218 $responseStructure->getScopePath('template/sitetitle')
219 );
220 static::assertSame(
221 $expectedPageTitle,
222 $responseStructure->getScopePath('page/title')
223 );
224 }
225
226 /**
227 * @return array
228 */
229 public function pageIsRenderedWithDomainsDataProvider(): array
230 {
231 $domainPaths = [
232 // @todo: This turns into a redirect to the default language (".us") making this function obsolete
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=1100',
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(1000, '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 restrictedPageIsRenderedDataProvider(): array
307 {
308 $instructions = [
309 // frontend user 1
310 ['https://website.local/?id=1510', 1, 'Whitepapers'],
311 ['https://website.local/?id=1511', 1, 'Products'],
312 ['https://website.local/?id=1512', 1, 'Solutions'],
313 // frontend user 2
314 ['https://website.local/?id=1510', 2, 'Whitepapers'],
315 ['https://website.local/?id=1511', 2, 'Products'],
316 ['https://website.local/?id=1515', 2, 'Research'],
317 ['https://website.local/?id=1520', 2, 'Forecasts'],
318 ['https://website.local/?id=1521', 2, 'Current Year'],
319 // frontend user 3
320 ['https://website.local/?id=1510', 3, 'Whitepapers'],
321 ['https://website.local/?id=1511', 3, 'Products'],
322 ['https://website.local/?id=1512', 3, 'Solutions'],
323 ['https://website.local/?id=1515', 3, 'Research'],
324 ['https://website.local/?id=1520', 3, 'Forecasts'],
325 ['https://website.local/?id=1521', 3, 'Current Year'],
326 ];
327
328 return $this->keysFromTemplate($instructions, '%1$s (user:%2$s)');
329 }
330
331 /**
332 * @param string $uri
333 * @param int $frontendUserId
334 * @param string $expectedPageTitle
335 *
336 * @test
337 * @dataProvider restrictedPageIsRenderedDataProvider
338 */
339 public function restrictedPageIsRendered(string $uri, int $frontendUserId, string $expectedPageTitle)
340 {
341 $this->writeSiteConfiguration(
342 'website-local',
343 $this->buildSiteConfiguration(1000, 'https://website.local/')
344 );
345
346 $response = $this->executeFrontendRequest(
347 new InternalRequest($uri),
348 $this->internalRequestContext
349 ->withFrontendUserId($frontendUserId)
350 );
351 $responseStructure = ResponseContent::fromString(
352 (string)$response->getBody()
353 );
354
355 static::assertSame(
356 200,
357 $response->getStatusCode()
358 );
359 static::assertSame(
360 $this->siteTitle,
361 $responseStructure->getScopePath('template/sitetitle')
362 );
363 static::assertSame(
364 $expectedPageTitle,
365 $responseStructure->getScopePath('page/title')
366 );
367 }
368
369 /**
370 * @return array
371 */
372 public function restrictedPageSendsForbiddenResponseWithUnauthorizedVisitorDataProvider(): array
373 {
374 $instructions = [
375 // no frontend user given
376 ['https://website.local/?id=1510', 0],
377 ['https://website.local/?id=1511', 0],
378 ['https://website.local/?id=1512', 0],
379 ['https://website.local/?id=1515', 0],
380 ['https://website.local/?id=1520', 0],
381 ['https://website.local/?id=1521', 0],
382 // frontend user 1
383 ['https://website.local/?id=1515', 1],
384 ['https://website.local/?id=1520', 1],
385 ['https://website.local/?id=1521', 1],
386 // frontend user 2
387 ['https://website.local/?id=1512', 2],
388 ];
389
390 return $this->keysFromTemplate($instructions, '%1$s (user:%2$s)');
391 }
392
393 /**
394 * @param string $uri
395 * @param int $frontendUserId
396 *
397 * @test
398 * @dataProvider restrictedPageSendsForbiddenResponseWithUnauthorizedVisitorDataProvider
399 */
400 public function restrictedPageSendsForbiddenResponseWithUnauthorizedVisitorWithoutHavingErrorHandling(string $uri, int $frontendUserId)
401 {
402 $this->writeSiteConfiguration(
403 'website-local',
404 $this->buildSiteConfiguration(1000, 'https://website.local/')
405 );
406
407 $response = $this->executeFrontendRequest(
408 new InternalRequest($uri),
409 $this->internalRequestContext
410 ->withFrontendUserId($frontendUserId)
411 );
412
413 static::assertSame(
414 403,
415 $response->getStatusCode()
416 );
417 static::assertThat(
418 (string)$response->getBody(),
419 static::logicalOr(
420 static::stringContains('Reason: ID was not an accessible page'),
421 static::stringContains('Reason: Subsection was found and not accessible')
422 )
423 );
424 }
425
426 /**
427 * @param string $uri
428 * @param int $frontendUserId
429 *
430 * @test
431 * @dataProvider restrictedPageSendsForbiddenResponseWithUnauthorizedVisitorDataProvider
432 */
433 public function restrictedPageSendsForbiddenResponseWithUnauthorizedVisitorWithHavingFluidErrorHandling(string $uri, int $frontendUserId)
434 {
435 $this->writeSiteConfiguration(
436 'website-local',
437 $this->buildSiteConfiguration(1000, 'https://website.local/'),
438 [],
439 $this->buildErrorHandlingConfiguration('Fluid', [403])
440 );
441
442 $response = $this->executeFrontendRequest(
443 new InternalRequest($uri),
444 $this->internalRequestContext
445 ->withFrontendUserId($frontendUserId)
446 );
447
448 static::assertSame(
449 403,
450 $response->getStatusCode()
451 );
452 static::assertContains(
453 'reasons: code,fe_group',
454 (string)$response->getBody()
455 );
456 static::assertThat(
457 (string)$response->getBody(),
458 static::logicalOr(
459 static::stringContains('message: ID was not an accessible page'),
460 static::stringContains('message: Subsection was found and not accessible')
461 )
462 );
463 }
464
465 /**
466 * @param string $uri
467 * @param int $frontendUserId
468 *
469 * @test
470 * @dataProvider restrictedPageSendsForbiddenResponseWithUnauthorizedVisitorDataProvider
471 * @todo Response body cannot be asserted since PageContentErrorHandler::handlePageError executes request via HTTP (not internally)
472 */
473 public function restrictedPageSendsForbiddenResponseWithUnauthorizedVisitorWithHavingPageErrorHandling(string $uri, int $frontendUserId)
474 {
475 $this->markTestSkipped('Skipped until PageContentErrorHandler::handlePageError does not use HTTP anymore');
476
477 $this->writeSiteConfiguration(
478 'website-local',
479 $this->buildSiteConfiguration(1000, 'https://website.local/'),
480 [],
481 $this->buildErrorHandlingConfiguration('Page', [403])
482 );
483
484 $response = $this->executeFrontendRequest(
485 new InternalRequest($uri),
486 $this->internalRequestContext
487 ->withFrontendUserId($frontendUserId)
488 );
489
490 static::assertSame(
491 403,
492 $response->getStatusCode()
493 );
494 }
495
496 /**
497 * @param string $uri
498 * @param int $frontendUserId
499 *
500 * @test
501 * @dataProvider restrictedPageSendsForbiddenResponseWithUnauthorizedVisitorDataProvider
502 */
503 public function restrictedPageSendsForbiddenResponseWithUnauthorizedVisitorWithHavingPhpErrorHandling(string $uri, int $frontendUserId)
504 {
505 $this->writeSiteConfiguration(
506 'website-local',
507 $this->buildSiteConfiguration(1000, 'https://website.local/'),
508 [],
509 $this->buildErrorHandlingConfiguration('PHP', [403])
510 );
511
512 $response = $this->executeFrontendRequest(
513 new InternalRequest($uri),
514 $this->internalRequestContext
515 ->withFrontendUserId($frontendUserId)
516 );
517 $json = json_decode((string)$response->getBody(), true);
518
519 static::assertSame(
520 403,
521 $response->getStatusCode()
522 );
523 static::assertThat(
524 $json['message'] ?? null,
525 static::logicalOr(
526 static::identicalTo('ID was not an accessible page'),
527 static::identicalTo('Subsection was found and not accessible')
528 )
529 );
530 }
531
532 /**
533 * @return array
534 */
535 public function pageRenderingStopsWithInvalidCacheHashDataProvider(): array
536 {
537 $domainPaths = [
538 'https://website.local/',
539 ];
540
541 $queries = [
542 '?',
543 '?id=1000',
544 '?id=acme-root',
545 '?id=1100',
546 '?id=acme-first',
547 ];
548
549 $customQueries = [
550 '&testing[value]=1',
551 '&testing[value]=1&cHash=',
552 '&testing[value]=1&cHash=WRONG',
553 ];
554
555 return $this->wrapInArray(
556 $this->keysFromValues(
557 $this->meltStrings([$domainPaths, $queries, $customQueries])
558 )
559 );
560 }
561
562 /**
563 * @param string $uri
564 *
565 * @test
566 * @dataProvider pageRenderingStopsWithInvalidCacheHashDataProvider
567 */
568 public function pageRequestThrowsExceptionWithInvalidCacheHashWithoutHavingErrorHandling(string $uri)
569 {
570 $this->writeSiteConfiguration(
571 'website-local',
572 $this->buildSiteConfiguration(1000, 'https://website.local/')
573 );
574
575 $this->expectExceptionCode(1518472189);
576 $this->expectException(PageNotFoundException::class);
577
578 $this->executeFrontendRequest(
579 new InternalRequest($uri),
580 $this->internalRequestContext
581 );
582 }
583
584 /**
585 * @param string $uri
586 *
587 * @test
588 * @dataProvider pageRenderingStopsWithInvalidCacheHashDataProvider
589 */
590 public function pageRequestSendsNotFoundResponseWithInvalidCacheHash(string $uri)
591 {
592 $response = $this->executeFrontendRequest(
593 new InternalRequest($uri),
594 $this->internalRequestContext->withMergedGlobalSettings([
595 'TYPO3_CONF_VARS' => [
596 'FE' => [
597 'pageNotFound_handling' => 'READFILE:typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/PageError.txt',
598 ]
599 ]
600 ])
601 );
602
603 static::assertSame(
604 404,
605 $response->getStatusCode()
606 );
607 static::assertThat(
608 (string)$response->getBody(),
609 static::logicalOr(
610 static::stringContains('reason: Request parameters could not be validated (&amp;cHash empty)'),
611 static::stringContains('reason: Request parameters could not be validated (&amp;cHash comparison failed)')
612 )
613 );
614 }
615
616 /**
617 * @param string $uri
618 *
619 * @test
620 * @dataProvider pageRenderingStopsWithInvalidCacheHashDataProvider
621 */
622 public function pageRequestSendsNotFoundResponseWithInvalidCacheHashWithHavingFluidErrorHandling(string $uri)
623 {
624 $this->writeSiteConfiguration(
625 'website-local',
626 $this->buildSiteConfiguration(1000, 'https://website.local/'),
627 [],
628 $this->buildErrorHandlingConfiguration('Fluid', [404])
629 );
630
631 $response = $this->executeFrontendRequest(
632 new InternalRequest($uri),
633 $this->internalRequestContext
634 );
635
636 static::assertSame(
637 404,
638 $response->getStatusCode()
639 );
640 static::assertThat(
641 (string)$response->getBody(),
642 static::logicalOr(
643 static::stringContains('message: Request parameters could not be validated (&amp;cHash empty)'),
644 static::stringContains('message: Request parameters could not be validated (&amp;cHash comparison failed)')
645 )
646 );
647 }
648
649 /**
650 * @param string $uri
651 *
652 * @test
653 * @dataProvider pageRenderingStopsWithInvalidCacheHashDataProvider
654 * @todo Response body cannot be asserted since PageContentErrorHandler::handlePageError executes request via HTTP (not internally)
655 */
656 public function pageRequestSendsNotFoundResponseWithInvalidCacheHashWithHavingPageErrorHandling(string $uri)
657 {
658 $this->markTestSkipped('Skipped until PageContentErrorHandler::handlePageError does not use HTTP anymore');
659
660 $this->writeSiteConfiguration(
661 'website-local',
662 $this->buildSiteConfiguration(1000, 'https://website.local/'),
663 [],
664 $this->buildErrorHandlingConfiguration('Page', [404])
665 );
666
667 $response = $this->executeFrontendRequest(
668 new InternalRequest($uri),
669 $this->internalRequestContext
670 );
671
672 static::assertSame(
673 404,
674 $response->getStatusCode()
675 );
676 }
677
678 /**
679 * @param string $uri
680 *
681 * @test
682 * @dataProvider pageRenderingStopsWithInvalidCacheHashDataProvider
683 */
684 public function pageRequestSendsNotFoundResponseWithInvalidCacheHashWithHavingPhpErrorHandling(string $uri)
685 {
686 $this->writeSiteConfiguration(
687 'website-local',
688 $this->buildSiteConfiguration(1000, 'https://website.local/'),
689 [],
690 $this->buildErrorHandlingConfiguration('PHP', [404])
691 );
692
693 $response = $this->executeFrontendRequest(
694 new InternalRequest($uri),
695 $this->internalRequestContext
696 );
697 $json = json_decode((string)$response->getBody(), true);
698
699 static::assertSame(
700 404,
701 $response->getStatusCode()
702 );
703 static::assertThat(
704 $json['message'] ?? null,
705 static::logicalOr(
706 static::identicalTo('Request parameters could not be validated (&cHash empty)'),
707 static::identicalTo('Request parameters could not be validated (&cHash comparison failed)')
708 )
709 );
710 }
711
712 /**
713 * @return array
714 */
715 public function pageIsRenderedWithValidCacheHashDataProvider(): array
716 {
717 $domainPaths = [
718 '/',
719 'https://localhost/',
720 'https://website.local/',
721 ];
722
723 // cHash has been calculated with encryption key set to
724 // '4408d27a916d51e624b69af3554f516dbab61037a9f7b9fd6f81b4d3bedeccb6'
725 $queries = [
726 // @todo Currently fails since cHash is verified after(!) redirect to page 1100
727 // '?&cHash=7d1f13fa91159dac7feb3c824936b39d&id=1000',
728 // '?&cHash=7d1f13fa91159dac7feb3c824936b39d=acme-root',
729 '?&cHash=f42b850e435f0cedd366f5db749fc1af&id=1100',
730 '?&cHash=f42b850e435f0cedd366f5db749fc1af&id=acme-first',
731 ];
732
733 $customQueries = [
734 '&testing[value]=1',
735 ];
736
737 $dataSet = $this->wrapInArray(
738 $this->keysFromValues(
739 $this->meltStrings([$domainPaths, $queries, $customQueries])
740 )
741 );
742
743 return $dataSet;
744 }
745
746 /**
747 * @param string $uri
748 *
749 * @test
750 * @dataProvider pageIsRenderedWithValidCacheHashDataProvider
751 */
752 public function pageIsRenderedWithValidCacheHash($uri)
753 {
754 $this->writeSiteConfiguration(
755 'website-local',
756 $this->buildSiteConfiguration(1000, 'https://website.local/')
757 );
758
759 $response = $this->executeFrontendRequest(
760 new InternalRequest($uri),
761 $this->internalRequestContext
762 );
763 $responseStructure = ResponseContent::fromString(
764 (string)$response->getBody()
765 );
766 static::assertSame(
767 '1',
768 $responseStructure->getScopePath('getpost/testing.value')
769 );
770 }
771 }