rules-8.x-3.x-dev/tests/src/Unit/Integration/RulesIntegrationTestBase.php
tests/src/Unit/Integration/RulesIntegrationTestBase.php
<?php
declare(strict_types=1);
namespace Drupal\Tests\rules\Unit\Integration;
use Drupal\Component\DependencyInjection\ReverseContainer;
use Drupal\Component\Uuid\Php;
use Drupal\Core\Cache\NullBackend;
use Drupal\Core\DependencyInjection\ClassResolverInterface;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Config\Entity\ConfigEntityStorageInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\Discovery\RecursiveExtensionFilterCallback;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Field\FieldTypeCategoryManager;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\Core\Plugin\Context\LazyContextRepository;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\Core\TypedData\TypedDataManager;
use Drupal\Tests\UnitTestCase;
use Drupal\Tests\rules\Unit\TestMessenger;
use Drupal\rules\Core\ConditionManager;
use Drupal\rules\Context\DataProcessorManager;
use Drupal\rules\Core\RulesActionManager;
use Drupal\rules\Engine\ExpressionManager;
use Drupal\typed_data\DataFetcher;
use Drupal\typed_data\DataFilterManager;
use Drupal\typed_data\PlaceholderResolver;
use Prophecy\Argument;
// cspell:ignore hardwiring
/**
* Base class for Rules integration tests.
*
* Rules integration tests leverage the services (plugin managers) of the Rules
* module to test the integration of an action or condition. Dependencies on
* other 3rd party modules or APIs can and should be mocked; e.g. the action
* to delete an entity would mock the call to the entity API.
*/
abstract class RulesIntegrationTestBase extends UnitTestCase {
/**
* @var \Drupal\Core\Entity\EntityTypeManagerInterface|\Prophecy\Prophecy\ProphecyInterface
*/
protected $entityTypeManager;
/**
* @var \Drupal\Core\Entity\EntityFieldManagerInterface|\Prophecy\Prophecy\ProphecyInterface
*/
protected $entityFieldManager;
/**
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface|\Prophecy\Prophecy\ProphecyInterface
*/
protected $entityTypeBundleInfo;
/**
* @var \Drupal\Core\TypedData\TypedDataManagerInterface
*/
protected $typedDataManager;
/**
* The field type category info plugin manager.
*
* @var \Drupal\Core\Field\FieldTypeCategoryManagerInterface
*/
protected $fieldTypeCategoryManager;
/**
* @var \Drupal\rules\Core\RulesActionManagerInterface
*/
protected $actionManager;
/**
* @var \Drupal\rules\Core\ConditionManager
*/
protected $conditionManager;
/**
* @var \Drupal\rules\Engine\ExpressionManager
*/
protected $rulesExpressionManager;
/**
* @var \Drupal\rules\Context\DataProcessorManager
*/
protected $rulesDataProcessorManager;
/**
* A mocked Rules logger.channel.rules_debug service.
*
* @var \Drupal\Core\Logger\LoggerChannelInterface|\Prophecy\Prophecy\ProphecyInterface
*/
protected $logger;
/**
* All setup'ed namespaces.
*
* @var \ArrayObject
*/
protected $namespaces;
/**
* @var \Drupal\Core\Cache\NullBackend
*/
protected $cacheBackend;
/**
* @var \Drupal\Core\Extension\ModuleHandlerInterface||\Prophecy\Prophecy\ProphecyInterface
*/
protected $moduleHandler;
/**
* Array object keyed with module names and TRUE as value.
*
* @var \ArrayObject
*/
protected $enabledModules;
/**
* The Drupal service container.
*
* @var \Drupal\Core\DependencyInjection\Container
*/
protected $container;
/**
* The class resolver mock for the typed data manager.
*
* @var \Drupal\Core\DependencyInjection\ClassResolverInterface|\Prophecy\Prophecy\ProphecyInterface
*/
protected $classResolver;
/**
* The data fetcher service.
*
* @var \Drupal\typed_data\DataFetcher
*/
protected $dataFetcher;
/**
* The placeholder resolver service.
*
* @var \Drupal\typed_data\PlaceholderResolver
*/
protected $placeholderResolver;
/**
* The data filter manager.
*
* @var \Drupal\typed_data\DataFilterManager
*/
protected $dataFilterManager;
/**
* The messenger service.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$container = new ContainerBuilder();
// Register plugin managers used by Rules, but mock some unwanted
// dependencies requiring more stuff to loaded.
$this->moduleHandler = $this->prophesize(ModuleHandlerInterface::class);
// Set all the modules as being existent.
$this->enabledModules = new \ArrayObject();
$this->enabledModules['rules'] = TRUE;
$this->enabledModules['rules_test'] = TRUE;
$enabled_modules = $this->enabledModules;
$this->moduleHandler->moduleExists(Argument::type('string'))
->will(function ($arguments) use ($enabled_modules) {
if (isset($enabled_modules[$arguments[0]])) {
return [$arguments[0], $enabled_modules[$arguments[0]]];
}
// Handle case where a plugin provider module is not enabled.
return [$arguments[0], FALSE];
});
// We don't care about alter() calls on the module handler.
$this->moduleHandler->alter(Argument::any(), Argument::any(), Argument::any(), Argument::any())
->willReturn(NULL);
$this->cacheBackend = new NullBackend('rules');
$rules_directory = __DIR__ . '/../../../..';
$this->namespaces = new \ArrayObject([
'Drupal\\rules' => $rules_directory . '/src',
'Drupal\\rules_test' => $rules_directory . '/tests/modules/rules_test/src',
'Drupal\\Core\\TypedData' => $this->root . '/core/lib/Drupal/Core/TypedData',
'Drupal\\Core\\Validation' => $this->root . '/core/lib/Drupal/Core/Validation',
]);
$this->actionManager = new RulesActionManager($this->namespaces, $this->cacheBackend, $this->moduleHandler->reveal());
$this->conditionManager = new ConditionManager($this->namespaces, $this->cacheBackend, $this->moduleHandler->reveal());
$uuid_service = new Php();
$this->rulesExpressionManager = new ExpressionManager($this->namespaces, $this->cacheBackend, $this->moduleHandler->reveal(), $uuid_service);
$this->classResolver = $this->prophesize(ClassResolverInterface::class);
$this->typedDataManager = new TypedDataManager(
$this->namespaces,
$this->cacheBackend,
$this->moduleHandler->reveal(),
$this->classResolver->reveal()
);
$this->fieldTypeCategoryManager = new FieldTypeCategoryManager(
$this->root,
$this->moduleHandler->reveal(),
$this->cacheBackend,
);
$this->rulesDataProcessorManager = new DataProcessorManager($this->namespaces, $this->cacheBackend, $this->moduleHandler->reveal());
$this->entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class);
$this->entityTypeManager->getDefinitions()->willReturn([]);
// Setup a rules_component storage mock which returns nothing by default.
$storage = $this->prophesize(ConfigEntityStorageInterface::class);
$storage->loadMultiple(NULL)->willReturn([]);
$this->entityTypeManager->getStorage('rules_component')->willReturn($storage->reveal());
$this->entityFieldManager = $this->prophesize(EntityFieldManagerInterface::class);
$this->entityFieldManager->getBaseFieldDefinitions()->willReturn([]);
$this->entityTypeBundleInfo = $this->prophesize(EntityTypeBundleInfoInterface::class);
$this->entityTypeBundleInfo->getBundleInfo()->willReturn([]);
$this->dataFetcher = new DataFetcher();
$this->messenger = new TestMessenger();
$this->dataFilterManager = new DataFilterManager($this->namespaces, $this->cacheBackend, $this->moduleHandler->reveal());
$this->placeholderResolver = new PlaceholderResolver($this->dataFetcher, $this->dataFilterManager);
// Mock the Rules debug logger service and make it return our mocked logger.
$this->logger = $this->prophesize(LoggerChannelInterface::class);
$container->set('entity_type.manager', $this->entityTypeManager->reveal());
$container->set('entity_field.manager', $this->entityFieldManager->reveal());
$container->set('entity_type.bundle.info', $this->entityTypeBundleInfo->reveal());
$container->set('context.repository', new LazyContextRepository($container, []));
$container->set('logger.channel.rules_debug', $this->logger->reveal());
$container->set('plugin.manager.rules_action', $this->actionManager);
$container->set('plugin.manager.condition', $this->conditionManager);
$container->set('plugin.manager.rules_expression', $this->rulesExpressionManager);
$container->set('plugin.manager.rules_data_processor', $this->rulesDataProcessorManager);
$container->set('messenger', $this->messenger);
$container->set('typed_data_manager', $this->typedDataManager);
$container->set('plugin.manager.field.field_type_category', $this->fieldTypeCategoryManager);
$container->set('string_translation', $this->getStringTranslationStub());
$container->set('uuid', $uuid_service);
$container->set('typed_data.data_fetcher', $this->dataFetcher);
$container->set('typed_data.placeholder_resolver', $this->placeholderResolver);
// The new ReverseContainer service needs to be present to prevent massive
// unit test failures.
// @see https://www.drupal.org/project/rules/issues/3346846
$container->set('Drupal\Component\DependencyInjection\ReverseContainer', new ReverseContainer($container));
\Drupal::setContainer($container);
$this->container = $container;
}
/**
* Fakes the enabling of a module and adds its namespace for plugin loading.
*
* This method allows plugins provided by a module to be discoverable.
*
* @param string $name
* The name of the module that's going to be enabled.
* @param array $namespaces
* Map of the association between module's namespaces and filesystem paths.
*/
protected function enableModule(string $name, array $namespaces = []): void {
$this->enabledModules[$name] = TRUE;
if (empty($namespaces)) {
$namespaces = ['Drupal\\' . $name => $this->root . '/' . $this->constructModulePath($name) . '/src'];
}
foreach ($namespaces as $namespace => $path) {
$this->namespaces[$namespace] = $path;
}
}
/**
* Determines the path to a module's class files.
*
* Core modules and contributed modules are located in different places and
* the testbot (GitLabCI) does not use same directory structure as most live
* Drupal sites. Thus we must discover the path instead of hardwiring it.
*
* This method discovers modules the same way as Drupal core, so it should
* work for core and contributed modules in all environments.
*
* @see \Drupal\Core\Extension\ExtensionDiscovery
*/
protected function constructModulePath(string $module) {
// Use Unix paths regardless of platform, skip dot directories, follow
// symlinks (to allow extensions to be linked from elsewhere), and return
// the RecursiveDirectoryIterator instance to have access to getSubPath(),
// since SplFileInfo does not support relative paths.
$flags = \FilesystemIterator::UNIX_PATHS;
$flags |= \FilesystemIterator::SKIP_DOTS;
$flags |= \FilesystemIterator::FOLLOW_SYMLINKS;
$flags |= \FilesystemIterator::CURRENT_AS_SELF;
$directory_iterator = new \RecursiveDirectoryIterator($this->root, $flags);
// Filter the recursive scan to discover extensions only.
// Ensure we find testing modules too!
$callback = new RecursiveExtensionFilterCallback([], TRUE);
$filter = new \RecursiveCallbackFilterIterator($directory_iterator, [$callback, 'accept']);
// The actual recursive filesystem scan is only invoked by instantiating the
// RecursiveIteratorIterator.
$iterator = new \RecursiveIteratorIterator($filter,
\RecursiveIteratorIterator::LEAVES_ONLY,
// Suppress filesystem errors in case a directory cannot be accessed.
\RecursiveIteratorIterator::CATCH_GET_CHILD
);
$info_files = new \RegexIterator($iterator, "/^$module.info.yml$/");
foreach ($info_files as $file) {
// There should only be one match.
return $file->getSubPath();
}
}
/**
* Returns a typed data object.
*
* This helper for quick creation of typed data objects.
*
* @param string $data_type
* The data type to create an object for.
* @param mixed $value
* The value to set.
*
* @return \Drupal\Core\TypedData\TypedDataInterface
* The created object.
*/
protected function getTypedData(string $data_type, mixed $value): TypedDataInterface {
$definition = $this->typedDataManager->createDataDefinition($data_type);
$data = $this->typedDataManager->create($definition);
$data->setValue($value);
return $data;
}
/**
* Helper method to mock irrelevant cache methods on entities.
*
* @param string $interface
* The interface that should be mocked, example: EntityInterface::class.
*
* @return \Drupal\Core\Entity\EntityInterface|\Prophecy\Prophecy\ProphecyInterface
* The mocked entity.
*/
protected function prophesizeEntity(string $interface) {
$entity = $this->prophesize($interface);
// Cache methods are irrelevant for the tests but might be called.
$entity->getCacheContexts()->willReturn([]);
$entity->getCacheTags()->willReturn([]);
$entity->getCacheMaxAge()->willReturn(0);
return $entity;
}
}
