From e9c3bc4df912a0fd5d1c69cc1cab0cca984b0207 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20B=C3=BCrk?= Date: Thu, 5 Dec 2024 12:15:17 +0100 Subject: [PATCH] [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`) 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 --- .../Extension/BackendEnvironment.php | 119 +++++++++++++- Classes/Core/Testbase.php | 148 +++++++++++++++++- 2 files changed, 265 insertions(+), 2 deletions(-) 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, + ); + } + } }