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