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