[TASK] Make base test classes always available
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / 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']['Connections']['Default']['dbname'];
227 // Append the unique identifier to the base database name to end up with a single database per test case
228 $localConfiguration['DB']['Connections']['Default']['dbname'] = $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']['Connections']['Default']['dbname'], $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 $testbase = new Testbase();
313 $testbase->importXmlDatabaseFixture($path);
314 }
315
316 /**
317 * @param int $pageId
318 * @param array $typoScriptFiles
319 */
320 protected function setUpFrontendRootPage($pageId, array $typoScriptFiles = array())
321 {
322 $pageId = (int)$pageId;
323 $page = $this->getDatabaseConnection()->exec_SELECTgetSingleRow('*', 'pages', 'uid=' . $pageId);
324
325 if (empty($page)) {
326 $this->fail('Cannot set up frontend root page "' . $pageId . '"');
327 }
328
329 $pagesFields = array(
330 'is_siteroot' => 1
331 );
332
333 $this->getDatabaseConnection()->exec_UPDATEquery('pages', 'uid=' . $pageId, $pagesFields);
334
335 $templateFields = array(
336 'pid' => $pageId,
337 'title' => '',
338 'config' => '',
339 'clear' => 3,
340 'root' => 1,
341 );
342
343 foreach ($typoScriptFiles as $typoScriptFile) {
344 $templateFields['config'] .= '<INCLUDE_TYPOSCRIPT: source="FILE:' . $typoScriptFile . '">' . LF;
345 }
346
347 $this->getDatabaseConnection()->exec_INSERTquery('sys_template', $templateFields);
348 }
349
350 /**
351 * @param int $pageId
352 * @param int $languageId
353 * @param int $backendUserId
354 * @param int $workspaceId
355 * @param bool $failOnFailure
356 * @param int $frontendUserId
357 * @return Response
358 */
359 protected function getFrontendResponse($pageId, $languageId = 0, $backendUserId = 0, $workspaceId = 0, $failOnFailure = true, $frontendUserId = 0)
360 {
361 $pageId = (int)$pageId;
362 $languageId = (int)$languageId;
363
364 $additionalParameter = '';
365
366 if (!empty($frontendUserId)) {
367 $additionalParameter .= '&frontendUserId=' . (int)$frontendUserId;
368 }
369 if (!empty($backendUserId)) {
370 $additionalParameter .= '&backendUserId=' . (int)$backendUserId;
371 }
372 if (!empty($workspaceId)) {
373 $additionalParameter .= '&workspaceId=' . (int)$workspaceId;
374 }
375
376 $arguments = array(
377 'documentRoot' => $this->instancePath,
378 'requestUrl' => 'http://localhost/?id=' . $pageId . '&L=' . $languageId . $additionalParameter,
379 );
380
381 $template = new \Text_Template(ORIGINAL_ROOT . 'typo3/sysext/core/Tests/Functional/Fixtures/Frontend/request.tpl');
382 $template->setVar(
383 array(
384 'arguments' => var_export($arguments, true),
385 'originalRoot' => ORIGINAL_ROOT,
386 )
387 );
388
389 $php = \PHPUnit_Util_PHP::factory();
390 $response = $php->runJob($template->render());
391 $result = json_decode($response['stdout'], true);
392
393 if ($result === null) {
394 $this->fail('Frontend Response is empty');
395 }
396
397 if ($failOnFailure && $result['status'] === Response::STATUS_Failure) {
398 $this->fail('Frontend Response has failure:' . LF . $result['error']);
399 }
400
401 $response = new Response($result['status'], $result['content'], $result['error']);
402 return $response;
403 }
404 }