From c959ebb2121a8f93f7be5c9691bd52d82719bfb7 Mon Sep 17 00:00:00 2001
From: phenaproxima <phenaproxima@205645.no-reply.drupal.org>
Date: Fri, 29 Oct 2021 15:47:03 +0000
Subject: [PATCH] Issue #3246638 by phenaproxima, tedbow: Move
 ComposerExecutableValidator into Package Manager

---
 automatic_updates.services.yml                |   5 +-
 package_manager/package_manager.services.yml  |   9 +
 .../ComposerExecutableValidator.php           |  17 +-
 .../StageValidatorInterface.php               |  21 +++
 .../ComposerExecutableValidatorTest.php       | 161 ++++++++++++++++++
 .../tests/src/Traits/ValidationTestTrait.php  |  32 ++++
 .../tests}/src/Unit/ValidationResultTest.php  |   4 +-
 .../PackageManagerReadinessCheck.php          |  54 ++++++
 .../ComposerExecutableValidatorTest.php       | 136 ++-------------
 tests/src/Traits/ValidationTestTrait.php      |  26 +--
 10 files changed, 310 insertions(+), 155 deletions(-)
 rename {src/Validator => package_manager/src/EventSubscriber}/ComposerExecutableValidator.php (81%)
 create mode 100644 package_manager/src/EventSubscriber/StageValidatorInterface.php
 create mode 100644 package_manager/tests/src/Kernel/ComposerExecutableValidatorTest.php
 create mode 100644 package_manager/tests/src/Traits/ValidationTestTrait.php
 rename {tests => package_manager/tests}/src/Unit/ValidationResultTest.php (97%)
 create mode 100644 src/Validator/PackageManagerReadinessCheck.php

diff --git a/automatic_updates.services.yml b/automatic_updates.services.yml
index 4850a512fd..bfcb27a22d 100644
--- a/automatic_updates.services.yml
+++ b/automatic_updates.services.yml
@@ -47,8 +47,9 @@ services:
     tags:
       - { name: event_subscriber }
   automatic_updates.composer_executable_validator:
-    class: Drupal\automatic_updates\Validator\ComposerExecutableValidator
-    arguments: ['@package_manager.composer_runner','@string_translation']
+    class: Drupal\automatic_updates\Validator\PackageManagerReadinessCheck
+    arguments:
+      - '@package_manager.validator.composer_executable'
     tags:
       - { name: event_subscriber }
   automatic_updates.disk_space_validator:
diff --git a/package_manager/package_manager.services.yml b/package_manager/package_manager.services.yml
index da8091f9d9..8b207d342e 100644
--- a/package_manager/package_manager.services.yml
+++ b/package_manager/package_manager.services.yml
@@ -84,3 +84,12 @@ services:
     arguments:
       - '@config.factory'
       - '%app.root%'
+
+  # Validators.
+  package_manager.validator.composer_executable:
+    class: Drupal\package_manager\EventSubscriber\ComposerExecutableValidator
+    arguments:
+      - '@package_manager.composer_runner'
+      - '@string_translation'
+    tags:
+      - { name: event_subscriber }
diff --git a/src/Validator/ComposerExecutableValidator.php b/package_manager/src/EventSubscriber/ComposerExecutableValidator.php
similarity index 81%
rename from src/Validator/ComposerExecutableValidator.php
rename to package_manager/src/EventSubscriber/ComposerExecutableValidator.php
index 3b2c337b91..1b12e8bcc1 100644
--- a/src/Validator/ComposerExecutableValidator.php
+++ b/package_manager/src/EventSubscriber/ComposerExecutableValidator.php
@@ -1,8 +1,9 @@
 <?php
 
-namespace Drupal\automatic_updates\Validator;
+namespace Drupal\package_manager\EventSubscriber;
 
-use Drupal\automatic_updates\Event\ReadinessCheckEvent;
+use Drupal\package_manager\Event\PreCreateEvent;
+use Drupal\package_manager\Event\StageEvent;
 use Drupal\package_manager\ValidationResult;
 use Drupal\Core\Extension\ExtensionVersion;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
