diff --git a/.gitignore b/.gitignore index 9bbe0e7..6a2abd2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,11 @@ # libraries lock file /installed-libraries.json +# ide folders +/.idea # composer artifacts -/vendor/ +/.phpunit.result.cache +/composer.lock +/composer.phar +/phpunit.xml +/reports +/vendor diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4587401 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,19 @@ +1.2.0 / 2020-07-14 +======================== + +* Quality of life improvements such as an alternative configuration +definition for supporting: + * Different file extensions. + * SHA1 checksum verification. + * Removing example/demo/test files from the libraries (see [PSA-2011-002](https://www.drupal.org/node/1189632)). +* Ability to pull in library dependencies declared in subpackages through the +`drupal-libraries-dependencies` extra option. +* Basic unit tests. + +1.0.1 / 2018-01-25 +======================== +* Relax composer-installers version requirement. + +1.0.0 / 2018-01-25 +======================== +* Initial MVP plugin. diff --git a/README.md b/README.md index 2c8ee61..ab88e13 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ external dependencies for a Drupal site in a single place: the `composer.json` f 1. Add _Drupal Libraries Installer_ to your Drupal site project: ```sh - composer require balbuf/drupal-libraries-installer + composer require zodiacmedia/drupal-libraries-installer ``` 1. Add libraries to your `composer.json` file via the `drupal-libraries` property diff --git a/composer.json b/composer.json index 9fe3cb9..5df181a 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "balbuf/drupal-libraries-installer", + "name": "zodiacmedia/drupal-libraries-installer", "description": "Install Drupal libraries via a simple listing in your composer.json file", "type": "composer-plugin", "license": "MIT", @@ -7,19 +7,42 @@ { "name": "Stephen Beemsterboer", "homepage": "https://github.com/balbuf" + }, + { + "name": "codebymikey", + "homepage": "https://www.drupal.org/u/codebymikey" } ], "support": { - "issues": "https://github.com/balbuf/drupal-libraries-installer/issues" + "issues": "https://github.com/zodiacmedia/drupal-libraries-installer/issues" }, "autoload": { - "psr-4": {"BalBuf\\DrupalLibrariesInstaller\\": "src/"} + "psr-4": { + "Zodiacmedia\\DrupalLibrariesInstaller\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Zodiacmedia\\DrupalLibrariesInstaller\\": "tests" + } }, "extra": { - "class": "BalBuf\\DrupalLibrariesInstaller\\Plugin" + "class": "Zodiacmedia\\DrupalLibrariesInstaller\\Plugin" + }, + "scripts": { + "phpunit": "phpunit", + "coverage": "phpunit --coverage-text", + "coverage:report": "phpunit --log-junit=reports/unitreport.xml --coverage-text --coverage-html=reports/coverage --coverage-clover=reports/coverage.xml" }, "require": { "composer-plugin-api": "^1.0", - "composer/installers": "^1.1" + "composer/installers": "^1.1", + "php": ">=7.2.0" + }, + "require-dev": { + "composer/composer": "^1.10", + "mikey179/vfsstream": "^1.6", + "phpunit/phpunit": "^8.5", + "ext-json": "*" } } diff --git a/composer.lock b/composer.lock deleted file mode 100644 index 50cf0c1..0000000 --- a/composer.lock +++ /dev/null @@ -1,138 +0,0 @@ -{ - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", - "This file is @generated automatically" - ], - "content-hash": "0d5407f7f7233a1bd639fd22d5542230", - "packages": [ - { - "name": "composer/installers", - "version": "v1.5.0", - "source": { - "type": "git", - "url": "https://github.com/composer/installers.git", - "reference": "049797d727261bf27f2690430d935067710049c2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/installers/zipball/049797d727261bf27f2690430d935067710049c2", - "reference": "049797d727261bf27f2690430d935067710049c2", - "shasum": "" - }, - "require": { - "composer-plugin-api": "^1.0" - }, - "replace": { - "roundcube/plugin-installer": "*", - "shama/baton": "*" - }, - "require-dev": { - "composer/composer": "1.0.*@dev", - "phpunit/phpunit": "^4.8.36" - }, - "type": "composer-plugin", - "extra": { - "class": "Composer\\Installers\\Plugin", - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "psr-4": { - "Composer\\Installers\\": "src/Composer/Installers" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Kyle Robinson Young", - "email": "kyle@dontkry.com", - "homepage": "https://github.com/shama" - } - ], - "description": "A multi-framework Composer library installer", - "homepage": "https://composer.github.io/installers/", - "keywords": [ - "Craft", - "Dolibarr", - "Eliasis", - "Hurad", - "ImageCMS", - "Kanboard", - "Lan Management System", - "MODX Evo", - "Mautic", - "Maya", - "OXID", - "Plentymarkets", - "Porto", - "RadPHP", - "SMF", - "Thelia", - "WolfCMS", - "agl", - "aimeos", - "annotatecms", - "attogram", - "bitrix", - "cakephp", - "chef", - "cockpit", - "codeigniter", - "concrete5", - "croogo", - "dokuwiki", - "drupal", - "eZ Platform", - "elgg", - "expressionengine", - "fuelphp", - "grav", - "installer", - "itop", - "joomla", - "kohana", - "laravel", - "lavalite", - "lithium", - "magento", - "majima", - "mako", - "mediawiki", - "modulework", - "modx", - "moodle", - "osclass", - "phpbb", - "piwik", - "ppi", - "puppet", - "pxcms", - "reindex", - "roundcube", - "shopware", - "silverstripe", - "sydes", - "symfony", - "typo3", - "wordpress", - "yawik", - "zend", - "zikula" - ], - "time": "2017-12-29T09:13:20+00:00" - } - ], - "packages-dev": [], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": [], - "prefer-stable": false, - "prefer-lowest": false, - "platform": [], - "platform-dev": [] -} diff --git a/example/dependency/README.md b/example/dependency/README.md new file mode 100644 index 0000000..2928ac0 --- /dev/null +++ b/example/dependency/README.md @@ -0,0 +1,5 @@ +# Demo dependency package. + +Demo dependency package includes a `moment` library version 2.0.0 which will +not be installed because it was already declared in the root project's +composer.json. diff --git a/example/dependency/composer.json b/example/dependency/composer.json new file mode 100644 index 0000000..3c41957 --- /dev/null +++ b/example/dependency/composer.json @@ -0,0 +1,17 @@ +{ + "name": "zodiacmedia/drupal-libraries-installer-demo-dependency", + "type": "project", + "license": "MIT", + "authors": [ + { + "name": "codebymikey", + "homepage": "https://www.drupal.org/u/codebymikey" + } + ], + "extra": { + "drupal-libraries": { + "dependency@flexslider": "https://github.com/woocommerce/FlexSlider/archive/2.6.4.zip", + "moment": "https://registry.npmjs.org/moment/-/moment-2.0.0.tgz" + } + } +} diff --git a/example/project/.gitignore b/example/project/.gitignore new file mode 100644 index 0000000..25f3ea5 --- /dev/null +++ b/example/project/.gitignore @@ -0,0 +1,3 @@ +/composer.lock +/vendor +/web diff --git a/example/project/README.md b/example/project/README.md new file mode 100644 index 0000000..df92d3c --- /dev/null +++ b/example/project/README.md @@ -0,0 +1,3 @@ +# Demo root project + +Demo root project integrating with Drupal libraries installer. diff --git a/example/project/composer.json b/example/project/composer.json new file mode 100644 index 0000000..66a9831 --- /dev/null +++ b/example/project/composer.json @@ -0,0 +1,75 @@ +{ + "name": "zodiacmedia/drupal-libraries-installer-demo-project", + "description": "Demo project using the installer.", + "type": "project", + "license": "MIT", + "authors": [ + { + "name": "codebymikey", + "homepage": "https://www.drupal.org/u/codebymikey" + } + ], + "repositories": { + "drupal-libraries-installer": { + "type": "vcs", + "url": "../.." + }, + "drupal-libraries-installer-demo-dependency": { + "type": "path", + "url": "../dependency/", + "options": { + "symlink": true + } + } + }, + "require": { + "zodiacmedia/drupal-libraries-installer": "*@dev", + "zodiacmedia/drupal-libraries-installer-demo-dependency": "*@dev" + }, + "config": { + "preferred-install": "source", + "classmap-authoritative": true, + "prepend-autoloader": false, + "optimize-autoloader": true + }, + "extra": { + "installer-paths": { + "web/libraries/{$name}/": [ + "type:drupal-library" + ] + }, + "drupal-libraries-dependencies": [ + "zodiacmedia/drupal-libraries-installer-demo-dependency" + ], + "drupal-libraries": { + "simple-color": "https://github.com/recurser/jquery-simple-color/archive/v1.2.2.zip", + "chosen": { + "url": "https://github.com/harvesthq/chosen/releases/download/v1.8.2/chosen_v1.8.2.zip", + "version": "1.8.2" + }, + "flexslider": { + "url": "https://github.com/woocommerce/FlexSlider/archive/2.6.4.zip", + "version": "2.6.4", + "type": "zip", + "ignore": [ + "bower_components", + "demo", + "node_modules" + ] + }, + "moment": { + "url": "https://registry.npmjs.org/moment/-/moment-2.25.0.tgz", + "shasum": "e961ab9a5848a1cf2c52b1af4e6c82a8401e7fe9" + }, + "select2": { + "url": "https://github.com/select2/select2/archive/4.0.13.zip", + "ignore": [ + ".*", + "*.{md}", + "Gruntfile.js", + "{docs,src,tests}" + ] + } + } + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..74cbacf --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,15 @@ + + + + ./tests + + + + + ./src + + + diff --git a/src/Plugin.php b/src/Plugin.php index bf827e3..110f009 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -1,20 +1,24 @@ composer = $composer; $this->io = $io; - $this->fileSystem = new Filesystem(); + $this->fileSystem = $filesystem ?? new Filesystem(); $this->downloadManager = $composer->getDownloadManager(); $this->installationManager = $composer->getInstallationManager(); } @@ -79,7 +100,8 @@ public static function getSubscribedEvents() { /** * Upon running composer install or update, install the drupal libraries. * - * @param Event $event install/update event + * @param \Composer\Script\Event $event + * The composer install/update event. * * @throws \Exception */ @@ -128,8 +150,8 @@ public function install(Event $event) { } // Remove unused libraries from disk before attempting to download new ones. - // Avoids the edge-case where the removed folder happens to be the same as the one where the new one is being - // installed to. + // Avoids the edge-case where the removed folder happens to be the same as + // the one where a new package dependency is being installed to. $removed_libraries = array_diff_key($applied_drupal_libraries, $processed_drupal_libraries); if ($removed_libraries) { $this->removeUnusedLibraries($removed_libraries); @@ -153,38 +175,40 @@ public function install(Event $event) { /** * Drupal library processor. * - * Inspired by https://github.com/civicrm/composer-downloads-plugin + * Inspired by https://github.com/civicrm/composer-downloads-plugin. * * @param array $processed_drupal_libraries + * The currently processed drupal libraries. * @param array $drupal_libraries - * Applied drupal libraries. + * The currently installed drupal libraries. * @param \Composer\Package\PackageInterface $package + * The package instance. * * @return array * The processed packages. */ - protected function processPackage($processed_drupal_libraries, $drupal_libraries, $package) { + protected function processPackage(array $processed_drupal_libraries, array $drupal_libraries, PackageInterface $package) { $extra = $package->getExtra(); if (empty($extra['drupal-libraries']) || !is_array($extra['drupal-libraries'])) { return $processed_drupal_libraries; } - // Install each library + // Install each library. foreach ($extra['drupal-libraries'] as $library => $library_definition) { $ignore_patterns = []; $sha1checksum = NULL; if (is_string($library_definition)) { // Simple format. $url = $library_definition; - list($version, $distribution_type) = $this->guessDefaultsFromUrl($url); + [$version, $distribution_type] = $this->guessDefaultsFromUrl($url); } else { if (empty($library_definition['url'])) { throw new \LogicException("The drupal-library '$library' does not contain a valid URL."); } $url = $library_definition['url']; - list($version, $distribution_type) = $this->guessDefaultsFromUrl($url); + [$version, $distribution_type] = $this->guessDefaultsFromUrl($url); $version = $library_definition['version'] ?? $version; $distribution_type = $library_definition['type'] ?? $distribution_type; $ignore_patterns = $library_definition['ignore'] ?? $ignore_patterns; @@ -192,8 +216,9 @@ protected function processPackage($processed_drupal_libraries, $drupal_libraries } if (isset($processed_drupal_libraries[$library])) { - // Only the first declaration of the library is ever used. This ensures that the root package always - // acts as the source of truth over what version of a library is installed. + // Only the first declaration of the library is ever used. This ensures + // that the root package always acts as the source of truth over what + // version of a library is installed. $old_definition = $processed_drupal_libraries[$library]; if ($this->io->isDebug()) { $this->io->write( @@ -207,7 +232,8 @@ protected function processPackage($processed_drupal_libraries, $drupal_libraries } } else { - // Track installed libraries in the package info in installed-libraries.json + // Track installed libraries in the package info in + // installed-libraries.json. $applied_library = [ 'version' => $version, 'url' => $url, @@ -230,8 +256,9 @@ protected function processPackage($processed_drupal_libraries, $drupal_libraries * Remove old unused libraries from disk. * * @param array $old_libraries + * The old libraries to remove from disk. */ - protected function removeUnusedLibraries($old_libraries) { + protected function removeUnusedLibraries(array $old_libraries) { foreach ($old_libraries as $library_name => $library_definition) { $library_package = $this->getLibraryPackage($library_name, $library_definition); @@ -247,8 +274,10 @@ protected function removeUnusedLibraries($old_libraries) { * * @param array $processed_libraries * The processed libraries. + * @param array $applied_drupal_libraries + * The currently installed libraries. */ - protected function downloadLibraries($processed_libraries, $applied_drupal_libraries) { + protected function downloadLibraries(array $processed_libraries, array $applied_drupal_libraries) { foreach ($processed_libraries as $library_name => $processed_library) { $library_package = $this->getLibraryPackage($library_name, $processed_library); @@ -280,7 +309,7 @@ protected function downloadLibraries($processed_libraries, $applied_drupal_libra * @param array $ignore_patterns * File patterns to ignore. */ - protected function downloadPackage(Package $library_package, $install_path, $ignore_patterns) { + protected function downloadPackage(Package $library_package, $install_path, array $ignore_patterns) { // Let composer download and unpack the library for us! $this->downloadManager->download($library_package, $install_path); @@ -328,12 +357,15 @@ function ($file) use ($patterns) { /** * Get a drupal-library package object from its definition. * - * @param $library_name - * @param $library_definition + * @param string $library_name + * The library name. + * @param array $library_definition + * The library definition. * * @return \Composer\Package\Package + * The pseudo-package for the library. */ - protected function getLibraryPackage($library_name, $library_definition) { + protected function getLibraryPackage($library_name, array $library_definition) { $library_package_name = 'drupal-library/' . $library_name; $library_package = new Package( $library_package_name, $library_definition['version'], $library_definition['version'] @@ -380,10 +412,11 @@ protected function guessDefaultsFromUrl($url) { * Get the current installed-libraries json file path. * * @return string + * The installed libraries json lock file path. */ - protected function getInstalledJsonPath() { + public function getInstalledJsonPath() { // Alternative approach. - // $installed_json_file = __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'installed-libraries.json'; + /*$installed_json_file = __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'installed-libraries.json';*/ /** @var \Composer\Package\CompletePackage $installer_library_package */ $installer_library_package = $this->composer->getRepositoryManager()->getLocalRepository()->findPackage( diff --git a/tests/PluginTest.php b/tests/PluginTest.php new file mode 100644 index 0000000..a85c224 --- /dev/null +++ b/tests/PluginTest.php @@ -0,0 +1,600 @@ +downloadedFiles = []; + $this->removedFiles = []; + + $this->setupVirtualFilesystem(); + + $this->installationManager = $this->createMock(InstallationManager::class); + $this->installationManager + ->method('getInstallPath') + ->willReturnCallback([$this, 'getInstallPathCallback']); + + $this->downloadManager = $this->createMock(DownloadManager::class); + $this->downloadManager->method('download')->willReturnCallback( + [$this, 'downloadManagerDownloadCallback'] + ); + + // Create a partial mock, keeping the normalize functionality. + $this->fileSystem = $this->createPartialMock(Filesystem::class, ['remove']); + $this->fileSystem->method('remove')->willReturnCallback( + [$this, 'fileSystemRemoveCallback'] + ); + + $this->composer = $this + ->createPartialMock( + Composer::class, + [ + 'getPackage', + 'getInstallationManager', + 'getDownloadManager', + 'getRepositoryManager', + ] + ); + $this->composer + ->method('getInstallationManager') + ->willReturn($this->installationManager); + + $this->composer + ->method('getDownloadManager') + ->willReturn($this->downloadManager); + + $repository_manager = $this->createMock(RepositoryManager::class); + $local_repository = $this->createMock(WritableRepositoryInterface::class); + $local_repository + ->method('getCanonicalPackages') + ->willReturnCallback([$this, 'getCanonicalPackagesCallback']); + $repository_manager + ->method('getLocalRepository') + ->willReturn($local_repository); + + $this->composer + ->method('getRepositoryManager') + ->willReturn($repository_manager); + + $this->io = $this->createMock(IOInterface::class); + + $this->fixture = $this->getPluginMockInstance(); + } + + /** + * Callback for the FileSystem's remove method. + * + * @param string $file + * The file/folder path. + */ + public function fileSystemRemoveCallback($file) { + $this->removedFiles[] = $file; + } + + /** + * Callback for the DownloadManager's download function. + */ + public function downloadManagerDownloadCallback(Package $package, $file) { + $this->downloadedFiles[] = $file; + } + + /** + * Normalize a libraries directory vfs file path for comparison. + * + * @param string $file + * The file path. + * + * @return string + * The normalized path. + */ + protected function normalizeVfsFilePath(string $file) { + $path_prefix = $this->rootDirectory->getChild(static::LIBRARIES_DIRECTORY)->url() . '/'; + $path_prefix_length = strlen($path_prefix); + if (substr($file, 0, $path_prefix_length) === $path_prefix) { + $file = substr($file, $path_prefix_length); + } + + return $file; + } + + /** + * Prepares the virtual file system. + */ + protected function setupVirtualFilesystem() { + // Not all tests require this, but we want to ensure we never accidentally + // touch our real composer.json. + $project_dependencies = [ + 'vendor' => [ + 'zodiacmedia' => [ + 'drupal-libraries-installer' => [ + // 'installed-libraries.json' => '{}', + ], + 'drupal-libraries-test-dependency' => [ + 'composer.json' => /* @lang JSON */ + <<rootDirectory = vfsStream::setup('root', NULL, $project_dependencies); + + $this->installedLibrariesJsonFile = vfsStream::url( + $this->rootDirectory->path() . '/vendor/zodiacmedia/drupal-libraries-installer/installed-libraries.json' + ); + } + + /** + * Test to ensure the correct events are acted upon. + * + * @covers \Zodiacmedia\DrupalLibrariesInstaller\Plugin::getSubscribedEvents() + */ + public function testGetSubscribedEvents() { + $this->assertEquals( + [ + ScriptEvents::POST_INSTALL_CMD => 'install', + ScriptEvents::POST_UPDATE_CMD => 'install', + ], + Plugin::getSubscribedEvents() + ); + } + + /** + * Test various drupal libraries scenarios. + * + * @param string $fixture_name + * The fixture directory name. + * @param array $expected_removed_files + * The list of removed files. + * @param array $expected_downloaded_dirs + * The list of downloaded package directories. + * @param bool $installed_libraries + * Whether to read an initial installed libraries. + * + * @dataProvider drupalLibrariesDownloadProvider + * + * @throws \Exception + */ + public function testDrupalLibrariesDownload( + $fixture_name, + array $expected_removed_files, + array $expected_downloaded_dirs, + $installed_libraries = FALSE + ) { + $this->fixtureDirectory = $this->fixtureDirectory($fixture_name); + $root_project = $this->rootFromJson("{$this->fixtureDirectory}/composer.json"); + + if ($installed_libraries) { + // Set the initial installed libraries content if any. + copy("{$this->fixtureDirectory}/installed-libraries.initial.json", $this->installedLibrariesJsonFile); + } + + // Create the libraries structure. + $this->createLibrariesPackageStructures("{$this->fixtureDirectory}/libraries-structure.json"); + + $this->triggerPlugin($root_project, $this->fixtureDirectory); + + $removed_files = array_map([$this, 'normalizeVfsFilePath'], $this->removedFiles); + $downloaded_files = array_map([$this, 'normalizeVfsFilePath'], $this->downloadedFiles); + $this->assertSame($expected_removed_files, $removed_files, 'Removed files'); + $this->assertSame($expected_downloaded_dirs, $downloaded_files, 'Downloaded files'); + + // Compare the output JSON files. + $expected_installed_libraries_file = "{$this->fixtureDirectory}/installed-libraries.expected.json"; + $this->assertFileExists($expected_installed_libraries_file); + $this->assertFileExists($this->installedLibrariesJsonFile); + $this->assertSame( + json_decode(file_get_contents($expected_installed_libraries_file), TRUE), + json_decode(file_get_contents($this->installedLibrariesJsonFile), TRUE), + 'installed-libraries.json output' + ); + } + + /** + * Fixtures data provider. + */ + public function drupalLibrariesDownloadProvider() { + return [ + 'download-and-ignore-library-files' => [ + /* + * Given a root package with + * a set of different drupal library definitions. + * When the plugin is run + * Then the libraries are installed and certain files matching specific + * ignore patterns are removed. + */ + 'download-and-ignore-library-files', + [ + 'flexslider/bower_components', + 'flexslider/demo', + 'flexslider/node_modules', + 'select2/.git', + 'select2/.hidden_directory', + 'select2/docs', + 'select2/src', + 'select2/tests', + 'select2/.hidden.txt', + 'select2/Gruntfile.js', + 'select2/README.md', + ], + [ + 'flexslider', + 'moment', + 'select2', + 'simple-color', + ], + ], + 'specific-dependency-libraries-with-override' => [ + /* + * Given a root package with + * a drupal library override and a package dependency with a library + * definition. + * When the plugin is run + * Then the libraries are installed and the override kept. + */ + 'specific-dependency-libraries-with-override', + [], + [ + 'test-moment', + 'test-library-dependency', + ], + ], + 'specific-dependency-libraries' => [ + /* + * Given a root package with + * a package dependency with a library definition. + * When the plugin is run + * Then the libraries are installed and ignore patterns applied. + */ + 'specific-dependency-libraries', + [ + 'test-moment/README.md', + ], + [ + 'test-library-dependency', + 'test-moment', + ], + ], + 'all-dependency-libraries' => [ + /* + * Given a root package with + * a wildcard package dependency library definition. + * When the plugin is run + * Then the dependency libraries are installed and their ignore + * patterns applied. + */ + 'all-dependency-libraries', + [ + 'test-moment/README.md', + ], + [ + 'test-library-dependency', + 'test-moment', + ], + ], + 'existing-lock-file' => [ + /* + * Given a project with an existing lock file with the library. + * and the project already exists on disk. + * When the plugin is run + * Then nothing is installed/removed. + */ + 'existing-lock-file', + [], + [], + TRUE, + ], + ]; + } + + /** + * Returns the mock plugin fixture instance. + */ + protected function getPluginMockInstance() { + /** @var \PHPUnit\Framework\MockObject\MockObject|\Zodiacmedia\DrupalLibrariesInstaller\Plugin $plugin */ + $plugin = $this->createPartialMock(Plugin::class, ['getInstalledJsonPath']); + $plugin->method('getInstalledJsonPath')->willReturn( + $this->installedLibrariesJsonFile + ); + + return $plugin; + } + + /** + * Trigger an installation of the specified plugin. + * + * @param \Composer\Package\RootPackage $package + * The package instance. + * @param string $directory + * Working directory for composer run. + * + * @throws \Exception + */ + protected function triggerPlugin(RootPackage $package, $directory) { + chdir($directory); + // Return the current package. + $this->composer->method('getPackage')->willReturn($package); + + // Activate the plugin. + $this->fixture->activate( + $this->composer, + $this->io, + $this->fileSystem + ); + + $event = new Event( + ScriptEvents::POST_INSTALL_CMD, + $this->composer, + $this->io, + // Dev mode. + FALSE, + [], + [] + ); + $this->fixture->install($event); + } + + /** + * Returns a fixture directory. + * + * @param string $sub_directory + * The subdirectory. + * + * @return string + * The fixture directory. + */ + protected function fixtureDirectory($sub_directory) { + return __DIR__ . "/fixtures/{$sub_directory}"; + } + + /** + * Returns the root package definition. + * + * @param string $file + * The fixture composer.json. + * + * @return \PHPUnit\Framework\MockObject\MockObject|\Composer\Package\RootPackage + * The root package prophecy instance. + */ + protected function rootFromJson($file) { + $json = json_decode(file_get_contents($file), TRUE); + $data = array_merge( + [ + 'name' => 'drupal-libraries-installer-test/root-package', + 'repositories' => [], + 'require' => [], + 'require-dev' => [], + 'suggest' => [], + 'extra' => [], + ], + $json + ); + + $root_name = $data['name']; + + foreach (['require', 'require-dev'] as $config) { + foreach ($data[$config] as $dependency => $dependency_version) { + $link = $this->createMock(Link::class); + $link->method('getSource')->willReturn($root_name); + $link->method('getTarget')->willReturn($dependency); + $link->method('getPrettyConstraint')->willReturn($dependency_version); + $data[$config][$dependency] = $link; + } + } + + $root_package = $this->createMock(RootPackage::class); + $root_package->method('getRequires')->willReturn($data['require']); + $root_package->method('getDevRequires')->willReturn($data['require-dev']); + $root_package->method('getRepositories')->willReturn($data['repositories']); + $root_package->method('getSuggests')->willReturn($data['suggest']); + $root_package->method('getName')->willReturn($root_name); + $root_package->expects($this->atLeastOnce())->method('getExtra')->willReturn($data['extra']); + + return $root_package; + } + + /** + * Returns the composer package directory path. + * + * It also declares a bunch of files for the globs to match. + * + * @param \Composer\Package\Package $package + * The composer package. + * + * @return string + * The install path. + */ + public function getInstallPathCallback(Package $package): string { + $name = explode('/', $package->getName())[1]; + + return vfsStream::url( + $this->rootDirectory->path() . '/' . static::LIBRARIES_DIRECTORY . '/' . $name + ); + } + + /** + * Callback for the LocalRepository getCanonicalPackages function. + */ + public function getCanonicalPackagesCallback() { + $root_package = $this->composer->getPackage(); + /** + * @var $dependencies Link[] + */ + $dependencies = array_merge($root_package->getRequires(), $root_package->getDevRequires()); + $return = []; + + foreach ($dependencies as $dependency) { + /* @see setupVirtualFilesystem + * Create pseudo dependency packages, reading from the virtual filesystem. + */ + $composer_path = 'vendor/' . $dependency->getTarget() . '/composer.json'; + $this->assertTrue($this->rootDirectory->hasChild($composer_path)); + $json = json_decode( + file_get_contents($this->rootDirectory->getChild($composer_path)->url()), + TRUE + ); + $data = array_merge( + [ + 'name' => 'undefined', + 'extra' => [], + ], + $json + ); + + $package = $this->createMock(Package::class); + $package->expects($this->atLeastOnce())->method('getExtra')->willReturn($data['extra']); + $package->method('getName')->willReturn($data['name']); + $return[] = $package; + } + + return $return; + } + + /** + * Create the libraries package structure. + * + * @param string $json_file + * The library structure JSON file. + */ + protected function createLibrariesPackageStructures(string $json_file) { + // Create the directory structure for the libraries so that they exist on + // disk for Finder to work with. + vfsStream::create( + [ + static::LIBRARIES_DIRECTORY => json_decode( + file_get_contents($json_file), + TRUE + ), + ] + ); + } + +} diff --git a/tests/fixtures/all-dependency-libraries/composer.json b/tests/fixtures/all-dependency-libraries/composer.json new file mode 100644 index 0000000..c7341bb --- /dev/null +++ b/tests/fixtures/all-dependency-libraries/composer.json @@ -0,0 +1,8 @@ +{ + "require": { + "zodiacmedia/drupal-libraries-test-dependency": "*@dev" + }, + "extra": { + "drupal-libraries-dependencies": true + } +} diff --git a/tests/fixtures/all-dependency-libraries/installed-libraries.expected.json b/tests/fixtures/all-dependency-libraries/installed-libraries.expected.json new file mode 100644 index 0000000..3191e2e --- /dev/null +++ b/tests/fixtures/all-dependency-libraries/installed-libraries.expected.json @@ -0,0 +1,22 @@ +{ + "schema-version": "1.0", + "installed": { + "test-library-dependency": { + "version": "1.0.0", + "url": "https://example.com/test-library-dependency.zip", + "type": "zip", + "ignore": [], + "package": "zodiacmedia/drupal-libraries-test-dependency" + }, + "test-moment": { + "version": "2.25.0", + "url": "https://registry.npmjs.org/moment/-/moment-2.25.0.tgz", + "type": "tar", + "ignore": [ + "*.md" + ], + "package": "zodiacmedia/drupal-libraries-test-dependency", + "shasum": "e961ab9a5848a1cf2c52b1af4e6c82a8401e7fe9" + } + } +} diff --git a/tests/fixtures/all-dependency-libraries/libraries-structure.json b/tests/fixtures/all-dependency-libraries/libraries-structure.json new file mode 100644 index 0000000..4e9c0dd --- /dev/null +++ b/tests/fixtures/all-dependency-libraries/libraries-structure.json @@ -0,0 +1,10 @@ +{ + "test-library-dependency": { + "index.js": "", + "README.md": "" + }, + "test-moment": { + "moment.js": "", + "README.md": "" + } +} diff --git a/tests/fixtures/download-and-ignore-library-files/composer.json b/tests/fixtures/download-and-ignore-library-files/composer.json new file mode 100644 index 0000000..a97a421 --- /dev/null +++ b/tests/fixtures/download-and-ignore-library-files/composer.json @@ -0,0 +1,30 @@ +{ + "extra": { + "drupal-libraries": { + "flexslider": { + "url": "https://github.com/woocommerce/FlexSlider/archive/2.6.4.zip", + "version": "2.6.4", + "type": "zip", + "ignore": [ + "bower_components", + "demo", + "node_modules" + ] + }, + "moment": { + "url": "https://registry.npmjs.org/moment/-/moment-2.25.0.tgz", + "shasum": "e961ab9a5848a1cf2c52b1af4e6c82a8401e7fe9" + }, + "select2": { + "url": "https://github.com/select2/select2/archive/4.0.13.zip", + "ignore": [ + ".*", + "*.{md}", + "Gruntfile.js", + "{docs,src,tests}" + ] + }, + "simple-color": "https://github.com/recurser/jquery-simple-color/archive/v1.2.2.zip" + } + } +} diff --git a/tests/fixtures/download-and-ignore-library-files/installed-libraries.expected.json b/tests/fixtures/download-and-ignore-library-files/installed-libraries.expected.json new file mode 100644 index 0000000..582531d --- /dev/null +++ b/tests/fixtures/download-and-ignore-library-files/installed-libraries.expected.json @@ -0,0 +1,43 @@ +{ + "schema-version": "1.0", + "installed": { + "flexslider": { + "version": "2.6.4", + "url": "https://github.com/woocommerce/FlexSlider/archive/2.6.4.zip", + "type": "zip", + "ignore": [ + "bower_components", + "demo", + "node_modules" + ], + "package": "drupal-libraries-installer-test/root-package" + }, + "moment": { + "version": "2.25.0", + "url": "https://registry.npmjs.org/moment/-/moment-2.25.0.tgz", + "type": "tar", + "ignore": [], + "package": "drupal-libraries-installer-test/root-package", + "shasum": "e961ab9a5848a1cf2c52b1af4e6c82a8401e7fe9" + }, + "select2": { + "version": "4.0.13", + "url": "https://github.com/select2/select2/archive/4.0.13.zip", + "type": "zip", + "ignore": [ + ".*", + "*.{md}", + "Gruntfile.js", + "{docs,src,tests}" + ], + "package": "drupal-libraries-installer-test/root-package" + }, + "simple-color": { + "version": "v1.2.2", + "url": "https://github.com/recurser/jquery-simple-color/archive/v1.2.2.zip", + "type": "zip", + "ignore": [], + "package": "drupal-libraries-installer-test/root-package" + } + } +} diff --git a/tests/fixtures/download-and-ignore-library-files/libraries-structure.json b/tests/fixtures/download-and-ignore-library-files/libraries-structure.json new file mode 100644 index 0000000..54b058e --- /dev/null +++ b/tests/fixtures/download-and-ignore-library-files/libraries-structure.json @@ -0,0 +1,55 @@ +{ + "flexslider": { + "bower_components": { + ".text.txt": "", + "test.php": "", + "text.txt": "" + }, + "demo": { + "index.html": "", + "demo.js": "", + "demo.css": "", + "Gruntfile.js": "" + }, + "node_modules": { + "sub_module1": [], + "sub_module2": [], + "sub_module3": [] + }, + ".hidden.txt": "", + "Gruntfile.js": "", + "README.md": "" + }, + "moment": { + "moment.js": "" + }, + "select2": { + ".git": [], + ".hidden_directory": [], + "directory-with-hidden-file": { + ".should-not-be-removed.txt": "" + }, + "docs": { + "index.html": "", + "readme.md": "", + "changes.rst": "" + }, + "empty_directory": [], + "src": { + "index.js": "", + "style.css": "", + "index.html": "" + }, + "tests": { + "Test.php": "", + "test.js": "", + "Gruntfile.js": "" + }, + ".hidden.txt": "", + "Gruntfile.js": "", + "README.md": "" + }, + "simple-color": { + "simple-color.js": "" + } +} diff --git a/tests/fixtures/existing-lock-file/composer.json b/tests/fixtures/existing-lock-file/composer.json new file mode 100644 index 0000000..3d9cd46 --- /dev/null +++ b/tests/fixtures/existing-lock-file/composer.json @@ -0,0 +1,7 @@ +{ + "extra": { + "drupal-libraries": { + "simple-color": "https://github.com/recurser/jquery-simple-color/archive/v1.2.2.zip" + } + } +} diff --git a/tests/fixtures/existing-lock-file/installed-libraries.expected.json b/tests/fixtures/existing-lock-file/installed-libraries.expected.json new file mode 100644 index 0000000..0ac0bca --- /dev/null +++ b/tests/fixtures/existing-lock-file/installed-libraries.expected.json @@ -0,0 +1,12 @@ +{ + "schema-version": "1.0", + "installed": { + "simple-color": { + "version": "v1.2.2", + "url": "https://github.com/recurser/jquery-simple-color/archive/v1.2.2.zip", + "type": "zip", + "ignore": [], + "package": "drupal-libraries-installer-test/root-package" + } + } +} diff --git a/tests/fixtures/existing-lock-file/installed-libraries.initial.json b/tests/fixtures/existing-lock-file/installed-libraries.initial.json new file mode 100644 index 0000000..0ac0bca --- /dev/null +++ b/tests/fixtures/existing-lock-file/installed-libraries.initial.json @@ -0,0 +1,12 @@ +{ + "schema-version": "1.0", + "installed": { + "simple-color": { + "version": "v1.2.2", + "url": "https://github.com/recurser/jquery-simple-color/archive/v1.2.2.zip", + "type": "zip", + "ignore": [], + "package": "drupal-libraries-installer-test/root-package" + } + } +} diff --git a/tests/fixtures/existing-lock-file/libraries-structure.json b/tests/fixtures/existing-lock-file/libraries-structure.json new file mode 100644 index 0000000..b58e30f --- /dev/null +++ b/tests/fixtures/existing-lock-file/libraries-structure.json @@ -0,0 +1,5 @@ +{ + "simple-color": { + "simple-color.js": "" + } +} diff --git a/tests/fixtures/specific-dependency-libraries-with-override/composer.json b/tests/fixtures/specific-dependency-libraries-with-override/composer.json new file mode 100644 index 0000000..fd09852 --- /dev/null +++ b/tests/fixtures/specific-dependency-libraries-with-override/composer.json @@ -0,0 +1,16 @@ +{ + "require": { + "zodiacmedia/drupal-libraries-test-dependency": "*@dev" + }, + "extra": { + "drupal-libraries-dependencies": [ + "zodiacmedia/drupal-libraries-test-dependency" + ], + "drupal-libraries": { + "test-moment": { + "url": "https://assets.example.com/moment-override.zip", + "ignore": [] + } + } + } +} diff --git a/tests/fixtures/specific-dependency-libraries-with-override/installed-libraries.expected.json b/tests/fixtures/specific-dependency-libraries-with-override/installed-libraries.expected.json new file mode 100644 index 0000000..55f02f1 --- /dev/null +++ b/tests/fixtures/specific-dependency-libraries-with-override/installed-libraries.expected.json @@ -0,0 +1,19 @@ +{ + "schema-version": "1.0", + "installed": { + "test-moment": { + "version": "1.0.0", + "url": "https://assets.example.com/moment-override.zip", + "type": "zip", + "ignore": [], + "package": "drupal-libraries-installer-test/root-package" + }, + "test-library-dependency": { + "version": "1.0.0", + "url": "https://example.com/test-library-dependency.zip", + "type": "zip", + "ignore": [], + "package": "zodiacmedia/drupal-libraries-test-dependency" + } + } +} diff --git a/tests/fixtures/specific-dependency-libraries-with-override/libraries-structure.json b/tests/fixtures/specific-dependency-libraries-with-override/libraries-structure.json new file mode 100644 index 0000000..4e9c0dd --- /dev/null +++ b/tests/fixtures/specific-dependency-libraries-with-override/libraries-structure.json @@ -0,0 +1,10 @@ +{ + "test-library-dependency": { + "index.js": "", + "README.md": "" + }, + "test-moment": { + "moment.js": "", + "README.md": "" + } +} diff --git a/tests/fixtures/specific-dependency-libraries/composer.json b/tests/fixtures/specific-dependency-libraries/composer.json new file mode 100644 index 0000000..3142496 --- /dev/null +++ b/tests/fixtures/specific-dependency-libraries/composer.json @@ -0,0 +1,10 @@ +{ + "require": { + "zodiacmedia/drupal-libraries-test-dependency": "*@dev" + }, + "extra": { + "drupal-libraries-dependencies": [ + "zodiacmedia/drupal-libraries-test-dependency" + ] + } +} diff --git a/tests/fixtures/specific-dependency-libraries/installed-libraries.expected.json b/tests/fixtures/specific-dependency-libraries/installed-libraries.expected.json new file mode 100644 index 0000000..3191e2e --- /dev/null +++ b/tests/fixtures/specific-dependency-libraries/installed-libraries.expected.json @@ -0,0 +1,22 @@ +{ + "schema-version": "1.0", + "installed": { + "test-library-dependency": { + "version": "1.0.0", + "url": "https://example.com/test-library-dependency.zip", + "type": "zip", + "ignore": [], + "package": "zodiacmedia/drupal-libraries-test-dependency" + }, + "test-moment": { + "version": "2.25.0", + "url": "https://registry.npmjs.org/moment/-/moment-2.25.0.tgz", + "type": "tar", + "ignore": [ + "*.md" + ], + "package": "zodiacmedia/drupal-libraries-test-dependency", + "shasum": "e961ab9a5848a1cf2c52b1af4e6c82a8401e7fe9" + } + } +} diff --git a/tests/fixtures/specific-dependency-libraries/libraries-structure.json b/tests/fixtures/specific-dependency-libraries/libraries-structure.json new file mode 100644 index 0000000..4e9c0dd --- /dev/null +++ b/tests/fixtures/specific-dependency-libraries/libraries-structure.json @@ -0,0 +1,10 @@ +{ + "test-library-dependency": { + "index.js": "", + "README.md": "" + }, + "test-moment": { + "moment.js": "", + "README.md": "" + } +}