[BUGFIX] Remove side effects from ErrorController tests
[Packages/TYPO3.CMS.git] / typo3 / sysext / frontend / Tests / Unit / Controller / ErrorControllerTest.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Frontend\Tests\Unit\Controller;
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 Prophecy\Argument;
19 use Psr\Http\Message\StreamInterface;
20 use TYPO3\CMS\Core\Controller\ErrorPageController;
21 use TYPO3\CMS\Core\Http\HtmlResponse;
22 use TYPO3\CMS\Core\Http\RedirectResponse;
23 use TYPO3\CMS\Core\Http\RequestFactory;
24 use TYPO3\CMS\Core\Http\Response;
25 use TYPO3\CMS\Core\Utility\GeneralUtility;
26 use TYPO3\CMS\Frontend\Controller\ErrorController;
27 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
28
29 /**
30 * Test case
31 */
32 class ErrorControllerTest extends UnitTestCase
33 {
34 /**
35 * @var array Backup of singleton instances
36 */
37 protected $backupSingletonInstances;
38
39 /**
40 * setUp() stores non singleton instances to reset in tearDown()
41 */
42 public function setUp()
43 {
44 parent::setUp();
45 $this->backupSingletonInstances = GeneralUtility::getSingletonInstances();
46 }
47
48 /**
49 * Purge instances and reset singleton instances
50 */
51 public function tearDown()
52 {
53 GeneralUtility::purgeInstances();
54 GeneralUtility::resetSingletonInstances($this->backupSingletonInstances);
55 parent::tearDown();
56 }
57
58 /**
59 * @test
60 */
61 public function pageNotFoundHandlingThrowsExceptionIfNotConfigured()
62 {
63 $this->expectExceptionMessage('This test page was not found!');
64 $this->expectExceptionCode(1518472189);
65 $GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFound_handling'] = false;
66 $subject = new ErrorController();
67 $subject->pageNotFoundAction('This test page was not found!');
68 }
69
70 /**
71 * Data Provider for 404
72 *
73 * @return array
74 */
75 public function errorPageHandlingDataProvider()
76 {
77 return [
78 '404 with default errorpage' => [
79 'handler' => true,
80 'header' => 'HTTP/1.0 404 Not Found',
81 'message' => 'Custom message',
82 'response' => [
83 'type' => HtmlResponse::class,
84 'statusCode' => 404,
85 'reasonPhrase' => 'Not Found',
86 'content' => 'Reason: Custom message',
87 'headers' => [
88 'Content-Type' => ['text/html; charset=utf-8'],
89 ],
90 ],
91 ],
92 '404 with default errorpage setting the handler to legacy value' => [
93 'handler' => '1',
94 'header' => 'HTTP/1.0 404 This is a dead end',
95 'message' => 'Come back tomorrow',
96 'response' => [
97 'type' => HtmlResponse::class,
98 'statusCode' => 404,
99 'reasonPhrase' => 'This is a dead end',
100 'content' => 'Reason: Come back tomorrow',
101 'headers' => [
102 'Content-Type' => ['text/html; charset=utf-8'],
103 ],
104 ],
105 ],
106 '404 with custom userfunction' => [
107 'handler' => 'USER_FUNCTION:' . ErrorControllerTest::class . '->mockedUserFunctionCall',
108 'header' => 'HTTP/1.0 404 Not Found',
109 'message' => 'Custom message',
110 'response' => [
111 'type' => HtmlResponse::class,
112 'statusCode' => 404,
113 'reasonPhrase' => 'Not Found',
114 'content' => 'It\'s magic, Michael: Custom message',
115 'headers' => [
116 'Content-Type' => ['text/html; charset=utf-8'],
117 ],
118 ],
119 ],
120 '404 with a readfile functionality' => [
121 'handler' => 'READFILE:typo3/sysext/frontend/Tests/Unit/Controller/Fixtures/error.txt',
122 'header' => 'HTTP/1.0 404 Not Found',
123 'message' => 'Custom message',
124 'response' => [
125 'type' => HtmlResponse::class,
126 'statusCode' => 404,
127 'reasonPhrase' => 'Not Found',
128 'content' => 'rama-lama-ding-dong',
129 'headers' => [
130 'Content-Type' => ['text/html; charset=utf-8'],
131 ],
132 ],
133 ],
134 '404 with a readfile functionality with an invalid file' => [
135 'handler' => 'READFILE:does_not_exist.php6',
136 'header' => 'HTTP/1.0 404 Not Found',
137 'message' => 'Custom message',
138 'response' => null,
139 'exceptionCode' => 1518472245,
140 ],
141 '404 with a redirect - never do that in production - it is bad for SEO. But with custom headers as well...' => [
142 'handler' => 'REDIRECT:www.typo3.org',
143 'header' => 'HTTP/1.0 404 Not Found
144 X-TYPO3-Additional-Header: Banana Stand',
145 'message' => 'Custom message',
146 'response' => [
147 'type' => RedirectResponse::class,
148 'statusCode' => 404,
149 'reasonPhrase' => 'Not Found',
150 'headers' => [
151 'location' => ['www.typo3.org'],
152 'X-TYPO3-Additional-Header' => ['Banana Stand'],
153 ],
154 ],
155 ],
156 ];
157 }
158
159 /**
160 * @test
161 * @dataProvider errorPageHandlingDataProvider
162 */
163 public function pageNotFoundHandlingReturnsConfiguredResponseObject(
164 $handler,
165 $header,
166 $message,
167 $expectedResponseDetails,
168 $expectedExceptionCode = null
169 ) {
170 if ($expectedExceptionCode !== null) {
171 $this->expectExceptionCode($expectedExceptionCode);
172 }
173 $GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFound_handling'] = $handler;
174 $GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFound_handling_statheader'] = $header;
175 // faking getIndpEnv() variables
176 $_SERVER['REQUEST_URI'] = '/unit-test/';
177 $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
178 $_SERVER['HTTP_HOST'] = 'localhost';
179 $_SERVER['SSL_SESSION_ID'] = true;
180
181 $this->prophesizeErrorPageController();
182
183 $subject = new ErrorController();
184 $response = $subject->pageNotFoundAction($message);
185 if (is_array($expectedResponseDetails)) {
186 $this->assertInstanceOf($expectedResponseDetails['type'], $response);
187 $this->assertEquals($expectedResponseDetails['statusCode'], $response->getStatusCode());
188 $this->assertEquals($expectedResponseDetails['reasonPhrase'], $response->getReasonPhrase());
189 if (isset($expectedResponseDetails['content'])) {
190 $this->assertContains($expectedResponseDetails['content'], $response->getBody()->getContents());
191 }
192 $this->assertEquals($expectedResponseDetails['headers'], $response->getHeaders());
193 }
194 }
195
196 /**
197 * @test
198 */
199 public function pageNotFoundHandlingReturnsResponseFromPrefix()
200 {
201 $GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFound_handling'] = '/404/';
202 $GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFound_handling_statheader'] = 'HTTP/1.0 404 Not Found
203 X-TYPO3-Additional-Header: Banana Stand';
204 // faking getIndpEnv() variables
205 $_SERVER['REQUEST_URI'] = '/unit-test/';
206 $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
207 $_SERVER['HTTP_HOST'] = 'localhost';
208 $_SERVER['SSL_SESSION_ID'] = true;
209 $this->prophesizeErrorPageController();
210 $subject = new ErrorController();
211
212 $this->prophesizeGetUrl();
213 $response = $subject->pageNotFoundAction('Custom message');
214
215 $expectedResponseDetails = [
216 'type' => HtmlResponse::class,
217 'statusCode' => 404,
218 'reasonPhrase' => 'Not Found',
219 'headers' => [
220 'Content-Type' => ['text/html; charset=utf-8'],
221 'X-TYPO3-Additional-Header' => ['Banana Stand'],
222 ],
223 ];
224 $this->assertInstanceOf($expectedResponseDetails['type'], $response);
225 $this->assertEquals($expectedResponseDetails['statusCode'], $response->getStatusCode());
226 $this->assertEquals($expectedResponseDetails['reasonPhrase'], $response->getReasonPhrase());
227 if (isset($expectedResponseDetails['content'])) {
228 $this->assertContains($expectedResponseDetails['content'], $response->getBody()->getContents());
229 }
230 $this->assertEquals($expectedResponseDetails['headers'], $response->getHeaders());
231 }
232
233 /**
234 * Data Provider for 403
235 *
236 * @return array
237 */
238 public function accessDeniedDataProvider()
239 {
240 return [
241 '403 with default errorpage' => [
242 'handler' => true,
243 'header' => 'HTTP/1.0 403 Who are you',
244 'message' => 'Be nice, do good',
245 'response' => [
246 'type' => HtmlResponse::class,
247 'statusCode' => 403,
248 'reasonPhrase' => 'Who are you',
249 'content' => 'Reason: Be nice, do good',
250 'headers' => [
251 'Content-Type' => ['text/html; charset=utf-8'],
252 ],
253 ],
254 ],
255 ];
256 }
257
258 /**
259 * @test
260 * @dataProvider accessDeniedDataProvider
261 */
262 public function accessDeniedReturnsProperHeaders($handler, $header, $message, $expectedResponseDetails)
263 {
264 $GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFound_handling'] = $handler;
265 $GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFound_handling_accessdeniedheader'] = $header;
266 // faking getIndpEnv() variables
267 $_SERVER['REQUEST_URI'] = '/unit-test/';
268 $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
269 $_SERVER['HTTP_HOST'] = 'localhost';
270 $_SERVER['SSL_SESSION_ID'] = true;
271 $subject = new ErrorController();
272 $response = $subject->accessDeniedAction($message);
273 if (is_array($expectedResponseDetails)) {
274 $this->assertInstanceOf($expectedResponseDetails['type'], $response);
275 $this->assertEquals($expectedResponseDetails['statusCode'], $response->getStatusCode());
276 $this->assertEquals($expectedResponseDetails['reasonPhrase'], $response->getReasonPhrase());
277 if (isset($expectedResponseDetails['content'])) {
278 $this->assertContains($expectedResponseDetails['content'], $response->getBody()->getContents());
279 }
280 $this->assertEquals($expectedResponseDetails['headers'], $response->getHeaders());
281 }
282 }
283
284 /**
285 * @test
286 */
287 public function unavailableHandlingThrowsExceptionIfNotConfigured()
288 {
289 $GLOBALS['TYPO3_CONF_VARS']['SYS']['devIPmask'] = '*';
290 $GLOBALS['TYPO3_CONF_VARS']['FE']['pageUnavailable_handling'] = true;
291 $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
292 $this->expectExceptionMessage('All your system are belong to us!');
293 $this->expectExceptionCode(1518472181);
294 $GLOBALS['TYPO3_CONF_VARS']['FE']['pageUnavailable_handling'] = false;
295 $subject = new ErrorController();
296 $subject->unavailableAction('All your system are belong to us!');
297 }
298
299 /**
300 * @test
301 */
302 public function unavailableHandlingDoesNotTriggerDueToDevIpMask()
303 {
304 $GLOBALS['TYPO3_CONF_VARS']['SYS']['devIPmask'] = '*';
305 $GLOBALS['TYPO3_CONF_VARS']['FE']['pageUnavailable_handling'] = true;
306 $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
307
308 $this->expectExceptionMessage('All your system are belong to us!');
309 $this->expectExceptionCode(1518472181);
310 $subject = new ErrorController();
311 $subject->unavailableAction('All your system are belong to us!');
312 }
313
314 /**
315 * Data Provider for 503
316 *
317 * @return array
318 */
319 public function unavailableHandlingDataProvider()
320 {
321 return [
322 '503 with default errorpage' => [
323 'handler' => true,
324 'header' => 'HTTP/1.0 503 Service Temporarily Unavailable',
325 'message' => 'Custom message',
326 'response' => [
327 'type' => HtmlResponse::class,
328 'statusCode' => 503,
329 'reasonPhrase' => 'Service Temporarily Unavailable',
330 'content' => 'Reason: Custom message',
331 'headers' => [
332 'Content-Type' => ['text/html; charset=utf-8'],
333 ],
334 ],
335 ],
336 '503 with default errorpage setting the handler to legacy value' => [
337 'handler' => '1',
338 'header' => 'HTTP/1.0 503 This is a dead end',
339 'message' => 'Come back tomorrow',
340 'response' => [
341 'type' => HtmlResponse::class,
342 'statusCode' => 503,
343 'reasonPhrase' => 'This is a dead end',
344 'content' => 'Reason: Come back tomorrow',
345 'headers' => [
346 'Content-Type' => ['text/html; charset=utf-8'],
347 ],
348 ],
349 ],
350 '503 with custom userfunction' => [
351 'handler' => 'USER_FUNCTION:' . ErrorControllerTest::class . '->mockedUserFunctionCall',
352 'header' => 'HTTP/1.0 503 Service Temporarily Unavailable',
353 'message' => 'Custom message',
354 'response' => [
355 'type' => HtmlResponse::class,
356 'statusCode' => 503,
357 'reasonPhrase' => 'Service Temporarily Unavailable',
358 'content' => 'It\'s magic, Michael: Custom message',
359 'headers' => [
360 'Content-Type' => ['text/html; charset=utf-8'],
361 ],
362 ],
363 ],
364 '503 with a readfile functionality' => [
365 'handler' => 'READFILE:typo3/sysext/frontend/Tests/Unit/Controller/Fixtures/error.txt',
366 'header' => 'HTTP/1.0 503 Service Temporarily Unavailable',
367 'message' => 'Custom message',
368 'response' => [
369 'type' => HtmlResponse::class,
370 'statusCode' => 503,
371 'reasonPhrase' => 'Service Temporarily Unavailable',
372 'content' => 'Let it snow',
373 'headers' => [
374 'Content-Type' => ['text/html; charset=utf-8'],
375 ],
376 ],
377 ],
378 '503 with a readfile functionality with an invalid file' => [
379 'handler' => 'READFILE:does_not_exist.php6',
380 'header' => 'HTTP/1.0 503 Service Temporarily Unavailable',
381 'message' => 'Custom message',
382 'response' => null,
383 'exceptionCode' => 1518472245,
384 ],
385 '503 with a redirect - never do that in production - it is bad for SEO. But with custom headers as well...' => [
386 'handler' => 'REDIRECT:www.typo3.org',
387 'header' => 'HTTP/1.0 503 Service Temporarily Unavailable
388 X-TYPO3-Additional-Header: Banana Stand',
389 'message' => 'Custom message',
390 'response' => [
391 'type' => RedirectResponse::class,
392 'statusCode' => 503,
393 'reasonPhrase' => 'Service Temporarily Unavailable',
394 'headers' => [
395 'location' => ['www.typo3.org'],
396 'X-TYPO3-Additional-Header' => ['Banana Stand'],
397 ],
398 ],
399 ],
400 ];
401 }
402
403 /**
404 * @test
405 * @dataProvider unavailableHandlingDataProvider
406 */
407 public function pageUnavailableHandlingReturnsConfiguredResponseObject(
408 $handler,
409 $header,
410 $message,
411 $expectedResponseDetails,
412 $expectedExceptionCode = null
413 ) {
414 if ($expectedExceptionCode !== null) {
415 $this->expectExceptionCode($expectedExceptionCode);
416 }
417 $GLOBALS['TYPO3_CONF_VARS']['SYS']['devIPmask'] = '-1';
418 $GLOBALS['TYPO3_CONF_VARS']['FE']['pageUnavailable_handling'] = $handler;
419 $GLOBALS['TYPO3_CONF_VARS']['FE']['pageUnavailable_handling_statheader'] = $header;
420 // faking getIndpEnv() variables
421 $_SERVER['REQUEST_URI'] = '/unit-test/';
422 $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
423 $_SERVER['HTTP_HOST'] = 'localhost';
424 $_SERVER['SSL_SESSION_ID'] = true;
425 $this->prophesizeGetUrl();
426 $this->prophesizeErrorPageController();
427 $subject = new ErrorController();
428 $response = $subject->unavailableAction($message);
429 if (is_array($expectedResponseDetails)) {
430 $this->assertInstanceOf($expectedResponseDetails['type'], $response);
431 $this->assertEquals($expectedResponseDetails['statusCode'], $response->getStatusCode());
432 $this->assertEquals($expectedResponseDetails['reasonPhrase'], $response->getReasonPhrase());
433 if (isset($expectedResponseDetails['content'])) {
434 $this->assertContains($expectedResponseDetails['content'], $response->getBody()->getContents());
435 }
436 $this->assertEquals($expectedResponseDetails['headers'], $response->getHeaders());
437 }
438 }
439
440 /**
441 * @test
442 */
443 public function pageUnavailableHandlingReturnsResponseOfPrefix()
444 {
445 $GLOBALS['TYPO3_CONF_VARS']['SYS']['devIPmask'] = '-1';
446 $GLOBALS['TYPO3_CONF_VARS']['FE']['pageUnavailable_handling'] = '/fail/';
447 $GLOBALS['TYPO3_CONF_VARS']['FE']['pageUnavailable_handling_statheader'] = 'HTTP/1.0 503 Service Temporarily Unavailable
448 X-TYPO3-Additional-Header: Banana Stand';
449 // faking getIndpEnv() variables
450 $_SERVER['REQUEST_URI'] = '/unit-test/';
451 $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
452 $_SERVER['HTTP_HOST'] = 'localhost';
453 $_SERVER['SSL_SESSION_ID'] = true;
454 $this->prophesizeErrorPageController();
455 $this->prophesizeGetUrl();
456 $subject = new ErrorController();
457 $response = $subject->unavailableAction('custom message');
458
459 $expectedResponseDetails = [
460 'type' => HtmlResponse::class,
461 'statusCode' => 503,
462 'reasonPhrase' => 'Service Temporarily Unavailable',
463 'headers' => [
464 'Content-Type' => ['text/html; charset=utf-8'],
465 'X-TYPO3-Additional-Header' => ['Banana Stand'],
466 ],
467 ];
468 $this->assertInstanceOf($expectedResponseDetails['type'], $response);
469 $this->assertEquals($expectedResponseDetails['statusCode'], $response->getStatusCode());
470 $this->assertEquals($expectedResponseDetails['reasonPhrase'], $response->getReasonPhrase());
471 if (isset($expectedResponseDetails['content'])) {
472 $this->assertContains($expectedResponseDetails['content'], $response->getBody()->getContents());
473 }
474 $this->assertEquals($expectedResponseDetails['headers'], $response->getHeaders());
475 }
476
477 /**
478 * Callback function when testing "USER_FUNCTION:" prefix
479 */
480 public function mockedUserFunctionCall($params)
481 {
482 return '<p>It\'s magic, Michael: ' . $params['reasonText'] . '</p>';
483 }
484
485 private function prophesizeErrorPageController(): void
486 {
487 $errorPageControllerProphecy = $this->prophesize(ErrorPageController::class);
488 $errorPageControllerProphecy->errorAction(Argument::cetera())
489 ->will(
490 function ($args) {
491 return 'Reason: ' . $args[1];
492 }
493 );
494 GeneralUtility::addInstance(ErrorPageController::class, $errorPageControllerProphecy->reveal());
495 }
496
497 private function prophesizeGetUrl(): void
498 {
499 $streamProphecy = $this->prophesize(StreamInterface::class);
500 $prefixPageResponseProphecy = $this->prophesize(Response::class);
501 $prefixPageResponseProphecy->getHeaders()->willReturn([]);
502 $prefixPageResponseProphecy->getBody()->willReturn($streamProphecy);
503 $prefixPageResponseProphecy->getStatusCode()->willReturn(200);
504 $prefixPageResponseProphecy->getReasonPhrase()->willReturn('OK');
505 $requestFactoryProphecy = $this->prophesize(RequestFactory::class);
506 $requestFactoryProphecy->request(Argument::cetera())->willReturn($prefixPageResponseProphecy->reveal());
507 GeneralUtility::addInstance(RequestFactory::class, $requestFactoryProphecy->reveal());
508 }
509 }