[BUGFIX] Fix functional tests for EXT:impexp on PostgreSQL
[Packages/TYPO3.CMS.git] / components / testing_framework / Classes / Core / Functional / FunctionalTestCase.php
1 <?php
2 namespace TYPO3\TestingFramework\Core\Functional;
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 Doctrine\DBAL\Platforms\MySqlPlatform;
18 use Doctrine\DBAL\Platforms\PostgreSqlPlatform;
19 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
20 use TYPO3\CMS\Core\Cache\Backend\NullBackend;
21 use TYPO3\CMS\Core\Database\ConnectionPool;
22 use TYPO3\CMS\Core\Utility\GeneralUtility;
23 use TYPO3\TestingFramework\Core\BaseTestCase;
24 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\Response;
25 use TYPO3\TestingFramework\Core\Testbase;
26
27 /**
28 * Base test case class for functional tests, all TYPO3 CMS
29 * functional tests should extend from this class!
30 *
31 * If functional tests need additional setUp() and tearDown() code,
32 * they *must* call parent::setUp() and parent::tearDown() to properly
33 * set up and destroy the test system.
34 *
35 * The functional test system creates a full new TYPO3 CMS instance
36 * within typo3temp/ of the base system and the bootstraps this TYPO3 instance.
37 * This abstract class takes care of creating this instance with its
38 * folder structure and a LocalConfiguration, creates an own database
39 * for each test run and imports tables of loaded extensions.
40 *
41 * Functional tests must be run standalone (calling native phpunit
42 * directly) and can not be executed by eg. the ext:phpunit backend module.
43 * Additionally, the script must be called from the document root
44 * of the instance, otherwise path calculation is not successfully.
45 *
46 * Call whole functional test suite, example:
47 * - cd /var/www/t3master/foo # Document root of CMS instance, here is index.php of frontend
48 * - typo3/../bin/phpunit -c components/testing_framework/core/Build/FunctionalTests.xml
49 *
50 * Call single test case, example:
51 * - cd /var/www/t3master/foo # Document root of CMS instance, here is index.php of frontend
52 * - typo3/../bin/phpunit \
53 * --process-isolation \
54 * --bootstrap components/testing_framework/core/Build/FunctionalTestsBootstrap.php \
55 * typo3/sysext/core/Tests/Functional/DataHandling/DataHandlerTest.php
56 */
57 abstract class FunctionalTestCase extends BaseTestCase
58 {
59 const DATABASE_PLATFORM_MYSQL = 'MySQL';
60 const DATABASE_PLATFORM_PDO = 'PDO';
61
62 /**
63 * Path to a XML fixture dependent on the current database.
64 * @var string
65 */
66 protected $fixturePath = '';
67
68 /**
69 * @var string
70 */
71 protected $databasePlatform;
72
73 /**
74 * An unique identifier for this test case. Location of the test
75 * instance and database name depend on this. Calculated early in setUp()
76 *
77 * @var string
78 */
79 protected $identifier;
80
81 /**
82 * Absolute path to test instance document root. Depends on $identifier.
83 * Calculated early in setUp()
84 *
85 * @var string
86 */
87 protected $instancePath;
88
89 /**
90 * Core extensions to load.
91 *
92 * If the test case needs additional core extensions as requirement,
93 * they can be noted here and will be added to LocalConfiguration
94 * extension list and ext_tables.sql of those extensions will be applied.
95 *
96 * This property will stay empty in this abstract, so it is possible
97 * to just overwrite it in extending classes. Extensions noted here will
98 * be loaded for every test of a test case and it is not possible to change
99 * the list of loaded extensions between single tests of a test case.
100 *
101 * A default list of core extensions is always loaded.
102 *
103 * @see FunctionalTestCaseUtility $defaultActivatedCoreExtensions
104 * @var array
105 */
106 protected $coreExtensionsToLoad = [];
107
108 /**
109 * Array of test/fixture extensions paths that should be loaded for a test.
110 *
111 * This property will stay empty in this abstract, so it is possible
112 * to just overwrite it in extending classes. Extensions noted here will
113 * be loaded for every test of a test case and it is not possible to change
114 * the list of loaded extensions between single tests of a test case.
115 *
116 * Given path is expected to be relative to your document root, example:
117 *
118 * array(
119 * 'typo3conf/ext/some_extension/Tests/Functional/Fixtures/Extensions/test_extension',
120 * 'typo3conf/ext/base_extension',
121 * );
122 *
123 * Extensions in this array are linked to the test instance, loaded
124 * and their ext_tables.sql will be applied.
125 *
126 * @var array
127 */
128 protected $testExtensionsToLoad = [];
129
130 /**
131 * Array of test/fixture folder or file paths that should be linked for a test.
132 *
133 * This property will stay empty in this abstract, so it is possible
134 * to just overwrite it in extending classes. Path noted here will
135 * be linked for every test of a test case and it is not possible to change
136 * the list of folders between single tests of a test case.
137 *
138 * array(
139 * 'link-source' => 'link-destination'
140 * );
141 *
142 * Given paths are expected to be relative to the test instance root.
143 * The array keys are the source paths and the array values are the destination
144 * paths, example:
145 *
146 * array(
147 * 'typo3/sysext/impext/Tests/Functional/Fixtures/Folders/fileadmin/user_upload' =>
148 * 'fileadmin/user_upload',
149 * 'typo3conf/ext/my_own_ext/Tests/Functional/Fixtures/Folders/uploads/tx_myownext' =>
150 * 'uploads/tx_myownext'
151 * );
152 *
153 * To be able to link from my_own_ext the extension path needs also to be registered in
154 * property $testExtensionsToLoad
155 *
156 * @var array
157 */
158 protected $pathsToLinkInTestInstance = [];
159
160 /**
161 * This configuration array is merged with TYPO3_CONF_VARS
162 * that are set in default configuration and factory configuration
163 *
164 * @var array
165 */
166 protected $configurationToUseInTestInstance = [];
167
168 /**
169 * Array of folders that should be created inside the test instance document root.
170 *
171 * This property will stay empty in this abstract, so it is possible
172 * to just overwrite it in extending classes. Path noted here will
173 * be linked for every test of a test case and it is not possible to change
174 * the list of folders between single tests of a test case.
175 *
176 * Per default the following folder are created
177 * /fileadmin
178 * /typo3temp
179 * /typo3conf
180 * /typo3conf/ext
181 * /uploads
182 *
183 * To create additional folders add the paths to this array. Given paths are expected to be
184 * relative to the test instance root and have to begin with a slash. Example:
185 *
186 * array(
187 * 'fileadmin/user_upload'
188 * );
189 *
190 * @var array
191 */
192 protected $additionalFoldersToCreate = [];
193
194 /**
195 * The fixture which is used when initializing a backend user
196 *
197 * @var string
198 */
199 protected $backendUserFixture = 'components/testing_framework/Resources/Core/Functional/Fixtures/be_users.xml';
200
201 /**
202 * Set up creates a test instance and database.
203 *
204 * This method should be called with parent::setUp() in your test cases!
205 *
206 * @return void
207 * @throws \Doctrine\DBAL\DBALException
208 */
209 protected function setUp()
210 {
211 if (!defined('ORIGINAL_ROOT')) {
212 $this->markTestSkipped('Functional tests must be called through phpunit on CLI');
213 }
214
215 // Use a 7 char long hash of class name as identifier
216 $this->identifier = substr(sha1(get_class($this)), 0, 7);
217 $this->instancePath = ORIGINAL_ROOT . 'typo3temp/var/tests/functional-' . $this->identifier;
218
219 $testbase = new Testbase();
220 $testbase->defineTypo3ModeBe();
221 $testbase->setTypo3TestingContext();
222 if ($testbase->recentTestInstanceExists($this->instancePath)) {
223 // Reusing an existing instance. This typically happens for the second, third, ... test
224 // in a test case, so environment is set up only once per test case.
225 $testbase->setUpBasicTypo3Bootstrap($this->instancePath);
226 $testbase->initializeTestDatabaseAndTruncateTables();
227 $testbase->loadExtensionTables();
228 } else {
229 $testbase->removeOldInstanceIfExists($this->instancePath);
230 // Basic instance directory structure
231 $testbase->createDirectory($this->instancePath . '/fileadmin');
232 $testbase->createDirectory($this->instancePath . '/typo3temp/var/transient');
233 $testbase->createDirectory($this->instancePath . '/typo3temp/assets');
234 $testbase->createDirectory($this->instancePath . '/typo3conf/ext');
235 $testbase->createDirectory($this->instancePath . '/uploads');
236 // Additionally requested directories
237 foreach ($this->additionalFoldersToCreate as $directory) {
238 $testbase->createDirectory($this->instancePath . '/' . $directory);
239 }
240 $testbase->createLastRunTextfile($this->instancePath);
241 $testbase->setUpInstanceCoreLinks($this->instancePath);
242 $testbase->linkTestExtensionsToInstance($this->instancePath, $this->testExtensionsToLoad);
243 $testbase->linkPathsInTestInstance($this->instancePath, $this->pathsToLinkInTestInstance);
244 $localConfiguration['DB'] = $testbase->getOriginalDatabaseSettingsFromEnvironmentOrLocalConfiguration();
245 $originalDatabaseName = $localConfiguration['DB']['Connections']['Default']['dbname'];
246 // Append the unique identifier to the base database name to end up with a single database per test case
247 $localConfiguration['DB']['Connections']['Default']['dbname'] = $originalDatabaseName . '_ft' . $this->identifier;
248 $testbase->testDatabaseNameIsNotTooLong($originalDatabaseName, $localConfiguration);
249 // Set some hard coded base settings for the instance. Those could be overruled by
250 // $this->configurationToUseInTestInstance if needed again.
251 $localConfiguration['SYS']['isInitialInstallationInProgress'] = false;
252 $localConfiguration['SYS']['isInitialDatabaseImportDone'] = true;
253 $localConfiguration['SYS']['displayErrors'] = '1';
254 $localConfiguration['SYS']['debugExceptionHandler'] = '';
255 $localConfiguration['SYS']['trustedHostsPattern'] = '.*';
256 // @todo: This should be moved over to DB/Connections/Default/initCommands
257 $localConfiguration['SYS']['setDBinit'] = 'SET SESSION sql_mode = \'STRICT_ALL_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_VALUE_ON_ZERO,NO_ENGINE_SUBSTITUTION,NO_ZERO_DATE,NO_ZERO_IN_DATE,ONLY_FULL_GROUP_BY\';';
258 $localConfiguration['SYS']['caching']['cacheConfigurations']['extbase_object']['backend'] = NullBackend::class;
259 $testbase->setUpLocalConfiguration($this->instancePath, $localConfiguration, $this->configurationToUseInTestInstance);
260 $defaultCoreExtensionsToLoad = [
261 'core',
262 'backend',
263 'frontend',
264 'lang',
265 'extbase',
266 'install',
267 ];
268 $testbase->setUpPackageStates($this->instancePath, $defaultCoreExtensionsToLoad, $this->coreExtensionsToLoad, $this->testExtensionsToLoad);
269 $testbase->setUpBasicTypo3Bootstrap($this->instancePath);
270 $testbase->setUpTestDatabase($localConfiguration['DB']['Connections']['Default']['dbname'], $originalDatabaseName);
271 $testbase->loadExtensionTables();
272 $testbase->createDatabaseStructure();
273 }
274
275 $databasePlatform = $this->getConnectionPool()
276 ->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME)
277 ->getDatabasePlatform();
278
279 if ($databasePlatform instanceof MySqlPlatform) {
280 $this->setDatabasePlatform(static::DATABASE_PLATFORM_MYSQL);
281 } else {
282 $this->setDatabasePlatform(static::DATABASE_PLATFORM_PDO);
283 }
284 }
285
286 /**
287 * Get DatabaseConnection instance - $GLOBALS['TYPO3_DB']
288 *
289 * This method should be used instead of direct access to
290 * $GLOBALS['TYPO3_DB'] for easy IDE auto completion.
291 *
292 * @return \TYPO3\CMS\Core\Database\DatabaseConnection
293 * @deprecated since TYPO3 v8, will be removed in TYPO3 v9
294 */
295 protected function getDatabaseConnection()
296 {
297 GeneralUtility::logDeprecatedFunction();
298 return $GLOBALS['TYPO3_DB'];
299 }
300
301 /**
302 * @return ConnectionPool
303 */
304 protected function getConnectionPool()
305 {
306 return GeneralUtility::makeInstance(ConnectionPool::class);
307 }
308
309 /**
310 * Initialize backend user
311 *
312 * @param int $userUid uid of the user we want to initialize. This user must exist in the fixture file
313 * @return BackendUserAuthentication
314 * @throws Exception
315 */
316 protected function setUpBackendUserFromFixture($userUid)
317 {
318 $this->importDataSet(ORIGINAL_ROOT . $this->backendUserFixture);
319
320 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('be_users');
321 $queryBuilder->getRestrictions()->removeAll();
322
323 $userRow = $queryBuilder->select('*')
324 ->from('be_users')
325 ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($userUid, \PDO::PARAM_INT)))
326 ->execute()
327 ->fetch();
328
329 /** @var $backendUser BackendUserAuthentication */
330 $backendUser = GeneralUtility::makeInstance(BackendUserAuthentication::class);
331 $sessionId = $backendUser->createSessionId();
332 $_COOKIE['be_typo_user'] = $sessionId;
333 $backendUser->id = $sessionId;
334 $backendUser->sendNoCacheHeaders = false;
335 $backendUser->dontSetCookie = true;
336 $backendUser->createUserSession($userRow);
337
338 $GLOBALS['BE_USER'] = $backendUser;
339 $GLOBALS['BE_USER']->start();
340 if (!is_array($GLOBALS['BE_USER']->user) || !$GLOBALS['BE_USER']->user['uid']) {
341 throw new Exception(
342 'Can not initialize backend user',
343 1377095807
344 );
345 }
346 $GLOBALS['BE_USER']->backendCheckLogin();
347
348 return $backendUser;
349 }
350
351 /**
352 * Imports a data set represented as XML into the test database,
353 *
354 * @param string $path Absolute path to the XML file containing the data set to load
355 * @return void
356 * @throws Exception
357 */
358 protected function importDataSet($path)
359 {
360 $testbase = new Testbase();
361 $testbase->importXmlDatabaseFixture($path);
362 }
363
364 /**
365 * @param int $pageId
366 * @param array $typoScriptFiles
367 */
368 protected function setUpFrontendRootPage($pageId, array $typoScriptFiles = [])
369 {
370 $pageId = (int)$pageId;
371
372 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('pages');
373 $page = $connection->select(['*'], 'pages', ['uid' => $pageId])->fetch();
374
375 if (empty($page)) {
376 $this->fail('Cannot set up frontend root page "' . $pageId . '"');
377 }
378
379 $databasePlatform = 'mysql';
380 if ($connection->getDatabasePlatform() instanceof PostgreSqlPlatform) {
381 $databasePlatform = 'postgresql';
382 }
383
384 $connection->update(
385 'pages',
386 ['is_siteroot' => 1],
387 ['uid' => $pageId]
388 );
389
390 $templateFields = [
391 'pid' => $pageId,
392 'title' => '',
393 'constants' => 'databasePlatform = ' . $databasePlatform . LF,
394 'config' => '',
395 'clear' => 3,
396 'root' => 1,
397 ];
398
399 foreach ($typoScriptFiles as $typoScriptFile) {
400 $templateFields['config'] .= '<INCLUDE_TYPOSCRIPT: source="FILE:' . $typoScriptFile . '">' . LF;
401 }
402 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('sys_template');
403 $connection->delete('sys_template', ['pid' => $pageId]);
404 $connection->insert(
405 'sys_template',
406 $templateFields
407 );
408 }
409
410 /**
411 * @param int $pageId
412 * @param int $languageId
413 * @param int $backendUserId
414 * @param int $workspaceId
415 * @param bool $failOnFailure
416 * @param int $frontendUserId
417 * @return Response
418 */
419 protected function getFrontendResponse($pageId, $languageId = 0, $backendUserId = 0, $workspaceId = 0, $failOnFailure = true, $frontendUserId = 0)
420 {
421 $pageId = (int)$pageId;
422 $languageId = (int)$languageId;
423
424 $additionalParameter = '';
425
426 if (!empty($frontendUserId)) {
427 $additionalParameter .= '&frontendUserId=' . (int)$frontendUserId;
428 }
429 if (!empty($backendUserId)) {
430 $additionalParameter .= '&backendUserId=' . (int)$backendUserId;
431 }
432 if (!empty($workspaceId)) {
433 $additionalParameter .= '&workspaceId=' . (int)$workspaceId;
434 }
435
436 $arguments = [
437 'documentRoot' => $this->instancePath,
438 'requestUrl' => 'http://localhost/?id=' . $pageId . '&L=' . $languageId . $additionalParameter,
439 ];
440
441 $template = new \Text_Template(ORIGINAL_ROOT . 'typo3/sysext/core/Tests/Functional/Fixtures/Frontend/request.tpl');
442 $template->setVar(
443 [
444 'arguments' => var_export($arguments, true),
445 'originalRoot' => ORIGINAL_ROOT,
446 ]
447 );
448
449 $php = \PHPUnit_Util_PHP::factory();
450 $response = $php->runJob($template->render());
451 $result = json_decode($response['stdout'], true);
452
453 if ($result === null) {
454 $this->fail('Frontend Response is empty');
455 }
456
457 if ($failOnFailure && $result['status'] === Response::STATUS_Failure) {
458 $this->fail('Frontend Response has failure:' . LF . $result['error']);
459 }
460
461 $response = new Response($result['status'], $result['content'], $result['error']);
462 return $response;
463 }
464
465 /**
466 * Return the path to a XML fixture dependent on the current database platform that tests are run against.
467 *
468 * @param string $fileName
469 *
470 * @return string
471 * @throws \Exception
472 */
473 protected function getXmlFilePath(string $fileName): string
474 {
475 $baseDir = $this->fixturePath . $this->databasePlatform . '/';
476 $xmlFilePath = $baseDir . $fileName;
477
478 if (!file_exists($xmlFilePath)) {
479 throw new \Exception(
480 'XML fixture file "' . $xmlFilePath . '" not found for database platform: ' . $this->databasePlatform,
481 1487620903
482 );
483 }
484
485 return $xmlFilePath;
486 }
487
488 /**
489 * @return string
490 */
491 public function getDatabasePlatform(): string
492 {
493 return $this->databasePlatform;
494 }
495
496 /**
497 * @param string $databasePlatform
498 *
499 * @return $this
500 */
501 public function setDatabasePlatform(string $databasePlatform)
502 {
503 $this->databasePlatform = $databasePlatform;
504 return $this;
505 }
506
507 /**
508 * @return string
509 */
510 public function getFixturePath(): string
511 {
512 return $this->fixturePath;
513 }
514
515 /**
516 * @param string $fixturePath
517 *
518 * @return $this
519 */
520 public function setFixturePath(string $fixturePath)
521 {
522 $this->fixturePath = $fixturePath;
523 return $this;
524 }
525 }