2e515d9b6b675f91213a6f2c6df4c78b2b1b98c6
[Packages/TYPO3.CMS.git] / typo3 / sysext / extbase / Tests / Unit / Mvc / Controller / ActionControllerTest.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Extbase\Tests\Unit\Mvc\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 TYPO3\CMS\Core\Page\PageRenderer;
19 use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
20 use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
21 use TYPO3\CMS\Extbase\Mvc\Controller\Arguments;
22 use TYPO3\CMS\Extbase\Mvc\Exception\InvalidArgumentTypeException;
23 use TYPO3\CMS\Extbase\Mvc\Exception\NoSuchActionException;
24 use TYPO3\CMS\Extbase\Mvc\Request;
25 use TYPO3\CMS\Extbase\Mvc\RequestInterface;
26 use TYPO3\CMS\Extbase\Mvc\View\ViewInterface;
27 use TYPO3\CMS\Extbase\Object\ObjectManager;
28 use TYPO3\CMS\Extbase\Reflection\ClassSchema;
29 use TYPO3\CMS\Extbase\Reflection\ReflectionService;
30 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
31 use TYPO3Fluid\Fluid\View\AbstractTemplateView;
32 use TYPO3Fluid\Fluid\View\TemplateView as FluidTemplateView;
33
34 /**
35 * Test case
36 */
37 class ActionControllerTest extends UnitTestCase
38 {
39 /**
40 * @var bool Reset singletons created by subject
41 */
42 protected $resetSingletonInstances = true;
43
44 /**
45 * @var \TYPO3\CMS\Extbase\Mvc\Controller\ActionController|\PHPUnit_Framework_MockObject_MockObject|\TYPO3\TestingFramework\Core\AccessibleObjectInterface
46 */
47 protected $actionController;
48
49 /**
50 * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
51 */
52 protected $mockObjectManager;
53
54 /**
55 * @var \TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder
56 */
57 protected $mockUriBuilder;
58
59 /**
60 * @var \TYPO3\CMS\Extbase\Mvc\Controller\MvcPropertyMappingConfigurationService
61 */
62 protected $mockMvcPropertyMappingConfigurationService;
63
64 /**
65 * @test
66 */
67 public function resolveActionMethodNameReturnsTheCurrentActionMethodNameFromTheRequest()
68 {
69 $mockRequest = $this->createMock(Request::class);
70 $mockRequest->expects($this->once())->method('getControllerActionName')->will($this->returnValue('fooBar'));
71 /** @var \TYPO3\CMS\Extbase\Mvc\Controller\ActionController|\PHPUnit_Framework_MockObject_MockObject|\TYPO3\TestingFramework\Core\AccessibleObjectInterface */
72 $mockController = $this->getAccessibleMock(ActionController::class, ['fooBarAction'], [], '', false);
73 $mockController->_set('request', $mockRequest);
74 $this->assertEquals('fooBarAction', $mockController->_call('resolveActionMethodName'));
75 }
76
77 /**
78 * @test
79 */
80 public function resolveActionMethodNameThrowsAnExceptionIfTheActionDefinedInTheRequestDoesNotExist()
81 {
82 $this->expectException(NoSuchActionException::class);
83 $this->expectExceptionCode(1186669086);
84 $mockRequest = $this->createMock(Request::class);
85 $mockRequest->expects($this->once())->method('getControllerActionName')->will($this->returnValue('fooBar'));
86 /** @var \TYPO3\CMS\Extbase\Mvc\Controller\ActionController|\PHPUnit_Framework_MockObject_MockObject|\TYPO3\TestingFramework\Core\AccessibleObjectInterface */
87 $mockController = $this->getAccessibleMock(ActionController::class, ['otherBarAction'], [], '', false);
88 $mockController->_set('request', $mockRequest);
89 $mockController->_call('resolveActionMethodName');
90 }
91
92 /**
93 * @test
94 */
95 public function initializeActionMethodArgumentsRegistersArgumentsFoundInTheSignatureOfTheCurrentActionMethod(): void
96 {
97 $mockRequest = $this->createMock(Request::class);
98 $mockArguments = $this->getMockBuilder(Arguments::class)
99 ->setMethods(['addNewArgument', 'removeAll'])
100 ->getMock();
101 $mockArguments->expects($this->at(0))->method('addNewArgument')->with('stringArgument', 'string', true);
102 $mockArguments->expects($this->at(1))->method('addNewArgument')->with('integerArgument', 'integer', true);
103 $mockArguments->expects($this->at(2))->method('addNewArgument')->with('objectArgument', 'F3_Foo_Bar', true);
104 $mockController = $this->getAccessibleMock(ActionController::class, ['fooAction', 'evaluateDontValidateAnnotations'], [], '', false);
105 $methodParameters = [
106 'stringArgument' => [
107 'position' => 0,
108 'byReference' => false,
109 'array' => false,
110 'optional' => false,
111 'allowsNull' => false,
112 'type' => 'string',
113 'hasDefaultValue' => false
114 ],
115 'integerArgument' => [
116 'position' => 1,
117 'byReference' => false,
118 'array' => false,
119 'optional' => false,
120 'allowsNull' => false,
121 'type' => 'integer',
122 'hasDefaultValue' => false
123 ],
124 'objectArgument' => [
125 'position' => 2,
126 'byReference' => false,
127 'array' => false,
128 'optional' => false,
129 'allowsNull' => false,
130 'type' => 'F3_Foo_Bar',
131 'hasDefaultValue' => false
132 ]
133 ];
134
135 $classSchemaMock = $this->createMock(ClassSchema::class);
136 $classSchemaMock
137 ->expects($this->any())
138 ->method('getMethod')
139 ->with('fooAction')
140 ->willReturn([
141 'params' => $methodParameters
142 ]);
143
144 $mockReflectionService = $this->createMock(ReflectionService::class);
145 $mockReflectionService
146 ->expects($this->any())
147 ->method('getClassSchema')
148 ->with(get_class($mockController))
149 ->willReturn($classSchemaMock);
150 $mockController->_set('reflectionService', $mockReflectionService);
151 $mockController->_set('request', $mockRequest);
152 $mockController->_set('arguments', $mockArguments);
153 $mockController->_set('actionMethodName', 'fooAction');
154 $mockController->_call('initializeActionMethodArguments');
155 }
156
157 /**
158 * @test
159 */
160 public function initializeActionMethodArgumentsRegistersOptionalArgumentsAsSuch(): void
161 {
162 $mockRequest = $this->createMock(Request::class);
163 $mockArguments = $this->createMock(Arguments::class);
164 $mockArguments->expects($this->at(0))->method('addNewArgument')->with('arg1', 'string', true);
165 $mockArguments->expects($this->at(1))->method('addNewArgument')->with('arg2', 'array', false, [21]);
166 $mockArguments->expects($this->at(2))->method('addNewArgument')->with('arg3', 'string', false, 42);
167 $mockController = $this->getAccessibleMock(ActionController::class, ['fooAction', 'evaluateDontValidateAnnotations'], [], '', false);
168 $methodParameters = [
169 'arg1' => [
170 'position' => 0,
171 'byReference' => false,
172 'array' => false,
173 'optional' => false,
174 'allowsNull' => false,
175 'type' => 'string',
176 'hasDefaultValue' => false
177 ],
178 'arg2' => [
179 'position' => 1,
180 'byReference' => false,
181 'array' => true,
182 'optional' => true,
183 'defaultValue' => [21],
184 'allowsNull' => false,
185 'hasDefaultValue' => true
186 ],
187 'arg3' => [
188 'position' => 2,
189 'byReference' => false,
190 'array' => false,
191 'optional' => true,
192 'defaultValue' => 42,
193 'allowsNull' => false,
194 'type' => 'string',
195 'hasDefaultValue' => true
196 ]
197 ];
198
199 $classSchemaMock = $this->createMock(ClassSchema::class);
200 $classSchemaMock
201 ->expects($this->any())
202 ->method('getMethod')
203 ->with('fooAction')
204 ->willReturn([
205 'params' => $methodParameters
206 ]);
207
208 $mockReflectionService = $this->createMock(ReflectionService::class);
209 $mockReflectionService
210 ->expects($this->any())
211 ->method('getClassSchema')
212 ->with(get_class($mockController))
213 ->willReturn($classSchemaMock);
214 $mockController->_set('reflectionService', $mockReflectionService);
215 $mockController->_set('request', $mockRequest);
216 $mockController->_set('arguments', $mockArguments);
217 $mockController->_set('actionMethodName', 'fooAction');
218 $mockController->_call('initializeActionMethodArguments');
219 }
220
221 /**
222 * @test
223 */
224 public function initializeActionMethodArgumentsThrowsExceptionIfDataTypeWasNotSpecified(): void
225 {
226 $this->expectException(InvalidArgumentTypeException::class);
227 $this->expectExceptionCode(1253175643);
228 $mockRequest = $this->createMock(Request::class);
229 $mockArguments = $this->createMock(Arguments::class);
230 $mockController = $this->getAccessibleMock(ActionController::class, ['fooAction'], [], '', false);
231 $methodParameters = [
232 'arg1' => [
233 'position' => 0,
234 'byReference' => false,
235 'array' => false,
236 'optional' => false,
237 'allowsNull' => false
238 ]
239 ];
240
241 $classSchemaMock = $this->createMock(ClassSchema::class);
242 $classSchemaMock
243 ->expects($this->any())
244 ->method('getMethod')
245 ->with('fooAction')
246 ->willReturn([
247 'params' => $methodParameters
248 ]);
249
250 $mockReflectionService = $this->createMock(ReflectionService::class);
251 $mockReflectionService
252 ->expects($this->any())
253 ->method('getClassSchema')
254 ->with(get_class($mockController))
255 ->willReturn($classSchemaMock);
256 $mockController->_set('reflectionService', $mockReflectionService);
257 $mockController->_set('request', $mockRequest);
258 $mockController->_set('arguments', $mockArguments);
259 $mockController->_set('actionMethodName', 'fooAction');
260 $mockController->_call('initializeActionMethodArguments');
261 }
262
263 /**
264 * @test
265 * @dataProvider templateRootPathDataProvider
266 * @param array $configuration
267 * @param array $expected
268 */
269 public function setViewConfigurationResolvesTemplateRootPathsForTemplateRootPath($configuration, $expected)
270 {
271 /** @var ActionController|\PHPUnit_Framework_MockObject_MockObject|\TYPO3\TestingFramework\Core\AccessibleObjectInterface $mockController */
272 $mockController = $this->getAccessibleMock(ActionController::class, ['dummy'], [], '', false);
273 /** @var ConfigurationManagerInterface|\PHPUnit_Framework_MockObject_MockObject $mockConfigurationManager */
274 $mockConfigurationManager = $this->createMock(ConfigurationManagerInterface::class);
275 $mockConfigurationManager->expects($this->any())->method('getConfiguration')->will($this->returnValue($configuration));
276 $mockController->injectConfigurationManager($mockConfigurationManager);
277 $mockController->_set('request', $this->createMock(Request::class), ['getControllerExtensionKey']);
278 $view = $this->getMockBuilder(ViewInterface::class)
279 ->setMethods(['setControllerContext', 'assign', 'assignMultiple', 'canRender', 'render', 'initializeView', 'setTemplateRootPaths'])
280 ->getMock();
281 $view->expects($this->once())->method('setTemplateRootPaths')->with($expected);
282 $mockController->_call('setViewConfiguration', $view);
283 }
284
285 /**
286 * @return array
287 */
288 public function templateRootPathDataProvider()
289 {
290 return [
291 'text keys' => [
292 [
293 'view' => [
294 'templateRootPaths' => [
295 'default' => 'some path',
296 'extended' => 'some other path'
297 ]
298 ]
299 ],
300 [
301 'extended' => 'some other path',
302 'default' => 'some path'
303 ]
304 ],
305 'numerical keys' => [
306 [
307 'view' => [
308 'templateRootPaths' => [
309 '10' => 'some path',
310 '20' => 'some other path',
311 '15' => 'intermediate specific path'
312 ]
313 ]
314 ],
315 [
316 '20' => 'some other path',
317 '15' => 'intermediate specific path',
318 '10' => 'some path'
319 ]
320 ],
321 'mixed keys' => [
322 [
323 'view' => [
324 'templateRootPaths' => [
325 '10' => 'some path',
326 'very_specific' => 'some other path',
327 '15' => 'intermediate specific path'
328 ]
329 ]
330 ],
331 [
332 '15' => 'intermediate specific path',
333 'very_specific' => 'some other path',
334 '10' => 'some path'
335 ]
336 ],
337 ];
338 }
339
340 /**
341 * @test
342 * @dataProvider layoutRootPathDataProvider
343 *
344 * @param array $configuration
345 * @param array $expected
346 */
347 public function setViewConfigurationResolvesLayoutRootPathsForLayoutRootPath($configuration, $expected)
348 {
349 /** @var ActionController|\PHPUnit_Framework_MockObject_MockObject|\TYPO3\TestingFramework\Core\AccessibleObjectInterface $mockController */
350 $mockController = $this->getAccessibleMock(ActionController::class, ['dummy'], [], '', false);
351 /** @var ConfigurationManagerInterface|\PHPUnit_Framework_MockObject_MockObject $mockConfigurationManager */
352 $mockConfigurationManager = $this->createMock(ConfigurationManagerInterface::class);
353 $mockConfigurationManager->expects($this->any())->method('getConfiguration')->will($this->returnValue($configuration));
354 $mockController->injectConfigurationManager($mockConfigurationManager);
355 $mockController->_set('request', $this->createMock(Request::class), ['getControllerExtensionKey']);
356 $view = $this->getMockBuilder(ViewInterface::class)
357 ->setMethods(['setControllerContext', 'assign', 'assignMultiple', 'canRender', 'render', 'initializeView', 'setlayoutRootPaths'])
358 ->getMock();
359 $view->expects($this->once())->method('setlayoutRootPaths')->with($expected);
360 $mockController->_call('setViewConfiguration', $view);
361 }
362
363 /**
364 * @return array
365 */
366 public function layoutRootPathDataProvider()
367 {
368 return [
369 'text keys' => [
370 [
371 'view' => [
372 'layoutRootPaths' => [
373 'default' => 'some path',
374 'extended' => 'some other path'
375 ]
376 ]
377 ],
378 [
379 'extended' => 'some other path',
380 'default' => 'some path'
381 ]
382 ],
383 'numerical keys' => [
384 [
385 'view' => [
386 'layoutRootPaths' => [
387 '10' => 'some path',
388 '20' => 'some other path',
389 '15' => 'intermediate specific path'
390 ]
391 ]
392 ],
393 [
394 '20' => 'some other path',
395 '15' => 'intermediate specific path',
396 '10' => 'some path'
397 ]
398 ],
399 'mixed keys' => [
400 [
401 'view' => [
402 'layoutRootPaths' => [
403 '10' => 'some path',
404 'very_specific' => 'some other path',
405 '15' => 'intermediate specific path'
406 ]
407 ]
408 ],
409 [
410 '15' => 'intermediate specific path',
411 'very_specific' => 'some other path',
412 '10' => 'some path'
413 ]
414 ],
415 ];
416 }
417
418 /**
419 * @test
420 * @dataProvider partialRootPathDataProvider
421 *
422 * @param array $configuration
423 * @param array $expected
424 */
425 public function setViewConfigurationResolvesPartialRootPathsForPartialRootPath($configuration, $expected)
426 {
427 /** @var ActionController|\PHPUnit_Framework_MockObject_MockObject|\TYPO3\TestingFramework\Core\AccessibleObjectInterface $mockController */
428 $mockController = $this->getAccessibleMock(ActionController::class, ['dummy'], [], '', false);
429 /** @var ConfigurationManagerInterface|\PHPUnit_Framework_MockObject_MockObject $mockConfigurationManager */
430 $mockConfigurationManager = $this->createMock(ConfigurationManagerInterface::class);
431 $mockConfigurationManager->expects($this->any())->method('getConfiguration')->will($this->returnValue($configuration));
432 $mockController->injectConfigurationManager($mockConfigurationManager);
433 $mockController->_set('request', $this->createMock(Request::class), ['getControllerExtensionKey']);
434 $view = $this->getMockBuilder(ViewInterface::class)
435 ->setMethods(['setControllerContext', 'assign', 'assignMultiple', 'canRender', 'render', 'initializeView', 'setpartialRootPaths'])
436 ->getMock();
437 $view->expects($this->once())->method('setpartialRootPaths')->with($expected);
438 $mockController->_call('setViewConfiguration', $view);
439 }
440
441 /**
442 * @return array
443 */
444 public function partialRootPathDataProvider()
445 {
446 return [
447 'text keys' => [
448 [
449 'view' => [
450 'partialRootPaths' => [
451 'default' => 'some path',
452 'extended' => 'some other path'
453 ]
454 ]
455 ],
456 [
457 'extended' => 'some other path',
458 'default' => 'some path'
459 ]
460 ],
461 'numerical keys' => [
462 [
463 'view' => [
464 'partialRootPaths' => [
465 '10' => 'some path',
466 '20' => 'some other path',
467 '15' => 'intermediate specific path'
468 ]
469 ]
470 ],
471 [
472 '20' => 'some other path',
473 '15' => 'intermediate specific path',
474 '10' => 'some path'
475 ]
476 ],
477 'mixed keys' => [
478 [
479 'view' => [
480 'partialRootPaths' => [
481 '10' => 'some path',
482 'very_specific' => 'some other path',
483 '15' => 'intermediate specific path'
484 ]
485 ]
486 ],
487 [
488 '15' => 'intermediate specific path',
489 'very_specific' => 'some other path',
490 '10' => 'some path'
491 ]
492 ],
493 ];
494 }
495
496 /**
497 * @param FluidTemplateView $viewMock
498 * @param string|null $expectedHeader
499 * @param string|null $expectedFooter
500 * @test
501 * @dataProvider headerAssetDataProvider
502 */
503 public function rendersAndAssignsAssetsFromViewIntoPageRenderer($viewMock, $expectedHeader, $expectedFooter)
504 {
505 $this->mockObjectManager = $this->getMockBuilder(ObjectManager::class)->setMethods(['get'])->getMock();
506 $pageRendererMock = $this->getMockBuilder(PageRenderer::class)->setMethods(['addHeaderData', 'addFooterData'])->getMock();
507 if (!$viewMock instanceof FluidTemplateView) {
508 $this->mockObjectManager->expects($this->never())->method('get');
509 } else {
510 $this->mockObjectManager->expects($this->any())->method('get')->with(PageRenderer::class)->willReturn($pageRendererMock);
511 }
512 if (!empty(trim($expectedHeader ?? ''))) {
513 $pageRendererMock->expects($this->once())->method('addHeaderData')->with($expectedHeader);
514 } else {
515 $pageRendererMock->expects($this->never())->method('addHeaderData');
516 }
517 if (!empty(trim($expectedFooter ?? ''))) {
518 $pageRendererMock->expects($this->once())->method('addFooterData')->with($expectedFooter);
519 } else {
520 $pageRendererMock->expects($this->never())->method('addFooterData');
521 }
522 $requestMock = $this->getMockBuilder(RequestInterface::class)->getMockForAbstractClass();
523 $subject = new ActionController();
524 $viewProperty = new \ReflectionProperty($subject, 'view');
525 $viewProperty->setAccessible(true);
526 $viewProperty->setValue($subject, $viewMock);
527 $objectManagerProperty = new \ReflectionProperty($subject, 'objectManager');
528 $objectManagerProperty->setAccessible(true);
529 $objectManagerProperty->setValue($subject, $this->mockObjectManager);
530
531 $method = new \ReflectionMethod($subject, 'renderAssetsForRequest');
532 $method->setAccessible(true);
533 $method->invokeArgs($subject, [$requestMock]);
534 }
535
536 /**
537 * @return array
538 */
539 public function headerAssetDataProvider()
540 {
541 $viewWithHeaderData = $this->getMockBuilder(FluidTemplateView::class)->setMethods(['renderSection'])->disableOriginalConstructor()->getMock();
542 $viewWithHeaderData->expects($this->at(0))->method('renderSection')->with('HeaderAssets', $this->anything(), true)->willReturn('custom-header-data');
543 $viewWithHeaderData->expects($this->at(1))->method('renderSection')->with('FooterAssets', $this->anything(), true)->willReturn(null);
544 $viewWithFooterData = $this->getMockBuilder(FluidTemplateView::class)->setMethods(['renderSection'])->disableOriginalConstructor()->getMock();
545 $viewWithFooterData->expects($this->at(0))->method('renderSection')->with('HeaderAssets', $this->anything(), true)->willReturn(null);
546 $viewWithFooterData->expects($this->at(1))->method('renderSection')->with('FooterAssets', $this->anything(), true)->willReturn('custom-footer-data');
547 $viewWithBothData = $this->getMockBuilder(FluidTemplateView::class)->setMethods(['renderSection'])->disableOriginalConstructor()->getMock();
548 $viewWithBothData->expects($this->at(0))->method('renderSection')->with('HeaderAssets', $this->anything(), true)->willReturn('custom-header-data');
549 $viewWithBothData->expects($this->at(1))->method('renderSection')->with('FooterAssets', $this->anything(), true)->willReturn('custom-footer-data');
550 $invalidView = $this->getMockBuilder(AbstractTemplateView::class)->disableOriginalConstructor()->getMockForAbstractClass();
551 return [
552 [$viewWithHeaderData, 'custom-header-data', null],
553 [$viewWithFooterData, null, 'custom-footer-data'],
554 [$viewWithBothData, 'custom-header-data', 'custom-footer-data'],
555 [$invalidView, null, null]
556 ];
557 }
558 }