diff --git a/Classes/Composer/ComposerPackageManager.php b/Classes/Composer/ComposerPackageManager.php index 212cf408..65930fa1 100644 --- a/Classes/Composer/ComposerPackageManager.php +++ b/Classes/Composer/ComposerPackageManager.php @@ -101,7 +101,7 @@ public function getPackageInfoWithFallback(string $nameOrExtensionKeyOrPath): ?P */ public function getPackageInfo(string $name): ?PackageInfo { - return self::$packages[$this->resolvePackageName($name)] ?? null; + return self::$packages[$name] ?? self::$packages[$this->resolvePackageName($name)] ?? null; } /** @@ -655,7 +655,6 @@ private function removePrefixPaths(string $path): string rtrim($this->rootPackage()->getVendorDir(), '/') . '/', rtrim($this->rootPackage()->getWebDir(), '/') . '/', rtrim($this->getRootPath(), '/') . '/', - basename($this->getRootPath()) . '/', ]; foreach ($removePaths as $removePath) { if (str_starts_with($path, $removePath)) { diff --git a/Classes/Core/Testbase.php b/Classes/Core/Testbase.php index 60f7fd53..010b61a7 100644 --- a/Classes/Core/Testbase.php +++ b/Classes/Core/Testbase.php @@ -628,7 +628,7 @@ public function setUpPackageStates( // Activate test extensions that have been symlinked before foreach ($testExtensionPaths as $extensionPath) { - if ($packageInfo = $this->composerPackageManager->getPackageInfo($extensionPath)) { + if ($packageInfo = $this->composerPackageManager->getPackageInfoWithFallback($extensionPath)) { $extensionName = $packageInfo->getExtensionKey(); } else { $extensionName = basename($extensionPath); diff --git a/Tests/Unit/Composer/ComposerPackageManagerTest.php b/Tests/Unit/Composer/ComposerPackageManagerTest.php index 8db5463e..2b3b5b3b 100644 --- a/Tests/Unit/Composer/ComposerPackageManagerTest.php +++ b/Tests/Unit/Composer/ComposerPackageManagerTest.php @@ -197,6 +197,27 @@ public function coreExtensionCanBeResolvedWithRelativeLegacyPathPrefix(): void self::assertTrue($packageInfo->isSystemExtension()); } + /** + * A composer package name whose vendor segment matches one of the path + * prefixes removed for classic mode notation must not be rewritten before + * looking it up. The fixture extension used here is named + * `testing-framework/extension-absolute`, and `testing-framework` is the + * project root folder name of a default checkout. + */ + #[Test] + public function knownPackageNameIsNotModifiedBeforeLookup(): void + { + $subject = new ComposerPackageManager(); + // Register the fixture extension, it is not required by the root composer.json. + $subject->getPackageInfoWithFallback(__DIR__ . '/Fixtures/Extensions/ext_absolute'); + + $packageInfo = $subject->getPackageInfo('testing-framework/extension-absolute'); + + self::assertInstanceOf(PackageInfo::class, $packageInfo); + self::assertSame('testing-framework/extension-absolute', $packageInfo->getName()); + self::assertSame('absolute_real', $packageInfo->getExtensionKey()); + } + #[Test] public function extensionWithoutJsonCanBeResolvedByAbsolutePath(): void { @@ -471,6 +492,24 @@ public function prepareResolvePackageNameReturnsExpectedValues(string $name, str self::assertSame($expected, $resolved, sprintf('"%s" resolved to "%s"', $name, $expected)); } + /** + * The project root folder name must not be removed as path prefix. It is a + * relative single segment prefix, and it therefore also matches the vendor + * segment of a composer package name, for example `typo3/cms-core` for a + * project checked out into a folder named `typo3`. + */ + #[Test] + public function prepareResolvePackageNameKeepsProjectRootFolderNamePrefix(): void + { + $composerPackageManager = new ComposerPackageManager(); + $name = basename($composerPackageManager->getRootPath()) . '/some-package'; + + $prepareResolvePackageNameReflectionMethod = new \ReflectionMethod($composerPackageManager, 'prepareResolvePackageName'); + $resolved = $prepareResolvePackageNameReflectionMethod->invoke($composerPackageManager, $name); + + self::assertSame($name, $resolved, sprintf('"%s" resolved to "%s"', $name, $resolved)); + } + public static function resolvePackageNameReturnsExpectedPackageNameDataProvider(): \Generator { yield 'Composer package name returns unchanged (not checked for existence)' => [ diff --git a/Tests/Unit/Core/Fixtures/Extensions/ext_folder_name/composer.json b/Tests/Unit/Core/Fixtures/Extensions/ext_folder_name/composer.json new file mode 100644 index 00000000..b372cab9 --- /dev/null +++ b/Tests/Unit/Core/Fixtures/Extensions/ext_folder_name/composer.json @@ -0,0 +1,20 @@ +{ + "name": "typo3/testing-framework-diverging-extension-key", + "description": "Test extension with a folder name differing from its extension key", + "type": "typo3-cms-extension", + "license": [ + "GPL-2.0-or-later" + ], + "require": { + "php": "*" + }, + "extra": { + "typo3/cms": { + "extension-key": "diverging_extension_key", + "version": "1.0.0", + "Package": { + "providesPackages": {} + } + } + } +} diff --git a/Tests/Unit/Core/TestbaseTest.php b/Tests/Unit/Core/TestbaseTest.php new file mode 100644 index 00000000..9d3419c8 --- /dev/null +++ b/Tests/Unit/Core/TestbaseTest.php @@ -0,0 +1,88 @@ +instancePath !== '' && is_dir($this->instancePath)) { + $this->removeDirectory($this->instancePath); + } + $this->instancePath = ''; + parent::tearDown(); + } + + /** + * Testbase::linkTestExtensionsToInstance() symlinks a test extension using the + * extension key determined by ComposerPackageManager::getPackageInfoWithFallback(). + * Testbase::setUpPackageStates() must determine the very same name, otherwise the + * written PackageStates.php points to a non-existing package path. + */ + #[Test] + public function setUpPackageStatesUsesExtensionKeyOfTestExtensionAndNotItsFolderName(): void + { + $extensionPath = __DIR__ . '/Fixtures/Extensions/ext_folder_name'; + // Folder name and extension key of the fixture extension differ on purpose. + self::assertSame('ext_folder_name', basename($extensionPath)); + + $this->instancePath = $this->createTestInstance(); + // Symlink the extension the way linkTestExtensionsToInstance() does it. + symlink($extensionPath, $this->instancePath . '/typo3conf/ext/diverging_extension_key'); + + $subject = new Testbase(); + $subject->setUpPackageStates($this->instancePath, [], [], [$extensionPath], []); + + $packageStates = require $this->instancePath . '/typo3conf/PackageStates.php'; + + self::assertArrayHasKey('diverging_extension_key', $packageStates['packages']); + self::assertArrayNotHasKey('ext_folder_name', $packageStates['packages']); + self::assertSame( + 'typo3conf/ext/diverging_extension_key/', + $packageStates['packages']['diverging_extension_key']['packagePath'] + ); + } + + private function createTestInstance(): string + { + $instancePath = sys_get_temp_dir() . '/testbase-' . bin2hex(random_bytes(8)); + mkdir($instancePath . '/typo3conf/ext', 0775, true); + return $instancePath; + } + + private function removeDirectory(string $path): void + { + foreach ((array)scandir($path) as $entry) { + if ($entry === '.' || $entry === '..') { + continue; + } + $entryPath = $path . '/' . $entry; + if (is_link($entryPath) || is_file($entryPath)) { + unlink($entryPath); + continue; + } + $this->removeDirectory($entryPath); + } + rmdir($path); + } +}