From 7933e07a80f61686b717cb1c1001b6c068a6830c Mon Sep 17 00:00:00 2001
From: tedbow <tedbow@240860.no-reply.drupal.org>
Date: Tue, 19 Apr 2022 22:00:13 +0000
Subject: [PATCH] Issue #3276072 by tedbow: UpdateReleaseValidator doesn't
 handle legacy version numbers

---
 .../src/ExtensionUpdater.php                  |  26 +--
 .../src/LegacyVersionUtility.php              |  84 ++++++++
 .../src/Validator/UpdateReleaseValidator.php  |  27 ++-
 .../release-history/aaa_update_test.1.1.xml   | 184 ++++++++++++++++++
 .../tests/src/Functional/UpdaterFormTest.php  |  46 +++--
 .../Validator/UpdateReleaseValidatorTest.php  |  23 ++-
 .../src/Unit/LegacyVersionUtilityTest.php     |  75 +++++++
 7 files changed, 415 insertions(+), 50 deletions(-)
 create mode 100644 automatic_updates_extensions/src/LegacyVersionUtility.php
 create mode 100644 automatic_updates_extensions/tests/fixtures/release-history/aaa_update_test.1.1.xml
 create mode 100644 automatic_updates_extensions/tests/src/Unit/LegacyVersionUtilityTest.php

diff --git a/automatic_updates_extensions/src/ExtensionUpdater.php b/automatic_updates_extensions/src/ExtensionUpdater.php
index c40d4b7c0f..5fc6e2057a 100644
--- a/automatic_updates_extensions/src/ExtensionUpdater.php
+++ b/automatic_updates_extensions/src/ExtensionUpdater.php
@@ -37,7 +37,7 @@ class ExtensionUpdater extends Stage {
     foreach ($project_versions as $project_name => $version) {
       $package = "drupal/$project_name";
       $group = array_key_exists($package, $require_dev) ? 'dev' : 'production';
-      $package_versions[$group][$package] = static::convertToSemanticVersion($version);
+      $package_versions[$group][$package] = LegacyVersionUtility::convertToSemanticVersion($version);
     }
 
     // Ensure that package versions are available to pre-create event
@@ -92,28 +92,4 @@ class ExtensionUpdater extends Stage {
     }
   }
 
-  /**
-   * Converts version numbers to semantic versions if needed.
-   *
-   * @param string $project_version
-   *   The version number.
-   *
-   * @return string
-   *   The version number, converted if needed.
-   */
-  private static function convertToSemanticVersion(string $project_version): string {
-    if (stripos($project_version, '8.x-') === 0) {
-      $project_version = substr($project_version, 4);
-      $version_parts = explode('-', $project_version);
-      $project_version = $version_parts[0] . '.0';
-      if (count($version_parts) === 2) {
-        $project_version .= '-' . $version_parts[1];
-      }
-      return $project_version;
-    }
-    else {
-      return $project_version;
-    }
-  }
-
 }
