[TASK] Add framework for frontend rendering functional tests
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Tests / FunctionalTestCase.php
1 <?php
2 namespace TYPO3\CMS\Core\Tests;
3
4 /***************************************************************
5 * Copyright notice
6 *
7 * (c) 2013 Christian Kuhn <lolli@schwarzbu.ch>
8 * All rights reserved
9 *
10 * This script is part of the TYPO3 project. The TYPO3 project is
11 * free software; you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License as published by
13 * the Free Software Foundation; either version 2 of the License, or
14 * (at your option) any later version.
15 *
16 * The GNU General Public License can be found at
17 * http://www.gnu.org/copyleft/gpl.html.
18 *
19 * This script is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
23 *
24 * This copyright notice MUST APPEAR in all copies of the script!
25 ***************************************************************/
26
27 use \TYPO3\CMS\Core\Tests\Functional\Framework\Frontend\Response;
28
29 /**
30 * Base test case class for functional tests, all TYPO3 CMS
31 * functional tests should extend from this class!
32 *
33 * If functional tests need additional setUp() and tearDown() code,
34 * they *must* call parent::setUp() and parent::tearDown() to properly
35 * set up and destroy the test system.
36 *
37 * The functional test system creates a full new TYPO3 CMS instance
38 * within typo3temp/ of the base system and the bootstraps this TYPO3 instance.
39 * This abstract class takes care of creating this instance with its
40 * folder structure and a LocalConfiguration, creates an own database
41 * for each test run and imports tables of loaded extensions.
42 *
43 * Functional tests must be run standalone (calling native phpunit
44 * directly) and can not be executed by eg. the ext:phpunit backend module.
45 * Additionally, the script must be called from the document root
46 * of the instance, otherwise path calculation is not successfully.
47 *
48 * Call whole functional test suite, example:
49 * - cd /var/www/t3master/foo # Document root of CMS instance, here is index.php of frontend
50 * - ./typo3conf/ext/phpunit/Composer/vendor/bin/phpunit -c typo3/sysext/core/Build/FunctionalTests.xml
51 *
52 * Call single test case, example:
53 * - cd /var/www/t3master/foo # Document root of CMS instance, here is index.php of frontend
54 * - ./typo3conf/ext/phpunit/Composer/vendor/bin/phpunit \
55 * --process-isolation \
56 * --bootstrap typo3/sysext/core/Build/FunctionalTestsBootstrap.php \
57 * typo3/sysext/core/Tests/Functional/DataHandling/DataHandlerTest.php
58 */
59 abstract class FunctionalTestCase extends BaseTestCase {
60
61 /**
62 * Core extensions to load.
63 *
64 * If the test case needs additional core extensions as requirement,
65 * they can be noted here and will be added to LocalConfiguration
66 * extension list and ext_tables.sql of those extensions will be applied.
67 *
68 * This property will stay empty in this abstract, so it is possible
69 * to just overwrite it in extending classes. Extensions noted here will
70 * be loaded for every test of a test case and it is not possible to change
71 * the list of loaded extensions between single tests of a test case.
72 *
73 * A default list of core extensions is always loaded.
74 *
75 * @see FunctionalTestCaseUtility $defaultActivatedCoreExtensions
76 * @var array
77 */
78 protected $coreExtensionsToLoad = array();
79
80 /**
81 * Array of test/fixture extensions paths that should be loaded for a test.
82 *
83 * This property will stay empty in this abstract, so it is possible
84 * to just overwrite it in extending classes. Extensions noted here will
85 * be loaded for every test of a test case and it is not possible to change
86 * the list of loaded extensions between single tests of a test case.
87 *
88 * Given path is expected to be relative to your document root, example:
89 *
90 * array(
91 * 'typo3conf/ext/some_extension/Tests/Functional/Fixtures/Extensions/test_extension',
92 * 'typo3conf/ext/base_extension',
93 * );
94 *
95 * Extensions in this array are linked to the test instance, loaded
96 * and their ext_tables.sql will be applied.
97 *
98 * @var array
99 */
100 protected $testExtensionsToLoad = array();
101
102 /**
103 * Array of test/fixture folder or file paths that should be linked for a test.
104 *
105 * This property will stay empty in this abstract, so it is possible
106 * to just overwrite it in extending classes. Path noted here will
107 * be linked for every test of a test case and it is not possible to change
108 * the list of folders between single tests of a test case.
109 *
110 * array(
111 * 'link-source' => 'link-destination'
112 * );
113 *
114 * Given paths are expected to be relative to the test instance root.
115 * The array keys are the source paths and the array values are the destination
116 * paths, example:
117 *
118 * array(
119 * 'typo3/sysext/impext/Tests/Functional/Fixtures/Folders/fileadmin/user_upload' =>
120 * 'fileadmin/user_upload',
121 * 'typo3conf/ext/my_own_ext/Tests/Functional/Fixtures/Folders/uploads/tx_myownext' =>
122 * 'uploads/tx_myownext'
123 * );
124 *
125 * To be able to link from my_own_ext the extension path needs also to be registered in
126 * property $testExtensionsToLoad
127 *
128 * @var array
129 */
130 protected $pathsToLinkInTestInstance = array();
131
132 /**
133 * Private utility class used in setUp() and tearDown(). Do NOT use in test cases!
134 *
135 * @var \TYPO3\CMS\Core\Tests\FunctionalTestCaseBootstrapUtility
136 */
137 private $bootstrapUtility = NULL;
138
139 /**
140 * Path to TYPO3 CMS test installation for this test case
141 *
142 * @var string
143 */
144 private $instancePath;
145
146 /**
147 * Set up creates a test instance and database.
148 *
149 * This method should be called with parent::setUp() in your test cases!
150 *
151 * @return void
152 */
153 public function setUp() {
154 if (!defined('ORIGINAL_ROOT')) {
155 $this->markTestSkipped('Functional tests must be called through phpunit on CLI');
156 }
157 $this->bootstrapUtility = new FunctionalTestCaseBootstrapUtility();
158 $this->instancePath = $this->bootstrapUtility->setUp(
159 get_class($this),
160 $this->coreExtensionsToLoad,
161 $this->testExtensionsToLoad,
162 $this->pathsToLinkInTestInstance
163 );
164 }
165
166 /**
167 * Tear down destroys the instance and database.
168 *
169 * This method should be called with parent::tearDown() in your test cases!
170 *
171 * @throws Exception
172 * @return void
173 */
174 public function tearDown() {
175 if (!($this->bootstrapUtility instanceof FunctionalTestCaseBootstrapUtility)) {
176 throw new Exception(
177 'Bootstrap utility not set. Is parent::setUp() called in setUp()?',
178 1376826527
179 );
180 }
181 $this->bootstrapUtility->tearDown();
182 }
183
184 /**
185 * Get DatabaseConnection instance - $GLOBALS['TYPO3_DB']
186 *
187 * This method should be used instead of direct access to
188 * $GLOBALS['TYPO3_DB'] for easy IDE auto completion.
189 *
190 * @return \TYPO3\CMS\Core\Database\DatabaseConnection
191 */
192 protected function getDatabase() {
193 return $GLOBALS['TYPO3_DB'];
194 }
195
196 /**
197 * Initialize backend user
198 *
199 * @param int $userUid uid of the user we want to initialize. This user must exist in the fixture file
200 * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
201 * @throws Exception
202 */
203 protected function setUpBackendUserFromFixture($userUid) {
204 $this->importDataSet(ORIGINAL_ROOT . 'typo3/sysext/core/Tests/Functional/Fixtures/be_users.xml');
205 $database = $this->getDatabase();
206 $userRow = $database->exec_SELECTgetSingleRow('*', 'be_users', 'uid = ' . $userUid);
207
208 /** @var $backendUser \TYPO3\CMS\Core\Authentication\BackendUserAuthentication */
209 $backendUser = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Authentication\\BackendUserAuthentication');
210 $sessionId = $backendUser->createSessionId();
211 $_SERVER['HTTP_COOKIE'] = 'be_typo_user=' . $sessionId . '; path=/';
212 $backendUser->id = $sessionId;
213 $backendUser->sendNoCacheHeaders = FALSE;
214 $backendUser->dontSetCookie = TRUE;
215 $backendUser->createUserSession($userRow);
216
217 $GLOBALS['BE_USER'] = $backendUser;
218 $GLOBALS['BE_USER']->start();
219 if (!is_array($GLOBALS['BE_USER']->user) || !$GLOBALS['BE_USER']->user['uid']) {
220 throw new Exception(
221 'Can not initialize backend user',
222 1377095807
223 );
224 }
225 $GLOBALS['BE_USER']->backendCheckLogin();
226
227 return $backendUser;
228 }
229
230 /**
231 * Imports a data set represented as XML into the test database,
232 *
233 * @param string $path Absolute path to the XML file containing the data set to load
234 * @return void
235 * @throws Exception
236 */
237 protected function importDataSet($path) {
238 if (!is_file($path)) {
239 throw new Exception(
240 'Fixture file ' . $path . ' not found',
241 1376746261
242 );
243 }
244
245 $database = $this->getDatabase();
246
247 $xml = simplexml_load_file($path);
248 $foreignKeys = array();
249
250 /** @var $table \SimpleXMLElement */
251 foreach ($xml->children() as $table) {
252 $insertArray = array();
253
254 /** @var $column \SimpleXMLElement */
255 foreach ($table->children() as $column) {
256 $columnName = $column->getName();
257 $columnValue = NULL;
258
259 if (isset($column['ref'])) {
260 list($tableName, $elementId) = explode('#', $column['ref']);
261 $columnValue = $foreignKeys[$tableName][$elementId];
262 } elseif (isset($column['is-NULL']) && ($column['is-NULL'] === 'yes')) {
263 $columnValue = NULL;
264 } else {
265 $columnValue = (string) $table->$columnName;
266 }
267
268 $insertArray[$columnName] = $columnValue;
269 }
270
271 $tableName = $table->getName();
272 $result = $database->exec_INSERTquery($tableName, $insertArray);
273 if ($result === FALSE) {
274 throw new Exception(
275 'Error when processing fixture file: ' . $path . ' Can not insert data to table ' . $tableName,
276 1376746262
277 );
278 }
279 if (isset($table['id'])) {
280 $elementId = (string) $table['id'];
281 $foreignKeys[$tableName][$elementId] = $database->sql_insert_id();
282 }
283 }
284 }
285
286 /**
287 * @param int $pageId
288 * @param array $typoScriptFiles
289 */
290 protected function setUpFrontendRootPage($pageId, array $typoScriptFiles = array()) {
291 $pageId = (int)$pageId;
292 $page = $this->getDatabase()->exec_SELECTgetSingleRow('*', 'pages', 'uid=' . $pageId);
293
294 if (empty($page)) {
295 $this->fail('Cannot set up frontend root page "' . $pageId . '"');
296 }
297
298 $pagesFields = array(
299 'is_siteroot' => 1
300 );
301
302 $this->getDatabase()->exec_UPDATEquery('pages', 'uid=' . $pageId, $pagesFields);
303
304 $templateFields = array(
305 'pid' => $pageId,
306 'title' => '',
307 'config' => '',
308 'clear' => 3,
309 'root' => 1,
310 );
311
312 foreach ($typoScriptFiles as $typoScriptFile) {
313 $templateFields['config'] .= '<INCLUDE_TYPOSCRIPT: source="FILE:' . $typoScriptFile . '">' . LF;
314 }
315
316 $this->getDatabase()->exec_INSERTquery('sys_template', $templateFields);
317 }
318
319 /**
320 * @param int $pageId
321 * @param int $languageId
322 * @param int $backendUserId
323 * @param int $workspaceId
324 * @param bool $failOnFailure
325 * @return Response
326 */
327 protected function getFrontendResponse($pageId, $languageId = 0, $backendUserId = 0, $workspaceId = 0, $failOnFailure = TRUE) {
328 $pageId = (int)$pageId;
329 $languageId = (int)$languageId;
330
331 if (defined('PHP_BINARY')) {
332 $phpExecutable = PHP_BINARY;
333 } else {
334 $phpExecutable = rtrim(PHP_BINDIR, '/') . '/php';
335 }
336
337 $additionalParameter = '';
338
339 if (!empty($backendUserId)) {
340 $additionalParameter .= '&backendUserId=' . (int)$backendUserId;
341 }
342 if (!empty($workspaceId)) {
343 $additionalParameter .= '&workspaceId=' . (int)$workspaceId;
344 }
345
346 $arguments = array(
347 'documentRoot' => $this->instancePath,
348 'requestUrl' => 'http://localhost/?id=' . $pageId . '&L=' . $languageId . $additionalParameter,
349 );
350
351 $commandParts = array(
352 escapeshellcmd($phpExecutable),
353 escapeshellarg(ORIGINAL_ROOT . 'typo3/sysext/core/Tests/Functional/Framework/Scripts/Request.php'),
354 escapeshellarg(json_encode($arguments)),
355 );
356
357 $command = trim(implode(' ', $commandParts));
358 $response = shell_exec($command);
359 $result = json_decode($response, TRUE);
360
361 if ($result === FALSE) {
362 $this->fail('Frontend Response is empty');
363 }
364
365 if ($failOnFailure && $result['status'] === Response::STATUS_Failure) {
366 $this->fail('Frontend Response has failure:' . LF . $result['error']);
367 }
368
369 $response = new Response($result['status'], $result['content'], $result['error']);
370 return $response;
371 }
372
373 }