Skip to content

Commit e9c3bc4

Browse files
committed
[TASK] Allow defining test instance files copy for acceptance tests
Running acceptance tests with codeception on a created test instance with a real `Apache2` webserver requires to have the `.htaccess` files in place, otherwise the required rewriting to the endpoints will not work. Since TYPO3 v13 with droppend backend entrypoint this grows to a even higher requirement. TYPO3 monorepo implemented that directly within the extended `BackendEnvironment` class. To make the live for developers easier using the `typo3/testing-framework` for project or extension acceptance testing a new tooling is now added based on the direct monorepo implementation. Following `BackendEnvironment::$config[]` options are now available: * `'copyInstanceFiles' => [],` (`array<string, string[]>`) to copy the soureFile to all listed target paths. * `'copyInstanceFilesCreateTargetPath' => true,` to configure if target folders should be created when missing or throw a excetion. `BackendEnvironment` applies default files to copy based on available core extensions: * `EXT:backend` source.: 'typo3/sysext/backend/Resources/Public/Icons/favicon.ico' targets: - 'favicon.ico' * `EXT:install` source.: 'typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles/root-htaccess' targets: - '.htaccess' source.: 'typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles/fileadmin-temp-htaccess' targets: - 'fileadmin/_temp_/.htaccess' source.: 'typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles/fileadmin-temp-index.html' targets: - 'fileadmin/_temp_/index.html' source.: 'typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles/typo3temp-var-htaccess' targets: . 'typo3temp/var/.htaccess', That way, additional files could be defined and configured instead of implementing custom code in the extended class. Note that files are always provided, which does not hurt when not using `Apache2` as acceptance instance webserver. Releases: main, 8
1 parent 4d0c36f commit e9c3bc4

File tree

2 files changed

+265
-2
lines changed

2 files changed

+265
-2
lines changed

Classes/Core/Acceptance/Extension/BackendEnvironment.php

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
use Codeception\Events;
2222
use Codeception\Extension;
2323
use TYPO3\CMS\Core\Database\ConnectionPool;
24+
use TYPO3\CMS\Core\Utility\ArrayUtility;
2425
use TYPO3\CMS\Core\Utility\GeneralUtility;
26+
use TYPO3\TestingFramework\Composer\ComposerPackageManager;
2527
use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\DataSet;
2628
use TYPO3\TestingFramework\Core\Testbase;
2729

@@ -147,6 +149,19 @@ abstract class BackendEnvironment extends Extension
147149
* Example: [ __DIR__ . '/../../Fixtures/BackendEnvironment.csv' ]
148150
*/
149151
'csvDatabaseFixtures' => [],
152+
153+
/**
154+
* Copy files within created test instance.
155+
*
156+
* @var array<string, string[]>
157+
*/
158+
'copyInstanceFiles' => [],
159+
160+
/**
161+
* Should target paths for self::$config['copyInstanceFiles'] be created.
162+
* Will throw an exception if folder does not exists and set to `false`.
163+
*/
164+
'copyInstanceFilesCreateTargetPath' => true,
150165
];
151166