diff --git a/automatic_updates_extensions/src/LegacyVersionUtility.php b/automatic_updates_extensions/src/LegacyVersionUtility.php
new file mode 100644
index 0000000000..d2daffa440
--- /dev/null
+++ b/automatic_updates_extensions/src/LegacyVersionUtility.php
@@ -0,0 +1,84 @@
+<?php
+
+namespace Drupal\automatic_updates_extensions;
+
+use Drupal\Core\Extension\ExtensionVersion;
+
+/**
+ * A utility class for dealing with legacy version numbers.
+ *
+ * @internal
+ *    This is an internal utility class that could change in any release and
+ *    should not be used by external code.
+ */
+final class LegacyVersionUtility {
+
+  /**
+   * Converts a version number to a semantic version if needed.
+   *
+   * @param string $version
+   *   The version number.
+   *
+   * @return string
+   *   The version number, converted if needed.
+   */
+  public static function convertToSemanticVersion(string $version): string {
+    if (self::isLegacyVersion($version)) {
+      $version = substr($version, 4);
+      $version_parts = explode('-', $version);
+      $version = $version_parts[0] . '.0';
+      if (count($version_parts) === 2) {
+        $version .= '-' . $version_parts[1];
+      }
+      return $version;
+    }
+    else {
+      return $version;
+    }
+  }
+
+  /**
+   * Converts a version number to a legacy version if needed and possible.
+   *
+   * @param string $version_string
+   *   The version number.
+   *
+   * @return string
+   *   The version number, converted if needed, or NULL if not possible. Only
+   *   semantic version numbers that have patch level of 0 can be converted into
+   *   legacy version numbers.
+   */
+  public static function convertToLegacyVersion($version_string): ?string {
+    if (self::isLegacyVersion($version_string)) {
+      return $version_string;
+    }
+    $version = ExtensionVersion::createFromVersionString($version_string);
+    if ($extra = $version->getVersionExtra()) {
+      $version_string_without_extra = str_replace("-$extra", '', $version_string);
+    }
+    else {
+      $version_string_without_extra = $version_string;
+    }
+    [,, $patch] = explode('.', $version_string_without_extra);
+    // A semantic version can only be converted to legacy if it's patch level is
+    // '0'.
+    if ($patch !== '0') {
+      return NULL;
+    }
+    return '8.x-' . $version->getMajorVersion() . '.' . $version->getMinorVersion() . ($extra ? "-$extra" : '');
+  }
+
+  /**
+   * Determines if a version is legacy.
+   *
+   * @param string $version
+   *   The version number.
+   *
+   * @return bool
+   *   TRUE if the version is a legacy version number, otherwise FALSE.
+   */
+  private static function isLegacyVersion(string $version): bool {
+    return stripos($version, '8.x-') === 0;
+  }
+
+}
diff --git a/automatic_updates_extensions/src/Validator/UpdateReleaseValidator.php b/automatic_updates_extensions/src/Validator/UpdateReleaseValidator.php
index 994ac31e6a..15c581ee11 100644
--- a/automatic_updates_extensions/src/Validator/UpdateReleaseValidator.php
+++ b/automatic_updates_extensions/src/Validator/UpdateReleaseValidator.php
@@ -4,6 +4,7 @@ namespace Drupal\automatic_updates_extensions\Validator;
 
 use Drupal\automatic_updates\ProjectInfo;
 use Drupal\automatic_updates_extensions\ExtensionUpdater;
