[BUGFIX] Allow range options to work for date/time fields
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Tests / Unit / DataHandling / DataHandlerTest.php
1 <?php
2 namespace TYPO3\CMS\Core\Tests\Unit\DataHandler;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use Prophecy\Argument;
18 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
19 use TYPO3\CMS\Core\Cache\CacheManager;
20 use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
21 use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
22 use TYPO3\CMS\Core\DataHandling\DataHandler;
23 use TYPO3\CMS\Core\Tests\Unit\DataHandling\Fixtures\AllowAccessHookFixture;
24 use TYPO3\CMS\Core\Tests\Unit\DataHandling\Fixtures\InvalidHookFixture;
25 use TYPO3\CMS\Core\Utility\GeneralUtility;
26 use TYPO3\TestingFramework\Core\AccessibleObjectInterface;
27
28 /**
29 * Test case
30 */
31 class DataHandlerTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
32 {
33 /**
34 * Subject is not notice free, disable E_NOTICES
35 */
36 protected static $suppressNotices = true;
37
38 /**
39 * @var array A backup of registered singleton instances
40 */
41 protected $singletonInstances = [];
42
43 /**
44 * @var DataHandler|\PHPUnit_Framework_MockObject_MockObject|AccessibleObjectInterface
45 */
46 protected $subject;
47
48 /**
49 * @var BackendUserAuthentication a mock logged-in back-end user
50 */
51 protected $backEndUser;
52
53 /**
54 * Set up the tests
55 */
56 protected function setUp()
57 {
58 $GLOBALS['TCA'] = [];
59 $this->singletonInstances = GeneralUtility::getSingletonInstances();
60 $cacheManagerProphecy = $this->prophesize(CacheManager::class);
61 GeneralUtility::setSingletonInstance(CacheManager::class, $cacheManagerProphecy->reveal());
62 $cacheFrontendProphecy = $this->prophesize(FrontendInterface::class);
63 $cacheManagerProphecy->getCache('cache_runtime')->willReturn($cacheFrontendProphecy->reveal());
64 $this->backEndUser = $this->createMock(BackendUserAuthentication::class);
65 $this->subject = $this->getAccessibleMock(DataHandler::class, ['dummy']);
66 $this->subject->start([], '', $this->backEndUser);
67 }
68
69 /**
70 * Tear down the tests
71 */
72 protected function tearDown()
73 {
74 GeneralUtility::resetSingletonInstances($this->singletonInstances);
75 parent::tearDown();
76 }
77
78 //////////////////////////////////////
79 // Tests for the basic functionality
80 //////////////////////////////////////
81 /**
82 * @test
83 */
84 public function fixtureCanBeCreated()
85 {
86 $this->assertTrue($this->subject instanceof DataHandler);
87 }
88
89 //////////////////////////////////////////
90 // Test concerning checkModifyAccessList
91 //////////////////////////////////////////
92 /**
93 * @test
94 */
95 public function adminIsAllowedToModifyNonAdminTable()
96 {
97 $this->subject->admin = true;
98 $this->assertTrue($this->subject->checkModifyAccessList('tt_content'));
99 }
100
101 /**
102 * @test
103 */
104 public function nonAdminIsNorAllowedToModifyNonAdminTable()
105 {
106 $this->subject->admin = false;
107 $this->assertFalse($this->subject->checkModifyAccessList('tt_content'));
108 }
109
110 /**
111 * @test
112 */
113 public function nonAdminWithTableModifyAccessIsAllowedToModifyNonAdminTable()
114 {
115 $this->subject->admin = false;
116 $this->backEndUser->groupData['tables_modify'] = 'tt_content';
117 $this->assertTrue($this->subject->checkModifyAccessList('tt_content'));
118 }
119
120 /**
121 * @test
122 */
123 public function adminIsAllowedToModifyAdminTable()
124 {
125 $this->subject->admin = true;
126 $this->assertTrue($this->subject->checkModifyAccessList('be_users'));
127 }
128
129 /**
130 * @test
131 */
132 public function nonAdminIsNotAllowedToModifyAdminTable()
133 {
134 $this->subject->admin = false;
135 $this->assertFalse($this->subject->checkModifyAccessList('be_users'));
136 }
137
138 /**
139 * @test
140 */
141 public function nonAdminWithTableModifyAccessIsNotAllowedToModifyAdminTable()
142 {
143 $tableName = $this->getUniqueId('aTable');
144 $GLOBALS['TCA'] = [
145 $tableName => [
146 'ctrl' => [
147 'adminOnly' => true,
148 ],
149 ],
150 ];
151 $this->subject->admin = false;
152 $this->backEndUser->groupData['tables_modify'] = $tableName;
153 $this->assertFalse($this->subject->checkModifyAccessList($tableName));
154 }
155
156 /**
157 * @test
158 */
159 public function evalCheckValueDouble2()
160 {
161 $testData = [
162 '-0,5' => '-0.50',
163 '1000' => '1000.00',
164 '1000,10' => '1000.10',
165 '1000,0' => '1000.00',
166 '600.000.000,00' => '600000000.00',
167 '60aaa00' => '6000.00'
168 ];
169 foreach ($testData as $value => $expectedReturnValue) {
170 $returnValue = $this->subject->checkValue_input_Eval($value, ['double2'], '');
171 $this->assertSame($returnValue['value'], $expectedReturnValue);
172 }
173 }
174
175 public function dataProviderDatetime()
176 {
177 // Three elements: input, timezone of input, expected output (UTC)
178 return [
179 'timestamp is passed through, as it is UTC' => [
180 1457103519, 'Europe/Berlin', 1457103519
181 ],
182 'ISO date is interpreted as local date and is output as correct timestamp' => [
183 '2017-06-07T00:10:00Z', 'Europe/Berlin', 1496787000
184 ],
185 ];
186 }
187
188 /**
189 * @test
190 * @dataProvider dataProviderDatetime
191 */
192 public function evalCheckValueDatetime($input, $serverTimezone, $expectedOutput)
193 {
194 $oldTimezone = date_default_timezone_get();
195 date_default_timezone_set($serverTimezone);
196
197 $output = $this->subject->checkValue_input_Eval($input, ['datetime'], '');
198
199 // set before the assertion is performed, so it is restored even for failing tests
200 date_default_timezone_set($oldTimezone);
201
202 $this->assertEquals($expectedOutput, $output['value']);
203 }
204
205 /**
206 * Data provider for inputValueCheckRecognizesStringValuesAsIntegerValuesCorrectly
207 *
208 * @return array
209 */
210 public function inputValuesStringsDataProvider()
211 {
212 return [
213 '"0" returns zero as integer' => [
214 '0',
215 0
216 ],
217 '"-2000001" is interpreted correctly as -2000001 but is lower than -2000000 and set to -2000000' => [
218 '-2000001',
219 -2000000
220 ],
221 '"-2000000" is interpreted correctly as -2000000 and is equal to -2000000' => [
222 '-2000000',
223 -2000000
224 ],
225 '"2000000" is interpreted correctly as 2000000 and is equal to 2000000' => [
226 '2000000',
227 2000000
228 ],
229 '"2000001" is interpreted correctly as 2000001 but is greater then 2000000 and set to 2000000' => [
230 '2000001',
231 2000000
232 ],
233 ];
234 }
235
236 /**
237 * @test
238 * @dataProvider inputValuesStringsDataProvider
239 * @param string $value
240 * @param int $expectedReturnValue
241 */
242 public function inputValueCheckRecognizesStringValuesAsIntegerValuesCorrectly($value, $expectedReturnValue)
243 {
244 $tcaFieldConf = [
245 'input' => [],
246 'eval' => 'int',
247 'range' => [
248 'lower' => '-2000000',
249 'upper' => '2000000'
250 ]
251 ];
252 $returnValue = $this->subject->_call('checkValueForInput', $value, $tcaFieldConf, '', 0, 0, '');
253 $this->assertSame($returnValue['value'], $expectedReturnValue);
254 }
255
256 /**
257 * @return array
258 */
259 public function inputValuesDataTimeDataProvider()
260 {
261 return [
262 'undershot date adjusted' => [
263 '2018-02-28T00:00:00Z',
264 1519862400,
265 ],
266 'exact lower date accepted' => [
267 '2018-03-01T00:00:00Z',
268 1519862400,
269 ],
270 'exact upper date accepted' => [
271 '2018-03-31T23:59:59Z',
272 1522540799,
273 ],
274 'exceeded date adjusted' => [
275 '2018-04-01T00:00:00Z',
276 1522540799,
277 ],
278 ];
279 }
280
281 /**
282 * @param string $value
283 * @param int $expected
284 *
285 * @test
286 * @dataProvider inputValuesDataTimeDataProvider
287 */
288 public function inputValueCheckRecognizesDateTimeValuesAsIntegerValuesCorrectly($value, int $expected)
289 {
290 $tcaFieldConf = [
291 'input' => [],
292 'eval' => 'datetime',
293 'range' => [
294 // unix timestamp: 1519862400
295 'lower' => gmmktime(0, 0, 0, 3, 1, 2018),
296 // unix timestamp: 1522540799
297 'upper' => gmmktime(23, 59, 59, 3, 31, 2018),
298 ]
299 ];
300
301 // @todo Switch to UTC since otherwise DataHandler removes timezone offset
302 $previousTimezone = date_default_timezone_get();
303 date_default_timezone_set('UTC');
304
305 $returnValue = $this->subject->_call('checkValueForInput', $value, $tcaFieldConf, '', 0, 0, '');
306
307 date_default_timezone_set($previousTimezone);
308
309 $this->assertSame($returnValue['value'], $expected);
310 }
311
312 /**
313 * @return array
314 */
315 public function inputValueCheckDoesNotCallGetDateTimeFormatsForNonDatetimeFieldsDataProvider()
316 {
317 return [
318 'tca without dbType' => [
319 [
320 'input' => []
321 ]
322 ],
323 'tca with dbType != date/datetime/time' => [
324 [
325 'input' => [],
326 'dbType' => 'foo'
327 ]
328 ]
329 ];
330 }
331
332 /**
333 * @test
334 * @param array $tcaFieldConf
335 * @dataProvider inputValueCheckDoesNotCallGetDateTimeFormatsForNonDatetimeFieldsDataProvider
336 */
337 public function inputValueCheckDoesNotCallGetDateTimeFormatsForNonDatetimeFields($tcaFieldConf)
338 {
339 $this->subject->_call('checkValueForInput', '', $tcaFieldConf, '', 0, 0, '');
340 }
341
342 ///////////////////////////////////////////
343 // Tests concerning checkModifyAccessList
344 ///////////////////////////////////////////
345 //
346 /**
347 * Tests whether a wrong interface on the 'checkModifyAccessList' hook throws an exception.
348 * @test
349 */
350 public function doesCheckModifyAccessListThrowExceptionOnWrongHookInterface()
351 {
352 $this->expectException(\UnexpectedValueException::class);
353 $this->expectExceptionCode(1251892472);
354
355 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['checkModifyAccessList'][] = InvalidHookFixture::class;
356 $this->subject->checkModifyAccessList('tt_content');
357 }
358
359 /**
360 * Tests whether the 'checkModifyAccessList' hook is called correctly.
361 *
362 * @test
363 */
364 public function doesCheckModifyAccessListHookGetsCalled()
365 {
366 $hookClass = $this->getUniqueId('tx_coretest');
367 $hookMock = $this->getMockBuilder(\TYPO3\CMS\Core\DataHandling\DataHandlerCheckModifyAccessListHookInterface::class)
368 ->setMethods(['checkModifyAccessList'])
369 ->setMockClassName($hookClass)
370 ->getMock();
371 $hookMock->expects($this->once())->method('checkModifyAccessList');
372 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['checkModifyAccessList'][] = $hookClass;
373 GeneralUtility::addInstance($hookClass, $hookMock);
374 $this->subject->checkModifyAccessList('tt_content');
375 }
376
377 /**
378 * Tests whether the 'checkModifyAccessList' hook modifies the $accessAllowed variable.
379 *
380 * @test
381 */
382 public function doesCheckModifyAccessListHookModifyAccessAllowed()
383 {
384 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['checkModifyAccessList'][] = AllowAccessHookFixture::class;
385 $this->assertTrue($this->subject->checkModifyAccessList('tt_content'));
386 }
387
388 /////////////////////////////////////
389 // Tests concerning process_datamap
390 /////////////////////////////////////
391 /**
392 * @test
393 */
394 public function processDatamapForFrozenNonZeroWorkspaceReturnsFalse()
395 {
396 /** @var DataHandler $subject */
397 $subject = $this->getMockBuilder(DataHandler::class)
398 ->setMethods(['newlog'])
399 ->getMock();
400 $this->backEndUser->workspace = 1;
401 $this->backEndUser->workspaceRec = ['freeze' => true];
402 $subject->BE_USER = $this->backEndUser;
403 $this->assertFalse($subject->process_datamap());
404 }
405
406 /**
407 * @test
408 */
409 public function processDatamapWhenEditingRecordInWorkspaceCreatesNewRecordInWorkspace()
410 {
411 $GLOBALS['TCA'] = [
412 'pages' => [
413 'columns' => [],
414 ],
415 ];
416
417 /** @var $subject DataHandler|\PHPUnit_Framework_MockObject_MockObject */
418 $subject = $this->getMockBuilder(DataHandler::class)
419 ->setMethods([
420 'newlog',
421 'checkModifyAccessList',
422 'tableReadOnly',
423 'checkRecordUpdateAccess',
424 'recordInfo',
425 'getCacheManager',
426 'registerElementsToBeDeleted',
427 'unsetElementsToBeDeleted',
428 'resetElementsToBeDeleted'
429 ])
430 ->disableOriginalConstructor()
431 ->getMock();
432
433 $subject->bypassWorkspaceRestrictions = false;
434 $subject->datamap = [
435 'pages' => [
436 '1' => [
437 'header' => 'demo'
438 ]
439 ]
440 ];
441
442 $cacheManagerMock = $this->getMockBuilder(CacheManager::class)
443 ->setMethods(['flushCachesInGroupByTags'])
444 ->getMock();
445 $cacheManagerMock->expects($this->once())->method('flushCachesInGroupByTags')->with('pages', []);
446
447 $subject->expects($this->once())->method('getCacheManager')->willReturn($cacheManagerMock);
448 $subject->expects($this->once())->method('recordInfo')->will($this->returnValue(null));
449 $subject->expects($this->once())->method('checkModifyAccessList')->with('pages')->will($this->returnValue(true));
450 $subject->expects($this->once())->method('tableReadOnly')->with('pages')->will($this->returnValue(false));
451 $subject->expects($this->once())->method('checkRecordUpdateAccess')->will($this->returnValue(true));
452 $subject->expects($this->once())->method('unsetElementsToBeDeleted')->willReturnArgument(0);
453
454 /** @var BackendUserAuthentication|\PHPUnit_Framework_MockObject_MockObject $backEndUser */
455 $backEndUser = $this->createMock(BackendUserAuthentication::class);
456 $backEndUser->workspace = 1;
457 $backEndUser->workspaceRec = ['freeze' => false];
458 $backEndUser->expects($this->once())->method('workspaceAllowAutoCreation')->will($this->returnValue(true));
459 $backEndUser->expects($this->once())->method('workspaceCannotEditRecord')->will($this->returnValue(true));
460 $backEndUser->expects($this->once())->method('recordEditAccessInternals')->with('pages', 1)->will($this->returnValue(true));
461 $subject->BE_USER = $backEndUser;
462 $createdDataHandler = $this->createMock(DataHandler::class);
463 $createdDataHandler->expects($this->once())->method('start')->with([], [
464 'pages' => [
465 1 => [
466 'version' => [
467 'action' => 'new',
468 'label' => 'Auto-created for WS #1'
469 ]
470 ]
471 ]
472 ]);
473 $createdDataHandler->expects($this->never())->method('process_datamap');
474 $createdDataHandler->expects($this->once())->method('process_cmdmap');
475 GeneralUtility::addInstance(DataHandler::class, $createdDataHandler);
476 $subject->process_datamap();
477 }
478
479 /**
480 * @test
481 */
482 public function doesCheckFlexFormValueHookGetsCalled()
483 {
484 $hookClass = $this->getUniqueId('tx_coretest');
485 $hookMock = $this->getMockBuilder($hookClass)
486 ->setMethods(['checkFlexFormValue_beforeMerge'])
487 ->getMock();
488 $hookMock->expects($this->once())->method('checkFlexFormValue_beforeMerge');
489 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['checkFlexFormValue'][] = $hookClass;
490 GeneralUtility::addInstance($hookClass, $hookMock);
491 $flexFormToolsProphecy = $this->prophesize(FlexFormTools::class);
492 $flexFormToolsProphecy->getDataStructureIdentifier(Argument::cetera())->willReturn('anIdentifier');
493 $flexFormToolsProphecy->parseDataStructureByIdentifier('anIdentifier')->willReturn([]);
494 GeneralUtility::addInstance(FlexFormTools::class, $flexFormToolsProphecy->reveal());
495 $this->subject->_call('checkValueForFlex', [], [], [], '', 0, '', '', 0, 0, 0, [], '');
496 }
497
498 /////////////////////////////////////
499 // Tests concerning log
500 /////////////////////////////////////
501 /**
502 * @test
503 */
504 public function logCallsWriteLogOfBackendUserIfLoggingIsEnabled()
505 {
506 $backendUser = $this->createMock(BackendUserAuthentication::class);
507 $backendUser->expects($this->once())->method('writelog');
508 $this->subject->enableLogging = true;
509 $this->subject->BE_USER = $backendUser;
510 $this->subject->log('', 23, 0, 42, 0, 'details');
511 }
512
513 /**
514 * @test
515 */
516 public function logDoesNotCallWriteLogOfBackendUserIfLoggingIsDisabled()
517 {
518 $backendUser = $this->createMock(BackendUserAuthentication::class);
519 $backendUser->expects($this->never())->method('writelog');
520 $this->subject->enableLogging = false;
521 $this->subject->BE_USER = $backendUser;
522 $this->subject->log('', 23, 0, 42, 0, 'details');
523 }
524
525 /**
526 * @test
527 */
528 public function logAddsEntryToLocalErrorLogArray()
529 {
530 $backendUser = $this->createMock(BackendUserAuthentication::class);
531 $this->subject->BE_USER = $backendUser;
532 $this->subject->enableLogging = true;
533 $this->subject->errorLog = [];
534 $logDetailsUnique = $this->getUniqueId('details');
535 $this->subject->log('', 23, 0, 42, 1, $logDetailsUnique);
536 $this->assertStringEndsWith($logDetailsUnique, $this->subject->errorLog[0]);
537 }
538
539 /**
540 * @test
541 */
542 public function logFormatsDetailMessageWithAdditionalDataInLocalErrorArray()
543 {
544 $backendUser = $this->createMock(BackendUserAuthentication::class);
545 $this->subject->BE_USER = $backendUser;
546 $this->subject->enableLogging = true;
547 $this->subject->errorLog = [];
548 $logDetails = $this->getUniqueId('details');
549 $this->subject->log('', 23, 0, 42, 1, '%1$s' . $logDetails . '%2$s', -1, ['foo', 'bar']);
550 $expected = 'foo' . $logDetails . 'bar';
551 $this->assertStringEndsWith($expected, $this->subject->errorLog[0]);
552 }
553
554 /**
555 * @param bool $expected
556 * @param string $submittedValue
557 * @param string $storedValue
558 * @param string $storedType
559 * @param bool $allowNull
560 *
561 * @dataProvider equalSubmittedAndStoredValuesAreDeterminedDataProvider
562 * @test
563 */
564 public function equalSubmittedAndStoredValuesAreDetermined($expected, $submittedValue, $storedValue, $storedType, $allowNull)
565 {
566 $result = $this->callInaccessibleMethod(
567 $this->subject,
568 'isSubmittedValueEqualToStoredValue',
569 $submittedValue,
570 $storedValue,
571 $storedType,
572 $allowNull
573 );
574 $this->assertEquals($expected, $result);
575 }
576
577 /**
578 * @return array
579 */
580 public function equalSubmittedAndStoredValuesAreDeterminedDataProvider()
581 {
582 return [
583 // String
584 'string value "" vs. ""' => [
585 true,
586 '', '', 'string', false
587 ],
588 'string value 0 vs. "0"' => [
589 true,
590 0, '0', 'string', false
591 ],
592 'string value 1 vs. "1"' => [
593 true,
594 1, '1', 'string', false
595 ],
596 'string value "0" vs. ""' => [
597 false,
598 '0', '', 'string', false
599 ],
600 'string value 0 vs. ""' => [
601 false,
602 0, '', 'string', false
603 ],
604 'string value null vs. ""' => [
605 true,
606 null, '', 'string', false
607 ],
608 // Integer
609 'integer value 0 vs. 0' => [
610 true,
611 0, 0, 'int', false
612 ],
613 'integer value "0" vs. "0"' => [
614 true,
615 '0', '0', 'int', false
616 ],
617 'integer value 0 vs. "0"' => [
618 true,
619 0, '0', 'int', false
620 ],
621 'integer value "" vs. "0"' => [
622 true,
623 '', '0', 'int', false
624 ],
625 'integer value "" vs. 0' => [
626 true,
627 '', 0, 'int', false
628 ],
629 'integer value "0" vs. 0' => [
630 true,
631 '0', 0, 'int', false
632 ],
633 'integer value 1 vs. 1' => [
634 true,
635 1, 1, 'int', false
636 ],
637 'integer value 1 vs. "1"' => [
638 true,
639 1, '1', 'int', false
640 ],
641 'integer value "1" vs. "1"' => [
642 true,
643 '1', '1', 'int', false
644 ],
645 'integer value "1" vs. 1' => [
646 true,
647 '1', 1, 'int', false
648 ],
649 'integer value "0" vs. "1"' => [
650 false,
651 '0', '1', 'int', false
652 ],
653 // String with allowed NULL values
654 'string with allowed null value "" vs. ""' => [
655 true,
656 '', '', 'string', true
657 ],
658 'string with allowed null value 0 vs. "0"' => [
659 true,
660 0, '0', 'string', true
661 ],
662 'string with allowed null value 1 vs. "1"' => [
663 true,
664 1, '1', 'string', true
665 ],
666 'string with allowed null value "0" vs. ""' => [
667 false,
668 '0', '', 'string', true
669 ],
670 'string with allowed null value 0 vs. ""' => [
671 false,
672 0, '', 'string', true
673 ],
674 'string with allowed null value null vs. ""' => [
675 false,
676 null, '', 'string', true
677 ],
678 'string with allowed null value "" vs. null' => [
679 false,
680 '', null, 'string', true
681 ],
682 'string with allowed null value null vs. null' => [
683 true,
684 null, null, 'string', true
685 ],
686 // Integer with allowed NULL values
687 'integer with allowed null value 0 vs. 0' => [
688 true,
689 0, 0, 'int', true
690 ],
691 'integer with allowed null value "0" vs. "0"' => [
692 true,
693 '0', '0', 'int', true
694 ],
695 'integer with allowed null value 0 vs. "0"' => [
696 true,
697 0, '0', 'int', true
698 ],
699 'integer with allowed null value "" vs. "0"' => [
700 true,
701 '', '0', 'int', true
702 ],
703 'integer with allowed null value "" vs. 0' => [
704 true,
705 '', 0, 'int', true
706 ],
707 'integer with allowed null value "0" vs. 0' => [
708 true,
709 '0', 0, 'int', true
710 ],
711 'integer with allowed null value 1 vs. 1' => [
712 true,
713 1, 1, 'int', true
714 ],
715 'integer with allowed null value "1" vs. "1"' => [
716 true,
717 '1', '1', 'int', true
718 ],
719 'integer with allowed null value "1" vs. 1' => [
720 true,
721 '1', 1, 'int', true
722 ],
723 'integer with allowed null value 1 vs. "1"' => [
724 true,
725 1, '1', 'int', true
726 ],
727 'integer with allowed null value "0" vs. "1"' => [
728 false,
729 '0', '1', 'int', true
730 ],
731 'integer with allowed null value null vs. ""' => [
732 false,
733 null, '', 'int', true
734 ],
735 'integer with allowed null value "" vs. null' => [
736 false,
737 '', null, 'int', true
738 ],
739 'integer with allowed null value null vs. null' => [
740 true,
741 null, null, 'int', true
742 ],
743 'integer with allowed null value null vs. "0"' => [
744 false,
745 null, '0', 'int', true
746 ],
747 'integer with allowed null value null vs. 0' => [
748 false,
749 null, 0, 'int', true
750 ],
751 'integer with allowed null value "0" vs. null' => [
752 false,
753 '0', null, 'int', true
754 ],
755 ];
756 }
757
758 /**
759 * @param bool $expected
760 * @param array $eval
761 * @dataProvider getPlaceholderTitleForTableLabelReturnsLabelThatsMatchesLabelFieldConditionsDataProvider
762 * @test
763 */
764 public function getPlaceholderTitleForTableLabelReturnsLabelThatsMatchesLabelFieldConditions($expected, $eval)
765 {
766 $table = 'phpunit_dummy';
767
768 /** @var DataHandler|\PHPUnit_Framework_MockObject_MockObject|AccessibleObjectInterface $subject */
769 $subject = $this->getAccessibleMock(
770 DataHandler::class,
771 ['dummy']
772 );
773
774 $backendUser = $this->createMock(BackendUserAuthentication::class);
775 $subject->BE_USER = $backendUser;
776 $subject->BE_USER->workspace = 1;
777
778 $GLOBALS['TCA'][$table] = [];
779 $GLOBALS['TCA'][$table]['ctrl'] = ['label' => 'dummy'];
780 $GLOBALS['TCA'][$table]['columns'] = [
781 'dummy' => [
782 'config' => [
783 'eval' => $eval
784 ]
785 ]
786 ];
787
788 $this->assertEquals($expected, $subject->_call('getPlaceholderTitleForTableLabel', $table));
789 }
790
791 /**
792 * @return array
793 */
794 public function getPlaceholderTitleForTableLabelReturnsLabelThatsMatchesLabelFieldConditionsDataProvider()
795 {
796 return [
797 [
798 0.10,
799 'double2'
800 ],
801 [
802 0,
803 'int'
804 ],
805 [
806 '0',
807 'datetime'
808 ],
809 [
810 '[PLACEHOLDER, WS#1]',
811 ''
812 ]
813 ];
814 }
815
816 /**
817 * @test
818 */
819 public function deletePagesOnRootLevelIsDenied()
820 {
821 /** @var DataHandler|\PHPUnit_Framework_MockObject_MockObject|AccessibleObjectInterface $dataHandlerMock */
822 $dataHandlerMock = $this->getMockBuilder(DataHandler::class)
823 ->setMethods(['canDeletePage', 'log'])
824 ->getMock();
825 $dataHandlerMock
826 ->expects($this->never())
827 ->method('canDeletePage');
828 $dataHandlerMock
829 ->expects($this->once())
830 ->method('log')
831 ->with('pages', 0, 0, 0, 2, 'Deleting all pages starting from the root-page is disabled.', -1, [], 0);
832
833 $dataHandlerMock->deletePages(0);
834 }
835
836 /**
837 * @test
838 */
839 public function deleteRecord_procBasedOnFieldTypeRespectsEnableCascadingDelete()
840 {
841 $table = $this->getUniqueId('foo_');
842 $conf = [
843 'type' => 'inline',
844 'foreign_table' => $this->getUniqueId('foreign_foo_'),
845 'behaviour' => [
846 'enableCascadingDelete' => 0,
847 ]
848 ];
849
850 /** @var \TYPO3\CMS\Core\Database\RelationHandler $mockRelationHandler */
851 $mockRelationHandler = $this->createMock(\TYPO3\CMS\Core\Database\RelationHandler::class);
852 $mockRelationHandler->itemArray = [
853 '1' => ['table' => $this->getUniqueId('bar_'), 'id' => 67]
854 ];
855
856 /** @var DataHandler|\PHPUnit_Framework_MockObject_MockObject|AccessibleObjectInterface $mockDataHandler */
857 $mockDataHandler = $this->getAccessibleMock(DataHandler::class, ['getInlineFieldType', 'deleteAction', 'createRelationHandlerInstance'], [], '', false);
858 $mockDataHandler->expects($this->once())->method('getInlineFieldType')->will($this->returnValue('field'));
859 $mockDataHandler->expects($this->once())->method('createRelationHandlerInstance')->will($this->returnValue($mockRelationHandler));
860 $mockDataHandler->expects($this->never())->method('deleteAction');
861 $mockDataHandler->deleteRecord_procBasedOnFieldType($table, 42, 'foo', 'bar', $conf);
862 }
863
864 /**
865 * @return array
866 */
867 public function checkValue_checkReturnsExpectedValuesDataProvider()
868 {
869 return [
870 'None item selected' => [
871 0,
872 0
873 ],
874 'All items selected' => [
875 7,
876 7
877 ],
878 'Item 1 and 2 are selected' => [
879 3,
880 3
881 ],
882 'Value is higher than allowed (all checkboxes checked)' => [
883 15,
884 7
885 ],
886 'Value is higher than allowed (some checkboxes checked)' => [
887 11,
888 3
889 ],
890 'Negative value' => [
891 -5,
892 0
893 ]
894 ];
895 }
896
897 /**
898 * @param string $value
899 * @param string $expectedValue
900 *
901 * @dataProvider checkValue_checkReturnsExpectedValuesDataProvider
902 * @test
903 */
904 public function checkValue_checkReturnsExpectedValues($value, $expectedValue)
905 {
906 $expectedResult = [
907 'value' => $expectedValue
908 ];
909 $result = [];
910 $tcaFieldConfiguration = [
911 'items' => [
912 ['Item 1', 0],
913 ['Item 2', 0],
914 ['Item 3', 0]
915 ]
916 ];
917 $this->assertSame($expectedResult, $this->subject->_call('checkValueForCheck', $result, $value, $tcaFieldConfiguration, '', 0, 0, ''));
918 }
919
920 /**
921 * @test
922 */
923 public function checkValueForInputConvertsNullToEmptyString()
924 {
925 $previousLanguageService = $GLOBALS['LANG'];
926 $GLOBALS['LANG'] = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Localization\LanguageService::class);
927 $GLOBALS['LANG']->init('default');
928 $expectedResult = ['value' => ''];
929 $this->assertSame($expectedResult, $this->subject->_call('checkValueForInput', null, ['type' => 'string', 'max' => 40], 'tt_content', 'NEW55c0e67f8f4d32.04974534', 89, 'table_caption'));
930 $GLOBALS['LANG'] = $previousLanguageService;
931 }
932
933 /**
934 * @param mixed $value
935 * @param array $configuration
936 * @param int|string $expected
937 * @test
938 * @dataProvider referenceValuesAreCastedDataProvider
939 */
940 public function referenceValuesAreCasted($value, array $configuration, $expected)
941 {
942 $this->assertEquals(
943 $expected,
944 $this->subject->_call('castReferenceValue', $value, $configuration)
945 );
946 }
947
948 /**
949 * @return array
950 */
951 public function referenceValuesAreCastedDataProvider()
952 {
953 return [
954 'all empty' => [
955 '', [], ''
956 ],
957 'cast zero with MM table' => [
958 '', ['MM' => 'table'], 0
959 ],
960 'cast zero with MM table with default value' => [
961 '', ['MM' => 'table', 'default' => 13], 0
962 ],
963 'cast zero with foreign field' => [
964 '', ['foreign_field' => 'table', 'default' => 13], 0
965 ],
966 'cast zero with foreign field with default value' => [
967 '', ['foreign_field' => 'table'], 0
968 ],
969 'pass zero' => [
970 '0', [], '0'
971 ],
972 'pass value' => [
973 '1', ['default' => 13], '1'
974 ],
975 'use default value' => [
976 '', ['default' => 13], 13
977 ],
978 ];
979 }
980
981 /**
982 * @return array
983 */
984 public function clearPrefixFromValueRemovesPrefixDataProvider(): array
985 {
986 return [
987 'normal case' => ['Test (copy 42)', 'Test'],
988 // all cases below look fishy and indicate bugs
989 'with double spaces before' => ['Test (copy 42)', 'Test '],
990 'with three spaces before' => ['Test (copy 42)', 'Test '],
991 'with space after' => ['Test (copy 42) ', 'Test (copy 42) '],
992 'with double spaces after' => ['Test (copy 42) ', 'Test (copy 42) '],
993 'with three spaces after' => ['Test (copy 42) ', 'Test (copy 42) '],
994 'with double tab before' => ['Test' . chr(9) . '(copy 42)', 'Test'],
995 'with double tab after' => ['Test (copy 42)' . chr(9), 'Test (copy 42)' . chr(9)],
996 ];
997 }
998
999 /**
1000 * @test
1001 * @dataProvider clearPrefixFromValueRemovesPrefixDataProvider
1002 * @param string $input
1003 * @param string $expected
1004 */
1005 public function clearPrefixFromValueRemovesPrefix(string $input, string $expected)
1006 {
1007 $languageServiceProphecy = $this->prophesize(\TYPO3\CMS\Core\Localization\LanguageService::class);
1008 $languageServiceProphecy->sL('testLabel')->willReturn('(copy %s)');
1009 $GLOBALS['LANG'] = $languageServiceProphecy->reveal();
1010 $GLOBALS['TCA']['testTable']['ctrl']['prependAtCopy'] = 'testLabel';
1011 $this->assertEquals($expected, (new DataHandler())->clearPrefixFromValue('testTable', $input));
1012 }
1013 }