152167
/**
@@ -170,6 +185,11 @@ abstract class BackendEnvironment extends Extension
170185
Events::TEST_BEFORE => 'cleanupTypo3Environment',
171186
];
172187

188+
/**
189+
* @var string|null Test instance path when created.
190+
*/
191+
protected ?string $instancePath = null;
192+
173193
/**
174194
* Initialize config array, called before events.
175195
*
@@ -230,7 +250,7 @@ public function bootstrapTypo3Environment(SuiteEvent $suiteEvent)
230250
$testbase->createDirectory(ORIGINAL_ROOT . 'typo3temp/var/tests/acceptance');
231251
$testbase->createDirectory(ORIGINAL_ROOT . 'typo3temp/var/transient');
232252

233-
$instancePath = ORIGINAL_ROOT . 'typo3temp/var/tests/acceptance';
253+
$instancePath = $this->instancePath = ORIGINAL_ROOT . 'typo3temp/var/tests/acceptance';
234254
putenv('TYPO3_PATH_ROOT=' . $instancePath);
235255
putenv('TYPO3_PATH_APP=' . $instancePath);
236256
$testbase->setTypo3TestingContext();
@@ -303,6 +323,13 @@ public function bootstrapTypo3Environment(SuiteEvent $suiteEvent)
303323
// @todo: See which other possible state should be dropped here again (singletons, ...?)
304324
restore_error_handler();
305325

326+
$this->applyDefaultCopyInstanceFilesConfiguration();
327+
$copyInstanceFilesCreateTargetPaths = (bool)($this->config['copyInstanceFilesCreateTargetPath'] ?? true);
328+
$copyInstanceFiles = $this->config['copyInstanceFiles'] ?? [];
329+
if (is_array($copyInstanceFiles) && $copyInstanceFiles !== []) {
330+
$testbase->copyInstanceFiles($instancePath, $copyInstanceFiles, $copyInstanceFilesCreateTargetPaths);
331+
}
332+
306333
// Unset a closure or phpunit kicks in with a 'serialization of \Closure is not allowed'
307334
// Alternative solution:
308335
// unset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['GLOBAL']['cliKeys']['extbase']);
@@ -328,4 +355,94 @@ public function cleanupTypo3Environment()
328355
->getConnectionForTable('be_users')
329356
->update('be_users', ['uc' => null], ['uid' => 1]);
330357
}
358+
359+
private function applyDefaultCopyInstanceFilesConfiguration(): void
360+
{
361+
if ($this->hasExtension('typo3/cms-backend')) {
362+
ArrayUtility::mergeRecursiveWithOverrule(
363+
$this->config,
364+
[
365+
'copyInstanceFiles' => [
366+
// Create favicon.ico to suppress potential javascript errors in console
367+
// which are caused by calling a non html in the browser, e.g. seo sitemap xml
368+
'typo3/sysext/backend/Resources/Public/Icons/favicon.ico' => [
369+
'favicon.ico',
370+
],
371+
],
372+
]
373+
);
374+
}
375+
if ($this->hasExtension('typo3/cms-install')) {
376+
ArrayUtility::mergeRecursiveWithOverrule(
377+
$this->config,
378+
[
379+
'copyInstanceFiles' => [
380+
// Provide some files into the test instance normally added by installer
381+
'typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles/root-htaccess' => [
382+
'.htaccess',
383+
],
384+
'typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles/resources-root-htaccess' => [
385+
'fileadmin/.htaccess',
386+
],
387+
'typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles/fileadmin-temp-htaccess' => [
388+
'fileadmin/_temp_/.htaccess',
389+
],
390+
'typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles/fileadmin-temp-index.html' => [
391+
'fileadmin/_temp_/index.html',
392+
],
393+
'typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles/typo3temp-var-htaccess' => [
394+
'typo3temp/var/.htaccess',
395+
],
396+
],
397+
]
398+
);
399+
}
400+
}
401+
402+
/**
403+
* Verify if extension is available in the system and within the acceptance test instance.
404+
*/
405+
protected function hasExtension(string $extensionKeyOrComposerPackageName): bool
406+
{
407+
$instanceExtensions = $this->getInstanceExtensionKeys(
408+
$this->config['coreExtensionsToLoad'],
409+
$this->config['testExtensionsToLoad'],
410+
);
411+
$packageInfo = (new ComposerPackageManager())->getPackageInfo($extensionKeyOrComposerPackageName);
412+
if ($packageInfo === null) {
413+
return false;
414+
}
415+
return $packageInfo->getExtensionKey() !== '' && in_array($packageInfo->getExtensionKey(), $instanceExtensions, true);
416+
}
417+
418+
/**
419+
* Gather list of extension keys available within created test instance
420+
* based on `coreExtensionsToLoad` and `testExtensionToLoad` config.
421+
*/
422+
private function getInstanceExtensionKeys(
423+
array $coreExtensionsToLoad,
424+
array $testExtensionsToLoad,
425+
): array {
426+
$composerPackageManager = new ComposerPackageManager();
427+
if ($coreExtensionsToLoad === []) {
428+
// Fallback to all system extensions needed for TYPO3 acceptanceInstall tests.
429+
$coreExtensionsToLoad = $composerPackageManager->getSystemExtensionExtensionKeys();
430+
}
431+
$result = [];
432+
foreach ($coreExtensionsToLoad as $extensionKeyOrComposerPackageName) {
433+
$packageInfo = $composerPackageManager->getPackageInfo($extensionKeyOrComposerPackageName);
434+
if ($packageInfo === null || $packageInfo->getExtensionKey() === '') {
435+
continue;
436+
}
437+
$result[] = $packageInfo->getExtensionKey();
438+
}
439+
foreach ($testExtensionsToLoad as $extensionKeyOrComposerPackageName) {
440+
$packageInfo = $composerPackageManager->getPackageInfo($extensionKeyOrComposerPackageName);
441+
if ($packageInfo === null || $packageInfo->getExtensionKey() === '') {
442+
continue;
443+
}
444+
$result[] = $packageInfo->getExtensionKey();
445+
}
446+
return $result;
447+
}
331448
}

