diff --git a/Classes/Composer/ComposerPackageManager.php b/Classes/Composer/ComposerPackageManager.php index deadac19..7f0db5b5 100644 --- a/Classes/Composer/ComposerPackageManager.php +++ b/Classes/Composer/ComposerPackageManager.php @@ -18,8 +18,22 @@ */ use Composer\InstalledVersions; +use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; /** + * `typo3/testing-framework` internal composer package manager, used to gather source + * information of extensions already loaded by the root composer installation with + * the additional ability to register test fixture packages and extensions during + * runtime to create {@see FunctionalTestCase} test instances and provide symlinks + * of extensions into the classic mode test instance or retrieve files from a composer + * package or extension unrelated where they are placed on the filesystem. + * + * - {@see Testbase::setUpInstanceCoreLinks()} + * - {@see Testbase::linkTestExtensionsToInstance()} + * - {@see Testbase::linkFrameworkExtensionsToInstance()} + * - {@see Testbase::setUpLocalConfiguration()} + * - {@see Testbase::setUpPackageStates()} + * * @internal This class is for testing-framework internal processing and not part of public testing API. */ final class ComposerPackageManager @@ -65,25 +79,29 @@ public function __construct() $this->build(); } - public function getPackageInfoWithFallback(string $name): ?PackageInfo + /** + * Get composer package information {@see PackageInfo} for `$nameOrExtensionKeyOrPath`. + */ + public function getPackageInfoWithFallback(string $nameOrExtensionKeyOrPath): ?PackageInfo { - if ($packageInfo = $this->getPackageInfo($name)) { + if ($packageInfo = $this->getPackageInfo($nameOrExtensionKeyOrPath)) { return $packageInfo; } - if ($packageInfo = $this->getPackageFromPath($name)) { + if ($packageInfo = $this->getPackageFromPath($nameOrExtensionKeyOrPath)) { return $packageInfo; } - if ($packageInfo = $this->getPackageFromPathFallback($name)) { + if ($packageInfo = $this->getPackageFromPathFallback($nameOrExtensionKeyOrPath)) { return $packageInfo; } - return null; } + /** + * Get {@see PackageInfo} for package name or extension key `$name`. + */ public function getPackageInfo(string $name): ?PackageInfo { - $name = $this->resolvePackageName($name); - return self::$packages[$name] ?? null; + return self::$packages[$this->resolvePackageName($name)] ?? null; } /** @@ -403,9 +421,23 @@ private function getExtEmConf(string $path): ?array return null; } + /** + * Returns resolved composer package name when $name is a known extension key + * for a known package, otherwise return $name unchanged. + * + * Used to determine the package name to look up as composer package within {@see self::$packages} + * + * Supports also relative classic mode notation: + * + * - typo3/sysext/backend + * - typo3conf/ext/my_ext_key + * + * {@see self::prepareResolvePackageName()} for details for normalisation. + */ private function resolvePackageName(string $name): string { - return self::$extensionKeyToPackageNameMap[$this->normalizeExtensionKey(basename($name))] ?? $name; + $name = $this->prepareResolvePackageName($name); + return self::$extensionKeyToPackageNameMap[$name] ?? $name; } /** @@ -640,4 +672,47 @@ private function getFirstPathElement(string $path): string } return explode('/', $path)[0] ?? ''; } + + /** + * Extension can be specified with their composer name, extension key or with classic mode relative path + * prefixes (`typo3/sysext/` or `typo3conf/ext/`) for functional tests to + * configure which extension should be provided in the test instance. + * + * This method normalizes a handed over name by removing the specified extra information, so it can be + * used to resolve it either as direct package name or as extension name. + * + * Handed over value also removes known environment prefix paths, like the full path to the root (project rook), + * vendor folder or web folder using {@see self::removePrefixPaths()} which is safe, as this method is and most + * only be used for {@see self::resolvePackageName()} to find a composer package in {@see self::$packages}, after + * mapping extension-key to composer package name. + * + * Example for processed changes: + * -----------------------------_ + * + * - typo3/sysext/backend => backend + * - typo3conf/ext/my_ext_key => my_ext_key + * + * Example not processed values: + * ----------------------------- + * + * valid names + * - typo3/cms-core => typo3/cms-core + * - my-vendor/my-package-name => my-vendor/my-package-name + * - my-package-name-without-vendor => my-package-name-without-vendor + */ + private function prepareResolvePackageName($name): string + { + $name = trim($this->removePrefixPaths($name), '/'); + $relativePrefixPaths = [ + 'typo3/sysext/', + 'typo3conf/ext/', + ]; + foreach ($relativePrefixPaths as $relativePrefixPath) { + if (!str_starts_with($name, $relativePrefixPath)) { + continue; + } + $name = substr($name, mb_strlen($relativePrefixPath)); + } + return $name; + } } diff --git a/Classes/Core/Functional/FunctionalTestCase.php b/Classes/Core/Functional/FunctionalTestCase.php index c755344f..bd744497 100644 --- a/Classes/Core/Functional/FunctionalTestCase.php +++ b/Classes/Core/Functional/FunctionalTestCase.php @@ -108,6 +108,23 @@ abstract class FunctionalTestCase extends BaseTestCase implements ContainerInter * * A default list of core extensions is always loaded. * + * System extension can be provided by their extension key or composer package name, + * and also as classic mode relative path + * + * ``` + * protected array $coreExensionToLoad = [ + * // As composer package name + * 'typo3/cms-core', + * // As extension-key + * 'core', + * // As relative classic mode system installation path + * 'typo3/sysext/core', + * ]; + * ``` + * + * Note that system extensions must be available, which means either added as require or + * require-dev to the root composer.json or required and installed by a required package. + * * @see FunctionalTestCaseUtility $defaultActivatedCoreExtensions * * @var non-empty-string[] @@ -118,16 +135,32 @@ abstract class FunctionalTestCase extends BaseTestCase implements ContainerInter * Array of test/fixture extensions paths that should be loaded for a test. * * This property will stay empty in this abstract, so it is possible - * to just overwrite it in extending classes. Extensions noted here will - * be loaded for every test of a test case, and it is not possible to change - * the list of loaded extensions between single tests of a test case. + * to just overwrite it in extending classes. + * + * IMPORTANT: Extension list is concrete and used to create the test instance on first + * test execution and is **NOT** changeable between single test permutations. * * Given path is expected to be relative to your document root, example: * - * array( - * 'typo3conf/ext/some_extension/Tests/Functional/Fixtures/Extensions/test_extension', + * ``` + * protected array $testExtensionToLoad = [ + * + * // Virtual relative classic mode installation path * 'typo3conf/ext/base_extension', - * ); + * + * // Virtual relative classic mode installation path subfolder test fixture + * 'typo3conf/ext/some_extension/Tests/Functional/Fixtures/Extensions/test_extension', + * + * // Relative to current test case (recommended for test fixture extension) + * __DIR__ . '/../Fixtures/Extensions/another_test_extension', + * + * // composer package name when available as `require` or `require-dev` in root composer.json + * 'vendor/some-extension', + * + * // extension key when available as package loaded as `require` or `require-dev` in root composer.json + * 'my_extension_key', + * ]; + * ``` * * Extensions in this array are linked to the test instance, loaded * and their ext_tables.sql will be applied. @@ -144,18 +177,22 @@ abstract class FunctionalTestCase extends BaseTestCase implements ContainerInter * be linked for every test of a test case, and it is not possible to change * the list of folders between single tests of a test case. * - * array( + * ``` + * protected array $pathsToLinkInTestInstance = [ * 'link-source' => 'link-destination' - * ); + * ]; + * ``` * * Given paths are expected to be relative to the test instance root. * The array keys are the source paths and the array values are the destination * paths, example: * - * [ + * ``` + * protected array $pathsToLinkInTestInstance = [ * 'typo3/sysext/impext/Tests/Functional/Fixtures/Folders/fileadmin/user_upload' => * 'fileadmin/user_upload', - * ] + * ]; + * ``` * * To be able to link from my_own_ext the extension path needs also to be registered in * property $testExtensionsToLoad @@ -169,12 +206,14 @@ abstract class FunctionalTestCase extends BaseTestCase implements ContainerInter * paths are really duplicated and provided in the instance - instead of * using symbolic links. Examples: * - * [ + * ``` + * protected array $pathsToProvideInTestInstance = [ * // Copy an entire directory recursive to fileadmin * 'typo3/sysext/lowlevel/Tests/Functional/Fixtures/testImages/' => 'fileadmin/', * // Copy a single file into some deep destination directory * 'typo3/sysext/lowlevel/Tests/Functional/Fixtures/testImage/someImage.jpg' => 'fileadmin/_processed_/0/a/someImage.jpg', - * ] + * ]; + * ``` * * @var array */ @@ -205,9 +244,11 @@ abstract class FunctionalTestCase extends BaseTestCase implements ContainerInter * To create additional folders add the paths to this array. Given paths are expected to be * relative to the test instance root and have to begin with a slash. Example: * - * [ + * ``` + * protected array $additionalFoldersToCreate = [ * 'fileadmin/user_upload' - * ] + * ]; + * ``` * * @var non-empty-string[] */ diff --git a/Tests/Unit/Composer/ComposerPackageManagerTest.php b/Tests/Unit/Composer/ComposerPackageManagerTest.php index 84d2a67a..8db5463e 100644 --- a/Tests/Unit/Composer/ComposerPackageManagerTest.php +++ b/Tests/Unit/Composer/ComposerPackageManagerTest.php @@ -201,8 +201,15 @@ public function coreExtensionCanBeResolvedWithRelativeLegacyPathPrefix(): void public function extensionWithoutJsonCanBeResolvedByAbsolutePath(): void { $subject = new ComposerPackageManager(); + $extensionMapPropertyReflection = new \ReflectionProperty($subject, 'extensionKeyToPackageNameMap'); + self::assertIsArray($extensionMapPropertyReflection->getValue($subject)); $packageInfo = $subject->getPackageInfoWithFallback(__DIR__ . '/Fixtures/Extensions/ext_without_composerjson_absolute'); + // Extension without composer.json registers basefolder as extension key + self::assertArrayHasKey('ext_without_composerjson_absolute', $extensionMapPropertyReflection->getValue($subject)); + self::assertSame('unknown-vendor/ext-without-composerjson-absolute', $extensionMapPropertyReflection->getValue($subject)['ext_without_composerjson_absolute']); + + // Verify package info self::assertInstanceOf(PackageInfo::class, $packageInfo); self::assertSame('ext_without_composerjson_absolute', $packageInfo->getExtensionKey()); self::assertSame('unknown-vendor/ext-without-composerjson-absolute', $packageInfo->getName()); @@ -215,8 +222,15 @@ public function extensionWithoutJsonCanBeResolvedByAbsolutePath(): void public function extensionWithoutJsonCanBeResolvedRelativeFromRoot(): void { $subject = new ComposerPackageManager(); + $extensionMapPropertyReflection = new \ReflectionProperty($subject, 'extensionKeyToPackageNameMap'); + self::assertIsArray($extensionMapPropertyReflection->getValue($subject)); $packageInfo = $subject->getPackageInfoWithFallback('Tests/Unit/Composer/Fixtures/Extensions/ext_without_composerjson_relativefromroot'); + // Extension without composer.json registers basefolder as extension key + self::assertArrayHasKey('ext_without_composerjson_relativefromroot', $extensionMapPropertyReflection->getValue($subject)); + self::assertSame('unknown-vendor/ext-without-composerjson-relativefromroot', $extensionMapPropertyReflection->getValue($subject)['ext_without_composerjson_relativefromroot']); + + // Verify package info self::assertInstanceOf(PackageInfo::class, $packageInfo); self::assertSame('ext_without_composerjson_relativefromroot', $packageInfo->getExtensionKey()); self::assertSame('unknown-vendor/ext-without-composerjson-relativefromroot', $packageInfo->getName()); @@ -229,22 +243,40 @@ public function extensionWithoutJsonCanBeResolvedRelativeFromRoot(): void public function extensionWithoutJsonCanBeResolvedByLegacyPath(): void { $subject = new ComposerPackageManager(); + $extensionMapPropertyReflection = new \ReflectionProperty($subject, 'extensionKeyToPackageNameMap'); + self::assertIsArray($extensionMapPropertyReflection->getValue($subject)); $packageInfo = $subject->getPackageInfoWithFallback('typo3conf/ext/testing_framework/Tests/Unit/Composer/Fixtures/Extensions/ext_without_composerjson_fallbackroot'); + // Extension without composer.json registers basefolder as extension key + self::assertArrayHasKey('ext_without_composerjson_fallbackroot', $extensionMapPropertyReflection->getValue($subject)); + self::assertSame('unknown-vendor/ext-without-composerjson-fallbackroot', $extensionMapPropertyReflection->getValue($subject)['ext_without_composerjson_fallbackroot']); + + // Verify package info self::assertInstanceOf(PackageInfo::class, $packageInfo); self::assertSame('ext_without_composerjson_fallbackroot', $packageInfo->getExtensionKey()); self::assertSame('unknown-vendor/ext-without-composerjson-fallbackroot', $packageInfo->getName()); self::assertSame('typo3-cms-extension', $packageInfo->getType()); self::assertNull($packageInfo->getInfo()); self::assertNotNull($packageInfo->getExtEmConf()); + } #[Test] public function extensionWithJsonCanBeResolvedByAbsolutePath(): void { $subject = new ComposerPackageManager(); + $extensionMapPropertyReflection = new \ReflectionProperty($subject, 'extensionKeyToPackageNameMap'); + self::assertIsArray($extensionMapPropertyReflection->getValue($subject)); $packageInfo = $subject->getPackageInfoWithFallback(__DIR__ . '/Fixtures/Extensions/ext_absolute'); + // Extension with composer.json and extension key does not register basepath as extension key + self::assertArrayNotHasKey('ext_absolute', $extensionMapPropertyReflection->getValue($subject)); + + // Extension with composer.json and extension key register extension key as composer package alias + self::assertArrayHasKey('absolute_real', $extensionMapPropertyReflection->getValue($subject)); + self::assertSame('testing-framework/extension-absolute', $extensionMapPropertyReflection->getValue($subject)['absolute_real']); + + // Verify package info self::assertInstanceOf(PackageInfo::class, $packageInfo); self::assertSame('absolute_real', $packageInfo->getExtensionKey()); self::assertSame('testing-framework/extension-absolute', $packageInfo->getName()); @@ -257,8 +289,18 @@ public function extensionWithJsonCanBeResolvedByAbsolutePath(): void public function extensionWithJsonCanBeResolvedRelativeFromRoot(): void { $subject = new ComposerPackageManager(); + $extensionMapPropertyReflection = new \ReflectionProperty($subject, 'extensionKeyToPackageNameMap'); + self::assertIsArray($extensionMapPropertyReflection->getValue($subject)); $packageInfo = $subject->getPackageInfoWithFallback('Tests/Unit/Composer/Fixtures/Extensions/ext_relativefromroot'); + // Extension with composer.json and extension key does not register basepath as extension key + self::assertArrayNotHasKey('ext_relativefromroot', $extensionMapPropertyReflection->getValue($subject)); + + // Extension with composer.json and extension key register extension key as composer package alias + self::assertArrayHasKey('relativefromroot_real', $extensionMapPropertyReflection->getValue($subject)); + self::assertSame('testing-framework/extension-relativefromroot', $extensionMapPropertyReflection->getValue($subject)['relativefromroot_real']); + + // Verify package info self::assertInstanceOf(PackageInfo::class, $packageInfo); self::assertSame('relativefromroot_real', $packageInfo->getExtensionKey()); self::assertSame('testing-framework/extension-relativefromroot', $packageInfo->getName()); @@ -271,8 +313,18 @@ public function extensionWithJsonCanBeResolvedRelativeFromRoot(): void public function extensionWithJsonCanBeResolvedByLegacyPath(): void { $subject = new ComposerPackageManager(); + $extensionMapPropertyReflection = new \ReflectionProperty($subject, 'extensionKeyToPackageNameMap'); + self::assertIsArray($extensionMapPropertyReflection->getValue($subject)); $packageInfo = $subject->getPackageInfoWithFallback('typo3conf/ext/testing_framework/Tests/Unit/Composer/Fixtures/Extensions/ext_fallbackroot'); + // Extension with composer.json and extension key does not register basepath as extension key + self::assertArrayNotHasKey('ext_fallbackroot', $extensionMapPropertyReflection->getValue($subject)); + + // Extension with composer.json and extension key register extension key as composer package alias + self::assertArrayHasKey('fallbackroot_real', $extensionMapPropertyReflection->getValue($subject)); + self::assertSame('testing-framework/extension-fallbackroot', $extensionMapPropertyReflection->getValue($subject)['fallbackroot_real']); + + // Verify package info self::assertInstanceOf(PackageInfo::class, $packageInfo); self::assertSame('fallbackroot_real', $packageInfo->getExtensionKey()); self::assertSame('testing-framework/extension-fallbackroot', $packageInfo->getName()); @@ -285,9 +337,19 @@ public function extensionWithJsonCanBeResolvedByLegacyPath(): void public function extensionWithJsonCanBeResolvedByRelativeLegacyPath(): void { $subject = new ComposerPackageManager(); + $extensionMapPropertyReflection = new \ReflectionProperty($subject, 'extensionKeyToPackageNameMap'); + self::assertIsArray($extensionMapPropertyReflection->getValue($subject)); $projectFolderName = basename($subject->getRootPath()); $packageInfo = $subject->getPackageInfoWithFallback('../' . $projectFolderName . '/typo3conf/ext/testing_framework/Tests/Unit/Composer/Fixtures/Extensions/ext_fallbackroot'); + // Extension with composer.json and extension key does not register basepath as extension key + self::assertArrayNotHasKey('ext_fallbackroot', $extensionMapPropertyReflection->getValue($subject)); + + // Extension with composer.json and extension key register extension key as composer package alias + self::assertArrayHasKey('fallbackroot_real', $extensionMapPropertyReflection->getValue($subject)); + self::assertSame('testing-framework/extension-fallbackroot', $extensionMapPropertyReflection->getValue($subject)['fallbackroot_real']); + + // Verify package info self::assertInstanceOf(PackageInfo::class, $packageInfo); self::assertSame('fallbackroot_real', $packageInfo->getExtensionKey()); self::assertSame('testing-framework/extension-fallbackroot', $packageInfo->getName()); @@ -358,4 +420,199 @@ public function getPackageInfoWithFallbackReturnsExtensionInfoWithCorrectExtensi self::assertSame($expectedPackageName, $packageInfo->getName()); self::assertSame($expectedExtensionKey, $packageInfo->getExtensionKey()); } + + public static function prepareResolvePackageNameReturnsExpectedValuesDataProvider(): \Generator + { + yield 'Composer package name returns unchanged (not checked for existence)' => [ + 'name' => 'typo3/cms-core', + 'expected' => 'typo3/cms-core', + ]; + yield 'Extension key returns unchanged (not checked for existence)' => [ + 'name' => 'core', + 'expected' => 'core', + ]; + yield 'Classic mode system path returns extension key (not checked for existence)' => [ + 'name' => 'typo3/sysext/core', + 'expected' => 'core', + ]; + yield 'Classic mode extension path returns extension key (not checked for existence)' => [ + 'name' => 'typo3conf/ext/some_ext', + 'expected' => 'some_ext', + ]; + yield 'Not existing full path to classic system extension path resolves to extension key (not checked for existence)' => [ + 'name' => 'ROOT:/typo3/sysext/core', + 'expected' => 'core', + ]; + yield 'Not existing full path to classic extension path resolves to extension key (not checked for existence)' => [ + 'name' => 'ROOT:/typo3conf/ext/some_ext', + 'expected' => 'some_ext', + ]; + yield 'Vendor path returns vendor with package subfolder' => [ + 'name' => 'VENDOR:/typo3/cms-core', + 'expected' => 'typo3/cms-core', + ]; + } + + #[DataProvider('prepareResolvePackageNameReturnsExpectedValuesDataProvider')] + #[Test] + public function prepareResolvePackageNameReturnsExpectedValues(string $name, string $expected): void + { + $composerPackageManager = new ComposerPackageManager(); + $replaceMap = [ + 'ROOT:/' => rtrim($composerPackageManager->getRootPath(), '/') . '/', + 'VENDOR:/' => rtrim($composerPackageManager->getVendorPath(), '/') . '/', + ]; + $name = str_replace(array_keys($replaceMap), array_values($replaceMap), $name); + foreach (array_keys($replaceMap) as $replaceKey) { + self::assertStringNotContainsString($replaceKey, $name, 'Key "%s" is replaced in name "%s"'); + } + $prepareResolvePackageNameReflectionMethod = new \ReflectionMethod($composerPackageManager, 'prepareResolvePackageName'); + $resolved = $prepareResolvePackageNameReflectionMethod->invoke($composerPackageManager, $name); + self::assertSame($expected, $resolved, sprintf('"%s" resolved to "%s"', $name, $expected)); + } + + public static function resolvePackageNameReturnsExpectedPackageNameDataProvider(): \Generator + { + yield 'Composer package name returns unchanged (not checked for existence)' => [ + 'name' => 'typo3/cms-core', + 'expected' => 'typo3/cms-core', + ]; + yield 'Extension key returns unchanged (not checked for existence)' => [ + 'name' => 'core', + 'expected' => 'typo3/cms-core', + ]; + yield 'Classic mode system path returns extension key (not checked for existence)' => [ + 'name' => 'typo3/sysext/core', + 'expected' => 'typo3/cms-core', + ]; + yield 'Not existing full path to classic system extension path resolves to extension key (not checked for existence)' => [ + 'name' => 'ROOT:/typo3/sysext/core', + 'expected' => 'typo3/cms-core', + ]; + yield 'Vendor path returns vendor with package subfolder' => [ + 'name' => 'VENDOR:/typo3/cms-core', + 'expected' => 'typo3/cms-core', + ]; + // Not loaded/known extension resolves only extension key and not to a composer package name. + yield 'Not existing full path to classic extension path resolves to extension key for unknown extension' => [ + 'name' => 'ROOT:/typo3conf/ext/some_ext', + 'expected' => 'some_ext', + ]; + // Not loaded/known extension resolves only extension key and not to a composer package name. + yield 'Classic mode extension path returns extension key for unknown extension' => [ + 'name' => 'typo3conf/ext/some_ext', + 'expected' => 'some_ext', + ]; + } + + #[DataProvider('resolvePackageNameReturnsExpectedPackageNameDataProvider')] + #[Test] + public function resolvePackageNameReturnsExpectedPackageName(string $name, string $expected): void + { + $composerPackageManager = new ComposerPackageManager(); + $replaceMap = [ + 'ROOT:/' => rtrim($composerPackageManager->getRootPath(), '/') . '/', + 'VENDOR:/' => rtrim($composerPackageManager->getVendorPath(), '/') . '/', + ]; + $name = str_replace(array_keys($replaceMap), array_values($replaceMap), $name); + foreach (array_keys($replaceMap) as $replaceKey) { + self::assertStringNotContainsString($replaceKey, $name, 'Key "%s" is replaced in name "%s"'); + } + $resolvePackageNameReflectionMethod = new \ReflectionMethod($composerPackageManager, 'resolvePackageName'); + $resolved = $resolvePackageNameReflectionMethod->invoke($composerPackageManager, $name); + self::assertSame($expected, $resolved, sprintf('"%s" resolved to "%s"', $name, $expected)); + } + + #[Test] + public function ensureEndingComposerPackageNameAndTypoExtensionPackageExtensionKeyResolvesCorrectPackage(): void + { + $composerManager = new ComposerPackageManager(); + $extensionMapPropertyReflection = new \ReflectionProperty($composerManager, 'extensionKeyToPackageNameMap'); + self::assertIsArray($extensionMapPropertyReflection->getValue($composerManager)); + + // verify initial composer package information + $initComposerPackage = $composerManager->getPackageInfoWithFallback(__DIR__ . '/Fixtures/Packages/sharedextensionkey'); + self::assertArrayNotHasKey('sharedextensionkey', $extensionMapPropertyReflection->getValue($composerManager)); + self::assertInstanceOf(PackageInfo::class, $initComposerPackage); + self::assertSame('testing-framework/sharedextensionkey', $initComposerPackage->getName(), 'PackageInfo->name is "testing-framework/sharedextensionkey"'); + self::assertFalse($initComposerPackage->isSystemExtension(), '"testing-framework/sharedextensionkey" is not a TYPO3 system extension'); + self::assertFalse($initComposerPackage->isExtension(), '"testing-framework/sharedextensionkey" is not a TYPO3 extension'); + self::assertTrue($initComposerPackage->isComposerPackage(), '"testing-framework/sharedextensionkey" is a composer package'); + self::assertSame('', $initComposerPackage->getExtensionKey()); + + // verify initial extension package information + $initExtensionPackage = $composerManager->getPackageInfoWithFallback(__DIR__ . '/Fixtures/Extensions/extension-key-shared-with-composer-package'); + self::assertArrayHasKey('sharedextensionkey', $extensionMapPropertyReflection->getValue($composerManager)); + self::assertSame('testing-framework/extension-key-shared-with-composer-package', $extensionMapPropertyReflection->getValue($composerManager)['sharedextensionkey']); + self::assertInstanceOf(PackageInfo::class, $initExtensionPackage); + self::assertSame('testing-framework/extension-key-shared-with-composer-package', $initExtensionPackage->getName(), 'PackageInfo->name is "testing-framework/extension-key-shared-with-composer-package"'); + self::assertFalse($initExtensionPackage->isSystemExtension(), '"testing-framework/extension-key-shared-with-composer-package" is not a TYPO3 system extension'); + self::assertTrue($initExtensionPackage->isExtension(), '"testing-framework/extension-key-shared-with-composer-package" is not a TYPO3 extension'); + self::assertTrue($initExtensionPackage->isComposerPackage(), '"testing-framework/extension-key-shared-with-composer-package" is a composer package'); + self::assertSame('sharedextensionkey', $initExtensionPackage->getExtensionKey()); + + // verify shared extension key retrieval returns the extension package + $extensionPackage = $composerManager->getPackageInfo('sharedextensionkey'); + self::assertInstanceOf(PackageInfo::class, $extensionPackage); + self::assertSame('testing-framework/extension-key-shared-with-composer-package', $extensionPackage->getName(), 'PackageInfo->name is "testing-framework/extension-key-shared-with-composer-package"'); + self::assertFalse($extensionPackage->isSystemExtension(), '"testing-framework/extension-key-shared-with-composer-package" is not a TYPO3 system extension'); + self::assertTrue($extensionPackage->isExtension(), '"testing-framework/extension-key-shared-with-composer-package" is not a TYPO3 extension'); + self::assertTrue($extensionPackage->isComposerPackage(), '"testing-framework/extension-key-shared-with-composer-package" is a composer package'); + self::assertSame('sharedextensionkey', $extensionPackage->getExtensionKey()); + + // verify shared extension key with classic mode prefix retrieval returns the extension package + $classicModeExtensionPackage = $composerManager->getPackageInfo('typo3conf/ext/sharedextensionkey'); + self::assertInstanceOf(PackageInfo::class, $classicModeExtensionPackage); + self::assertSame('testing-framework/extension-key-shared-with-composer-package', $classicModeExtensionPackage->getName(), 'PackageInfo->name is "testing-framework/extension-key-shared-with-composer-package"'); + self::assertFalse($classicModeExtensionPackage->isSystemExtension(), '"testing-framework/extension-key-shared-with-composer-package" is not a TYPO3 system extension'); + self::assertTrue($classicModeExtensionPackage->isExtension(), '"testing-framework/extension-key-shared-with-composer-package" is not a TYPO3 extension'); + self::assertTrue($classicModeExtensionPackage->isComposerPackage(), '"testing-framework/extension-key-shared-with-composer-package" is a composer package'); + self::assertSame('sharedextensionkey', $classicModeExtensionPackage->getExtensionKey()); + } + + /** + * @todo Remove this when fluid/standalone fluid is no longer available by default due to core dependencies. + * {@see ensureEndingComposerPackageNameAndTypoExtensionPackageExtensionKeyResolvesCorrectPackage} + */ + #[Test] + public function ensureStandaloneFluidDoesNotBreakCoreFluidExtension(): void + { + $composerManager = new ComposerPackageManager(); + + // Verify standalone fluid composer package + $standaloneFluid = $composerManager->getPackageInfo('typo3fluid/fluid'); + self::assertInstanceOf(PackageInfo::class, $standaloneFluid); + self::assertSame('typo3fluid/fluid', $standaloneFluid->getName(), 'PackageInfo->name is not "typo3fluid/fluid"'); + self::assertFalse($standaloneFluid->isSystemExtension(), '"typo3fluid/fluid" is not a TYPO3 system extension'); + self::assertFalse($standaloneFluid->isExtension(), '"typo3fluid/fluid" is not a TYPO3 extension'); + self::assertTrue($standaloneFluid->isComposerPackage(), '"typo3fluid/fluid" is a composer package'); + self::assertSame('', $standaloneFluid->getExtensionKey()); + + // Verify TYPO3 system extension fluid. + $coreFluid = $composerManager->getPackageInfo('typo3/cms-fluid'); + self::assertInstanceOf(PackageInfo::class, $coreFluid); + self::assertSame('typo3/cms-fluid', $coreFluid->getName(), 'PackageInfo->name is not "typo3/cms-fluid"'); + self::assertTrue($coreFluid->isSystemExtension(), '"typo3/cms-fluid" is a TYPO3 system extension'); + self::assertFalse($coreFluid->isExtension(), '"typo3/cms-fluid" is not a TYPO3 extension'); + self::assertTrue($coreFluid->isComposerPackage(), '"typo3/cms-fluid" is a composer package'); + self::assertSame('fluid', $coreFluid->getExtensionKey()); + + // Verify TYPO3 system extension fluid resolved using extension key. + $extensionKeyRetrievesCoreFluid = $composerManager->getPackageInfo('fluid'); + self::assertInstanceOf(PackageInfo::class, $extensionKeyRetrievesCoreFluid); + self::assertSame('typo3/cms-fluid', $extensionKeyRetrievesCoreFluid->getName(), 'PackageInfo->name is not "typo3/cms-fluid"'); + self::assertTrue($extensionKeyRetrievesCoreFluid->isSystemExtension(), '"typo3/cms-fluid" is a TYPO3 system extension'); + self::assertFalse($extensionKeyRetrievesCoreFluid->isExtension(), '"typo3/cms-fluid" is not a TYPO3 extension'); + self::assertTrue($extensionKeyRetrievesCoreFluid->isComposerPackage(), '"typo3/cms-fluid" is a composer package'); + self::assertSame('fluid', $extensionKeyRetrievesCoreFluid->getExtensionKey()); + + // Verify TYPO3 system extension fluid resolved using relative classic mode path. + $extensionRelativeSystemExtensionPath = $composerManager->getPackageInfo('typo3/sysext/fluid'); + self::assertInstanceOf(PackageInfo::class, $extensionRelativeSystemExtensionPath); + self::assertSame('typo3/cms-fluid', $extensionRelativeSystemExtensionPath->getName(), 'PackageInfo->name is not "typo3/cms-fluid"'); + self::assertTrue($extensionRelativeSystemExtensionPath->isSystemExtension(), '"typo3/cms-fluid" is a TYPO3 system extension'); + self::assertFalse($extensionRelativeSystemExtensionPath->isExtension(), '"typo3/cms-fluid" is not a TYPO3 extension'); + self::assertTrue($extensionRelativeSystemExtensionPath->isComposerPackage(), '"typo3/cms-fluid" is a composer package'); + self::assertSame('fluid', $extensionRelativeSystemExtensionPath->getExtensionKey()); + } } diff --git a/Tests/Unit/Composer/Fixtures/Extensions/extension-key-shared-with-composer-package/composer.json b/Tests/Unit/Composer/Fixtures/Extensions/extension-key-shared-with-composer-package/composer.json new file mode 100644 index 00000000..60065539 --- /dev/null +++ b/Tests/Unit/Composer/Fixtures/Extensions/extension-key-shared-with-composer-package/composer.json @@ -0,0 +1,18 @@ +{ + "name": "testing-framework/extension-key-shared-with-composer-package", + "description": "TYPO3 extension shareing extension-key with last part of composer package ", + "type": "typo3-cms-extension", + "license": "GPL-2.0-or-later", + "authors": [ + { + "name": "Stefan Bürk", + "email": "stefan@buerk.tech" + } + ], + "require": {}, + "extra": { + "typo3/cms": { + "extension-key": "sharedextensionkey" + } + } +} diff --git a/Tests/Unit/Composer/Fixtures/Packages/sharedextensionkey/composer.json b/Tests/Unit/Composer/Fixtures/Packages/sharedextensionkey/composer.json new file mode 100644 index 00000000..de1adcd0 --- /dev/null +++ b/Tests/Unit/Composer/Fixtures/Packages/sharedextensionkey/composer.json @@ -0,0 +1,12 @@ +{ + "name": "testing-framework/sharedextensionkey", + "description": "TYPO3 extension shareing extension-key with last part of composer package ", + "license": "GPL-2.0-or-later", + "authors": [ + { + "name": "Stefan Bürk", + "email": "stefan@buerk.tech" + } + ], + "require": {} +}