diff --git a/Classes/Core/Acceptance/Extension/BackendEnvironment.php b/Classes/Core/Acceptance/Extension/BackendEnvironment.php index 758e936e..2c41795f 100644 --- a/Classes/Core/Acceptance/Extension/BackendEnvironment.php +++ b/Classes/Core/Acceptance/Extension/BackendEnvironment.php @@ -21,7 +21,9 @@ use Codeception\Events; use Codeception\Extension; use TYPO3\CMS\Core\Database\ConnectionPool; +use TYPO3\CMS\Core\Utility\ArrayUtility; use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\TestingFramework\Composer\ComposerPackageManager; use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\DataSet; use TYPO3\TestingFramework\Core\Testbase; @@ -147,6 +149,19 @@ abstract class BackendEnvironment extends Extension * Example: [ __DIR__ . '/../../Fixtures/BackendEnvironment.csv' ] */ 'csvDatabaseFixtures' => [], + + /** + * Copy files within created test instance. + * + * @var array + */ + 'copyInstanceFiles' => [], + + /** + * Should target paths for self::$config['copyInstanceFiles'] be created. + * Will throw an exception if folder does not exists and set to `false`. + */ + 'copyInstanceFilesCreateTargetPath' => true, ]; /** @@ -170,6 +185,11 @@ abstract class BackendEnvironment extends Extension Events::TEST_BEFORE => 'cleanupTypo3Environment', ]; + /** + * @var string|null Test instance path when created. + */ + protected ?string $instancePath = null; + /** * Initialize config array, called before events. * @@ -230,7 +250,7 @@ public function bootstrapTypo3Environment(SuiteEvent $suiteEvent) $testbase->createDirectory(ORIGINAL_ROOT . 'typo3temp/var/tests/acceptance'); $testbase->createDirectory(ORIGINAL_ROOT . 'typo3temp/var/transient'); - $instancePath = ORIGINAL_ROOT . 'typo3temp/var/tests/acceptance'; + $instancePath = $this->instancePath = ORIGINAL_ROOT . 'typo3temp/var/tests/acceptance'; putenv('TYPO3_PATH_ROOT=' . $instancePath); putenv('TYPO3_PATH_APP=' . $instancePath); $testbase->setTypo3TestingContext(); @@ -303,6 +323,13 @@ public function bootstrapTypo3Environment(SuiteEvent $suiteEvent) // @todo: See which other possible state should be dropped here again (singletons, ...?) restore_error_handler(); + $this->applyDefaultCopyInstanceFilesConfiguration(); + $copyInstanceFilesCreateTargetPaths = (bool)($this->config['copyInstanceFilesCreateTargetPath'] ?? true); + $copyInstanceFiles = $this->config['copyInstanceFiles'] ?? []; + if (is_array($copyInstanceFiles) && $copyInstanceFiles !== []) { + $testbase->copyInstanceFiles($instancePath, $copyInstanceFiles, $copyInstanceFilesCreateTargetPaths); + } + // Unset a closure or phpunit kicks in with a 'serialization of \Closure is not allowed' // Alternative solution: // unset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['GLOBAL']['cliKeys']['extbase']); @@ -328,4 +355,94 @@ public function cleanupTypo3Environment() ->getConnectionForTable('be_users') ->update('be_users', ['uc' => null], ['uid' => 1]); } + + private function applyDefaultCopyInstanceFilesConfiguration(): void + { + if ($this->hasExtension('typo3/cms-backend')) { + ArrayUtility::mergeRecursiveWithOverrule( + $this->config, + [ + 'copyInstanceFiles' => [ + // Create favicon.ico to suppress potential javascript errors in console + // which are caused by calling a non html in the browser, e.g. seo sitemap xml + 'typo3/sysext/backend/Resources/Public/Icons/favicon.ico' => [ + 'favicon.ico', + ], + ], + ] + ); + } + if ($this->hasExtension('typo3/cms-install')) { + ArrayUtility::mergeRecursiveWithOverrule( + $this->config, + [ + 'copyInstanceFiles' => [ + // Provide some files into the test instance normally added by installer + 'typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles/root-htaccess' => [ + '.htaccess', + ], + 'typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles/resources-root-htaccess' => [ + 'fileadmin/.htaccess', + ], + 'typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles/fileadmin-temp-htaccess' => [ + 'fileadmin/_temp_/.htaccess', + ], + 'typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles/fileadmin-temp-index.html' => [ + 'fileadmin/_temp_/index.html', + ], + 'typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles/typo3temp-var-htaccess' => [ + 'typo3temp/var/.htaccess', + ], + ], + ] + ); + } + } + + /** + * Verify if extension is available in the system and within the acceptance test instance. + */ + protected function hasExtension(string $extensionKeyOrComposerPackageName): bool + { + $instanceExtensions = $this->getInstanceExtensionKeys( + $this->config['coreExtensionsToLoad'], + $this->config['testExtensionsToLoad'], + ); + $packageInfo = (new ComposerPackageManager())->getPackageInfo($extensionKeyOrComposerPackageName); + if ($packageInfo === null) { + return false; + } + return $packageInfo->getExtensionKey() !== '' && in_array($packageInfo->getExtensionKey(), $instanceExtensions, true); + } + + /** + * Gather list of extension keys available within created test instance + * based on `coreExtensionsToLoad` and `testExtensionToLoad` config. + */ + private function getInstanceExtensionKeys( + array $coreExtensionsToLoad, + array $testExtensionsToLoad, + ): array { + $composerPackageManager = new ComposerPackageManager(); + if ($coreExtensionsToLoad === []) { + // Fallback to all system extensions needed for TYPO3 acceptanceInstall tests. + $coreExtensionsToLoad = $composerPackageManager->getSystemExtensionExtensionKeys(); + } + $result = []; + foreach ($coreExtensionsToLoad as $extensionKeyOrComposerPackageName) { + $packageInfo = $composerPackageManager->getPackageInfo($extensionKeyOrComposerPackageName); + if ($packageInfo === null || $packageInfo->getExtensionKey() === '') { + continue; + } + $result[] = $packageInfo->getExtensionKey(); + } + foreach ($testExtensionsToLoad as $extensionKeyOrComposerPackageName) { + $packageInfo = $composerPackageManager->getPackageInfo($extensionKeyOrComposerPackageName); + if ($packageInfo === null || $packageInfo->getExtensionKey() === '') { + continue; + } + $result[] = $packageInfo->getExtensionKey(); + } + return $result; + } } diff --git a/Classes/Core/Testbase.php b/Classes/Core/Testbase.php index eafdb12f..839af1d8 100644 --- a/Classes/Core/Testbase.php +++ b/Classes/Core/Testbase.php @@ -199,7 +199,7 @@ public function setUpInstanceCoreLinks( $linksToSet = []; $coreExtensions = array_unique(array_merge($defaultCoreExtensionsToLoad, $coreExtensionsToLoad)); - // @todo Fallback to all system extensions needed for TYPO3 acceptanceInstall tests. + // Fallback to all system extensions needed for TYPO3 acceptanceInstall tests. if ($coreExtensions === []) { $coreExtensions = $this->composerPackageManager->getSystemExtensionExtensionKeys(); } @@ -269,6 +269,46 @@ public function setUpInstanceCoreLinks( } } + public function provideInstance(array $additionalHtaccessFiles = []): void + { + $copyFiles = [ + // Create favicon.ico to suppress potential javascript errors in console + // which are caused by calling a non html in the browser, e.g. seo sitemap xml + 'typo3/sysext/backend/Resources/Public/Icons/favicon.ico' => [ + 'favicon.ico', + ], + // Provide some files into the test instance normally added by installer + 'typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles/root-htaccess' => [ + '.htaccess', + ], + 'typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles/resources-root-htaccess' => [ + 'fileadmin/.htaccess', + ], + 'typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles/fileadmin-temp-htaccess' => [ + 'fileadmin/_temp_/.htaccess', + ], + 'typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles/fileadmin-temp-index.html' => [ + 'fileadmin/_temp_/index.html', + ], + 'typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles/typo3temp-var-htaccess' => [ + 'typo3temp/var/.htaccess', + ], + ]; + foreach ($copyFiles as $sourceFile => $targetFiles) { + foreach ($targetFiles as $targetFile) { + $this->createDirectory(dirname(ltrim($targetFile, '/'))); + $sourceFile = ltrim($sourceFile, '/'); + $targetFile = ltrim($targetFile, '/'); + if (!@copy($sourceFile, $targetFile)) { + throw new \RuntimeException( + sprintf('Could not copy "%s" to "%s".', $sourceFile, $targetFile), + 1733391799, + ); + } + } + } + } + /** * Link test extensions to the typo3conf/ext folder of the instance. * For functional and acceptance tests. @@ -978,4 +1018,110 @@ protected function exitWithMessage(string $message): never echo $message . chr(10); exit(1); } + + /** + * Copy files within the test instance path `$instancePath`. + * + * @param string $instancePath The test instance path. + * @param array $files + * @throws Exception + */ + public function copyInstanceFiles(string $instancePath, array $files, bool $createTargetFolder = true): void + { + if ($files === []) { + return; + } + foreach ($files as $sourceFile => $targetFiles) { + foreach ($targetFiles as $targetFile) { + $this->copyInstanceFile($instancePath, $sourceFile, $targetFile, $createTargetFolder); + } + } + } + + /** + * Copy one file within the test instance with the option to create the target path. + * + * @param string $instancePath The test instance path. + * @param string $sourceFile Relative source file path within test instance. + * @param string $targetFile Target file path within test instance. + * @param bool $createTargetFolder True to create target folder it does not exists, otherwise exception is thrown. + * @throws Exception + */ + public function copyInstanceFile(string $instancePath, string $sourceFile, string $targetFile, bool $createTargetFolder = true): void + { + if (str_starts_with($sourceFile, '/')) { + throw new \RuntimeException( + sprintf( + 'Source "%s" must be relative from test instance path and must not start with "/".', + $sourceFile, + ), + 1733392183, + ); + } + if (str_starts_with($targetFile, '/')) { + throw new \RuntimeException( + sprintf( + 'Target "%s" must be relative from test instance path and must not start with "/".', + $targetFile, + ), + 1733392258, + ); + } + if (trim($sourceFile, '/') === '') { + throw new \RuntimeException( + sprintf( + 'Source "%s" must not be empty or "/".', + $sourceFile, + ), + 1733392321, + ); + } + if (trim($targetFile, '/') === '') { + throw new \RuntimeException( + sprintf( + 'Target "%s" must not be empty or "/".', + $targetFile, + ), + 1733392321, + ); + } + $instancePath = rtrim($instancePath, '/'); + $sourceFileFull = $instancePath . '/' . $sourceFile; + $targetFileFull = $instancePath . '/' . $targetFile; + $targetPath = rtrim(dirname($targetFile), '/'); + $targetPathFull = $instancePath . '/' . $targetPath; + if (!is_dir($targetPathFull)) { + if (!$createTargetFolder) { + throw new \RuntimeException( + sprintf( + 'Target instance path "%s" does not exists and should not be created, but is required.', + $targetPath, + ), + 1733392917, + ); + } + $this->createDirectory($targetPathFull); + } + if (!file_exists($sourceFileFull)) { + throw new \RuntimeException( + sprintf( + 'Source file "%s" does not exists within test instance "%s" and could not be copied to "%s".', + $sourceFile, + $instancePath, + $targetFile, + ), + 1733393186, + ); + } + if (!@copy($sourceFileFull, $targetFileFull)) { + throw new \RuntimeException( + sprintf( + 'Could not copy "%s" to "%s".', + $sourceFile, + $targetFile, + ), + 1733391799, + ); + } + } }