You are here

class CoreUpdateTest in Automatic Updates 8.2

Tests an end-to-end update of Drupal core.

@group automatic_updates

Hierarchy

Expanded class hierarchy of CoreUpdateTest

File

tests/src/Build/CoreUpdateTest.php, line 10

Namespace

Drupal\Tests\automatic_updates\Build
View source
class CoreUpdateTest extends UpdateTestBase {

  /**
   * {@inheritdoc}
   */
  protected function createTestSite(string $template) : void {

    // Build the test site and alter its copy of core so that it thinks it's
    // running Drupal 9.8.0, which will never actually exist in the real world.
    // Then, prepare a secondary copy of the core code base, masquerading as
    // Drupal 9.8.1, which will be the version of core we update to. These two
    // versions are referenced in the fake release metadata in our fake release
    // metadata (see fixtures/release-history/drupal.0.0.xml).
    parent::createTestSite($template);
    $this
      ->setCoreVersion($this
      ->getWebRoot() . '/core', '9.8.0');
    $this
      ->alterPackage($this
      ->getWorkspaceDirectory(), $this
      ->getConfigurationForUpdate('9.8.1'));

    // Install Drupal and ensure it's using the fake release metadata to fetch
    // information about available updates.
    $this
      ->installQuickStart('minimal');
    $this
      ->setReleaseMetadata([
      'drupal' => '9.8.1-security',
    ]);
    $this
      ->formLogin($this->adminUsername, $this->adminPassword);
    $this
      ->installModules([
      'automatic_updates',
      'automatic_updates_test',
      'update_test',
    ]);

    // Ensure that Drupal thinks we are running 9.8.0, then refresh information
    // about available updates.
    $this
      ->assertCoreVersion('9.8.0');
    $this
      ->checkForUpdates();

    // Ensure that an update to 9.8.1 is available.
    $this
      ->visit('/admin/modules/automatic-update');
    $this
      ->getMink()
      ->assertSession()
      ->pageTextContains('9.8.1');
  }

  /**
   * {@inheritdoc}
   */
  protected function tearDown() : void {
    if ($this->destroyBuild) {
      $this
        ->deleteCopiedPackages();
    }
    parent::tearDown();
  }

  /**
   * Modifies a Drupal core code base to set its version.
   *
   * @param string $dir
   *   The directory of the Drupal core code base.
   * @param string $version
   *   The version number to set.
   */
  private function setCoreVersion(string $dir, string $version) : void {
    $this
      ->alterPackage($dir, [
      'version' => $version,
    ]);
    $drupal_php = "{$dir}/lib/Drupal.php";
    $this
      ->assertIsWritable($drupal_php);
    $code = file_get_contents($drupal_php);
    $code = preg_replace("/const VERSION = '([0-9]+\\.?){3}(-dev)?';/", "const VERSION = '{$version}';", $code);
    file_put_contents($drupal_php, $code);
  }

  /**
   * Returns composer.json changes that are needed to update core.
   *
   * This will clone the following packages into temporary directories:
   * - drupal/core
   * - drupal/core-recommended
   * - drupal/core-project-message
   * - drupal/core-composer-scaffold
   * The cloned packages will be assigned the given version number, and the test
   * site's composer.json will use the clones as path repositories.
   *
   * @param string $version
   *   The version of core we will be updating to.
   *
   * @return array
   *   The changes to merge into the test site's composer.json.
   */
  protected function getConfigurationForUpdate(string $version) : array {
    $repositories = [];

    // Create a fake version of core with the given version number, and change
    // its README so that we can actually be certain that we update to this
    // fake version.
    $dir = $this
      ->copyPackage($this
      ->getWebRoot() . '/core');
    $this
      ->setCoreVersion($dir, $version);
    file_put_contents("{$dir}/README.txt", "Placeholder for Drupal core {$version}.");
    $repositories['drupal/core'] = $this
      ->createPathRepository($dir);
    $drupal_root = $this
      ->getDrupalRoot();

    // Create a fake version of drupal/core-recommended which itself requires
    // the fake version of core we just created.
    $dir = $this
      ->copyPackage("{$drupal_root}/composer/Metapackage/CoreRecommended");
    $this
      ->alterPackage($dir, [
      'require' => [
        'drupal/core' => $version,
      ],
      'version' => $version,
    ]);
    $repositories['drupal/core-recommended'] = $this
      ->createPathRepository($dir);

    // Create a fake version of drupal/core-project-message.
    $dir = $this
      ->copyPackage("{$drupal_root}/composer/Plugin/ProjectMessage");
    $this
      ->alterPackage($dir, [
      'version' => $version,
    ]);
    $repositories['drupal/core-project-message'] = $this
      ->createPathRepository($dir);

    // Create a fake version of drupal/core-composer-scaffold.
    $dir = $this
      ->copyPackage("{$drupal_root}/composer/Plugin/Scaffold");
    $this
      ->alterPackage($dir, [
      'version' => $version,
    ]);
    $repositories['drupal/core-composer-scaffold'] = $this
      ->createPathRepository($dir);

    // Create a fake version of drupal/core-vendor-hardening.
    $dir = $this
      ->copyPackage("{$drupal_root}/composer/Plugin/VendorHardening");
    $this
      ->alterPackage($dir, [
      'version' => $version,
    ]);
    $repositories['drupal/core-vendor-hardening'] = $this
      ->createPathRepository($dir);
    return [
      'repositories' => $repositories,
    ];
  }

