Skip to content
Snippets Groups Projects
Verified Commit a55df217 authored by Alex Pott's avatar Alex Pott
Browse files

Issue #3423526 by srishtiiee, phenaproxima, narendraR, Wim Leers: Validate...

Issue #3423526 by srishtiiee, phenaproxima, narendraR, Wim Leers: Validate that every config entity listed in the config.actions section of recipe.yml belongs to an extension that is installed, or will be by the recipe or one of its dependencies

(cherry picked from commit 2d6152b4e5909bdca3c370c4081f50102e7e5976)
parent 9da91eb5
No related branches found
No related tags found
1 merge request!7908Recipes API on 10.3.x
Showing
with 165 additions and 5 deletions
......@@ -82,6 +82,11 @@ private static function parse(string $file): array {
if (!$recipe_contents) {
throw new RecipeFileException($file, "$file does not exist or could not be read.");
}
// Certain parts of our validation need to be able to scan for other
// recipes.
// @see ::validateRecipeExists()
// @see ::validateConfigActions()
$discovery = self::getRecipeDiscovery(dirname($file, 2));
$constraints = new Collection([
'name' => new Required([
......@@ -127,7 +132,7 @@ private static function parse(string $file): array {
),
new Callback(
callback: self::validateRecipeExists(...),
payload: dirname(dirname($file))
payload: $discovery,
),
]),
]),
......@@ -162,6 +167,10 @@ private static function parse(string $file): array {
new All([
new Type('array'),
new NotBlank(),
new Callback(
callback: self::validateConfigActions(...),
payload: $discovery,
),
]),
]),
]),
......@@ -219,15 +228,15 @@ private static function validateExtensionIsAvailable(string $value, ExecutionCon
* The machine name of the recipe to look for.
* @param \Symfony\Component\Validator\Context\ExecutionContextInterface $context
* The validator execution context.
* @param string $recipeDirectory
* The directory the contains the recipe being validated.
* @param \Drupal\Core\Recipe\RecipeDiscovery $discovery
* A discovery object to find other recipes.
*/
private static function validateRecipeExists(string $name, ExecutionContextInterface $context, string $recipeDirectory): void {
private static function validateRecipeExists(string $name, ExecutionContextInterface $context, RecipeDiscovery $discovery): void {
if (empty($name)) {
return;
}
try {
static::getRecipeDiscovery($recipeDirectory)->getRecipe($name);
$discovery->getRecipe($name);
}
catch (UnknownRecipeException) {
$context->addViolation('The %name recipe does not exist.', ['%name' => $name]);
......@@ -246,4 +255,43 @@ private static function getRecipeDiscovery(string $recipeDirectory): RecipeDisco
return new RecipeDiscovery([$recipeDirectory]);
}
/**
* Validates that the corresponding extension is enabled for a config action.
*
* @param mixed $value
* The config action; not used.
* @param \Symfony\Component\Validator\Context\ExecutionContextInterface $context
* The validator execution context.
* @param \Drupal\Core\Recipe\RecipeDiscovery $discovery
* A discovery object to find other recipes.
*/
private static function validateConfigActions(mixed $value, ExecutionContextInterface $context, RecipeDiscovery $discovery): void {
$config_name = str_replace(['[config][actions]', '[', ']'], '', $context->getPropertyPath());
[$config_provider] = explode('.', $config_name);
if ($config_provider === 'core') {
return;
}
$recipe_being_validated = $context->getRoot();
assert(is_array($recipe_being_validated));
$configurator = new RecipeConfigurator($recipe_being_validated['recipes'] ?? [], $discovery);
// The config provider must either be an already-installed module or theme,
// or an extension being installed by this recipe or a recipe it depends on.
$all_extensions = [
...array_keys(\Drupal::service('extension.list.module')->getAllInstalledInfo()),
...array_keys(\Drupal::service('extension.list.theme')->getAllInstalledInfo()),
...$recipe_being_validated['install'] ?? [],
...$configurator->listAllExtensions(),
];
if (!in_array($config_provider, $all_extensions, TRUE)) {
$context->addViolation('Config actions cannot be applied to %config_name because the %config_provider extension is not installed, and is not installed by this recipe or any of the recipes it depends on.', [
'%config_name' => $config_name,
'%config_provider' => $config_provider,
]);
}
}
}
......@@ -21,4 +21,34 @@ public function __construct(array $recipes, RecipeDiscovery $recipeDiscovery) {
$this->recipes = array_map([$recipeDiscovery, 'getRecipe'], $recipes);
}
/**
* Returns all the recipes installed by this recipe.
*
* @return \Drupal\Core\Recipe\Recipe[]
* An array of all the recipes being installed.
*/
private function listAllRecipes(): array {
$recipes = [];
foreach ($this->recipes as $recipe) {
$recipes[] = $recipe;
$recipes = array_merge($recipes, $recipe->recipes->listAllRecipes());
}
return $recipes;
}
/**
* List all the extensions installed by this recipe and its dependencies.
*
* @return string[]
* All the modules and themes that will be installed by the current
* recipe and all the recipes it depends on.
*/
public function listAllExtensions(): array {
$extensions = [];
foreach ($this->listAllRecipes() as $recipe) {
$extensions = array_merge($extensions, $recipe->install->modules, $recipe->install->themes);
}
return $extensions;
}
}
......@@ -7,6 +7,7 @@
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Recipe\InvalidConfigException;
use Drupal\Core\Recipe\Recipe;
use Drupal\Core\Recipe\RecipeFileException;
use Drupal\Core\Recipe\RecipeRunner;
use Drupal\KernelTests\KernelTestBase;
......@@ -94,4 +95,47 @@ public function testConfigActionsAreValidated(string $entity_type_id): void {
}
}
/**
* Tests validating that config actions' dependencies are present.
*
* Tests that the all of the config listed in a recipe's config actions are
* provided by extensions that will be installed by the recipe, or one of its
* dependencies (no matter how deeply nested).
*
* @testWith ["direct_dependency"]
* ["indirect_dependency_one_level_down"]
* ["indirect_dependency_two_levels_down"]
*/
public function testConfigActionDependenciesAreValidated(string $name): void {
Recipe::createFromDirectory("core/tests/fixtures/recipes/config_actions_dependency_validation/$name");
}
/**
* Tests config action validation for missing dependency.
*/
public function testConfigActionMissingDependency(): void {
$recipe_data = <<<YAML
name: Config actions making bad decisions
config:
actions:
random.config:
simple_config_update:
label: ''
YAML;
$dir = uniqid('public://');
mkdir($dir);
file_put_contents($dir . '/recipe.yml', $recipe_data);
try {
Recipe::createFromDirectory($dir);
$this->fail('An exception should have been thrown.');
}
catch (RecipeFileException $e) {
$this->assertIsObject($e->violations);
$this->assertCount(1, $e->violations);
$this->assertSame('[config][actions][random.config]', $e->violations[0]->getPropertyPath());
$this->assertSame("Config actions cannot be applied to random.config because the random extension is not installed, and is not installed by this recipe or any of the recipes it depends on.", (string) $e->violations[0]->getMessage());
}
}
}
......@@ -259,6 +259,8 @@ public static function providerRecipeValidation(): iterable {
yield 'config actions list is valid' => [
<<<YAML
name: 'Correct config actions list'
install:
- config_test
config:
actions:
config_test.dynamic.recipe:
......@@ -288,6 +290,7 @@ public static function providerRecipeValidation(): iterable {
'[config][actions][0]' => [
'This value should be of type array.',
'This value should not be blank.',
'Config actions cannot be applied to 0 because the 0 extension is not installed, and is not installed by this recipe or any of the recipes it depends on.',
],
],
];
......
name: Recipe with direct dependency present
type: 'Testing'
install:
- node
config:
actions:
node.settings:
simple_config_update:
use_admin_theme: true
name: Recipe with first level indirect dependency
type: 'Testing'
recipes:
- level_2
config:
actions:
node.settings:
simple_config_update:
use_admin_theme: true
name: Recipe with second level indirect dependency
type: 'Testing'
recipes:
- level_1
config:
actions:
node.settings:
simple_config_update:
use_admin_theme: true
name: First level sub recipe
type: 'Testing'
recipes:
- level_2
name: Second level sub recipe
type: 'Testing'
install:
- node
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