Skip to content

Commit 7396ef0

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 5dba0f3 commit 7396ef0

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
@@ -22,7 +22,9 @@
2222
use Codeception\Extension;
2323
use TYPO3\CMS\Core\Database\ConnectionPool;
2424
use TYPO3\CMS\Core\Information\Typo3Version;
25+
use TYPO3\CMS\Core\Utility\ArrayUtility;
2526
use TYPO3\CMS\Core\Utility\GeneralUtility;
27+
use TYPO3\TestingFramework\Composer\ComposerPackageManager;
2628
use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\DataSet;
2729
use TYPO3\TestingFramework\Core\Testbase;
2830

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

153168
/**
@@ -171,6 +186,11 @@ abstract class BackendEnvironment extends Extension
171186
Events::TEST_BEFORE => 'cleanupTypo3Environment',
172187
];
173188

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

234-
$instancePath = ORIGINAL_ROOT . 'typo3temp/var/tests/acceptance';
254+
$instancePath = $this->instancePath = ORIGINAL_ROOT . 'typo3temp/var/tests/acceptance';
235255
putenv('TYPO3_PATH_ROOT=' . $instancePath);
236256
putenv('TYPO3_PATH_APP=' . $instancePath);
237257
$testbase->setTypo3TestingContext();
@@ -312,6 +332,13 @@ public function bootstrapTypo3Environment(SuiteEvent $suiteEvent)
312332
// @todo: See which other possible state should be dropped here again (singletons, ...?)
313333
restore_error_handler();
314334

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

Classes/Core/Testbase.php

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

208208
$linksToSet = [];
209209
$coreExtensions = array_unique(array_merge($defaultCoreExtensionsToLoad, $coreExtensionsToLoad));
210-
// @todo Fallback to all system extensions needed for TYPO3 acceptanceInstall tests.
210+
// Fallback to all system extensions needed for TYPO3 acceptanceInstall tests.
211211
if ($coreExtensions === []) {
212212
$coreExtensions = $this->composerPackageManager->getSystemExtensionExtensionKeys();
213213
}
@@ -304,6 +304,46 @@ public function setUpInstanceCoreLinks(
304304
}
305305
}
306306

307+
public function provideInstance(array $additionalHtaccessFiles = []): void
308+
{
309+
$copyFiles = [
310+
// Create favicon.ico to suppress potential javascript errors in console
311+
// which are caused by calling a non html in the browser, e.g. seo sitemap xml
312+
'typo3/sysext/backend/Resources/Public/Icons/favicon.ico' => [
313+
'favicon.ico',
314+
],
315+
// Provide some files into the test instance normally added by installer
316+
'typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles/root-htaccess' => [
317+
'.htaccess',
318+
],
319+
'typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles/resources-root-htaccess' => [
320+
'fileadmin/.htaccess',
321+
],
322+
'typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles/fileadmin-temp-htaccess' => [
323+
'fileadmin/_temp_/.htaccess',
324+
],
325+
'typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles/fileadmin-temp-index.html' => [
326+
'fileadmin/_temp_/index.html',
327+
],
328+
'typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles/typo3temp-var-htaccess' => [
329+
'typo3temp/var/.htaccess',
330+
],
331+
];
332+
foreach ($copyFiles as $sourceFile => $targetFiles) {
333+
foreach ($targetFiles as $targetFile) {
334+
$this->createDirectory(dirname(ltrim($targetFile, '/')));
335+
$sourceFile = ltrim($sourceFile, '/');
336+
$targetFile = ltrim($targetFile, '/');
337+
if (!@copy($sourceFile, $targetFile)) {
338+
throw new \RuntimeException(
339+
sprintf('Could not copy "%s" to "%s".', $sourceFile, $targetFile),
340+
1733391799,
341+
);
342+
}
343+
}
344+
}
345+
}
346+
307347
/**
308348
* Link test extensions to the typo3conf/ext folder of the instance.
309349
* For functional and acceptance tests.
@@ -1063,4 +1103,110 @@ private static function isSQLite(Connection|DoctrineConnection $connection): boo
10631103
{
10641104
return $connection->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\SQLitePlatform;
10651105
}
1106+
1107+
/**
1108+
* Copy files within the test instance path `$instancePath`.
1109+
*
1110+
* @param string $instancePath The test instance path.
1111+
* @param array<string, string[]> $files
1112+
* @throws Exception
1113+
*/
1114+
public function copyInstanceFiles(string $instancePath, array $files, bool $createTargetFolder = true): void
1115+
{
1116+
if ($files === []) {
1117+
return;
1118+
}
1119+
foreach ($files as $sourceFile => $targetFiles) {
1120+
foreach ($targetFiles as $targetFile) {
1121+
$this->copyInstanceFile($instancePath, $sourceFile, $targetFile, $createTargetFolder);
1122+
}
1123+
}
1124+
}
1125+
1126+
/**
1127+
* Copy one file within the test instance with the option to create the target path.
1128+
*
1129+
* @param string $instancePath The test instance path.
1130+
* @param string $sourceFile Relative source file path within test instance.
1131+
* @param string $targetFile Target file path within test instance.
1132+
* @param bool $createTargetFolder True to create target folder it does not exists, otherwise exception is thrown.
1133+
* @throws Exception
1134+
*/
1135+
public function copyInstanceFile(string $instancePath, string $sourceFile, string $targetFile, bool $createTargetFolder = true): void
1136+
{
1137+
if (str_starts_with($sourceFile, '/')) {
1138+
throw new \RuntimeException(
1139+
sprintf(
1140+
'Source "%s" must be relative from test instance path and must not start with "/".',
1141+
$sourceFile,
1142+
),
1143+
1733392183,
1144+
);
1145+
}
1146+
if (str_starts_with($targetFile, '/')) {
1147+
throw new \RuntimeException(
1148+
sprintf(
1149+
'Target "%s" must be relative from test instance path and must not start with "/".',
1150+
$targetFile,
1151+
),
1152+
1733392258,
1153+
);
1154+
}
1155+
if (trim($sourceFile, '/') === '') {
1156+
throw new \RuntimeException(
1157+
sprintf(
1158+
'Source "%s" must not be empty or "/".',
1159+
$sourceFile,
1160+
),
1161+
1733392321,
1162+
);
1163+
}
1164+
if (trim($targetFile, '/') === '') {
1165+
throw new \RuntimeException(
1166+
sprintf(
1167+
'Target "%s" must not be empty or "/".',
1168+
$targetFile,
1169+
),
1170+
1733392321,
1171+
);
1172+
}
1173+
$instancePath = rtrim($instancePath, '/');
1174+
$sourceFileFull = $instancePath . '/' . $sourceFile;
1175+
$targetFileFull = $instancePath . '/' . $targetFile;
1176+
$targetPath = rtrim(dirname($targetFile), '/');
1177+
$targetPathFull = $instancePath . '/' . $targetPath;
1178+
if (!is_dir($targetPathFull)) {
1179+
if (!$createTargetFolder) {
1180+
throw new \RuntimeException(
1181+
sprintf(
1182+
'Target instance path "%s" does not exists and should not be created, but is required.',
1183+
$targetPath,
1184+
),
1185+
1733392917,
1186+
);
1187+
}
1188+
$this->createDirectory($targetPathFull);
1189+
}
1190+
if (!file_exists($sourceFileFull)) {
1191+
throw new \RuntimeException(
1192+
sprintf(
1193+
'Source file "%s" does not exists within test instance "%s" and could not be copied to "%s".',
1194+
$sourceFile,
1195+
$instancePath,
1196+
$targetFile,
1197+
),
1198+
1733393186,
1199+
);
1200+
}
1201+
if (!@copy($sourceFileFull, $targetFileFull)) {
1202+
throw new \RuntimeException(
1203+
sprintf(
1204+
'Could not copy "%s" to "%s".',
1205+
$sourceFile,
1206+
$targetFile,
1207+
),
1208+
1733391799,
1209+
);
1210+
}
1211+
}
10661212
}

0 commit comments

Comments
 (0)