  /**
   * Data provider for end-to-end update tests.
   *
   * @return array[]
   *   Sets of arguments to pass to the test method.
   */
  public function providerTemplate() : array {
    return [
      [
        'drupal/recommended-project',
      ],
      [
        'drupal/legacy-project',
      ],
    ];
  }

  /**
   * Tests an end-to-end core update via the API.
   *
   * @param string $template
   *   The template project from which to build the test site.
   *
   * @dataProvider providerTemplate
   */
  public function testApi(string $template) : void {
    $this
      ->createTestSite($template);
    $mink = $this
      ->getMink();
    $assert_session = $mink
      ->assertSession();

    // Ensure that the update is prevented if the web root and/or vendor
    // directories are not writable.
    $this
      ->assertReadOnlyFileSystemError($template, '/automatic-update-test/update/9.8.1');
    $mink
      ->getSession()
      ->reload();
    $assert_session
      ->pageTextContains('9.8.1');
  }

  /**
   * Tests an end-to-end core update via the UI.
   *
   * @param string $template
   *   The template project from which to build the test site.
   *
   * @dataProvider providerTemplate
   */
  public function testUi(string $template) : void {
    $this
      ->createTestSite($template);
    $mink = $this
      ->getMink();
    $session = $mink
      ->getSession();
    $page = $session
      ->getPage();
    $assert_session = $mink
      ->assertSession();
    $this
      ->visit('/admin/modules');
    $assert_session
      ->pageTextContains('There is a security update available for your version of Drupal.');
    $page
      ->clickLink('Update');

    // Ensure that the update is prevented if the web root and/or vendor
    // directories are not writable.
    $this
      ->assertReadOnlyFileSystemError($template, parse_url($session
      ->getCurrentUrl(), PHP_URL_PATH));
    $session
      ->reload();
    $assert_session
      ->pageTextNotContains('There is a security update available for your version of Drupal.');
    $page
      ->pressButton('Update');
    $this
      ->waitForBatchJob();
    $assert_session
      ->pageTextContains('Ready to update');
    $page
      ->pressButton('Continue');
    $this
      ->waitForBatchJob();
    $assert_session
      ->pageTextContains('Update complete!');
    $assert_session
      ->pageTextNotContains('There is a security update available for your version of Drupal.');
    $this
      ->assertUpdateSuccessful();
  }

  /**
   * Tests an end-to-end core update via cron.
   *
   * @param string $template
   *   The template project from which to build the test site.
   *
   * @dataProvider providerTemplate
   */
  public function testCron(string $template) : void {
    $this
      ->createTestSite($template);
    $this
      ->visit('/admin/reports/status');
    $this
      ->getMink()
      ->getSession()
      ->getPage()
      ->clickLink('Run cron');
    $this
      ->assertUpdateSuccessful();
  }

