Skip to content
Snippets Groups Projects
Commit 92b7cbf4 authored by Theresa Grannum's avatar Theresa Grannum Committed by Ted Bowman
Browse files

Issue #3273812 by Theresa.Grannum: AU Extensions: Create a validator to ensure...

Issue #3273812 by Theresa.Grannum: AU Extensions: Create a validator to ensure updated packages are secure and supported
parent 8a66e0a6
No related branches found
No related tags found
1 merge request!261Issue #3273812: AU Extensions: Create a validator to ensure updated packages are secure and supported
Showing
with 354 additions and 6 deletions
...@@ -11,3 +11,7 @@ services: ...@@ -11,3 +11,7 @@ services:
- '@event_dispatcher' - '@event_dispatcher'
- '@tempstore.shared' - '@tempstore.shared'
- '@datetime.time' - '@datetime.time'
automatic_updates_extensions.validator.target_release:
class: Drupal\automatic_updates_extensions\Validator\UpdateReleaseValidator
tags:
- { name: event_subscriber }
<?php
namespace Drupal\automatic_updates_extensions\Validator;
use Drupal\automatic_updates\ProjectInfo;
use Drupal\automatic_updates_extensions\ExtensionUpdater;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\package_manager\Event\PreCreateEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Validates that updated projects are secure and supported.
*/
class UpdateReleaseValidator implements EventSubscriberInterface {
use StringTranslationTrait;
/**
* Checks that the update projects are secure and supported.
*
* @param \Drupal\package_manager\Event\PreCreateEvent $event
* The event object.
*/
public function checkRelease(PreCreateEvent $event): void {
$stage = $event->getStage();
// This check only works with Automatic Updates Extensions.
if (!$stage instanceof ExtensionUpdater) {
return;
}
$all_versions = $stage->getPackageVersions();
$messages = [];
foreach (['production', 'dev'] as $package_type) {
foreach ($all_versions[$package_type] as $package_name => $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)) {
$messages[] = $this->t('Project @project_name to version @version', [
'@project_name' => $project_name,
'@version' => $version,
]);
}
}
}
if ($messages) {
$summary = $this->formatPlural(
count($messages),
'Cannot update because the following project version is not in the list of installable releases.',
'Cannot update because the following project versions are not in the list of installable releases.'
);
$event->addError($messages, $summary);
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
return [
PreCreateEvent::class => 'checkRelease',
];
}
}
<?xml version="1.0" encoding="utf-8"?>
<project xmlns:dc="http://purl.org/dc/elements/1.1/">
<title>Alpha</title>
<short_name>alpha</short_name>
<dc:creator>Drupal</dc:creator>
<supported_branches>1.1.,1.0.</supported_branches>
<project_status>published</project_status>
<link>http://example.com/project/alpha</link>
<terms>
<term><name>Projects</name><value>Alpha project</value></term>
</terms>
<releases>
<release>
<name>Alpha 1.1.0</name>
<version>1.1.0</version>
<tag>1.1.0</tag>
<status>published</status>
<release_link>http://example.com/alpha-1-1-0-release</release_link>
<download_link>http://example.com/alpha-1-1-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>Alpha 1.0.0</name>
<version>1.0.0</version>
<tag>1.0.0</tag>
<status>published</status>
<release_link>http://example.com/alpha-1-0-0-release</release_link>
<download_link>http://example.com/alpha-1-0-0.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>
</releases>
</project>
...@@ -11,7 +11,6 @@ ...@@ -11,7 +11,6 @@
</terms> </terms>
<releases> <releases>
<release> <release>
<!-- This release is not in a supported branch; therefore it should not be recommended. -->
<name>Semver Test 8.2.0</name> <name>Semver Test 8.2.0</name>
<version>8.2.0</version> <version>8.2.0</version>
<tag>8.2.0</tag> <tag>8.2.0</tag>
......
...@@ -2,27 +2,38 @@ ...@@ -2,27 +2,38 @@
namespace Drupal\Tests\automatic_updates_extensions\Build; namespace Drupal\Tests\automatic_updates_extensions\Build;
use Drupal\Tests\package_manager\Build\TemplateProjectTestBase; use Drupal\Tests\automatic_updates\Build\UpdateTestBase;
/** /**
* Tests updating modules in a staging area. * Tests updating modules in a staging area.
* *
* @group automatic_updates_extensions * @group automatic_updates_extensions
*/ */
class ModuleUpdateTest extends TemplateProjectTestBase { class ModuleUpdateTest extends UpdateTestBase {
/** /**
* Tests updating a module in a staging area. * Tests updating a module in a staging area.
*/ */
public function testApi(): void { public function testApi(): void {
$this->createTestProject('RecommendedProject'); $this->createTestProject('RecommendedProject');
$this->setReleaseMetadata([
'drupal' => __DIR__ . '/../../../../tests/fixtures/release-history/drupal.9.8.1-security.xml',
'alpha' => __DIR__ . '/../../fixtures/release-history/alpha.1.1.0.xml',
]);
// Set 'version' and 'project' for the 'alpha' module to enable the Update
// to determine the update status.
$system_info = ['alpha' => ['version' => '1.0.0', 'project' => 'alpha']];
$system_info = var_export($system_info, TRUE);
$code = <<<END
\$config['update_test.settings']['system_info'] = $system_info;
END;
$this->writeSettings($code);
$this->addRepository('alpha', __DIR__ . '/../../../../package_manager/tests/fixtures/alpha/1.0.0'); $this->addRepository('alpha', __DIR__ . '/../../../../package_manager/tests/fixtures/alpha/1.0.0');
$this->runComposer('COMPOSER_MIRROR_PATH_REPOS=1 composer require drupal/alpha --update-with-all-dependencies', 'project'); $this->runComposer('COMPOSER_MIRROR_PATH_REPOS=1 composer require drupal/alpha --update-with-all-dependencies', 'project');
$this->installQuickStart('minimal'); $this->installModules(['automatic_updates_extensions_test_api', 'alpha']);
$this->formLogin($this->adminUsername, $this->adminPassword);
$this->installModules(['automatic_updates_extensions_test_api']);
// Change both modules' upstream version. // Change both modules' upstream version.
$this->addRepository('alpha', __DIR__ . '/../../../../package_manager/tests/fixtures/alpha/1.1.0'); $this->addRepository('alpha', __DIR__ . '/../../../../package_manager/tests/fixtures/alpha/1.1.0');
......
<?php
namespace Drupal\Tests\automatic_updates_extensions\Kernel;
use Drupal\automatic_updates_extensions\ExtensionUpdater;
use Drupal\package_manager\Event\StageEvent;
use Drupal\package_manager\Exception\StageException;
use Drupal\package_manager\Exception\StageValidationException;
use Drupal\Tests\automatic_updates\Kernel\AutomaticUpdatesKernelTestBase;
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Psr7\Utils;
use Psr\Http\Message\RequestInterface;
/**
* Base class for kernel tests of the Automatic Updates Extensions module.
*/
abstract class AutomaticUpdatesExtensionsKernelTestBase extends AutomaticUpdatesKernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'automatic_updates_extensions',
'automatic_updates_test_release_history',
];
/**
* The client.
*
* @var \GuzzleHttp\Client
*/
protected $client;
/**
* Asserts validation results are returned from a stage life cycle event.
*
* @param string[] $project_versions
* The project versions.
* @param \Drupal\package_manager\ValidationResult[] $expected_results
* The expected validation results.
* @param string|null $event_class
* (optional) The class of the event which should return the results. Must
* be passed if $expected_results is not empty.
*/
protected function assertUpdaterResults(array $project_versions, array $expected_results, string $event_class = NULL): void {
$updater = $this->createExtensionUpdater();
try {
$updater->begin($project_versions);
$updater->stage();
$updater->apply();
$updater->destroy();
// If we did not get an exception, ensure we didn't expect any results.
$this->assertEmpty($expected_results);
}
catch (StageValidationException $e) {
$this->assertNotEmpty($expected_results);
$this->assertValidationResultsEqual($expected_results, $e->getResults());
// TestStage::dispatch() attaches the event object to the exception so
// that we can analyze it.
$this->assertNotEmpty($event_class);
$this->assertInstanceOf($event_class, $e->event);
}
}
/**
* Sets the release metadata file to use when fetching available updates.
*
* @param string[] $files
* The paths of the XML metadata files to use, keyed by project name.
*/
protected function setReleaseMetadataForProjects(array $files): void {
$responses = [];
foreach ($files as $project => $file) {
$metadata = Utils::tryFopen($file, 'r');
$responses["/release-history/$project/current"] = new Response(200, [], Utils::streamFor($metadata));
}
$callable = function (RequestInterface $request) use ($responses): Response {
return $responses[$request->getUri()->getPath()] ?? new Response(404);
};
// The mock handler's queue consist of same callable as many times as the
// number of requests we expect to be made for update XML because it will
// retrieve one item off the queue for each request.
// @see \GuzzleHttp\Handler\MockHandler::__invoke()
$handler = new MockHandler(array_fill(0, count($responses), $callable));
$this->client = new Client([
'handler' => HandlerStack::create($handler),
]);
$this->container->set('http_client', $this->client);
}
/**
* Creates an extension updater object for testing purposes.
*
* @return \Drupal\Tests\automatic_updates_extensions\Kernel\TestExtensionUpdater
* A extension updater object, with test-only modifications.
*/
protected function createExtensionUpdater(): TestExtensionUpdater {
return new TestExtensionUpdater(
$this->container->get('config.factory'),
$this->container->get('package_manager.path_locator'),
$this->container->get('package_manager.beginner'),
$this->container->get('package_manager.stager'),
$this->container->get('package_manager.committer'),
$this->container->get('file_system'),
$this->container->get('event_dispatcher'),
$this->container->get('tempstore.shared'),
$this->container->get('datetime.time')
);
}
}
/**
* Defines a updater specifically for testing purposes.
*/
class TestExtensionUpdater extends ExtensionUpdater {
/**
* The directory where staging areas will be created.
*
* @var string
*/
public static $stagingRoot;
/**
* {@inheritdoc}
*/
public function getStagingRoot(): string {
return static::$stagingRoot ?: parent::getStagingRoot();
}
/**
* {@inheritdoc}
*/
protected function dispatch(StageEvent $event, callable $on_error = NULL): void {
try {
parent::dispatch($event, $on_error);
}
catch (StageException $e) {
// Attach the event object to the exception so that test code can verify
// that the exception was thrown when a specific event was dispatched.
$e->event = $event;
throw $e;
}
}
}
...@@ -28,6 +28,7 @@ class ExtensionUpdaterTest extends AutomaticUpdatesKernelTestBase { ...@@ -28,6 +28,7 @@ class ExtensionUpdaterTest extends AutomaticUpdatesKernelTestBase {
* {@inheritdoc} * {@inheritdoc}
*/ */
protected function setUp(): void { protected function setUp(): void {
$this->disableValidators[] = 'automatic_updates_extensions.validator.target_release';
parent::setUp(); parent::setUp();
$this->installEntitySchema('user'); $this->installEntitySchema('user');
} }
......
<?php
namespace Drupal\Tests\automatic_updates_extensions\Kernel\Valdiator;
use Drupal\package_manager\Event\PreCreateEvent;
use Drupal\package_manager\ValidationResult;
use Drupal\Tests\automatic_updates_extensions\Kernel\AutomaticUpdatesExtensionsKernelTestBase;
/**
* @coversDefaultClass \Drupal\automatic_updates_extensions\Validator\UpdateReleaseValidator
*
* @group automatic_updates_extensions
*/
class UpdateReleaseValidatorTest extends AutomaticUpdatesExtensionsKernelTestBase {
/**
* Tests updating to a release.
*
* @param string $installed_version
* The installed version of the project.
* @param string $update_version
* The version to update to.
* @param bool $error_expected
* Whether an error is expected in the update.
*
* @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'];
$this->config('update_test.settings')
->set("system_info.semver_test", $module_info)
->save();
$this->setReleaseMetadataForProjects([
'semver_test' => __DIR__ . '/../../../fixtures/release-history/semver_test.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"],
t('Cannot update because the following project version is not in the list of installable releases.')
),
];
}
else {
$expected_results = [];
}
$this->assertUpdaterResults(['semver_test' => $update_version], $expected_results, PreCreateEvent::class);
}
/**
* Data provider for testRelease().
*
* @return array[]
* The test cases.
*/
public function providerTestRelease() {
return [
'supported update' => ['8.1.0', '8.1.1', FALSE],
'update to unsupported branch' => ['8.1.0', '8.2.0', TRUE],
];
}
}
name: Alpha
type: module
core_version_requirement: ^9
name: Alpha
type: module
core_version_requirement: ^9
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment