[TASK] Integrate tests for DataHandler hook invocations
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Tests / Functional / DataHandling / DataHandler / HookTest.php
1 <?php
2 declare(strict_types=1);
3 namespace TYPO3\CMS\Core\Tests\Functional\DataHandling\DataHandler;
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\Tests\Functional\DataHandling\AbstractDataHandlerActionTestCase;
19 use TYPO3\CMS\Core\Tests\Functional\DataHandling\DataHandler\Fixtures\HookFixture;
20 use TYPO3\CMS\Core\Utility\GeneralUtility;
21 use TYPO3\CMS\Core\Utility\StringUtility;
22
23 /**
24 * Tests triggering hook execution in DataHandler.
25 */
26 class HookTest extends AbstractDataHandlerActionTestCase
27 {
28 const VALUE_PageId = 89;
29 const VALUE_ContentId = 297;
30 const TABLE_Content = 'tt_content';
31 const TABLE_Hotel = 'tx_irretutorial_1nff_hotel';
32 const TABLE_Category = 'sys_category';
33 const FIELD_ContentHotel = 'tx_irretutorial_1nff_hotels';
34 const FIELD_Categories = 'categories';
35
36 /**
37 * @var HookFixture
38 */
39 protected $hookFixture;
40
41 /**
42 * @var string
43 */
44 protected $scenarioDataSetDirectory = 'typo3/sysext/core/Tests/Functional/DataHandling/DataHandler/DataSet/';
45
46 protected function setUp()
47 {
48 parent::setUp();
49 $this->importScenarioDataSet('LiveDefaultPages');
50 $this->importScenarioDataSet('LiveDefaultElements');
51 $this->backendUser->workspace = 0;
52
53 $this->hookFixture = GeneralUtility::makeInstance(HookFixture::class);
54 $this->hookFixture->purge();
55 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'][__CLASS__] = HookFixture::class;
56 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass'][__CLASS__] = HookFixture::class;
57 }
58
59 protected function tearDown()
60 {
61 parent::tearDown();
62
63 unset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'][__CLASS__]);
64 unset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass'][__CLASS__]);
65 unset($this->hookFixture);
66 }
67
68 /**
69 * @test
70 */
71 public function hooksAreExecutedForNewRecords()
72 {
73 $newTableIds = $this->actionService->createNewRecord(
74 self::TABLE_Content,
75 self::VALUE_PageId,
76 ['header' => 'Testing #1']
77 );
78 $this->recordIds['newContentId'] = $newTableIds[self::TABLE_Content][0];
79
80 $this->assertHookInvocationsCount([
81 'processDatamap_beforeStart',
82 'processDatamap_afterAllOperations'
83 ], 1);
84
85 $this->assertHookInvocationsPayload([
86 'processDatamap_preProcessFieldArray',
87 'processDatamap_postProcessFieldArray',
88 'processDatamap_afterDatabaseOperations',
89 ], [
90 [
91 'table' => self::TABLE_Content,
92 'fieldArray' => [ 'header' => 'Testing #1', 'pid' => self::VALUE_PageId ]
93 ]
94 ]);
95 }
96
97 /**
98 * @test
99 */
100 public function hooksAreExecutedForExistingRecords()
101 {
102 $this->actionService->modifyRecord(
103 self::TABLE_Content,
104 self::VALUE_ContentId,
105 ['header' => 'Testing #1']
106 );
107
108 $this->assertHookInvocationsCount([
109 'processDatamap_beforeStart',
110 'processDatamap_afterAllOperations'
111 ], 1);
112
113 $this->assertHookInvocationsPayload([
114 'processDatamap_preProcessFieldArray',
115 'processDatamap_postProcessFieldArray',
116 'processDatamap_afterDatabaseOperations',
117 ], [
118 [
119 'table' => self::TABLE_Content,
120 'fieldArray' => [ 'header' => 'Testing #1' ]
121 ]
122 ]);
123 }
124
125 /**
126 * @test
127 */
128 public function hooksAreExecutedForNewRelations()
129 {
130 $contentNewId = StringUtility::getUniqueId('NEW');
131 $hotelNewId = StringUtility::getUniqueId('NEW');
132 $categoryNewId = StringUtility::getUniqueId('NEW');
133
134 $this->actionService->modifyRecords(
135 self::VALUE_PageId,
136 [
137 self::TABLE_Content => [
138 'uid' => $contentNewId,
139 'header' => 'Testing #1',
140 self::FIELD_ContentHotel => $hotelNewId,
141 self::FIELD_Categories => $categoryNewId,
142 ],
143 self::TABLE_Hotel => [
144 'uid' => $hotelNewId,
145 'title' => 'Hotel #1',
146 ],
147 self::TABLE_Category => [
148 'uid' => $categoryNewId,
149 'title' => 'Category #1',
150 ],
151 ]
152 );
153
154 $this->assertHookInvocationsCount([
155 'processDatamap_beforeStart',
156 'processDatamap_afterAllOperations'
157 ], 1);
158
159 $this->assertHookInvocationPayload(
160 'processDatamap_preProcessFieldArray',
161 [
162 [
163 'table' => self::TABLE_Content,
164 'fieldArray' => [
165 'header' => 'Testing #1',
166 self::FIELD_ContentHotel => $hotelNewId,
167 self::FIELD_Categories => $categoryNewId,
168 ],
169 ],
170 [
171 'table' => self::TABLE_Hotel,
172 'fieldArray' => [ 'title' => 'Hotel #1' ],
173 ],
174 [
175 'table' => self::TABLE_Category,
176 'fieldArray' => [ 'title' => 'Category #1' ],
177 ],
178 ]
179 );
180
181 $this->assertHookInvocationPayload(
182 'processDatamap_postProcessFieldArray',
183 [
184 [
185 'table' => self::TABLE_Content,
186 'fieldArray' => [ 'header' => 'Testing #1' ],
187 ],
188 [
189 'table' => self::TABLE_Hotel,
190 'fieldArray' => [ 'title' => 'Hotel #1' ],
191 ],
192 [
193 'table' => self::TABLE_Category,
194 'fieldArray' => [ 'title' => 'Category #1' ],
195 ],
196 ]
197 );
198
199 $this->assertHookInvocationPayload(
200 'processDatamap_afterDatabaseOperations',
201 [
202 [
203 'table' => self::TABLE_Content,
204 'fieldArray' => [
205 'header' => 'Testing #1',
206 self::FIELD_ContentHotel => 1,
207 ],
208 ],
209 // @todo Fix the double invocation for this tt_content record
210 [
211 'table' => self::TABLE_Content,
212 'fieldArray' => [
213 'header' => 'Testing #1',
214 self::FIELD_Categories => 1,
215 ],
216 ],
217 [
218 'table' => self::TABLE_Hotel,
219 'fieldArray' => [ 'title' => 'Hotel #1' ],
220 ],
221 [
222 'table' => self::TABLE_Category,
223 'fieldArray' => [ 'title' => 'Category #1' ],
224 ],
225 ]
226 );
227 }
228
229 /**
230 * @test
231 */
232 public function hooksAreExecutedForExistingRelations()
233 {
234 $this->actionService->modifyRecord(
235 self::TABLE_Content,
236 self::VALUE_ContentId,
237 [
238 'header' => 'Testing #1',
239 self::FIELD_ContentHotel => '3,4,5',
240 self::FIELD_Categories => '28,29,30',
241 ]
242 );
243
244 $this->assertHookInvocationsCount([
245 'processDatamap_beforeStart',
246 'processDatamap_afterAllOperations'
247 ], 1);
248
249 $this->assertHookInvocationPayload(
250 'processDatamap_preProcessFieldArray',
251 [
252 [
253 'table' => self::TABLE_Content,
254 'fieldArray' => [
255 'header' => 'Testing #1',
256 self::FIELD_ContentHotel => '3,4,5',
257 self::FIELD_Categories => '28,29,30',
258 ]
259 ]
260 ]
261 );
262
263 $this->assertHookInvocationsPayload([
264 'processDatamap_postProcessFieldArray',
265 'processDatamap_afterDatabaseOperations',
266 ], [
267 [
268 'table' => self::TABLE_Content,
269 'fieldArray' => [
270 'header' => 'Testing #1',
271 self::FIELD_ContentHotel => 3,
272 self::FIELD_Categories => 3,
273 ]
274 ]
275 ]);
276 }
277
278 /**
279 * @param string[] $methodNames
280 * @param int $count
281 */
282 protected function assertHookInvocationsCount(array $methodNames, int $count)
283 {
284 $message = 'Unexpected invocations of method "%s"';
285 foreach ($methodNames as $methodName) {
286 $invocations = $this->hookFixture->findInvocationsByMethodName($methodName);
287 $this->assertCount(
288 $count,
289 $invocations,
290 sprintf($message, $methodName)
291 );
292 }
293 }
294
295 /**
296 * @param string[] $methodNames
297 * @param array $assertions
298 */
299 protected function assertHookInvocationsPayload(array $methodNames, array $assertions)
300 {
301 foreach ($methodNames as $methodName) {
302 $this->assertHookInvocationPayload($methodName, $assertions);
303 }
304 }
305
306 /**
307 * @param string $methodName
308 * @param array $assertions
309 */
310 protected function assertHookInvocationPayload(string $methodName, array $assertions)
311 {
312 $message = 'Unexpected hook payload amount found for method "%s"';
313 $invocations = $this->hookFixture->findInvocationsByMethodName($methodName);
314 $this->assertNotNull($invocations);
315
316 foreach ($assertions as $assertion) {
317 $indexes = $this->findAllArrayValuesInHaystack($invocations, $assertion);
318 $this->assertCount(
319 1,
320 $indexes,
321 sprintf($message, $methodName)
322 );
323 $index = $indexes[0];
324 unset($invocations[$index]);
325 }
326 }
327
328 /**
329 * @param array $haystack
330 * @param array $assertion
331 * @return int[]
332 */
333 protected function findAllArrayValuesInHaystack(array $haystack, array $assertion)
334 {
335 $found = [];
336 foreach ($haystack as $index => $item) {
337 if ($this->equals($assertion, $item)) {
338 $found[] = $index;
339 }
340 }
341 return $found;
342 }
343
344 /**
345 * @param array $left
346 * @param array $right
347 * @return bool
348 */
349 protected function equals(array $left, array $right)
350 {
351 foreach ($left as $key => $leftValue) {
352 $rightValue = $right[$key] ?? null;
353 if (!is_array($leftValue) && (string)$leftValue !== (string)$rightValue) {
354 return false;
355 } elseif (is_array($leftValue)) {
356 if (!$this->equals($leftValue, $rightValue)) {
357 return false;
358 }
359 }
360 }
361 return true;
362 }
363 }