  /**
   * Asserts that the update is prevented if the filesystem isn't writable.
   *
   * @param string $template
   *   The project template used to build the test site. See ::createTestSite()
   *   for the possible values.
   * @param string $url
   *   A URL where we can see the error message which is raised when parts of
   *   the file system are not writable. This URL will be visited twice: once
   *   for the web root, and once for the vendor directory.
   */
  private function assertReadOnlyFileSystemError(string $template, string $url) : void {
    $directories = [
      'Drupal' => rtrim($this
        ->getWebRoot(), './'),
    ];

    // The location of the vendor directory depends on which project template
    // was used to build the test site.
    if ($template === 'drupal/recommended-project') {
      $directories['vendor'] = $this
        ->getWorkspaceDirectory() . '/vendor';
    }
    elseif ($template === 'drupal/legacy-project') {
      $directories['vendor'] = $directories['Drupal'] . '/vendor';
    }
    $assert_session = $this
      ->getMink()
      ->assertSession();
    foreach ($directories as $type => $path) {
      chmod($path, 0555);
      $this
        ->assertDirectoryIsNotWritable($path);
      $this
        ->visit($url);
      $assert_session
        ->pageTextContains("The {$type} directory \"{$path}\" is not writable.");
      chmod($path, 0755);
      $this
        ->assertDirectoryIsWritable($path);
    }
  }

  /**
   * Asserts that Drupal core was successfully updated.
   */
  private function assertUpdateSuccessful() : void {

    // The update form should not have any available updates.
    // @todo Figure out why this assertion fails when the batch processor
    //   redirects directly to the update form, instead of update.status, when
    //   updating via the UI.
    $this
      ->visit('/admin/modules/automatic-update');
    $this
      ->getMink()
      ->assertSession()
      ->pageTextContains('No update available');

    // The status page should report that we're running Drupal 9.8.1.
    $this
      ->assertCoreVersion('9.8.1');

    // The fake placeholder text from ::getConfigurationForUpdate() should be
    // present in the README.
    $placeholder = file_get_contents($this
      ->getWebRoot() . '/core/README.txt');
    $this
      ->assertSame('Placeholder for Drupal core 9.8.1.', $placeholder);
  }

}

Members

