[BUGFIX] Invalid query generation in extbase QueryParser
[Packages/TYPO3.CMS.git] / typo3 / sysext / extbase / Tests / Unit / Persistence / Generic / Storage / Typo3DbQueryParserTest.php
1 <?php
2 namespace TYPO3\CMS\Extbase\Tests\Unit\Persistence\Generic\Storage;
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\Database\Connection;
19 use TYPO3\CMS\Core\Database\ConnectionPool;
20 use TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression;
21 use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
22 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
23 use TYPO3\CMS\Core\Tests\UnitTestCase;
24 use TYPO3\CMS\Core\Utility\GeneralUtility;
25 use TYPO3\CMS\Extbase\Persistence\Generic\Exception\InconsistentQuerySettingsException;
26 use TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnsupportedOrderException;
27 use TYPO3\CMS\Extbase\Persistence\Generic\Qom\AndInterface;
28 use TYPO3\CMS\Extbase\Persistence\Generic\Qom\ComparisonInterface;
29 use TYPO3\CMS\Extbase\Persistence\Generic\Qom\ConstraintInterface;
30 use TYPO3\CMS\Extbase\Persistence\Generic\Qom\NotInterface;
31 use TYPO3\CMS\Extbase\Persistence\Generic\Qom\OrInterface;
32 use TYPO3\CMS\Extbase\Persistence\Generic\Qom\SourceInterface;
33 use TYPO3\CMS\Extbase\Persistence\Generic\Storage\Typo3DbQueryParser;
34 use TYPO3\CMS\Extbase\Persistence\QueryInterface;
35
36 class Typo3DbQueryParserTest extends UnitTestCase
37 {
38 /**
39 * @var arary
40 */
41 protected $singletonInstances;
42
43 /**
44 * Set up tests
45 */
46 protected function setUp()
47 {
48 parent::setUp();
49 $this->singletonInstances = GeneralUtility::getSingletonInstances();
50 }
51
52 /**
53 * Clean up after tests
54 */
55 protected function tearDown()
56 {
57 GeneralUtility::purgeInstances();
58 GeneralUtility::resetSingletonInstances($this->singletonInstances);
59 parent::tearDown();
60 }
61
62 /**
63 * @test
64 */
65 public function convertQueryToDoctrineQueryBuilderDoesNotAddAndWhereWithEmptyConstraint()
66 {
67 // Prepare subject, turn off initialize qb method and inject qb prophecy revelation
68 $subject = $this->getAccessibleMock(
69 Typo3DbQueryParser::class,
70 // Shut down some methods not important for this test
71 ['initializeQueryBuilder', 'parseOrderings', 'addTypo3Constraints']
72 );
73 $queryBuilderProphecy = $this->prophesize(QueryBuilder::class);
74 $subject->_set('queryBuilder', $queryBuilderProphecy->reveal());
75
76 $queryProphecy = $this->prophesize(QueryInterface::class);
77 $sourceProphecy = $this->prophesize(SourceInterface::class);
78 $queryProphecy->getSource()->willReturn($sourceProphecy->reveal());
79 $queryProphecy->getOrderings()->willReturn([]);
80
81 // Test part: getConstraint returns no constraint object, andWhere() should not be called
82 $queryProphecy->getConstraint()->willReturn(null);
83 $queryBuilderProphecy->andWhere()->shouldNotBeCalled();
84
85 $subject->convertQueryToDoctrineQueryBuilder($queryProphecy->reveal());
86 }
87
88 /**
89 * @test
90 */
91 public function convertQueryToDoctrineQueryBuilderThrowsExceptionOnNotImplementedConstraint()
92 {
93 // Prepare subject, turn off initialize qb method and inject qb prophecy revelation
94 $subject = $this->getAccessibleMock(
95 Typo3DbQueryParser::class,
96 // Shut down some methods not important for this test
97 ['initializeQueryBuilder', 'parseOrderings', 'addTypo3Constraints', 'parseComparison']
98 );
99 $queryBuilderProphecy = $this->prophesize(QueryBuilder::class);
100 $subject->_set('queryBuilder', $queryBuilderProphecy->reveal());
101
102 $queryProphecy = $this->prophesize(QueryInterface::class);
103 $sourceProphecy = $this->prophesize(SourceInterface::class);
104 $queryProphecy->getSource()->willReturn($sourceProphecy->reveal());
105 $queryProphecy->getOrderings()->willReturn([]);
106
107 // Test part: getConstraint returns not implemented object
108 $constraintProphecy = $this->prophesize(ConstraintInterface::class);
109 $queryProphecy->getConstraint()->willReturn($constraintProphecy->reveal());
110
111 $this->expectException(\RuntimeException::class);
112 $this->expectExceptionCode(1476199898);
113 $subject->convertQueryToDoctrineQueryBuilder($queryProphecy->reveal());
114 }
115
116 /**
117 * @test
118 */
119 public function convertQueryToDoctrineQueryBuilderAddsSimpleAndWhere()
120 {
121 // Prepare subject, turn off initialize qb method and inject qb prophecy revelation
122 $subject = $this->getAccessibleMock(
123 Typo3DbQueryParser::class,
124 // Shut down some methods not important for this test
125 ['initializeQueryBuilder', 'parseOrderings', 'addTypo3Constraints', 'parseComparison']
126 );
127 $queryBuilderProphecy = $this->prophesize(QueryBuilder::class);
128 $subject->_set('queryBuilder', $queryBuilderProphecy->reveal());
129
130 $queryProphecy = $this->prophesize(QueryInterface::class);
131 $sourceProphecy = $this->prophesize(SourceInterface::class);
132 $queryProphecy->getSource()->willReturn($sourceProphecy->reveal());
133 $queryProphecy->getOrderings()->willReturn([]);
134
135 // Test part: getConstraint returns simple constraint, and should push to andWhere()
136 $constraintProphecy = $this->prophesize(ComparisonInterface::class);
137 $queryProphecy->getConstraint()->willReturn($constraintProphecy->reveal());
138 $subject->expects($this->once())->method('parseComparison')->willReturn('heinz');
139 $queryBuilderProphecy->andWhere('heinz')->shouldBeCalled();
140
141 $subject->convertQueryToDoctrineQueryBuilder($queryProphecy->reveal());
142 }
143
144 /**
145 * @test
146 */
147 public function convertQueryToDoctrineQueryBuilderAddsNotConstraint()
148 {
149 // Prepare subject, turn off initialize qb method and inject qb prophecy revelation
150 $subject = $this->getAccessibleMock(
151 Typo3DbQueryParser::class,
152 // Shut down some methods not important for this test
153 ['initializeQueryBuilder', 'parseOrderings', 'addTypo3Constraints', 'parseComparison']
154 );
155 $queryBuilderProphecy = $this->prophesize(QueryBuilder::class);
156 $subject->_set('queryBuilder', $queryBuilderProphecy->reveal());
157
158 $queryProphecy = $this->prophesize(QueryInterface::class);
159 $sourceProphecy = $this->prophesize(SourceInterface::class);
160 $queryProphecy->getSource()->willReturn($sourceProphecy->reveal());
161 $queryProphecy->getOrderings()->willReturn([]);
162
163 $constraintProphecy = $this->prophesize(NotInterface::class);
164 $subConstraintProphecy = $this->prophesize(ComparisonInterface::class);
165 $constraintProphecy->getConstraint()->shouldBeCalled()->willReturn($subConstraintProphecy->reveal());
166 $queryProphecy->getConstraint()->willReturn($constraintProphecy->reveal());
167 $subject->expects($this->once())->method('parseComparison')->willReturn('heinz');
168 $queryBuilderProphecy->andWhere(' NOT(heinz)')->shouldBeCalled();
169
170 $subject->convertQueryToDoctrineQueryBuilder($queryProphecy->reveal());
171 }
172
173 /**
174 * @test
175 */
176 public function convertQueryToDoctrineQueryBuilderAddsAndConstraint()
177 {
178 // Prepare subject, turn off initialize qb method and inject qb prophecy revelation
179 $subject = $this->getAccessibleMock(
180 Typo3DbQueryParser::class,
181 // Shut down some methods not important for this test
182 ['initializeQueryBuilder', 'parseOrderings', 'addTypo3Constraints', 'parseComparison']
183 );
184 $queryBuilderProphecy = $this->prophesize(QueryBuilder::class);
185 $subject->_set('queryBuilder', $queryBuilderProphecy->reveal());
186
187 $queryProphecy = $this->prophesize(QueryInterface::class);
188 $sourceProphecy = $this->prophesize(SourceInterface::class);
189 $queryProphecy->getSource()->willReturn($sourceProphecy->reveal());
190 $queryProphecy->getOrderings()->willReturn([]);
191
192 $constraintProphecy = $this->prophesize(AndInterface::class);
193 $queryProphecy->getConstraint()->willReturn($constraintProphecy->reveal());
194 $constraint1Prophecy = $this->prophesize(ComparisonInterface::class);
195 $constraintProphecy->getConstraint1()->willReturn($constraint1Prophecy->reveal());
196 $constraint2Prophecy = $this->prophesize(ComparisonInterface::class);
197 $constraintProphecy->getConstraint2()->willReturn($constraint2Prophecy->reveal());
198 $subject->expects($this->any())->method('parseComparison')->willReturn('heinz');
199 $expressionProphecy = $this->prophesize(ExpressionBuilder::class);
200 $queryBuilderProphecy->expr()->shouldBeCalled()->willReturn($expressionProphecy->reveal());
201 $compositeExpressionProphecy = $this->prophesize(CompositeExpression::class);
202 $compositeExpressionProphecy->__toString()->willReturn('heinz AND heinz');
203 $compositeExpressionRevelation = $compositeExpressionProphecy->reveal();
204 $expressionProphecy->andX('heinz', 'heinz')->shouldBeCalled()->willReturn($compositeExpressionRevelation);
205 $queryBuilderProphecy->andWhere($compositeExpressionRevelation)->shouldBeCalled();
206
207 $subject->convertQueryToDoctrineQueryBuilder($queryProphecy->reveal());
208 }
209
210 /**
211 * @test
212 */
213 public function convertQueryToDoctrineQueryBuilderNotAddsInvalidAndConstraint()
214 {
215 // Prepare subject, turn off initialize qb method and inject qb prophecy revelation
216 $subject = $this->getAccessibleMock(
217 Typo3DbQueryParser::class,
218 // Shut down some methods not important for this test
219 ['initializeQueryBuilder', 'parseOrderings', 'addTypo3Constraints', 'parseComparison']
220 );
221 $queryBuilderProphecy = $this->prophesize(QueryBuilder::class);
222 $subject->_set('queryBuilder', $queryBuilderProphecy->reveal());
223
224 $queryProphecy = $this->prophesize(QueryInterface::class);
225 $sourceProphecy = $this->prophesize(SourceInterface::class);
226 $queryProphecy->getSource()->willReturn($sourceProphecy->reveal());
227 $queryProphecy->getOrderings()->willReturn([]);
228
229 $constraintProphecy = $this->prophesize(AndInterface::class);
230 $queryProphecy->getConstraint()->willReturn($constraintProphecy->reveal());
231 $constraint1Prophecy = $this->prophesize(ComparisonInterface::class);
232 $constraintProphecy->getConstraint1()->willReturn($constraint1Prophecy->reveal());
233 // no result for constraint2
234 $constraintProphecy->getConstraint2()->willReturn(null);
235
236 // not be called
237 $queryBuilderProphecy->andWhere()->shouldNotBeCalled();
238
239 $subject->convertQueryToDoctrineQueryBuilder($queryProphecy->reveal());
240 }
241
242 /**
243 * @test
244 */
245 public function convertQueryToDoctrineQueryBuilderAddsOrConstraint()
246 {
247 // Prepare subject, turn off initialize qb method and inject qb prophecy revelation
248 $subject = $this->getAccessibleMock(
249 Typo3DbQueryParser::class,
250 // Shut down some methods not important for this test
251 ['initializeQueryBuilder', 'parseOrderings', 'addTypo3Constraints', 'parseComparison']
252 );
253 $queryBuilderProphecy = $this->prophesize(QueryBuilder::class);
254 $subject->_set('queryBuilder', $queryBuilderProphecy->reveal());
255
256 $queryProphecy = $this->prophesize(QueryInterface::class);
257 $sourceProphecy = $this->prophesize(SourceInterface::class);
258 $queryProphecy->getSource()->willReturn($sourceProphecy->reveal());
259 $queryProphecy->getOrderings()->willReturn([]);
260
261 $constraintProphecy = $this->prophesize(OrInterface::class);
262 $queryProphecy->getConstraint()->willReturn($constraintProphecy->reveal());
263 $constraint1Prophecy = $this->prophesize(ComparisonInterface::class);
264 $constraintProphecy->getConstraint1()->willReturn($constraint1Prophecy->reveal());
265 $constraint2Prophecy = $this->prophesize(ComparisonInterface::class);
266 $constraintProphecy->getConstraint2()->willReturn($constraint2Prophecy->reveal());
267 $subject->expects($this->any())->method('parseComparison')->willReturn('heinz');
268 $expressionProphecy = $this->prophesize(ExpressionBuilder::class);
269 $queryBuilderProphecy->expr()->shouldBeCalled()->willReturn($expressionProphecy->reveal());
270 $compositeExpressionProphecy = $this->prophesize(CompositeExpression::class);
271 $compositeExpressionProphecy->__toString()->willReturn('heinz OR heinz');
272 $compositeExpressionRevelation = $compositeExpressionProphecy->reveal();
273 $expressionProphecy->orX('heinz', 'heinz')->shouldBeCalled()->willReturn($compositeExpressionRevelation);
274 $queryBuilderProphecy->andWhere($compositeExpressionRevelation)->shouldBeCalled();
275
276 $subject->convertQueryToDoctrineQueryBuilder($queryProphecy->reveal());
277 }
278
279 /**
280 * @test
281 */
282 public function convertQueryToDoctrineQueryBuilderNotAddsInvalidOrConstraint()
283 {
284 // Prepare subject, turn off initialize qb method and inject qb prophecy revelation
285 $subject = $this->getAccessibleMock(
286 Typo3DbQueryParser::class,
287 // Shut down some methods not important for this test
288 ['initializeQueryBuilder', 'parseOrderings', 'addTypo3Constraints', 'parseComparison']
289 );
290 $queryBuilderProphecy = $this->prophesize(QueryBuilder::class);
291 $subject->_set('queryBuilder', $queryBuilderProphecy->reveal());
292
293 $queryProphecy = $this->prophesize(QueryInterface::class);
294 $sourceProphecy = $this->prophesize(SourceInterface::class);
295 $queryProphecy->getSource()->willReturn($sourceProphecy->reveal());
296 $queryProphecy->getOrderings()->willReturn([]);
297
298 $constraintProphecy = $this->prophesize(OrInterface::class);
299 $queryProphecy->getConstraint()->willReturn($constraintProphecy->reveal());
300 $constraint1Prophecy = $this->prophesize(ComparisonInterface::class);
301 $constraintProphecy->getConstraint1()->willReturn($constraint1Prophecy->reveal());
302 // no result for constraint2
303 $constraintProphecy->getConstraint2()->willReturn(null);
304
305 // not be called
306 $queryBuilderProphecy->andWhere()->shouldNotBeCalled();
307
308 $subject->convertQueryToDoctrineQueryBuilder($queryProphecy->reveal());
309 }
310
311 /**
312 * @return \Prophecy\Prophecy\ObjectProphecy
313 */
314 protected function getQueryBuilderWithExpressionBuilderProphet()
315 {
316 $connectionProphet = $this->prophesize(Connection::class);
317 $connectionProphet->quoteIdentifier(Argument::cetera())->willReturnArgument(0);
318 $querBuilderProphet = $this->prophesize(QueryBuilder::class, $connectionProphet->reveal());
319 $expr = GeneralUtility::makeInstance(ExpressionBuilder::class, $connectionProphet->reveal());
320 $querBuilderProphet->expr()->willReturn($expr);
321 return $querBuilderProphet;
322 }
323
324 /**
325 * @return \Prophecy\Prophecy\ObjectProphecy
326 */
327 protected function getQueryBuilderProphetWithQueryBuilderForSubselect()
328 {
329 $connectionProphet = $this->prophesize(Connection::class);
330 $connectionProphet->quoteIdentifier(Argument::cetera())->willReturnArgument(0);
331 $queryBuilderProphet = $this->prophesize(QueryBuilder::class, $connectionProphet->reveal());
332 $expr = GeneralUtility::makeInstance(ExpressionBuilder::class, $connectionProphet->reveal());
333 $queryBuilderProphet->expr()->willReturn(
334 $expr
335 );
336 $queryBuilderProphet->getConnection()->willReturn($connectionProphet->reveal());
337 $queryBuilderForSubselectMock = $this->getMockBuilder(QueryBuilder::class)
338 ->setMethods(['expr', 'unquoteSingleIdentifier'])
339 ->setConstructorArgs([$connectionProphet->reveal()])
340 ->getMock();
341 $connectionProphet->createQueryBuilder()->willReturn($queryBuilderForSubselectMock);
342 $queryBuilderForSubselectMock->expects($this->any())->method('expr')->will($this->returnValue($expr));
343 $queryBuilderForSubselectMock->expects($this->any())->method('unquoteSingleIdentifier')->will($this->returnCallback(function ($identifier) {
344 return $identifier;
345 }));
346 return $queryBuilderProphet;
347 }
348
349 /**
350 * @test
351 */
352 public function addSysLanguageStatementWorksForDefaultLanguage()
353 {
354 $table = $this->getUniqueId('tx_coretest_table');
355 $GLOBALS['TCA'][$table]['ctrl'] = [
356 'languageField' => 'sys_language_uid'
357 ];
358 /** @var \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings|\PHPUnit_Framework_MockObject_MockObject $querySettings */
359 $querySettings = $this->createMock(\TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings::class);
360 $mockTypo3DbQueryParser = $this->getAccessibleMock(Typo3DbQueryParser::class, ['dummy'], [], '', false);
361 $queryBuilderProphet = $this->getQueryBuilderWithExpressionBuilderProphet();
362 $mockTypo3DbQueryParser->_set('queryBuilder', $queryBuilderProphet->reveal());
363 $sql = $mockTypo3DbQueryParser->_callRef('getSysLanguageStatement', $table, $table, $querySettings);
364 $expectedSql = $table . '.sys_language_uid IN (0, -1)';
365 $this->assertSame($expectedSql, $sql);
366 }
367
368 /**
369 * @test
370 */
371 public function addSysLanguageStatementWorksForNonDefaultLanguage()
372 {
373 $table = $this->getUniqueId('tx_coretest_table');
374 $GLOBALS['TCA'][$table]['ctrl'] = [
375 'languageField' => 'sys_language_uid'
376 ];
377 /** @var \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings|\PHPUnit_Framework_MockObject_MockObject $querySettings */
378 $querySettings = $this->getMockBuilder(\TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings::class)
379 ->setMethods(['dummy'])
380 ->getMock();
381 $querySettings->setLanguageUid('1');
382 $mockTypo3DbQueryParser = $this->getAccessibleMock(Typo3DbQueryParser::class, ['dummy'], [], '', false);
383 $queryBuilderProphet = $this->getQueryBuilderWithExpressionBuilderProphet();
384 $mockTypo3DbQueryParser->_set('queryBuilder', $queryBuilderProphet->reveal());
385 $sql = $mockTypo3DbQueryParser->_callRef('getSysLanguageStatement', $table, $table, $querySettings);
386 $result = $table . '.sys_language_uid IN (1, -1)';
387 $this->assertSame($result, $sql);
388 }
389
390 /**
391 * @test
392 */
393 public function addSysLanguageStatementWorksInBackendContextWithNoGlobalTypoScriptFrontendControllerAvailable()
394 {
395 $table = $this->getUniqueId('tx_coretest_table');
396 $GLOBALS['TCA'][$table]['ctrl'] = [
397 'languageField' => 'sys_language_uid'
398 ];
399 $querySettings = new \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings();
400 $mockTypo3DbQueryParser = $this->getAccessibleMock(Typo3DbQueryParser::class, ['dummy'], [], '', false);
401 $queryBuilderProphet = $this->getQueryBuilderWithExpressionBuilderProphet();
402 $mockTypo3DbQueryParser->_set('queryBuilder', $queryBuilderProphet->reveal());
403 $sql = $mockTypo3DbQueryParser->_callRef('getSysLanguageStatement', $table, $table, $querySettings);
404 $expectedSql = $table . '.sys_language_uid IN (0, -1)';
405 $this->assertSame($expectedSql, $sql);
406 }
407
408 /**
409 * @test
410 */
411 public function addSysLanguageStatementWorksForDefaultLanguageWithoutDeleteStatementReturned()
412 {
413 $table = $this->getUniqueId('tx_coretest_table');
414 $GLOBALS['TCA'][$table]['ctrl'] = [
415 'languageField' => 'sys_language_uid',
416 'delete' => 'deleted'
417 ];
418 $querySettings = new \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings();
419 $querySettings->setLanguageUid(0);
420 $mockTypo3DbQueryParser = $this->getAccessibleMock(Typo3DbQueryParser::class, ['dummy'], [], '', false);
421 $queryBuilderProphet = $this->getQueryBuilderWithExpressionBuilderProphet();
422 $mockTypo3DbQueryParser->_set('queryBuilder', $queryBuilderProphet->reveal());
423 $sql = $mockTypo3DbQueryParser->_callRef('getSysLanguageStatement', $table, $table, $querySettings);
424 $expectedSql = $table . '.sys_language_uid IN (0, -1)';
425 $this->assertSame($expectedSql, $sql);
426 }
427
428 /**
429 * @test
430 */
431 public function addSysLanguageStatementWorksForForeignLanguageWithoutSubselection()
432 {
433 $table = $this->getUniqueId('tx_coretest_table');
434 $GLOBALS['TCA'][$table]['ctrl'] = [
435 'languageField' => 'sys_language_uid'
436 ];
437 $querySettings = new \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings();
438 $querySettings->setLanguageUid(2);
439 $mockTypo3DbQueryParser = $this->getAccessibleMock(Typo3DbQueryParser::class, ['dummy'], [], '', false);
440 $queryBuilderProphet = $this->getQueryBuilderWithExpressionBuilderProphet();
441 $mockTypo3DbQueryParser->_set('queryBuilder', $queryBuilderProphet->reveal());
442 $sql = $mockTypo3DbQueryParser->_callRef('getSysLanguageStatement', $table, $table, $querySettings);
443 $expectedSql = $table . '.sys_language_uid IN (2, -1)';
444 $this->assertSame($expectedSql, $sql);
445 }
446
447 /**
448 * @test
449 */
450 public function addSysLanguageStatementWorksForForeignLanguageWithSubselectionWithoutDeleteStatementReturned()
451 {
452 $table = $this->getUniqueId('tx_coretest_table');
453 $GLOBALS['TCA'][$table]['ctrl'] = [
454 'languageField' => 'sys_language_uid',
455 'transOrigPointerField' => 'l10n_parent'
456 ];
457 $querySettings = new \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings();
458 $querySettings->setLanguageUid(2);
459 $mockTypo3DbQueryParser = $this->getAccessibleMock(Typo3DbQueryParser::class, ['dummy'], [], '', false);
460
461 $queryBuilderProphet = $this->getQueryBuilderProphetWithQueryBuilderForSubselect();
462
463 $mockTypo3DbQueryParser->_set('queryBuilder', $queryBuilderProphet->reveal());
464
465 $compositeExpression = $mockTypo3DbQueryParser->_callRef('getSysLanguageStatement', $table, $table, $querySettings);
466 $expectedSql = '(' . $table . '.sys_language_uid IN (2, -1)) OR ((' . $table . '.sys_language_uid = 0) AND (' . $table . '.uid NOT IN (SELECT ' . $table . '.l10n_parent FROM ' . $table . ' WHERE (' . $table . '.l10n_parent > 0) AND (' . $table . '.sys_language_uid = 2))))';
467 $this->assertSame($expectedSql, $compositeExpression->__toString());
468 }
469
470 /**
471 * @test
472 */
473 public function addSysLanguageStatementWorksForForeignLanguageWithSubselectionTakesDeleteStatementIntoAccountIfNecessary()
474 {
475 $table = $this->getUniqueId('tx_coretest_table');
476 $GLOBALS['TCA'][$table]['ctrl'] = [
477 'languageField' => 'sys_language_uid',
478 'transOrigPointerField' => 'l10n_parent',
479 'delete' => 'deleted'
480 ];
481 $querySettings = new \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings();
482 $querySettings->setLanguageUid(2);
483 $mockTypo3DbQueryParser = $this->getAccessibleMock(Typo3DbQueryParser::class, ['dummy'], [], '', false);
484 $queryBuilderProphet = $this->getQueryBuilderProphetWithQueryBuilderForSubselect();
485 $mockTypo3DbQueryParser->_set('queryBuilder', $queryBuilderProphet->reveal());
486 $compositeExpression= $mockTypo3DbQueryParser->_callRef('getSysLanguageStatement', $table, $table, $querySettings);
487 $expectedSql = '(' . $table . '.sys_language_uid IN (2, -1))' .
488 ' OR ((' . $table . '.sys_language_uid = 0) AND (' . $table . '.uid NOT IN (' .
489 'SELECT ' . $table . '.l10n_parent FROM ' . $table .
490 ' WHERE (' . $table . '.l10n_parent > 0) AND (' .
491 $table . '.sys_language_uid = 2) AND (' .
492 $table . '.deleted = 0))))';
493 $this->assertSame($expectedSql, $compositeExpression->__toString());
494 }
495
496 /**
497 * @test
498 */
499 public function addSysLanguageStatementWorksInBackendContextWithSubselectionTakesDeleteStatementIntoAccountIfNecessary()
500 {
501 $table = 'tt_content';
502 $GLOBALS['TCA'][$table]['ctrl'] = [
503 'languageField' => 'sys_language_uid',
504 'transOrigPointerField' => 'l10n_parent',
505 'delete' => 'deleted'
506 ];
507 $querySettings = new \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings();
508 $querySettings->setLanguageUid(2);
509 $mockTypo3DbQueryParser = $this->getAccessibleMock(Typo3DbQueryParser::class, ['dummy'], [], '', false);
510
511 $queryBuilderProphet = $this->getQueryBuilderProphetWithQueryBuilderForSubselect();
512
513 $mockTypo3DbQueryParser->_set('queryBuilder', $queryBuilderProphet->reveal());
514 $compositeExpression = $mockTypo3DbQueryParser->_callRef('getSysLanguageStatement', $table, $table, $querySettings);
515 $expectedSql = '(' . $table . '.sys_language_uid IN (2, -1))' .
516 ' OR ((' . $table . '.sys_language_uid = 0) AND (' . $table . '.uid NOT IN (' .
517 'SELECT ' . $table . '.l10n_parent FROM ' . $table .
518 ' WHERE (' . $table . '.l10n_parent > 0) AND (' .
519 $table . '.sys_language_uid = 2) AND (' .
520 $table . '.deleted = 0))))';
521 $this->assertSame($expectedSql, $compositeExpression->__toString());
522 }
523
524 /**
525 * @test
526 */
527 public function orderStatementGenerationWorks()
528 {
529 $mockSource = $this->getMockBuilder(\TYPO3\CMS\Extbase\Persistence\Generic\Qom\Selector::class)
530 ->setMethods(['getNodeTypeName'])
531 ->disableOriginalConstructor()
532 ->getMock();
533 $mockSource->expects($this->any())->method('getNodeTypeName')->will($this->returnValue('foo'));
534 $mockDataMapper = $this->getMockBuilder(\TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper::class)
535 ->setMethods(['convertPropertyNameToColumnName', 'convertClassNameToTableName'])
536 ->disableOriginalConstructor()
537 ->getMock();
538 $mockDataMapper->expects($this->once())->method('convertClassNameToTableName')->with('foo')->will($this->returnValue('tx_myext_tablename'));
539 $mockDataMapper->expects($this->once())->method('convertPropertyNameToColumnName')->with('fooProperty', 'foo')->will($this->returnValue('converted_fieldname'));
540 $queryBuilderProphet = $this->prophesize(QueryBuilder::class);
541 $queryBuilderProphet->addOrderBy('tx_myext_tablename.converted_fieldname', 'ASC')->shouldBeCalledTimes(1);
542
543 $orderings = ['fooProperty' => QueryInterface::ORDER_ASCENDING];
544 $mockTypo3DbQueryParser = $this->getAccessibleMock(Typo3DbQueryParser::class, ['dummy'], [], '', false);
545 $mockTypo3DbQueryParser->_set('dataMapper', $mockDataMapper);
546 $mockTypo3DbQueryParser->_set('queryBuilder', $queryBuilderProphet->reveal());
547 $mockTypo3DbQueryParser->_callRef('parseOrderings', $orderings, $mockSource);
548 }
549
550 /**
551 * @test
552 */
553 public function orderStatementGenerationThrowsExceptionOnUnsupportedOrder()
554 {
555 $this->expectException(UnsupportedOrderException::class);
556 $this->expectExceptionCode(1242816074);
557 $mockSource = $this->getMockBuilder(\TYPO3\CMS\Extbase\Persistence\Generic\Qom\Selector::class)
558 ->setMethods(['getNodeTypeName'])
559 ->disableOriginalConstructor()
560 ->getMock();
561 $mockSource->expects($this->never())->method('getNodeTypeName');
562 $mockDataMapper = $this->getMockBuilder(\TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper::class)
563 ->setMethods(['convertPropertyNameToColumnName', 'convertClassNameToTableName'])
564 ->disableOriginalConstructor()
565 ->getMock();
566 $mockDataMapper->expects($this->never())->method('convertClassNameToTableName');
567 $mockDataMapper->expects($this->never())->method('convertPropertyNameToColumnName');
568 $orderings = ['fooProperty' => 'unsupported_order'];
569 $mockTypo3DbQueryParser = $this->getAccessibleMock(Typo3DbQueryParser::class, ['dummy'], [], '', false);
570 $mockTypo3DbQueryParser->_set('dataMapper', $mockDataMapper);
571
572 $mockTypo3DbQueryParser->_callRef('parseOrderings', $orderings, $mockSource);
573 }
574
575 /**
576 * @test
577 */
578 public function orderStatementGenerationWorksWithMultipleOrderings()
579 {
580 $mockSource = $this->getMockBuilder(\TYPO3\CMS\Extbase\Persistence\Generic\Qom\Selector::class)
581 ->setMethods(['getNodeTypeName'])
582 ->disableOriginalConstructor()
583 ->getMock();
584 $mockSource->expects($this->any())->method('getNodeTypeName')->will($this->returnValue('Tx_MyExt_ClassName'));
585 $mockDataMapper = $this->getMockBuilder(\TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper::class)
586 ->setMethods(['convertPropertyNameToColumnName', 'convertClassNameToTableName'])
587 ->disableOriginalConstructor()
588 ->getMock();
589 $mockDataMapper->expects($this->any())->method('convertClassNameToTableName')->with('Tx_MyExt_ClassName')->will($this->returnValue('tx_myext_tablename'));
590 $mockDataMapper->expects($this->any())->method('convertPropertyNameToColumnName')->will($this->returnValue('converted_fieldname'));
591 $orderings = [
592 'fooProperty' => QueryInterface::ORDER_ASCENDING,
593 'barProperty' => QueryInterface::ORDER_DESCENDING
594 ];
595 $mockTypo3DbQueryParser = $this->getAccessibleMock(Typo3DbQueryParser::class, ['dummy'], [], '', false);
596 $mockTypo3DbQueryParser->_set('dataMapper', $mockDataMapper);
597
598 $queryBuilder = $this->getMockBuilder(QueryBuilder::class)
599 ->disableOriginalConstructor()
600 ->setMethods(['addOrderBy'])
601 ->getMock();
602 $queryBuilder->expects($this->at(0))->method('addOrderBy')->with('tx_myext_tablename.converted_fieldname', 'ASC');
603 $queryBuilder->expects($this->at(1))->method('addOrderBy')->with('tx_myext_tablename.converted_fieldname', 'DESC');
604
605 $mockTypo3DbQueryParser->_set('queryBuilder', $queryBuilder);
606 $mockTypo3DbQueryParser->_callRef('parseOrderings', $orderings, $mockSource);
607 }
608
609 public function providerForVisibilityConstraintStatement()
610 {
611 return [
612 'in be: include all' => ['BE', true, [], true, ''],
613 'in be: ignore enable fields but do not include deleted' => ['BE', true, [], false, 'tx_foo_table.deleted_column = 0'],
614 'in be: respect enable fields but include deleted' => ['BE', false, [], true, 'tx_foo_table.disabled_column=0 AND (tx_foo_table.starttime_column<=123456789)'],
615 'in be: respect enable fields and do not include deleted' => ['BE', false, [], false, 'tx_foo_table.disabled_column=0 AND (tx_foo_table.starttime_column<=123456789) AND tx_foo_table.deleted_column = 0'],
616 'in fe: include all' => ['FE', true, [], true, ''],
617 'in fe: ignore enable fields but do not include deleted' => ['FE', true, [], false, 'tx_foo_table.deleted_column=0'],
618 'in fe: ignore only starttime and do not include deleted' => ['FE', true, ['starttime'], false, '(tx_foo_table.deleted_column = 0) AND (tx_foo_table.disabled_column = 0)'],
619 'in fe: respect enable fields and do not include deleted' => ['FE', false, [], false, '(tx_foo_table.deleted_column = 0) AND (tx_foo_table.disabled_column = 0) AND (tx_foo_table.starttime_column <= 123456789)']
620 ];
621 }
622
623 /**
624 * @test
625 * @dataProvider providerForVisibilityConstraintStatement
626 */
627 public function visibilityConstraintStatementIsGeneratedAccordingToTheQuerySettings($mode, $ignoreEnableFields, $enableFieldsToBeIgnored, $deletedValue, $expectedSql)
628 {
629 $tableName = 'tx_foo_table';
630 $GLOBALS['TCA'][$tableName]['ctrl'] = [
631 'enablecolumns' => [
632 'disabled' => 'disabled_column',
633 'starttime' => 'starttime_column'
634 ],
635 'delete' => 'deleted_column'
636 ];
637 $GLOBALS['TSFE'] = new \stdClass();
638 $GLOBALS['TSFE']->sys_page = new \TYPO3\CMS\Frontend\Page\PageRepository();
639 $GLOBALS['SIM_ACCESS_TIME'] = 123456789;
640
641 $connectionProphet = $this->prophesize(Connection::class);
642 $connectionProphet->quoteIdentifier(Argument::cetera())->willReturnArgument(0);
643
644 $queryBuilderProphet = $this->prophesize(QueryBuilder::class);
645 $queryBuilderProphet->expr()->willReturn(
646 GeneralUtility::makeInstance(ExpressionBuilder::class, $connectionProphet->reveal())
647 );
648
649 $connectionPoolProphet = $this->prophesize(ConnectionPool::class);
650 $connectionPoolProphet->getQueryBuilderForTable(Argument::any($tableName, 'pages'))->willReturn($queryBuilderProphet->reveal());
651 GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphet->reveal());
652
653 $mockQuerySettings = $this->getMockBuilder(\TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings::class)
654 ->setMethods(['getIgnoreEnableFields', 'getEnableFieldsToBeIgnored', 'getIncludeDeleted'])
655 ->disableOriginalConstructor()
656 ->getMock();
657 $mockQuerySettings->expects($this->once())->method('getIgnoreEnableFields')->will($this->returnValue($ignoreEnableFields));
658 $mockQuerySettings->expects($this->once())->method('getEnableFieldsToBeIgnored')->will($this->returnValue($enableFieldsToBeIgnored));
659 $mockQuerySettings->expects($this->once())->method('getIncludeDeleted')->will($this->returnValue($deletedValue));
660
661 /** @var $mockEnvironmentService \TYPO3\CMS\Extbase\Service\EnvironmentService | \PHPUnit_Framework_MockObject_MockObject */
662 $mockEnvironmentService = $this->getMockBuilder(\TYPO3\CMS\Extbase\Service\EnvironmentService::class)
663 ->setMethods(['isEnvironmentInFrontendMode'])
664 ->getMock();
665 $mockEnvironmentService->expects($this->any())->method('isEnvironmentInFrontendMode')->will($this->returnValue($mode == 'FE'));
666
667 $mockTypo3DbQueryParser = $this->getAccessibleMock(Typo3DbQueryParser::class, ['dummy'], [], '', false);
668 $mockTypo3DbQueryParser->_set('environmentService', $mockEnvironmentService);
669 $resultSql = $mockTypo3DbQueryParser->_callRef('getVisibilityConstraintStatement', $mockQuerySettings, $tableName, $tableName);
670 $this->assertSame($expectedSql, $resultSql);
671 unset($GLOBALS['TCA'][$tableName]);
672 }
673
674 public function providerForRespectEnableFields()
675 {
676 return [
677 'in be: respectEnableFields=false' => ['BE', false, ''],
678 'in be: respectEnableFields=true' => ['BE', true, 'tx_foo_table.disabled_column=0 AND (tx_foo_table.starttime_column<=123456789) AND tx_foo_table.deleted_column = 0'],
679 'in FE: respectEnableFields=false' => ['FE', false, ''],
680 'in FE: respectEnableFields=true' => ['FE', true, '(tx_foo_table.deleted_column = 0) AND (tx_foo_table.disabled_column = 0) AND (tx_foo_table.starttime_column <= 123456789)']
681 ];
682 }
683
684 /**
685 * @test
686 * @dataProvider providerForRespectEnableFields
687 */
688 public function respectEnableFieldsSettingGeneratesCorrectStatement($mode, $respectEnableFields, $expectedSql)
689 {
690 $tableName = 'tx_foo_table';
691 $GLOBALS['TCA'][$tableName]['ctrl'] = [
692 'enablecolumns' => [
693 'disabled' => 'disabled_column',
694 'starttime' => 'starttime_column'
695 ],
696 'delete' => 'deleted_column'
697 ];
698 $GLOBALS['TSFE'] = new \stdClass();
699 $GLOBALS['TSFE']->sys_page = new \TYPO3\CMS\Frontend\Page\PageRepository();
700 $GLOBALS['SIM_ACCESS_TIME'] = 123456789;
701
702 $connectionProphet = $this->prophesize(Connection::class);
703 $connectionProphet->quoteIdentifier(Argument::cetera())->willReturnArgument(0);
704
705 $queryBuilderProphet = $this->prophesize(QueryBuilder::class);
706 $queryBuilderProphet->expr()->willReturn(
707 GeneralUtility::makeInstance(ExpressionBuilder::class, $connectionProphet->reveal())
708 );
709
710 $connectionPoolProphet = $this->prophesize(ConnectionPool::class);
711 $connectionPoolProphet->getQueryBuilderForTable(Argument::any($tableName, 'pages'))->willReturn($queryBuilderProphet->reveal());
712 GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphet->reveal());
713
714 /** @var \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings $mockQuerySettings */
715 $mockQuerySettings = $this->getMockBuilder(\TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings::class)
716 ->setMethods(['dummy'])
717 ->disableOriginalConstructor()
718 ->getMock();
719 $mockQuerySettings->setIgnoreEnableFields(!$respectEnableFields);
720 $mockQuerySettings->setIncludeDeleted(!$respectEnableFields);
721
722 /** @var $mockEnvironmentService \TYPO3\CMS\Extbase\Service\EnvironmentService | \PHPUnit_Framework_MockObject_MockObject */
723 $mockEnvironmentService = $this->getMockBuilder(\TYPO3\CMS\Extbase\Service\EnvironmentService::class)
724 ->setMethods(['isEnvironmentInFrontendMode'])
725 ->getMock();
726 $mockEnvironmentService->expects($this->any())->method('isEnvironmentInFrontendMode')->will($this->returnValue($mode == 'FE'));
727
728 $mockTypo3DbQueryParser = $this->getAccessibleMock(Typo3DbQueryParser::class, ['dummy'], [], '', false);
729 $mockTypo3DbQueryParser->_set('environmentService', $mockEnvironmentService);
730 $actualSql = $mockTypo3DbQueryParser->_callRef('getVisibilityConstraintStatement', $mockQuerySettings, $tableName, $tableName);
731 $this->assertSame($expectedSql, $actualSql);
732 unset($GLOBALS['TCA'][$tableName]);
733 }
734
735 /**
736 * @test
737 */
738 public function visibilityConstraintStatementGenerationThrowsExceptionIfTheQuerySettingsAreInconsistent()
739 {
740 $this->expectException(InconsistentQuerySettingsException::class);
741 $this->expectExceptionCode(1460975922);
742 $tableName = 'tx_foo_table';
743 $GLOBALS['TCA'][$tableName]['ctrl'] = [
744 'enablecolumns' => [
745 'disabled' => 'disabled_column'
746 ],
747 'delete' => 'deleted_column'
748 ];
749 $mockQuerySettings = $this->getMockBuilder(\TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings::class)
750 ->setMethods(['getIgnoreEnableFields', 'getEnableFieldsToBeIgnored', 'getIncludeDeleted'])
751 ->disableOriginalConstructor()
752 ->getMock();
753 $mockQuerySettings->expects($this->once())->method('getIgnoreEnableFields')->will($this->returnValue(false));
754 $mockQuerySettings->expects($this->once())->method('getEnableFieldsToBeIgnored')->will($this->returnValue([]));
755 $mockQuerySettings->expects($this->once())->method('getIncludeDeleted')->will($this->returnValue(true));
756
757 /** @var $mockEnvironmentService \TYPO3\CMS\Extbase\Service\EnvironmentService | \PHPUnit_Framework_MockObject_MockObject */
758 $mockEnvironmentService = $this->getMockBuilder(\TYPO3\CMS\Extbase\Service\EnvironmentService::class)
759 ->setMethods(['isEnvironmentInFrontendMode'])
760 ->getMock();
761 $mockEnvironmentService->expects($this->any())->method('isEnvironmentInFrontendMode')->will($this->returnValue(true));
762
763 $mockTypo3DbQueryParser = $this->getAccessibleMock(Typo3DbQueryParser::class, ['dummy'], [], '', false);
764 $mockTypo3DbQueryParser->_set('environmentService', $mockEnvironmentService);
765 $mockTypo3DbQueryParser->_callRef('getVisibilityConstraintStatement', $mockQuerySettings, $tableName, $tableName);
766 unset($GLOBALS['TCA'][$tableName]);
767 }
768
769 /**
770 * DataProvider for addPageIdStatement Tests
771 */
772 public function providerForAddPageIdStatementData()
773 {
774 $table = $this->getUniqueId('tx_coretest_table');
775 return [
776 'set Pid to zero if rootLevel = 1' => [
777 '1',
778 $table,
779 $table . '.pid = 0'
780 ],
781 'set Pid to given Pids if rootLevel = 0' => [
782 '0',
783 $table,
784 $table . '.pid IN (42, 27)'
785 ],
786 'add 0 to given Pids if rootLevel = -1' => [
787 '-1',
788 $table,
789 $table . '.pid IN (42, 27, 0)'
790 ],
791 'set Pid to zero if rootLevel = -1 and no further pids given' => [
792 '-1',
793 $table,
794 $table . '.pid = 0',
795 []
796 ],
797 'set no statement for invalid configuration' => [
798 '2',
799 $table,
800 ''
801 ]
802 ];
803 }
804
805 /**
806 * @test
807 * @dataProvider providerForAddPageIdStatementData
808 */
809 public function addPageIdStatementSetsPidToZeroIfTableDeclaresRootlevel($rootLevel, $table, $expectedSql, $storagePageIds = [42, 27])
810 {
811 $GLOBALS['TCA'][$table]['ctrl'] = [
812 'rootLevel' => $rootLevel
813 ];
814 $mockTypo3DbQueryParser = $this->getAccessibleMock(Typo3DbQueryParser::class, ['dummy'], [], '', false);
815 $queryBuilderProphet = $this->getQueryBuilderWithExpressionBuilderProphet();
816 $mockTypo3DbQueryParser->_set('queryBuilder', $queryBuilderProphet->reveal());
817 $sql = $mockTypo3DbQueryParser->_callRef('getPageIdStatement', $table, $table, $storagePageIds);
818
819 $this->assertSame($expectedSql, $sql);
820 }
821 }