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