Namesort descending Modifiers Type Description Overrides
BuildTestBase::$commandProcess private property The most recent command process.
BuildTestBase::$destroyBuild protected property Default to destroying build artifacts after a test finishes.
BuildTestBase::$hostName private static property Our native host name, used by PHP when it starts up the server.
BuildTestBase::$hostPort private property Port that will be tested.
BuildTestBase::$mink private property The Mink session manager.
BuildTestBase::$portLocks private property A list of ports used by the test.
BuildTestBase::$serverDocroot private property The docroot for the server process.
BuildTestBase::$serverProcess private property The process that's running the HTTP server.
BuildTestBase::$workspaceDir private property The working directory where this test will manipulate files.
BuildTestBase::assertCommandExitCode public function Asserts that the last command returned the specified exit code.
BuildTestBase::assertCommandOutputContains public function Assert that text is present in the output of the most recent command.
BuildTestBase::assertCommandSuccessful public function Asserts that the last command ran without error.
BuildTestBase::assertDrupalVisit public function Helper function to assert that the last visit was a Drupal site.
BuildTestBase::assertErrorOutputContains public function Assert that text is present in the error output of the most recent command.
BuildTestBase::checkPortIsAvailable protected function Checks whether a port is available.
BuildTestBase::copyCodebase public function Copy the current working codebase into a workspace.
BuildTestBase::executeCommand public function Run a command.
BuildTestBase::findAvailablePort protected function Discover an available port number.
BuildTestBase::getCodebaseFinder public function Get a default Finder object for a Drupal codebase.
BuildTestBase::getDrupalRoot protected function Get the root path of this Drupal codebase.
BuildTestBase::getMink public function Get the Mink instance.
BuildTestBase::getPortNumber protected function Get the port number for requests.
BuildTestBase::getWorkingPath protected function Get the working directory within the workspace, creating if necessary.
BuildTestBase::getWorkspaceDirectory public function Full path to the workspace where this test can build.
BuildTestBase::initMink protected function Set up the Mink session manager.
BuildTestBase::instantiateServer protected function Do the work of making a server process.
BuildTestBase::setUp protected function
BuildTestBase::setUpBeforeClass public static function
BuildTestBase::standUpServer protected function Makes a local test server using PHP's internal HTTP server.
BuildTestBase::stopServer protected function Stop the HTTP server, zero out all necessary variables.
CoreUpdateTest::assertReadOnlyFileSystemError private function Asserts that the update is prevented if the filesystem isn't writable.
CoreUpdateTest::assertUpdateSuccessful private function Asserts that Drupal core was successfully updated.
CoreUpdateTest::createTestSite protected function Uses our already-installed dependencies to build a test site to update. Overrides UpdateTestBase::createTestSite
CoreUpdateTest::getConfigurationForUpdate protected function Returns composer.json changes that are needed to update core.
CoreUpdateTest::providerTemplate public function Data provider for end-to-end update tests.
CoreUpdateTest::setCoreVersion private function Modifies a Drupal core code base to set its version.
CoreUpdateTest::tearDown protected function Overrides UpdateTestBase::tearDown
CoreUpdateTest::testApi public function Tests an end-to-end core update via the API.
CoreUpdateTest::testCron public function Tests an end-to-end core update via cron.
CoreUpdateTest::testUi public function Tests an end-to-end core update via the UI.
ExternalCommandRequirementsTrait::$existingCommands private static property A list of existing external commands we've already discovered.
ExternalCommandRequirementsTrait::checkClassCommandRequirements private static function Checks whether required external commands are available per test class.
ExternalCommandRequirementsTrait::checkExternalCommandRequirements private static function Checks missing external command requirements.
ExternalCommandRequirementsTrait::checkMethodCommandRequirements private static function Checks whether required external commands are available per method.
ExternalCommandRequirementsTrait::externalCommandIsAvailable private static function Determine if an external command is available. 3
JsonTrait::readJson protected function Reads JSON data from a file and returns it as an array.
JsonTrait::writeJson protected function Writes an array of data to a file as JSON.
LocalPackagesTrait::$copiedPackages private property The paths of temporary copies of packages.
LocalPackagesTrait::alterPackage protected function Alters a package's composer.json file.
LocalPackagesTrait::copyPackage protected function Copies a package's entire directory to another location. Aliased as: traitCopyPackage
LocalPackagesTrait::createPathRepository protected function Defines a local path repository for a given path.
LocalPackagesTrait::deleteCopiedPackages protected function Deletes all copied packages.
LocalPackagesTrait::getLocalPackageRepositories protected function Generates local path repositories for a set of installed packages.
LocalPackagesTrait::getPackagePath protected function Returns the path of an installed package, relative to composer.json. Aliased as: traitGetPackagePath
LocalPackagesTrait::getPackagesFromLockFile private function Reads all package information from a composer.lock file.
PhpunitCompatibilityTrait::getMock Deprecated public function Returns a mock object for the specified class using the available method.
PhpunitCompatibilityTrait::setExpectedException Deprecated public function Compatibility layer for PHPUnit 6 to support PHPUnit 4 code.
QuickStartTestBase::$adminPassword protected property Password of the admin account generated during install.
QuickStartTestBase::$adminUsername protected property User name of the admin account generated during install.
SettingsTrait::addSettings protected function Appends some PHP code to settings.php.
SettingsTrait::makeSettingsWritable private function Ensures that settings.php is writable.
UpdateTestBase::$metadataServer private property A secondary server instance, to serve XML metadata about available updates.
UpdateTestBase::$webRoot private property The test site's document root, relative to the workspace directory.
UpdateTestBase::assertCoreVersion protected function Asserts that a specific version of Drupal core is running.
UpdateTestBase::checkForUpdates protected function Checks for available updates.
UpdateTestBase::copyPackage protected function
UpdateTestBase::formLogin public function Helper that uses Drupal's user/login form to log in. Overrides QuickStartTestBase::formLogin
UpdateTestBase::getInitialConfiguration protected function Returns the initial data to write to the test site's composer.json.
UpdateTestBase::getPackagePath protected function
UpdateTestBase::getWebRoot protected function Returns the full path to the test site's document root.
UpdateTestBase::installModules protected function Installs modules in the UI.
UpdateTestBase::installQuickStart public function Install a Drupal site using the quick start feature. Overrides QuickStartTestBase::installQuickStart
UpdateTestBase::setReleaseMetadata protected function Prepares the test site to serve an XML feed of available release metadata.
UpdateTestBase::visit public function Visit a URI on the HTTP server. Overrides BuildTestBase::visit
UpdateTestBase::waitForBatchJob protected function Waits for an active batch job to finish.