@@ -10,12 +11,11 @@ use Drupal\Core\StringTranslation\TranslationInterface;
 use PhpTuf\ComposerStager\Domain\Output\ProcessOutputCallbackInterface;
 use PhpTuf\ComposerStager\Exception\ExceptionInterface;
 use PhpTuf\ComposerStager\Infrastructure\Process\Runner\ComposerRunnerInterface;
-use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 
 /**
  * Validates that the Composer executable can be found in the correct version.
  */
-class ComposerExecutableValidator implements EventSubscriberInterface, ProcessOutputCallbackInterface {
+class ComposerExecutableValidator implements StageValidatorInterface, ProcessOutputCallbackInterface {
 
   use StringTranslationTrait;
 
@@ -47,12 +47,9 @@ class ComposerExecutableValidator implements EventSubscriberInterface, ProcessOu
   }
 
   /**
-   * Validates that the Composer executable can be found.
-   *
-   * @param \Drupal\automatic_updates\Event\ReadinessCheckEvent $event
-   *   The event object.
+   * {@inheritdoc}
    */
-  public function checkForComposerExecutable(ReadinessCheckEvent $event): void {
+  public function validateStage(StageEvent $event): void {
     try {
       $this->composer->run(['--version'], $this);
     }
@@ -90,7 +87,7 @@ class ComposerExecutableValidator implements EventSubscriberInterface, ProcessOu
    */
   public static function getSubscribedEvents() {
     return [
-      ReadinessCheckEvent::class => 'checkForComposerExecutable',
+      PreCreateEvent::class => 'validateStage',
     ];
   }
 
diff --git a/package_manager/src/EventSubscriber/StageValidatorInterface.php b/package_manager/src/EventSubscriber/StageValidatorInterface.php
new file mode 100644
index 0000000000..d232683c44
--- /dev/null
+++ b/package_manager/src/EventSubscriber/StageValidatorInterface.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace Drupal\package_manager\EventSubscriber;
+
+use Drupal\package_manager\Event\StageEvent;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * Defines an interface for services that validate a stage over its life cycle.
+ */
+interface StageValidatorInterface extends EventSubscriberInterface {
+
+  /**
+   * Validates a stage at various points during its life cycle.
+   *
+   * @param \Drupal\package_manager\Event\StageEvent $event
+   *   The event object.
+   */
+  public function validateStage(StageEvent $event): void;
+
+}
diff --git a/package_manager/tests/src/Kernel/ComposerExecutableValidatorTest.php b/package_manager/tests/src/Kernel/ComposerExecutableValidatorTest.php
new file mode 100644
index 0000000000..73379cbf9e
--- /dev/null
+++ b/package_manager/tests/src/Kernel/ComposerExecutableValidatorTest.php
@@ -0,0 +1,161 @@
+<?php
+
+namespace Drupal\Tests\package_manager\Kernel;
+
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\package_manager\Event\PreCreateEvent;
+use Drupal\package_manager\EventSubscriber\ComposerExecutableValidator;
+use Drupal\package_manager\ValidationResult;
+use Drupal\Tests\package_manager\Traits\ValidationTestTrait;
+use PhpTuf\ComposerStager\Exception\IOException;
+use PhpTuf\ComposerStager\Infrastructure\Process\ExecutableFinderInterface;
+use Prophecy\Argument;
+
+/**
+ * @covers \Drupal\package_manager\EventSubscriber\ComposerExecutableValidator
+ *
+ * @group package_manager
+ */
+class ComposerExecutableValidatorTest extends KernelTestBase {
+
+  use ValidationTestTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['package_manager'];
+
+  /**
+   * Runs the validator under test, and asserts its results match expectations.
+   *
+   * @param \Drupal\package_manager\ValidationResult[] $expected_results
+   *   The expected validation results.
+   */
+  private function assertResults(array $expected_results): void {
+    $stage = $this->prophesize('\Drupal\package_manager\Stage');
+    $event = new PreCreateEvent($stage->reveal());
+    $this->container->get('package_manager.validator.composer_executable')
+      ->validateStage($event);
+
+    $this->assertValidationResultsEqual($expected_results, $event->getResults());
+  }
+
+  /**
+   * Tests that an error is raised if the Composer executable isn't found.
+   */
+  public function testErrorIfComposerNotFound(): void {
+    $exception = new IOException("This is your regularly scheduled error.");
+
+    // The executable finder throws an exception if it can't find the requested
+    // executable.
+    $exec_finder = $this->prophesize(ExecutableFinderInterface::class);
+    $exec_finder->find('composer')
+      ->willThrow($exception)
+      ->shouldBeCalled();
+    $this->container->set('package_manager.executable_finder', $exec_finder->reveal());
+
+    // The validator should translate that exception into an error.
+    $error = ValidationResult::createError([
+      $exception->getMessage(),
+    ]);
+    $this->assertResults([$error]);
+  }
+
+  /**
+   * Data provider for ::testComposerVersionValidation().
+   *
+   * @return array[]
+   *   Sets of arguments to pass to the test method.
+   */
+  public function providerComposerVersionValidation(): array {
+    // Invalid or undetectable Composer versions will always produce the same
+    // error.
+    $invalid_version = ValidationResult::createError(['The Composer version could not be detected.']);
+
+    // Unsupported Composer versions will report the detected version number
+    // in the validation result, so we need a function to churn out those fake
+    // results for the test method.
+    $unsupported_version = function (string $version): ValidationResult {
+      return ValidationResult::createError([
+        "Composer 2 or later is required, but version $version was detected.",
+      ]);
+    };
+
+    return [
+      // A valid 2.x version of Composer should not produce any errors.
+      [
+        '2.1.6',
+        [],
+      ],
+      [
+        '1.10.22',
+        [$unsupported_version('1.10.22')],
+      ],
+      [
+        '1.7.3',
+        [$unsupported_version('1.7.3')],
+      ],
+      [
+        '2.0.0-alpha3',
+        [],
+      ],
+      [
+        '2.1.0-RC1',
+        [],
+      ],
+      [
+        '1.0.0-RC',
+        [$unsupported_version('1.0.0-RC')],
+      ],
+      [
+        '1.0.0-beta1',
+        [$unsupported_version('1.0.0-beta1')],
+      ],
+      [
+        '1.9-dev',
+        [$invalid_version],
+      ],
+      [
+        '@package_version@',
+        [$invalid_version],
+      ],
+    ];
+  }
+
+  /**
+   * Tests validation of various Composer versions.
+   *
+   * @param string $reported_version
+   *   The version of Composer that `composer --version` should report.
+   * @param \Drupal\package_manager\ValidationResult[] $expected_results
+   *   The expected validation results.
+   *
+   * @dataProvider providerComposerVersionValidation
+   */
+  public function testComposerVersionValidation(string $reported_version, array $expected_results): void {
+    // Mock the output of `composer --version`, will be passed to the validator,
+    // which is itself a callback function that gets called repeatedly as
+    // Composer produces output.
+    /** @var \PhpTuf\ComposerStager\Infrastructure\Process\Runner\ComposerRunnerInterface|\Prophecy\Prophecy\ObjectProphecy $runner */
+    $runner = $this->prophesize('\PhpTuf\ComposerStager\Infrastructure\Process\Runner\ComposerRunnerInterface');
+
+    $runner->run(['--version'], Argument::type(ComposerExecutableValidator::class))
+      // Whatever is passed to ::run() will be passed to this mock callback in
+      // $arguments, and we know exactly what that will contain: an array of
+      // command arguments for Composer, and the validator object.
+      ->will(function (array $arguments) use ($reported_version) {
+        /** @var \Drupal\package_manager\EventSubscriber\ComposerExecutableValidator $validator */
+        $validator = $arguments[1];
+        // Invoke the validator (which, as mentioned, is a callback function),
+        // with fake output from `composer --version`. It should try to tease a
+        // recognized, supported version number out of this output.
+        $validator($validator::OUT, "Composer version $reported_version");
+      });
+    $this->container->set('package_manager.composer_runner', $runner->reveal());
+
+    // If the validator can't find a recognized, supported version of Composer,
+    // it should produce errors.
+    $this->assertResults($expected_results);
+  }
+
+}
diff --git a/package_manager/tests/src/Traits/ValidationTestTrait.php b/package_manager/tests/src/Traits/ValidationTestTrait.php
new file mode 100644
index 0000000000..9cfeff13a2
--- /dev/null
+++ b/package_manager/tests/src/Traits/ValidationTestTrait.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace Drupal\Tests\package_manager\Traits;
+
+/**
+ * Contains helpful methods for testing stage validators.
+ */
+trait ValidationTestTrait {
+
+  /**
+   * Asserts two validation result sets are equal.
+   *
+   * @param \Drupal\package_manager\ValidationResult[] $expected_results
+   *   The expected validation results.
+   * @param \Drupal\package_manager\ValidationResult[] $actual_results
+   *   The actual validation results.
+   */
+  protected function assertValidationResultsEqual(array $expected_results, array $actual_results): void {
+    $this->assertCount(count($expected_results), $actual_results);
+
+    foreach ($expected_results as $expected_result) {
+      $actual_result = array_shift($actual_results);
+      $this->assertSame($expected_result->getSeverity(), $actual_result->getSeverity());
+      $this->assertSame((string) $expected_result->getSummary(), (string) $actual_result->getSummary());
+      $this->assertSame(
+        array_map('strval', $expected_result->getMessages()),
+        array_map('strval', $actual_result->getMessages())
+      );
+    }
+  }
+
+}
diff --git a/tests/src/Unit/ValidationResultTest.php b/package_manager/tests/src/Unit/ValidationResultTest.php
similarity index 97%
rename from tests/src/Unit/ValidationResultTest.php
rename to package_manager/tests/src/Unit/ValidationResultTest.php
index 69a32aa1fc..26de5cd6e7 100644
--- a/tests/src/Unit/ValidationResultTest.php
+++ b/package_manager/tests/src/Unit/ValidationResultTest.php
@@ -1,6 +1,6 @@
 <?php
 
-namespace Drupal\Tests\automatic_updates\Unit;
+namespace Drupal\Tests\package_manager\Unit;
 
 use Drupal\package_manager\ValidationResult;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
@@ -10,7 +10,7 @@ use Drupal\Tests\UnitTestCase;
 /**
  * @coversDefaultClass \Drupal\package_manager\ValidationResult
  *
- * @group automatic_updates
+ * @group package_manager
  */
 class ValidationResultTest extends UnitTestCase {
 
diff --git a/src/Validator/PackageManagerReadinessCheck.php b/src/Validator/PackageManagerReadinessCheck.php
new file mode 100644
index 0000000000..72d8bb142d
--- /dev/null
+++ b/src/Validator/PackageManagerReadinessCheck.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Drupal\automatic_updates\Validator;
+
+use Drupal\automatic_updates\Event\ReadinessCheckEvent;
+use Drupal\package_manager\EventSubscriber\StageValidatorInterface;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * An adapter to run another stage validator during readiness checking.
+ *
+ * This class exists to facilitate re-use of Package Manager's stage validators
+ * during update readiness checks, in addition to whatever events they normally
+ * subscribe to.
+ */
+class PackageManagerReadinessCheck implements EventSubscriberInterface {
+
+  /**
+   * The validator to run.
+   *
+   * @var \Drupal\package_manager\EventSubscriber\StageValidatorInterface
+   */
+  protected $validator;
+
+  /**
+   * Constructs a PackageManagerReadinessCheck object.
+   *
+   * @param \Drupal\package_manager\EventSubscriber\StageValidatorInterface $validator
+   *   The Package Manager validator to run during readiness checking.
+   */
+  public function __construct(StageValidatorInterface $validator) {
+    $this->validator = $validator;
+  }
+
+  /**
+   * Performs a readiness check by proxying to a Package Manager validator.
+   *
+   * @param \Drupal\automatic_updates\Event\ReadinessCheckEvent $event
+   *   The event object.
+   */
+  public function validate(ReadinessCheckEvent $event): void {
+    $this->validator->validateStage($event);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    return [
+      ReadinessCheckEvent::class => 'validate',
+    ];
+  }
+
+}
diff --git a/tests/src/Kernel/ReadinessValidation/ComposerExecutableValidatorTest.php b/tests/src/Kernel/ReadinessValidation/ComposerExecutableValidatorTest.php
index 2a389bfb70..fd7fe341f0 100644
--- a/tests/src/Kernel/ReadinessValidation/ComposerExecutableValidatorTest.php
+++ b/tests/src/Kernel/ReadinessValidation/ComposerExecutableValidatorTest.php
@@ -2,17 +2,17 @@
 
 namespace Drupal\Tests\automatic_updates\Kernel\ReadinessValidation;
 
-use Drupal\package_manager\ValidationResult;
-use Drupal\automatic_updates\Validator\ComposerExecutableValidator;
+use Drupal\automatic_updates\Event\ReadinessCheckEvent;
+use Drupal\package_manager\EventSubscriber\StageValidatorInterface;
 use Drupal\Tests\automatic_updates\Kernel\AutomaticUpdatesKernelTestBase;
-use PhpTuf\ComposerStager\Exception\IOException;
-use PhpTuf\ComposerStager\Infrastructure\Process\ExecutableFinderInterface;
 use Prophecy\Argument;
 
 /**
- * @covers \Drupal\automatic_updates\Validator\ComposerExecutableValidator
+ * Tests that the Composer executable is validated during readiness checking.
  *
  * @group automatic_updates
+ *
+ * @see \Drupal\Tests\package_manager\Kernel\ComposerExecutableValidatorTest
  */
 class ComposerExecutableValidatorTest extends AutomaticUpdatesKernelTestBase {
 
@@ -25,121 +25,19 @@ class ComposerExecutableValidatorTest extends AutomaticUpdatesKernelTestBase {
   ];
 
   /**
-   * Tests that an error is raised if the Composer executable isn't found.
-   */
-  public function testErrorIfComposerNotFound(): void {
-    $exception = new IOException("This is your regularly scheduled error.");
-
-    // The executable finder throws an exception if it can't find the requested
-    // executable.
-    $exec_finder = $this->prophesize(ExecutableFinderInterface::class);
-    $exec_finder->find('composer')
-      ->willThrow($exception)
-      ->shouldBeCalled();
-    $this->container->set('package_manager.executable_finder', $exec_finder->reveal());
-
-    // The validator should translate that exception into an error.
-    $error = ValidationResult::createError([
-      $exception->getMessage(),
-    ]);
-    $this->assertCheckerResultsFromManager([$error], TRUE);
-  }
-
-  /**
-   * Data provider for ::testComposerVersionValidation().
-   *
-   * @return array[]
-   *   Sets of arguments to pass to the test method.
+   * Tests that the Composer executable is validated during readiness checking.
    */
-  public function providerComposerVersionValidation(): array {
-    // Invalid or undetectable Composer versions will always produce the same
-    // error.
-    $invalid_version = ValidationResult::createError(['The Composer version could not be detected.']);
-
-    // Unsupported Composer versions will report the detected version number
-    // in the validation result, so we need a function to churn out those fake
-    // results for the test method.
-    $unsupported_version = function (string $version): ValidationResult {
-      return ValidationResult::createError([
-        "Composer 2 or later is required, but version $version was detected.",
-      ]);
-    };
-
-    return [
-      // A valid 2.x version of Composer should not produce any errors.
-      [
-        '2.1.6',
-        [],
-      ],
-      [
-        '1.10.22',
-        [$unsupported_version('1.10.22')],
-      ],
-      [
-        '1.7.3',
-        [$unsupported_version('1.7.3')],
-      ],
-      [
-        '2.0.0-alpha3',
-        [],
-      ],
-      [
-        '2.1.0-RC1',
-        [],
-      ],
-      [
-        '1.0.0-RC',
-        [$unsupported_version('1.0.0-RC')],
-      ],
-      [
-        '1.0.0-beta1',
-        [$unsupported_version('1.0.0-beta1')],
-      ],
-      [
-        '1.9-dev',
-        [$invalid_version],
-      ],
-      [
-        '@package_version@',
-        [$invalid_version],
-      ],
-    ];
-  }
-
-  /**
-   * Tests validation of various Composer versions.
-   *
-   * @param string $reported_version
-   *   The version of Composer that `composer --version` should report.
-   * @param \Drupal\package_manager\ValidationResult[] $expected_results
-   *   The expected validation results.
-   *
-   * @dataProvider providerComposerVersionValidation
-   */
-  public function testComposerVersionValidation(string $reported_version, array $expected_results): void {
-    // Mock the output of `composer --version`, will be passed to the validator,
-    // which is itself a callback function that gets called repeatedly as
-    // Composer produces output.
-    /** @var \PhpTuf\ComposerStager\Infrastructure\Process\Runner\ComposerRunnerInterface|\Prophecy\Prophecy\ObjectProphecy $runner */
-    $runner = $this->prophesize('\PhpTuf\ComposerStager\Infrastructure\Process\Runner\ComposerRunnerInterface');
-
-    $runner->run(['--version'], Argument::type(ComposerExecutableValidator::class))
-      // Whatever is passed to ::run() will be passed to this mock callback in
-      // $arguments, and we know exactly what that will contain: an array of
-      // command arguments for Composer, and the validator object.
-      ->will(function (array $arguments) use ($reported_version) {
-        /** @var \Drupal\automatic_updates\Validator\ComposerExecutableValidator $validator */
-        $validator = $arguments[1];
-        // Invoke the validator (which, as mentioned, is a callback function),
-        // with fake output from `composer --version`. It should try to tease a
-        // recognized, supported version number out of this output.
-        $validator($validator::OUT, "Composer version $reported_version");
-      });
-    $this->container->set('package_manager.composer_runner', $runner->reveal());
-
-    // If the validator can't find a recognized, supported version of Composer,
-    // it should produce errors.
-    $this->assertCheckerResultsFromManager($expected_results, TRUE);
+  public function testComposerExecutableIsValidated(): void {
+    // Set up a mocked version of the Composer executable validator, to prove
+    // that it gets called with a readiness check event, when we run readiness
+    // checks.
+    $event = Argument::type(ReadinessCheckEvent::class);
+    $validator = $this->prophesize(StageValidatorInterface::class);
+    $validator->validateStage($event)->shouldBeCalled();
+    $this->container->set('package_manager.validator.composer_executable', $validator->reveal());
+
+    $this->container->get('automatic_updates.readiness_validation_manager')
+      ->run();
   }
 
 }
diff --git a/tests/src/Traits/ValidationTestTrait.php b/tests/src/Traits/ValidationTestTrait.php
index b7020c5ea7..592d24b39a 100644
--- a/tests/src/Traits/ValidationTestTrait.php
+++ b/tests/src/Traits/ValidationTestTrait.php
@@ -4,11 +4,15 @@ namespace Drupal\Tests\automatic_updates\Traits;
 
 use Drupal\package_manager\ValidationResult;
 
+use Drupal\Tests\package_manager\Traits\ValidationTestTrait as PackageManagerValidationTestTrait;
+
 /**
  * Common methods for testing validation.
  */
 trait ValidationTestTrait {
 
+  use PackageManagerValidationTestTrait;
+
   /**
    * Expected explanation text when readiness checkers return error messages.
    *
@@ -88,28 +92,6 @@ trait ValidationTestTrait {
     }
   }
 
-  /**
-   * Asserts two validation result sets are equal.
-   *
-   * @param \Drupal\package_manager\ValidationResult[] $expected_results
-   *   The expected validation results.
-   * @param \Drupal\package_manager\ValidationResult[]|null $actual_results
-   *   The actual validation results or NULL if known are available.
-   */
-  protected function assertValidationResultsEqual(array $expected_results, array $actual_results): void {
-    $this->assertCount(count($expected_results), $actual_results);
-
-    foreach ($expected_results as $expected_result) {
-      $actual_result = array_shift($actual_results);
-      $this->assertSame($expected_result->getSeverity(), $actual_result->getSeverity());
-      $this->assertSame((string) $expected_result->getSummary(), (string) $actual_result->getSummary());
-      $this->assertSame(
-        array_map('strval', $expected_result->getMessages()),
-        array_map('strval', $actual_result->getMessages())
-      );
-    }
-  }
-
   /**
    * Gets the messages of a particular type from the manager.
    *
-- 
GitLab