diff --git a/automatic_updates.module b/automatic_updates.module index 9f389b04f7128d9d2910f98abbde67ce577c6c6d..c95de6fb40d6d71ec8584f29b44f09c3c638c1d6 100644 --- a/automatic_updates.module +++ b/automatic_updates.module @@ -55,7 +55,7 @@ function automatic_updates_page_top(array &$page_top) { } $results = $checker->getResults('warning'); if ($results) { - \Drupal::messenger()->addWarning(t('Your site does not pass some readiness checks for automatic updates. It might not be completely eligible for <a href="@readiness_checks">automatic updates</a>.', ['@readiness_checks' => 'https://www.drupal.org/docs/8/update/automatic-updates#readiness-checks'])); + \Drupal::messenger()->addWarning(t('Your site does not pass some readiness checks for automatic updates. Depending on the nature of the failures, it might effect the eligibility for <a href="@readiness_checks">automatic updates</a>.', ['@readiness_checks' => 'https://www.drupal.org/docs/8/update/automatic-updates#readiness-checks'])); foreach ($results as $message) { \Drupal::messenger()->addWarning($message); } diff --git a/automatic_updates.services.yml b/automatic_updates.services.yml index ba47c91629d52da91cd1a49685505ec4721ad621..d0168241b455acd54a764d3ac948f428d69430ae 100644 --- a/automatic_updates.services.yml +++ b/automatic_updates.services.yml @@ -48,11 +48,20 @@ services: - '@automatic_updates.drupal_finder' tags: - { name: readiness_checker, category: error} - automatic_updates.modified_code: - class: Drupal\automatic_updates\ReadinessChecker\ModifiedCode + automatic_updates.modified_files: + class: Drupal\automatic_updates\Services\ModifiedFiles arguments: - '@logger.channel.automatic_updates' - '@automatic_updates.drupal_finder' + - '@http_client' + - '@config.factory' + automatic_updates.modified_files_checker: + class: Drupal\automatic_updates\ReadinessChecker\ModifiedFiles + arguments: + - '@automatic_updates.modified_files' + - '@extension.list.module' + - '@extension.list.profile' + - '@extension.list.theme' tags: - { name: readiness_checker, category: warning} automatic_updates.file_ownership: diff --git a/config/install/automatic_updates.settings.yml b/config/install/automatic_updates.settings.yml index 18afbc79f6de87e5cb576b2906e8b4393b81fbdd..799251acd21d103ec82f2f6a74987a342dfec665 100644 --- a/config/install/automatic_updates.settings.yml +++ b/config/install/automatic_updates.settings.yml @@ -1,8 +1,9 @@ # Public service announcement URI endpoint. # TODO: Update to correct end point once it is available. See # https://www.drupal.org/project/automatic_updates/issues/3045273 -psa_endpoint: 'http://localhost/automatic_updates/test-json' +psa_endpoint: 'http://example.com/automatic_updates/test-json' enable_psa: true notify: true check_frequency: 43200 enable_readiness_checks: true +download_uri: 'https://ftp.drupal.org/files/projects' diff --git a/config/schema/automatic_updates.schema.yml b/config/schema/automatic_updates.schema.yml index db243d342027609388026a3a520398e74bbf496e..35a15326ad4a91c74bfa6b1b33e9eb082f119dba 100644 --- a/config/schema/automatic_updates.schema.yml +++ b/config/schema/automatic_updates.schema.yml @@ -17,3 +17,6 @@ automatic_updates.settings: enable_readiness_checks: type: boolean label: 'Enable readiness checks' + download_uri: + type: string + label: 'Endpoint URI for file hashes and quasi patch files' diff --git a/src/ReadinessChecker/ModifiedCode.php b/src/ReadinessChecker/ModifiedCode.php deleted file mode 100644 index f8c0b05b949cdf4131cd05feded04c77de2ad4a9..0000000000000000000000000000000000000000 --- a/src/ReadinessChecker/ModifiedCode.php +++ /dev/null @@ -1,106 +0,0 @@ -<?php - -namespace Drupal\automatic_updates\ReadinessChecker; - -use Drupal\Core\StringTranslation\StringTranslationTrait; -use DrupalFinder\DrupalFinder; -use Psr\Log\LoggerInterface; - -/** - * Modified code checker. - */ -class ModifiedCode implements ReadinessCheckerInterface { - use StringTranslationTrait; - - /** - * The logger. - * - * @var \Psr\Log\LoggerInterface - */ - protected $logger; - - /** - * The Drupal root path. - * - * @var string - */ - protected $drupalRoot; - - /** - * The vendor path. - * - * @var string - */ - protected $vendorPath; - - /** - * The drupal finder service. - * - * @var \DrupalFinder\DrupalFinder - */ - protected $drupalFinder; - - /** - * ReadOnlyFilesystem constructor. - * - * @param \Psr\Log\LoggerInterface $logger - * The logger. - * @param \DrupalFinder\DrupalFinder $drupal_finder - * The Drupal finder. - */ - public function __construct(LoggerInterface $logger, DrupalFinder $drupal_finder) { - $this->logger = $logger; - $this->drupalFinder = $drupal_finder; - } - - /** - * {@inheritdoc} - */ - public function run() { - $messages = []; - if (!$this->getDrupalRoot()) { - $messages[] = $this->t('The Drupal root directory could not be located.'); - return $messages; - } - $this->modifiedCode($messages); - return $messages; - } - - /** - * Get the Drupal root path. - * - * @return string - * The Drupal root path. - */ - protected function getDrupalRoot() { - if (!$this->drupalRoot && $this->drupalFinder->locateRoot(getcwd())) { - $this->drupalRoot = $this->drupalFinder->getDrupalRoot(); - } - return $this->drupalRoot; - } - - /** - * Get the vendor path. - * - * @return string - * The vendor path. - */ - protected function getVendorPath() { - if (!$this->vendorPath && $this->drupalFinder->locateRoot(getcwd())) { - $this->vendorPath = $this->drupalFinder->getVendorDir(); - } - return $this->vendorPath; - } - - /** - * Check if the site contains any modified code. - * - * @param array $messages - * The messages array of translatable strings. - */ - protected function modifiedCode(array &$messages) { - // TODO: Implement file hashing logic against all code files. - // See: https://www.drupal.org/project/automatic_updates/issues/3050804 - } - -} diff --git a/src/ReadinessChecker/ModifiedFiles.php b/src/ReadinessChecker/ModifiedFiles.php new file mode 100644 index 0000000000000000000000000000000000000000..3991254369870459c2c40bb8ca674c7232d7d0d8 --- /dev/null +++ b/src/ReadinessChecker/ModifiedFiles.php @@ -0,0 +1,142 @@ +<?php + +namespace Drupal\automatic_updates\ReadinessChecker; + +use Drupal\automatic_updates\Services\ModifiedFilesInterface; +use Drupal\Core\Extension\ExtensionList; +use Drupal\Core\StringTranslation\StringTranslationTrait; + +/** + * Modified code checker. + */ +class ModifiedFiles implements ReadinessCheckerInterface { + use StringTranslationTrait; + + /** + * The modified files service. + * + * @var \Drupal\automatic_updates\Services\ModifiedFilesInterface + */ + protected $modifiedFiles; + + /** + * The module extension list. + * + * @var \Drupal\Core\Extension\ExtensionList + */ + protected $modules; + + /** + * The profile extension list. + * + * @var \Drupal\Core\Extension\ExtensionList + */ + protected $profiles; + + /** + * The theme extension list. + * + * @var \Drupal\Core\Extension\ExtensionList + */ + protected $themes; + + /** + * An array of array of strings of extension paths. + * + * @var string[]string[] + */ + protected $paths; + + /** + * ModifiedFiles constructor. + * + * @param \Drupal\automatic_updates\Services\ModifiedFilesInterface $modified_files + * The modified files service. + * The config factory. + * @param \Drupal\Core\Extension\ExtensionList $modules + * The module extension list. + * @param \Drupal\Core\Extension\ExtensionList $profiles + * The profile extension list. + * @param \Drupal\Core\Extension\ExtensionList $themes + * The theme extension list. + */ + public function __construct(ModifiedFilesInterface $modified_files, ExtensionList $modules, ExtensionList $profiles, ExtensionList $themes) { + $this->modifiedFiles = $modified_files; + $this->modules = $modules; + $this->profiles = $profiles; + $this->themes = $themes; + } + + /** + * {@inheritdoc} + */ + public function run() { + return $this->modifiedFilesCheck(); + } + + /** + * Check if the site contains any modified code. + * + * @return array + * An array of translatable strings if any checks fail. + */ + protected function modifiedFilesCheck() { + $messages = []; + $extensions = []; + $extensions['drupal'] = $this->modules->get('system')->info; + foreach ($this->getExtensionsTypes() as $extension_type) { + foreach ($this->getInfos($extension_type) as $extension_name => $info) { + if (substr($this->getPath($extension_type, $extension_name), 0, 4) !== 'core') { + $extensions[$extension_name] = $info; + } + } + } + foreach ($this->modifiedFiles->getModifiedFiles($extensions) as $file) { + $messages[] = $this->t('The hash for @file does not match its original. Updates that include that file will fail and require manual intervention.', ['@file' => $file]); + } + return $messages; + } + + /** + * Get the extension types. + * + * @return array + * The extension types. + */ + protected function getExtensionsTypes() { + return ['modules', 'profiles', 'themes']; + } + + /** + * Returns an array of info files information of available extensions. + * + * @param string $extension_type + * The extension type. + * + * @return array + * An associative array of extension information arrays, keyed by extension + * name. + */ + protected function getInfos($extension_type) { + return $this->{$extension_type}->getAllAvailableInfo(); + } + + /** + * Returns an extension file path. + * + * @param string $extension_type + * The extension type. + * @param string $extension_name + * The extension name. + * + * @return string + * An extension file path or NULL if it does not exist. + */ + protected function getPath($extension_type, $extension_name) { + if (!isset($this->paths[$extension_type])) { + $this->paths[$extension_type] = $this->{$extension_type}->getPathnames(); + } + return isset($this->paths[$extension_type][$extension_name]) ? $this->paths[$extension_type][$extension_name] : NULL; + } + +} diff --git a/src/ReadinessChecker/ReadOnlyFilesystem.php b/src/ReadinessChecker/ReadOnlyFilesystem.php index e172e92bb45fad90feae0b7e23b63b82d899266e..2fb8c9ef074ff089c4f29aef78753dc094b80ab7 100644 --- a/src/ReadinessChecker/ReadOnlyFilesystem.php +++ b/src/ReadinessChecker/ReadOnlyFilesystem.php @@ -54,6 +54,9 @@ class ReadOnlyFilesystem extends Filesystem { /** * Check if the filesystem is read only. + * + * @return array + * An array of translatable strings if any checks fail. */ protected function readOnlyCheck() { $messages = []; diff --git a/src/Services/ModifiedFiles.php b/src/Services/ModifiedFiles.php new file mode 100644 index 0000000000000000000000000000000000000000..c0e250a5786b198c539718ee3cf478aaf913c494 --- /dev/null +++ b/src/Services/ModifiedFiles.php @@ -0,0 +1,225 @@ +<?php + +namespace Drupal\automatic_updates\Services; + +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Url; +use DrupalFinder\DrupalFinder; +use GuzzleHttp\ClientInterface; +use GuzzleHttp\Promise\EachPromise; +use Psr\Http\Message\ResponseInterface; +use Psr\Log\LoggerInterface; + +/** + * Modified files service. + */ +class ModifiedFiles implements ModifiedFilesInterface { + + /** + * The logger. + * + * @var \Psr\Log\LoggerInterface + */ + protected $logger; + + /** + * The HTTP client. + * + * @var \GuzzleHttp\ClientInterface + */ + protected $httpClient; + + /** + * The config factory. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected $configFactory; + + /** + * The drupal finder service. + * + * @var \DrupalFinder\DrupalFinder + */ + protected $drupalFinder; + + /** + * ModifiedCode constructor. + * + * @param \Psr\Log\LoggerInterface $logger + * The logger. + * @param \DrupalFinder\DrupalFinder $drupal_finder + * The Drupal finder. + * @param \GuzzleHttp\ClientInterface $http_client + * The HTTP client. + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The config factory. + */ + public function __construct(LoggerInterface $logger, DrupalFinder $drupal_finder, ClientInterface $http_client, ConfigFactoryInterface $config_factory) { + $this->logger = $logger; + $this->drupalFinder = $drupal_finder; + $this->httpClient = $http_client; + $this->configFactory = $config_factory; + $this->drupalFinder->locateRoot(getcwd()); + } + + /** + * {@inheritdoc} + */ + public function getModifiedFiles(array $extensions = []) { + $modified_files = []; + $promises = $this->getHashRequests($extensions); + // Wait until all the requests are finished. + (new EachPromise($promises, [ + 'concurrency' => 4, + 'fulfilled' => function ($resource) use (&$modified_files) { + $this->processHashes($resource, $modified_files); + }, + ]))->promise()->wait(); + return $modified_files; + } + + /** + * Process checking hashes of files from external URL. + * + * @param resource $resource + * A resource handle. + * @param array $modified_files + * The list of modified files. + */ + protected function processHashes($resource, array &$modified_files) { + while (($line = fgets($resource)) !== FALSE) { + list($hash, $file) = preg_split('/\s+/', $line, 2); + $file = trim($file); + // If the line is empty, proceed to the next line. + if (empty($hash) && empty($file)) { + continue; + } + // If one of the values is invalid, log and continue. + if (!$hash || !$file) { + $this->logger->error('@hash or @file is empty; the hash file is malformed for this line.', ['@hash' => $hash, '@file' => $file]); + continue; + } + $file_path = $this->drupalFinder->getDrupalRoot() . DIRECTORY_SEPARATOR . $file; + if (!file_exists($file_path) || hash_file('sha512', $file_path) !== $hash) { + $modified_files[] = $file_path; + } + } + if (!feof($resource)) { + $this->logger->error('Stream for resource closed prematurely.'); + } + fclose($resource); + } + + /** + * Get an iterator of promises that return a resource stream. + * + * @param array $extensions + * The list of extensions, keyed by extension name and value the info array. + * + * @codingStandardsIgnoreStart + * + * @return \Generator + * + * @@codingStandardsIgnoreEnd + */ + protected function getHashRequests(array $extensions) { + foreach ($extensions as $extension_name => $info) { + $url = $this->buildUrl($extension_name, $info); + yield $this->getPromise($url); + } + } + + /** + * Get a promise. + * + * @param string $url + * The URL. + * + * @return \GuzzleHttp\Promise\PromiseInterface + * The promise. + */ + protected function getPromise($url) { + return $this->httpClient->requestAsync('GET', $url, [ + 'stream' => TRUE, + 'read_timeout' => 30, + ]) + ->then(function (ResponseInterface $response) { + return $response->getBody()->detach(); + }); + } + + /** + * Build an extension's hash file URL. + * + * @param string $extension_name + * The extension name. + * @param array $info + * The extension's info. + * + * @return string + * The URL endpoint with for an extension. + */ + protected function buildUrl($extension_name, array $info) { + $version = $this->getExtensionVersion($extension_name, $info); + $project_name = $this->getProjectName($extension_name, $info); + $hash_name = $this->getHashName($info); + $uri = ltrim($this->configFactory->get('automatic_updates.settings')->get('download_uri'), '/'); + return Url::fromUri($uri . "/$project_name/$version/$hash_name")->toString(); + } + + /** + * Get the extension version. + * + * @param string $extension_name + * The extension name. + * @param array $info + * The extension's info. + * + * @return string|null + * The version or NULL if undefined. + */ + protected function getExtensionVersion($extension_name, array $info) { + $version = isset($info['version']) ? $info['version'] : NULL; + // TODO: consider using ocramius/package-versions to discover the installed + // version from composer.lock. + // See https://www.drupal.org/project/automatic_updates/issues/3054002 + return $version; + } + + /** + * Get the extension's project name. + * + * @param string $extension_name + * The extension name. + * @param array $info + * The extension's info. + * + * @return string + * The project name or fallback to extension name if project is undefined. + */ + protected function getProjectName($extension_name, array $info) { + $project_name = isset($info['project']) ? $info['project'] : $extension_name; + // TODO: parse the composer.json for the name if it isn't set in info. + // See https://www.drupal.org/project/automatic_updates/issues/3054002. + return $project_name; + } + + /** + * Get the hash file name. + * + * @param array $info + * The extension's info. + * + * @return string|null + * The hash name. + */ + protected function getHashName(array $info) { + $hash_name = 'SHA512SUMS'; + if (isset($info['project'])) { + $hash_name .= '-package'; + } + return $hash_name; + } + +} diff --git a/src/Services/ModifiedFilesInterface.php b/src/Services/ModifiedFilesInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..982bd3ed425afc4bac2064c5b371a6ef09efa56e --- /dev/null +++ b/src/Services/ModifiedFilesInterface.php @@ -0,0 +1,22 @@ +<?php + +namespace Drupal\automatic_updates\Services; + +/** + * Modified files service interface. + */ +interface ModifiedFilesInterface { + + /** + * Get list of modified files. + * + * @param array $extensions + * The list of extensions, keyed by extension name with values an info + * array. + * + * @return array + * The modified files. + */ + public function getModifiedFiles(array $extensions = []); + +} diff --git a/tests/modules/test_automatic_updates/src/Controller/HashesController.php b/tests/modules/test_automatic_updates/src/Controller/HashesController.php new file mode 100644 index 0000000000000000000000000000000000000000..f2026c9eb4b15e19d009b9a37a4f29fd8ad80d87 --- /dev/null +++ b/tests/modules/test_automatic_updates/src/Controller/HashesController.php @@ -0,0 +1,37 @@ +<?php + +namespace Drupal\test_automatic_updates\Controller; + +use Drupal\Core\Controller\ControllerBase; +use Symfony\Component\HttpFoundation\Response; + +/** + * Class HashesController. + */ +class HashesController extends ControllerBase { + + /** + * Test hashes controller. + * + * @param string $extension + * The extension name. + * @param string $version + * The version string. + * + * @return \Symfony\Component\HttpFoundation\Response + * A file with hashes. + */ + public function hashes($extension, $version) { + $response = Response::create(); + $response->headers->set('Content-Type', 'text/plain'); + if ($extension === 'core' && $version === '8.7.0') { + $response->setContent("2cedbfcde76961b1f65536e3c69e13d8ad850619235f4aa2752ae66fe5e5a2d928578279338f099b5318d92c410040e995cb62ba1cc4512ec17cf21715c760a2 core/LICENSE.txt\n"); + } + elseif ($extension === 'core' && $version === '8.0.0') { + // Fake out a change in the LICENSE.txt. + $response->setContent("2d4ce6b272311ca4159056fb75138eba1814b65323c35ae5e0978233918e45e62bb32fdd2e0e8f657954fd5823c045762b3b59645daf83246d88d8797726e02c core/LICENSE.txt\n"); + } + return $response; + } + +} diff --git a/tests/modules/test_automatic_updates/test_automatic_updates.routing.yml b/tests/modules/test_automatic_updates/test_automatic_updates.routing.yml index 1920578446758ecc6567b1a121808fa97f9f9e35..7c9ce3413268e95199dd72aef1517776cb2dc40b 100644 --- a/tests/modules/test_automatic_updates/test_automatic_updates.routing.yml +++ b/tests/modules/test_automatic_updates/test_automatic_updates.routing.yml @@ -12,3 +12,10 @@ test_automatic_updates.json_test_denied_controller: _title: 'JSON' requirements: _access: 'FALSE' +test_automatic_updates.hashes_endpoint: + path: '/automatic_updates/{extension}/{version}/SHA512SUMS' + defaults: + _controller: '\Drupal\test_automatic_updates\Controller\HashesController::hashes' + _title: 'SHA512SUMS' + requirements: + _access: 'TRUE' diff --git a/tests/src/Functional/AutomaticUpdatesTest.php b/tests/src/Functional/AutomaticUpdatesTest.php index 1ce69e985b4103473810c3af10893efff17cc6c2..7f5c337b63ff99cadfd0f0948bf51c95e335fb39 100644 --- a/tests/src/Functional/AutomaticUpdatesTest.php +++ b/tests/src/Functional/AutomaticUpdatesTest.php @@ -92,9 +92,11 @@ class AutomaticUpdatesTest extends BrowserTestBase { */ public function testReadinessChecks() { // Test manually running readiness checks. + $url = $this->buildUrl('<front>') . '/automatic_updates'; + $this->config('automatic_updates.settings')->set('download_uri', $url); $this->drupalGet(Url::fromRoute('automatic_updates.settings')); $this->clickLink('run the readiness checks'); - $this->assertSession()->pageTextContains('Your site does not pass some readiness checks for automatic updates. It might not be completely eligible for automatic updates.'); + $this->assertSession()->pageTextContains('Your site does not pass some readiness checks for automatic updates. Depending on the nature of the failures, it might effect the eligibility for automatic updates.'); } } diff --git a/tests/src/Functional/ModifiedFilesTest.php b/tests/src/Functional/ModifiedFilesTest.php new file mode 100644 index 0000000000000000000000000000000000000000..e7df3e7acdb97ba61a15f0d5924ad25370d8f4e7 --- /dev/null +++ b/tests/src/Functional/ModifiedFilesTest.php @@ -0,0 +1,84 @@ +<?php + +namespace Drupal\Tests\automatic_updates\Functional; + +use Drupal\automatic_updates\Services\ModifiedFiles; +use Drupal\Core\Url; +use Drupal\Tests\BrowserTestBase; + +/** + * Tests of automatic updates. + * + * @group automatic_updates + */ +class ModifiedFilesTest extends BrowserTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'automatic_updates', + 'test_automatic_updates', + ]; + + /** + * Tests modified files service. + */ + public function testModifiedFiles() { + // No modified code. + $modified_files = new TestModifiedFiles( + $this->container->get('logger.channel.automatic_updates'), + $this->container->get('automatic_updates.drupal_finder'), + $this->container->get('http_client'), + $this->container->get('config.factory') + ); + $this->initHashesEndpoint($modified_files, 'core', '8.7.0'); + $files = $modified_files->getModifiedFiles(['system' => []]); + $this->assertEmpty($files); + + // Hash doesn't match i.e. modified code, including contrib logic. + $this->initHashesEndpoint($modified_files, 'core', '8.0.0'); + $files = $modified_files->getModifiedFiles(['system' => []]); + $this->assertCount(1, $files); + $this->assertStringEndsWith('core/LICENSE.txt', $files[0]); + } + + /** + * Set the hashes endpoint. + * + * @param TestModifiedFiles $modified_code + * The modified code object. + * @param string $extension + * The extension name. + * @param string $version + * The version. + */ + protected function initHashesEndpoint(TestModifiedFiles $modified_code, $extension, $version) { + $modified_code->endpoint = $this->buildUrl(Url::fromRoute('test_automatic_updates.hashes_endpoint', [ + 'extension' => $extension, + 'version' => $version, + ])); + } + +} + +/** + * Class TestModifiedCode. + */ +class TestModifiedFiles extends ModifiedFiles { + + /** + * The endpoint url. + * + * @var string + */ + public $endpoint; + + /** + * {@inheritdoc} + */ + protected function buildUrl($extension_name, array $info) { + return $this->endpoint; + } + +} diff --git a/tests/src/Kernel/ReadinessChecker/ModifiedCodeTest.php b/tests/src/Kernel/ReadinessChecker/ModifiedCodeTest.php deleted file mode 100644 index 0606aa64ad38ddf9614b0e0fe5e9c20e6114691a..0000000000000000000000000000000000000000 --- a/tests/src/Kernel/ReadinessChecker/ModifiedCodeTest.php +++ /dev/null @@ -1,32 +0,0 @@ -<?php - -namespace Drupal\Tests\automatic_updates\Kernel\ReadinessChecker; - -use Drupal\automatic_updates\ReadinessChecker\ModifiedCode; -use Drupal\KernelTests\KernelTestBase; - -/** - * Tests modified code readiness checking. - * - * @group automatic_updates - */ -class ModifiedCodeTest extends KernelTestBase { - - /** - * {@inheritdoc} - */ - public static $modules = [ - 'automatic_updates', - ]; - - /** - * Tests the functionality of modified code readiness checks. - */ - public function testModifiedCode() { - // No modified code. - $modified_code = new ModifiedCode($this->container->get('logger.channel.automatic_updates'), $this->container->get('automatic_updates.drupal_finder')); - $messages = $modified_code->run(); - $this->assertEmpty($messages); - } - -} diff --git a/tests/src/Kernel/ReadinessChecker/ModifiedFilesTest.php b/tests/src/Kernel/ReadinessChecker/ModifiedFilesTest.php new file mode 100644 index 0000000000000000000000000000000000000000..0aa2340d8dc5eac119f0fed530d9f76d4bfe51f0 --- /dev/null +++ b/tests/src/Kernel/ReadinessChecker/ModifiedFilesTest.php @@ -0,0 +1,52 @@ +<?php + +namespace Drupal\Tests\automatic_updates\Kernel; + +use Drupal\automatic_updates\ReadinessChecker\ModifiedFiles; +use Drupal\automatic_updates\Services\ModifiedFilesInterface; +use Drupal\KernelTests\KernelTestBase; +use Prophecy\Argument; + +/** + * Tests of automatic updates. + * + * @group automatic_updates + */ +class ModifiedFilesTest extends KernelTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'automatic_updates', + 'test_automatic_updates', + ]; + + /** + * Tests modified files service. + */ + public function testModifiedFiles() { + /** @var \Prophecy\Prophecy\ObjectProphecy|\Drupal\automatic_updates\Services\ModifiedFilesInterface $service */ + $service = $this->prophesize(ModifiedFilesInterface::class); + $service->getModifiedFiles(Argument::type('array'))->willReturn([]); + $modules = $this->container->get('extension.list.module'); + $profiles = $this->container->get('extension.list.profile'); + $themes = $this->container->get('extension.list.theme'); + + // No modified code. + $modified_files = new ModifiedFiles( + $service->reveal(), + $modules, + $profiles, + $themes + ); + $messages = $modified_files->run(); + $this->assertEmpty($messages); + + // Hash doesn't match i.e. modified code. + $service->getModifiedFiles(Argument::type('array'))->willReturn(['core/LICENSE.txt']); + $messages = $modified_files->run(); + $this->assertCount(1, $messages); + } + +}