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