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