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