Classes/Core/Testbase.php

Lines changed: 147 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ public function setUpInstanceCoreLinks(
199199

200200
$linksToSet = [];
201201
$coreExtensions = array_unique(array_merge($defaultCoreExtensionsToLoad, $coreExtensionsToLoad));
202-
// @todo Fallback to all system extensions needed for TYPO3 acceptanceInstall tests.
202+
// Fallback to all system extensions needed for TYPO3 acceptanceInstall tests.
203203
if ($coreExtensions === []) {
204204
$coreExtensions = $this->composerPackageManager->getSystemExtensionExtensionKeys();
205205
}
@@ -269,6 +269,46 @@ public function setUpInstanceCoreLinks(
269269
}
270270
}
271271

272+
public function provideInstance(array $additionalHtaccessFiles = []): void
273+
{
274+
$copyFiles = [
275+
// Create favicon.ico to suppress potential javascript errors in console
276+
// which are caused by calling a non html in the browser, e.g. seo sitemap xml
277+
'typo3/sysext/backend/Resources/Public/Icons/favicon.ico' => [
278+
'favicon.ico',
279+
],
280+
// Provide some files into the test instance normally added by installer
281+
'typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles/root-htaccess' => [
282+
'.htaccess',
283+
],
284+
'typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles/resources-root-htaccess' => [
285+
'fileadmin/.htaccess',
286+
],
287+
'typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles/fileadmin-temp-htaccess' => [
288+
'fileadmin/_temp_/.htaccess',
289+
],
290+
'typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles/fileadmin-temp-index.html' => [
291+
'fileadmin/_temp_/index.html',
292+
],
293+
'typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles/typo3temp-var-htaccess' => [
294+
'typo3temp/var/.htaccess',
295+
],
296+
];
297+
foreach ($copyFiles as $sourceFile => $targetFiles) {
298+
foreach ($targetFiles as $targetFile) {
299+
$this->createDirectory(dirname(ltrim($targetFile, '/')));
300+
$sourceFile = ltrim($sourceFile, '/');
301+
$targetFile = ltrim($targetFile, '/');
302+
if (!@copy($sourceFile, $targetFile)) {
303+
throw new \RuntimeException(
304+
sprintf('Could not copy "%s" to "%s".', $sourceFile, $targetFile),
305+
1733391799,
306+
);
307+
}
308+
}
309+
}
310+
}
311+
272312
/**
273313
* Link test extensions to the typo3conf/ext folder of the instance.
274314
* For functional and acceptance tests.
@@ -978,4 +1018,110 @@ protected function exitWithMessage(string $message): never
9781018
echo $message . chr(10);
9791019
exit(1);
9801020
}
1021+
1022+
/**
1023+
* Copy files within the test instance path `$instancePath`.
1024+
*
1025+
* @param string $instancePath The test instance path.
1026+
* @param array<string, string[]> $files
1027+
* @throws Exception
1028+
*/
1029+
public function copyInstanceFiles(string $instancePath, array $files, bool $createTargetFolder = true): void
1030+
{
1031+
if ($files === []) {
1032+
return;
1033+
}
1034+
foreach ($files as $sourceFile => $targetFiles) {
1035+
foreach ($targetFiles as $targetFile) {
1036+
$this->copyInstanceFile($instancePath, $sourceFile, $targetFile, $createTargetFolder);
1037+
}
1038+
}
1039+
}
1040+
1041+
/**
1042+
* Copy one file within the test instance with the option to create the target path.
1043+
*
1044+
* @param string $instancePath The test instance path.
1045+
* @param string $sourceFile Relative source file path within test instance.
1046+
* @param string $targetFile Target file path within test instance.
1047+
* @param bool $createTargetFolder True to create target folder it does not exists, otherwise exception is thrown.
1048+
* @throws Exception
1049+
*/
1050+
public function copyInstanceFile(string $instancePath, string $sourceFile, string $targetFile, bool $createTargetFolder = true): void
1051+
{
1052+
if (str_starts_with($sourceFile, '/')) {
1053+
throw new \RuntimeException(
1054+
sprintf(
1055+
'Source "%s" must be relative from test instance path and must not start with "/".',
1056+
$sourceFile,
1057+
),
1058+
1733392183,
1059+
);
1060+
}
1061+
if (str_starts_with($targetFile, '/')) {
1062+
throw new \RuntimeException(
1063+
sprintf(
1064+
'Target "%s" must be relative from test instance path and must not start with "/".',
1065+
$targetFile,
1066+
),
1067+
1733392258,
1068+
);
1069+
}
1070+
if (trim($sourceFile, '/') === '') {
1071+
throw new \RuntimeException(
1072+
sprintf(
1073+
'Source "%s" must not be empty or "/".',
1074+
$sourceFile,
1075+
),
1076+
1733392321,
1077+
);
1078+
}
1079+
if (trim($targetFile, '/') === '') {
1080+
throw new \RuntimeException(
1081+
sprintf(
1082+
'Target "%s" must not be empty or "/".',
1083+
$targetFile,
1084+
),
1085+
1733392321,
1086+
);
1087+
}
1088+
$instancePath = rtrim($instancePath, '/');
1089+
$sourceFileFull = $instancePath . '/' . $sourceFile;
1090+
$targetFileFull = $instancePath . '/' . $targetFile;
1091+
$targetPath = rtrim(dirname($targetFile), '/');
1092+
$targetPathFull = $instancePath . '/' . $targetPath;
1093+
if (!is_dir($targetPathFull)) {
1094+
if (!$createTargetFolder) {
1095+
throw new \RuntimeException(
1096+
sprintf(
1097+
'Target instance path "%s" does not exists and should not be created, but is required.',
1098+
$targetPath,
1099+
),
1100+
1733392917,
1101+
);
1102+
}
1103+
$this->createDirectory($targetPathFull);
1104+
}
1105+
if (!file_exists($sourceFileFull)) {
1106+
throw new \RuntimeException(
1107+
sprintf(
1108+
'Source file "%s" does not exists within test instance "%s" and could not be copied to "%s".',
1109+
$sourceFile,
1110+
$instancePath,
1111+
$targetFile,
1112+
),
1113+
1733393186,
1114+
);
1115+
}
1116+
if (!@copy($sourceFileFull, $targetFileFull)) {
1117+
throw new \RuntimeException(
1118+
sprintf(
1119+
'Could not copy "%s" to "%s".',
1120+
$sourceFile,
1121+
$targetFile,
1122+
),
1123+
1733391799,
1124+
);
1125+
}
1126+
}
9811127
}

0 commit comments

Comments
 (0)