+use Drupal\automatic_updates_extensions\LegacyVersionUtility;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\package_manager\Event\PreCreateEvent;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
@@ -31,18 +32,38 @@ class UpdateReleaseValidator implements EventSubscriberInterface {
     $all_versions = $stage->getPackageVersions();
     $messages = [];
     foreach (['production', 'dev'] as $package_type) {
-      foreach ($all_versions[$package_type] as $package_name => $version) {
+      foreach ($all_versions[$package_type] as $package_name => $sematic_version) {
         $package_parts = explode('/', $package_name);
         $project_name = $package_parts[1];
         // If the version isn't in the list of installable releases, then it
         // isn't secure and supported and the user should receive an error.
         $releases = (new ProjectInfo($project_name))->getInstallableReleases();
-        if (empty($releases) || !array_key_exists($version, $releases)) {
+        $is_missing_version = FALSE;
+        if (empty($releases)) {
+          $is_missing_version = TRUE;
+        }
+        elseif (!array_key_exists($sematic_version, $releases)) {
+          $legacy_version = LegacyVersionUtility::convertToLegacyVersion($sematic_version);
+          if ($legacy_version) {
+            if (!array_key_exists($legacy_version, $releases)) {
+              // If we cannot find the version using semantic or legacy then the
+              // version is missing.
+              $is_missing_version = TRUE;
+            }
+          }
+          else {
+            // If we cannot convert the semantic version into a legacy version
+            // then the version is missing.
+            $is_missing_version = TRUE;
+          }
+        }
+        if ($is_missing_version) {
           $messages[] = $this->t('Project @project_name to version @version', [
             '@project_name' => $project_name,
-            '@version' => $version,
+            '@version' => $sematic_version,
           ]);
         }
+
       }
     }
     if ($messages) {
diff --git a/automatic_updates_extensions/tests/fixtures/release-history/aaa_update_test.1.1.xml b/automatic_updates_extensions/tests/fixtures/release-history/aaa_update_test.1.1.xml
new file mode 100644
index 0000000000..2451a4bd4f
--- /dev/null
+++ b/automatic_updates_extensions/tests/fixtures/release-history/aaa_update_test.1.1.xml
@@ -0,0 +1,184 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Test legacy versions -->
+<project xmlns:dc="http://purl.org/dc/elements/1.1/">
+<title>AAA Update test</title>
+<short_name>aaa_update_test</short_name>
+<dc:creator>Drupal</dc:creator>
+<supported_branches>8.x-2.,8.x-1.</supported_branches>
+<project_status>published</project_status>
+<link>http://example.com/project/aaa_update_test</link>
+  <terms>
+   <term><name>Projects</name><value>AAA Update test project</value></term>
+  </terms>
+<releases>
+  <release>
+    <name>AAA Update test 8.x-3.0</name>
+    <version>8.x-3.0</version>
+    <tag>8.x-3.0</tag>
+    <status>published</status>
+    <release_link>http://example.com/aaa_update_test-8-3-0-release</release_link>
+    <download_link>http://example.com/aaa_update_test-8-3-0.tar.gz</download_link>
+    <date>1584195300</date>
+    <terms>
+      <term><name>Release type</name><value>New features</value></term>
+      <term><name>Release type</name><value>Bug fixes</value></term>
+    </terms>
+  </release>
+ <release>
+   <name>AAA Update test 8.x-2.1</name>
+   <version>8.x-2.1</version>
+   <tag>8.x-2.1</tag>
+   <status>published</status>
+   <release_link>http://example.com/aaa_update_test-8-x-2-1-release</release_link>
+   <download_link>http://example.com/aaa_update_test-8-x-2-1.tar.gz</download_link>
+   <date>1581603300</date>
+   <terms>
+     <term><name>Release type</name><value>New features</value></term>
+     <term><name>Release type</name><value>Bug fixes</value></term>
+   </terms>
+ </release>
+ <release>
+   <name>AAA Update test 8.x-2.1-beta1</name>
+   <version>8.x-2.1-beta1</version>
+   <tag>8.x-2.1-beta1</tag>
+   <status>published</status>
+   <release_link>http://example.com/aaa_update_test-8-x-2-1-beta1-release</release_link>
+   <download_link>http://example.com/aaa_update_test-8-x-2-1-beta1.tar.gz</download_link>
+   <date>1579011300</date>
+   <terms>
+     <term><name>Release type</name><value>New features</value></term>
+     <term><name>Release type</name><value>Bug fixes</value></term>
+   </terms>
+ </release>
+ <release>
+   <name>AAA Update test 8.x-2.1-alpha1</name>
+   <version>8.x-2.1-alpha1</version>
+   <tag>8.x-2.1-alpha1</tag>
+   <status>published</status>
+   <release_link>http://example.com/aaa_update_test-8-x-2-1-alpha1-release</release_link>
+   <download_link>http://example.com/aaa_update_test-8-x-2-1-alpha1.tar.gz</download_link>
+   <date>1576419300</date>
+   <terms>
+     <term><name>Release type</name><value>New features</value></term>
+     <term><name>Release type</name><value>Bug fixes</value></term>
+   </terms>
+ </release>
+ <release>
+   <name>AAA Update test 8.x-2.0</name>
+   <version>8.x-2.0</version>
+   <tag>8.x-2.0</tag>
+   <status>published</status>
+   <release_link>http://example.com/aaa_update_test-8-x-2-0-release</release_link>
+   <download_link>http://example.com/aaa_update_test-8-x-2-0.tar.gz</download_link>
+   <date>1573827300</date>
+   <terms>
+     <term><name>Release type</name><value>New features</value></term>
+     <term><name>Release type</name><value>Bug fixes</value></term>
+   </terms>
+ </release>
+ <release>
+   <name>AAA Update test 8.x-2.0-beta1</name>
+   <version>8.x-2.0-beta1</version>
+   <tag>8.x-2.0-beta1</tag>
+   <status>published</status>
+   <release_link>http://example.com/aaa_update_test-8-x-2-0-beta1-release</release_link>
+   <download_link>http://example.com/aaa_update_test-8-x-2-0-beta1.tar.gz</download_link>
+   <date>1571235300</date>
+   <terms>
+     <term><name>Release type</name><value>New features</value></term>
+     <term><name>Release type</name><value>Bug fixes</value></term>
+   </terms>
+ </release>
+ <release>
+   <name>AAA Update test 8.x-2.0-alpha1</name>
+   <version>8.x-2.0-alpha1</version>
+   <tag>8.x-2.0-alpha1</tag>
+   <status>published</status>
+   <release_link>http://example.com/aaa_update_test-8-x-2-0-alpha1-release</release_link>
+   <download_link>http://example.com/aaa_update_test-8-x-2-0-alpha1.tar.gz</download_link>
+   <date>1568643300</date>
+   <terms>
+     <term><name>Release type</name><value>New features</value></term>
+     <term><name>Release type</name><value>Bug fixes</value></term>
+   </terms>
+ </release>
+ <release>
+   <name>AAA Update test 8.x-1.1</name>
+   <version>8.x-1.1</version>
+   <tag>8.x-1.1</tag>
+   <status>published</status>
+   <release_link>http://example.com/aaa_update_test-8-x-1-1-release</release_link>
+   <download_link>http://example.com/aaa_update_test-8-x-1-1.tar.gz</download_link>
+   <date>1566051300</date>
+   <terms>
+     <term><name>Release type</name><value>New features</value></term>
+     <term><name>Release type</name><value>Bug fixes</value></term>
+   </terms>
+ </release>
+ <release>
+   <name>AAA Update test 8.x-1.1-beta1</name>
+   <version>8.x-1.1-beta1</version>
+   <tag>8.x-1.1-beta1</tag>
+   <status>published</status>
+   <release_link>http://example.com/aaa_update_test-8-x-1-1-beta1-release</release_link>
+   <download_link>http://example.com/aaa_update_test-8-x-1-1-beta1.tar.gz</download_link>
+   <date>1563459300</date>
+   <terms>
+     <term><name>Release type</name><value>New features</value></term>
+     <term><name>Release type</name><value>Bug fixes</value></term>
+   </terms>
+ </release>
+ <release>
+   <name>AAA Update test 8.x-1.1-alpha1</name>
+   <version>8.x-1.1-alpha1</version>
+   <tag>8.x-1.1-alpha1</tag>
+   <status>published</status>
+   <release_link>http://example.com/aaa_update_test-8-x-1-1-alpha1-release</release_link>
+   <download_link>http://example.com/aaa_update_test-8-x-1-1-alpha1.tar.gz</download_link>
+   <date>1560867300</date>
+   <terms>
+     <term><name>Release type</name><value>New features</value></term>
+     <term><name>Release type</name><value>Bug fixes</value></term>
+   </terms>
+ </release>
+ <release>
+   <name>AAA Update test 8.x-1.0</name>
+   <version>8.x-1.0</version>
+   <tag>8.x-1.0</tag>
+   <status>published</status>
+   <release_link>http://example.com/aaa_update_test-8-x-1-0-release</release_link>
+   <download_link>http://example.com/aaa_update_test-8-x-1-0.tar.gz</download_link>
+   <date>1558275300</date>
+   <terms>
+     <term><name>Release type</name><value>New features</value></term>
+     <term><name>Release type</name><value>Bug fixes</value></term>
+   </terms>
+ </release>
+ <release>
+   <name>AAA Update test 8.x-1.0-beta1</name>
+   <version>8.x-1.0-beta1</version>
+   <tag>8.x-1.0-beta1</tag>
+   <status>published</status>
+   <release_link>http://example.com/aaa_update_test-8-x-1-0-beta1-release</release_link>
+   <download_link>http://example.com/aaa_update_test-8-x-1-0-beta1.tar.gz</download_link>
+   <date>1555683300</date>
+   <terms>
+     <term><name>Release type</name><value>New features</value></term>
+     <term><name>Release type</name><value>Bug fixes</value></term>
+   </terms>
+ </release>
+ <release>
+   <name>AAA Update test 8.x-1.0-alpha1</name>
+   <version>8.x-1.0-alpha1</version>
+   <tag>8.x-1.0-alpha1</tag>
+   <status>published</status>
+   <release_link>http://example.com/aaa_update_test-8-x-1-0-alpha1-release</release_link>
+   <download_link>http://example.com/aaa_update_test-8-x-1-0-alpha1.tar.gz</download_link>
+   <date>1553091300</date>
+   <terms>
+     <term><name>Release type</name><value>New features</value></term>
+     <term><name>Release type</name><value>Bug fixes</value></term>
+   </terms>
+ </release>
+</releases>
+</project>
diff --git a/automatic_updates_extensions/tests/src/Functional/UpdaterFormTest.php b/automatic_updates_extensions/tests/src/Functional/UpdaterFormTest.php
index a5ed47c6c6..5a3cb665f5 100644
--- a/automatic_updates_extensions/tests/src/Functional/UpdaterFormTest.php
+++ b/automatic_updates_extensions/tests/src/Functional/UpdaterFormTest.php
@@ -35,6 +35,7 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase {
     'automatic_updates_extensions',
     'block',
     'semver_test',
+    'aaa_update_test',
   ];
 
   /**
@@ -47,13 +48,13 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase {
   /**
    * Data provider for testSuccessfulUpdate().
    *
-   * @return bool[]
+   * @return array[]
    *   The test cases.
    */
-  public function providerMaintanceMode() {
+  public function providerSuccessfulUpdate() {
     return [
-      'maintiance_mode_on' => [TRUE],
-      'maintiance_mode_off' => [FALSE],
+      'maintiance_mode_on, semver' => [TRUE, 'semver_test', '8.1.0', '8.1.1'],
+      'maintiance_mode_off, legacy' => [FALSE, 'aaa_update_test', '8.x-2.0', '8.x-2.1'],
     ];
   }
 
@@ -97,9 +98,16 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase {
 
   /**
    * Asserts the table shows the updates.
+   *
+   * @param string $expected_project_title
+   *   The expected project title.
+   * @param string $expected_installed_version
+   *   The expected installed version.
+   * @param string $expected_update_version
+   *   The expected update version.
    */
-  private function assertTableShowsUpdates() {
-    $this->assertUpdateTableRow($this->assertSession(), 'Semver Test', '8.1.0', '8.1.1');
+  private function assertTableShowsUpdates(string $expected_project_title, string $expected_installed_version, string $expected_update_version): void {
+    $this->assertUpdateTableRow($this->assertSession(), $expected_project_title, $expected_installed_version, $expected_update_version);
   }
 
   /**
@@ -107,11 +115,19 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase {
    *
    * @param bool $maintenance_mode_on
    *   Whether maintenance should be on at the beginning of the update.
+   * @param string $project_name
+   *   The project name.
+   * @param string $installed_version
+   *   The installed version.
+   * @param string $update_version
+   *   The update version.
    *
-   * @dataProvider providerMaintanceMode
+   * @dataProvider providerSuccessfulUpdate
    */
-  public function testSuccessfulUpdate(bool $maintenance_mode_on): void {
-    $this->setProjectInstalledVersion('8.1.0');
+  public function testSuccessfulUpdate(bool $maintenance_mode_on, string $project_name, string $installed_version, string $update_version): void {
+    $this->updateProject = $project_name;
+    $this->setReleaseMetadata(__DIR__ . "/../../fixtures/release-history/$project_name.1.1.xml");
+    $this->setProjectInstalledVersion($installed_version);
     $this->checkForUpdates();
     $state = $this->container->get('state');
     $state->set('system.maintenance_mode', $maintenance_mode_on);
@@ -120,7 +136,11 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase {
     // Navigate to the automatic updates form.
     $this->drupalGet('/admin/reports/updates');
     $this->clickLink('Update Extensions');
-    $this->assertTableShowsUpdates();
+    $this->assertTableShowsUpdates(
+      $project_name === 'semver_test' ? 'Semver Test' : 'AAA Update test',
+      $installed_version,
+      $update_version
+    );
     $page->checkField('projects[' . $this->updateProject . ']');
     $page->pressButton('Update');
     $this->checkForMetaRefresh();
@@ -154,7 +174,7 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase {
     $user = $this->createUser(['administer software updates']);
     $this->drupalLogin($user);
     $this->drupalGet('admin/reports/updates/automatic-update-extensions');
-    $this->assertTableShowsUpdates();
+    $this->assertTableShowsUpdates('Semver Test', '8.1.0', '8.1.1');
     $assert->pageTextContains('Automatic Updates Form');
     $assert->buttonExists('Update');
   }
@@ -189,7 +209,7 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase {
     $this->setProjectInstalledVersion('8.1.0');
     $this->checkForUpdates();
     $this->drupalGet('admin/reports/updates/automatic-update-extensions');
-    $this->assertTableShowsUpdates();
+    $this->assertTableShowsUpdates('Semver Test', '8.1.0', '8.1.1');
     $message = t("You've not experienced Shakespeare until you have read him in the original Klingon.");
     $error = ValidationResult::createError([$message]);
     TestSubscriber1::setTestResult([$error], ReadinessCheckEvent::class);
@@ -213,7 +233,7 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase {
     // Navigate to the automatic updates form.
     $this->drupalGet('/admin/reports/updates');
     $this->clickLink('Update Extensions');
-    $this->assertTableShowsUpdates();
+    $this->assertTableShowsUpdates('Semver Test', '8.1.0', '8.1.1');
     $assert->pageTextContains(static::$warningsExplanation);
     $assert->pageTextNotContains(static::$errorsExplanation);
     $assert->buttonExists('Update');
diff --git a/automatic_updates_extensions/tests/src/Kernel/Validator/UpdateReleaseValidatorTest.php b/automatic_updates_extensions/tests/src/Kernel/Validator/UpdateReleaseValidatorTest.php
index 4c6fccfaef..de0f6e2eb0 100644
--- a/automatic_updates_extensions/tests/src/Kernel/Validator/UpdateReleaseValidatorTest.php
+++ b/automatic_updates_extensions/tests/src/Kernel/Validator/UpdateReleaseValidatorTest.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Tests\automatic_updates_extensions\Kernel\Valdiator;
 
+use Drupal\automatic_updates_extensions\LegacyVersionUtility;
 use Drupal\package_manager\Event\PreCreateEvent;
 use Drupal\package_manager\ValidationResult;
 use Drupal\Tests\automatic_updates_extensions\Kernel\AutomaticUpdatesExtensionsKernelTestBase;
@@ -16,6 +17,8 @@ class UpdateReleaseValidatorTest extends AutomaticUpdatesExtensionsKernelTestBas
   /**
    * Tests updating to a release.
    *
+   * @param string $project
+   *   The project to update.
    * @param string $installed_version
    *   The installed version of the project.
    * @param string $update_version
@@ -25,20 +28,20 @@ class UpdateReleaseValidatorTest extends AutomaticUpdatesExtensionsKernelTestBas
    *
    * @dataProvider providerTestRelease
    */
-  public function testRelease(string $installed_version, string $update_version, bool $error_expected) {
-    $this->enableModules(['semver_test']);
-    $module_info = ['version' => $installed_version, 'project' => 'semver_test'];
+  public function testRelease(string $project, string $installed_version, string $update_version, bool $error_expected) {
+    $this->enableModules([$project]);
+    $module_info = ['version' => $installed_version, 'project' => $project];
     $this->config('update_test.settings')
-      ->set("system_info.semver_test", $module_info)
+      ->set("system_info.$project", $module_info)
       ->save();
     $this->setReleaseMetadataForProjects([
-      'semver_test' => __DIR__ . '/../../../fixtures/release-history/semver_test.1.1.xml',
+      $project => __DIR__ . "/../../../fixtures/release-history/$project.1.1.xml",
       'drupal' => __DIR__ . '/../../../../../tests/fixtures/release-history/drupal.9.8.2.xml',
     ]);
     if ($error_expected) {
       $expected_results = [
         ValidationResult::createError(
-          ["Project semver_test to version $update_version"],
+          ["Project $project to version " . LegacyVersionUtility::convertToSemanticVersion($update_version)],
           t('Cannot update because the following project version is not in the list of installable releases.')
         ),
       ];
@@ -47,7 +50,7 @@ class UpdateReleaseValidatorTest extends AutomaticUpdatesExtensionsKernelTestBas
       $expected_results = [];
     }
 
-    $this->assertUpdaterResults(['semver_test' => $update_version], $expected_results, PreCreateEvent::class);
+    $this->assertUpdaterResults([$project => $update_version], $expected_results, PreCreateEvent::class);
   }
 
   /**
@@ -58,8 +61,10 @@ class UpdateReleaseValidatorTest extends AutomaticUpdatesExtensionsKernelTestBas
    */
   public function providerTestRelease() {
     return [
-      'supported update' => ['8.1.0', '8.1.1', FALSE],
-      'update to unsupported branch' => ['8.1.0', '8.2.0', TRUE],
+      'semver, supported update' => ['semver_test', '8.1.0', '8.1.1', FALSE],
+      'semver, update to unsupported branch' => ['semver_test', '8.1.0', '8.2.0', TRUE],
+      'legacy, supported update' => ['aaa_update_test', '8.x-2.0', '8.x-2.1', FALSE],
+      'legacy, update to unsupported branch' => ['aaa_update_test', '8.x-2.0', '8.x-3.0', TRUE],
     ];
   }
 
diff --git a/automatic_updates_extensions/tests/src/Unit/LegacyVersionUtilityTest.php b/automatic_updates_extensions/tests/src/Unit/LegacyVersionUtilityTest.php
new file mode 100644
index 0000000000..df52a4b2b5
--- /dev/null
+++ b/automatic_updates_extensions/tests/src/Unit/LegacyVersionUtilityTest.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace Drupal\Tests\automatic_updates_extensions\Unit;
+
+use Drupal\automatic_updates_extensions\LegacyVersionUtility;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\automatic_updates_extensions\LegacyVersionUtility
+ *
+ * @group automatic_updates_extensions
+ */
+class LegacyVersionUtilityTest extends UnitTestCase {
+
+  /**
+   * @covers ::convertToSemanticVersion
+   *
+   * @param string $version_number
+   *   The version number to covert.
+   * @param string $expected
+   *   The expected result.
+   *
+   * @dataProvider providerConvertToSemanticVersion
+   */
+  public function testConvertToSemanticVersion(string $version_number, string $expected) {
+    $this->assertSame($expected, LegacyVersionUtility::convertToSemanticVersion($version_number));
+  }
+
+  /**
+   * Data provider for testConvertToSemanticVersion()
+   *
+   * @return string[][]
+   *   The test cases.
+   */
+  public function providerConvertToSemanticVersion() {
+    return [
+      '8.x-1.2' => ['8.x-1.2', '1.2.0'],
+      '8.x-1.2-alpha1' => ['8.x-1.2-alpha1', '1.2.0-alpha1'],
+      '1.2.0' => ['1.2.0', '1.2.0'],
+      '1.2.0-alpha1' => ['1.2.0-alpha1', '1.2.0-alpha1'],
+    ];
+  }
+
+  /**
+   * @covers ::convertToLegacyVersion
+   *
+   * @param string $version_number
+   *   The version number to covert.
+   * @param string|null $expected
+   *   The expected result.
+   *
+   * @dataProvider providerConvertToLegacyVersion
+   */
+  public function testConvertToLegacyVersion(string $version_number, ?string $expected) {
+    $this->assertSame($expected, LegacyVersionUtility::convertToLegacyVersion($version_number));
+  }
+
+  /**
+   * Data provider for testConvertToLegacyVersion()
+   *
+   * @return array[]
+   *   The test cases.
+   */
+  public function providerConvertToLegacyVersion() {
+    return [
+      '1.2.0' => ['1.2.0', '8.x-1.2'],
+      '1.2.0-alpha1' => ['1.2.0-alpha1', '8.x-1.2-alpha1'],
+      '8.x-1.2' => ['8.x-1.2', '8.x-1.2'],
+      '8.x-1.2-alpha1' => ['8.x-1.2-alpha1', '8.x-1.2-alpha1'],
+      '1.2.3' => ['1.2.3', NULL],
+      '1.2.3-alpha1' => ['1.2.3-alpha1', NULL],
+    ];
+  }
+
+}
-- 
GitLab