xero_sync-8.x-1.x-dev/tests/src/Kernel/XeroSyncTestBase.php
tests/src/Kernel/XeroSyncTestBase.php
<?php
namespace Drupal\Tests\xero_sync\Kernel;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\xero\Normalizer\XeroNormalizer;
use Drupal\xero\TypedData\XeroComplexItemInterface;
use Drupal\xero_sync\XeroSyncEntityHandler;
use Radcliffe\Xero\XeroClient;
/**
* A base class for testing that uses the mock itemManager instead of Xero API.
*
* @group xero_sync
*/
abstract class XeroSyncTestBase extends EntityKernelTestBase {
/**
* The calls the mock itemManager should receive.
*
* @var array
*/
protected $expectedItemManagerCalls = [];
/**
* {@inheritDoc}
*/
protected function setUp(): void {
parent::setUp();
// Any calls that reach the Xero client will fail with an obscure
// container error because it is not configured, so mock it.
$client = $this->createMock(XeroClient::class);
$this->container->set('xero.client', $client);
}
/**
* Set up the mock itemManager.
*
* @param array $expectations
* The calls the itemManager expects to get, & the values it should return.
* The first array value is the itemManager method name, the second is an
* array of arguments for the method, and third is the value to return.
*/
protected function itemManagerExpects(array $expectations) {
$returns = [];
$this->expectedItemManagerCalls = [];
foreach ($expectations as $expectation) {
$this->expectedItemManagerCalls[] = [
'method' => $expectation['method'],
'arguments' => $expectation['arguments'],
];
$returns[] = $expectation['return'];
}
$this->getItemManager()->setReturns($returns);
}
/**
* Get the item manager, if not already mocked and set.
*
* @return \Drupal\xero\XeroItemManagerInterface
* The item manager.
*/
protected function getItemManager() {
if (!isset($this->itemManager)) {
$this->itemManager = \Drupal::service('xero.item_manager');
}
return $this->itemManager;
}
/**
* Assert that the mock item manager was called in the expected ways.
*/
protected function assertItemManagerExpectationsMet() {
$this->assertNotNull($this->expectedItemManagerCalls);
$calls = $this->getItemManager()->getCalls();
$message = (string) new FormattableMarkup("itemManager was not called the expected number of times. \n Actual: \n @actual \n \n Expected: \n @expected", [
'@actual' => print_r(array_column($calls, 'method'), TRUE),
'@expected' => print_r(array_column($this->expectedItemManagerCalls, 'method'), TRUE),
]);
$this->assertEquals(count($this->expectedItemManagerCalls), count($calls), $message);
for ($i = 0; $i < count($calls); $i++) {
$this->assertEquals($this->expectedItemManagerCalls[$i]['method'], $calls[$i]['method'], "Call to unexpected method on itemManager.");
$actualArgs = $calls[$i]['arguments'];
$expectedArgs = $this->expectedItemManagerCalls[$i]['arguments'];
$this->assertEquals(count($expectedArgs), count($actualArgs), "itemManager invocation " . $i . " (" . $calls[$i]['method'] . ") has unexpected number of arguments");
// Provide special comparison for any arguments that are Xero items,
// to help debugging by providing better visibility of item properties.
foreach ($actualArgs as $index => $actualArg) {
$expectedArg = $expectedArgs[$index];
$expectedIsXeroItem = $expectedArg instanceof XeroComplexItemInterface;
$actualIsXeroItem = $actualArg instanceof XeroComplexItemInterface;
$this->assertFalse(
(($expectedIsXeroItem && !$actualIsXeroItem) ||
(!$expectedIsXeroItem && $actualIsXeroItem)),
"If actual or expected is a Xero item, then the other should be too"
);
if ($expectedIsXeroItem && $actualIsXeroItem) {
$message = (string) new FormattableMarkup(
"Mock query manager call @i to '@method' method contained a Xero item in its @index argument.",
[
'@i' => $i + 1,
'@method' => $calls[$i]['method'],
'@index' => $index + 1,
]);
$this->assertEqualXeroItems($expectedArg, $actualArg, $message);
$expectedArgs[$index] = "Xero item";
$actualArgs[$index] = "Xero item";
}
}
$this->assertEquals($expectedArgs, $actualArgs, "Unexpected arguments to '" . $calls[$i]['method'] . "' method on itemManager.");
}
}
/**
* Assert that two Xero items are equal, with helpful messages if not.
*
* @param \Drupal\xero\TypedData\XeroComplexItemInterface $expected
* The expected item.
* @param mixed $actual
* The actual item.
* @param string|null $message
* (optional) An additional message to output if the items are not equal.
*/
protected function assertEqualXeroItems(XeroComplexItemInterface $expected, $actual, $message = NULL) {
$this->assertInstanceOf(XeroComplexItemInterface::class, $actual, "Actual item should be a Xero typed data object");
$this->assertEquals($expected->getPluginId(), $actual->getPluginId(), "Items should use same Xero type plugin");
$normalizer = new XeroNormalizer(\Drupal::service('typed_data_manager'));
$normalizer->setSerializer(\Drupal::service('serializer'));
$message = $message . " The Xero item different to expected.";
$context = [
'plugin_id' => $expected->getPluginId(),
];
$actualNormalized = $normalizer->normalize($actual, 'xml', $context);
$expectedNormalized = $normalizer->normalize($expected, 'xml', $context);
$message = (string) new FormattableMarkup("@message \n Actual: \n @actual \n \n Expected: \n @expected", [
'@message' => $message,
'@actual' => print_r($actualNormalized, TRUE),
'@expected' => print_r($expectedNormalized, TRUE),
]);
$this->assertEquals($expectedNormalized, $actualNormalized, $message);
}
/**
* Convert a typed data Xero item into an array of strings.
*
* @param \Drupal\xero\TypedData\XeroComplexItemInterface $item
* The Xero item.
*
* @return array
* The array of strings.
*/
protected function simplifyXeroItem(XeroComplexItemInterface $item) {
$strings = [];
foreach ($item->getSpecifiedProperties() as $name => $property) {
$string = $property->getString();
if (!empty($string)) {
$strings[$name] = $string;
}
}
return $strings;
}
/**
* Build a typed data object representing a Xero item.
*
* @param string $type
* The Xero type.
* @param array $properties
* The item's initial properties.
*
* @return \Drupal\Core\TypedData\TypedDataInterface
* The typed data object.
*/
protected function buildXeroItem($type, array $properties) {
$definition = \Drupal::typedDataManager()->createDataDefinition($type);
$item = \Drupal::typedDataManager()->create($definition, $properties);
$this->assertInstanceOf(XeroComplexItemInterface::class, $item);
return $item;
}
/**
* Set up a Xero reference field on an entity type.
*
* @param string $entityType
* The entity type.
*
* @throws \Drupal\Core\Entity\EntityStorageException
*/
protected function setUpXeroField($entityType) {
$this->entityHandler = \Drupal::service('xero_sync.entity_handler');
$this->entityTypeManager->clearCachedDefinitions();
$this->installEntitySchema($entityType);
/** @var \Drupal\field\Entity\FieldStorageConfig $field_storage */
FieldStorageConfig::create([
'field_name' => $this->fieldName,
'type' => 'xero_reference',
'entity_type' => $entityType,
'cardinality' => 1,
])->save();
FieldConfig::create([
'entity_type' => $entityType,
'field_name' => $this->fieldName,
'bundle' => $entityType,
'label' => 'Test Xero field',
])->save();
$this->entityTypeManager->clearCachedDefinitions();
$entityFieldManager = $this->container->get('entity_field.manager');
$definitions = $entityFieldManager->getFieldStorageDefinitions($entityType);
$this->assertTrue(!empty($definitions[$this->fieldName]));
}
/**
* Assert that a specific item is referenced in an entity's Xero Sync field.
*
* @param \Drupal\Core\Entity\FieldableEntityInterface $entity
* The entity.
* @param string $expectedType
* The Xero type of the expected item.
* @param string $expectedId
* The guid of the expected item.
*/
protected function assertItemReferencedInField(FieldableEntityInterface $entity, $expectedType, $expectedId) {
$entity = $this->reloadEntity($entity);
$field = $entity->get(XeroSyncEntityHandler::DEFAULT_FIELD_NAME);
$this->assertFalse($field->isEmpty(), "Field should not be empty.");
$storedId = $field->first()->guid;
$storedType = $field->first()->type;
$this->assertEquals($storedType, $expectedType);
$this->assertEquals($storedId, $expectedId);
}
/**
* Assert that there is no item referenced in an entity's Xero Sync field.
*
* @param \Drupal\Core\Entity\FieldableEntityInterface $entity
* The entity.
*/
protected function assertNoItemReferencedInField(FieldableEntityInterface $entity) {
$entity = $this->reloadEntity($entity);
$field = $entity->get(XeroSyncEntityHandler::DEFAULT_FIELD_NAME);
$this->assertTrue($field->isEmpty());
}
/**
* Store a value in the Xero Sync field on an entity.
*
* @param \Drupal\Core\Entity\FieldableEntityInterface $entity
* The entity.
* @param string $type
* The Xero type.
* @param string $id
* The Xero guid.
*/
protected function setXeroField(FieldableEntityInterface &$entity, $type, $id) {
$entity->set(XeroSyncEntityHandler::DEFAULT_FIELD_NAME, [
'guid' => $id,
'type' => $type,
]);
}
/**
* Creates an entity_test entity and stores a contact id on it.
*
* @param string $contactGuid
* The guid to be stored in the field.
*
* @return \Drupal\Core\Entity\ContentEntityInterface
* The created entity.
*
* @throws \Drupal\Core\Entity\EntityStorageException
*/
protected function createEntityWithContact($contactGuid) {
$entity = EntityTest::create([
$this->fieldName => [
'type' => 'xero_contact',
'guid' => $contactGuid,
],
]);
$entity->save();
$this->assertItemReferencedInField($entity, 'xero_contact', $contactGuid);
return $entity;
}
/**
* Create a Guid with or without curly braces.
*
* @param bool|null $braces
* (Optional) Return Guid wrapped in curly braces.
*
* @return string
* Guid string.
*/
protected function createGuid($braces = TRUE) {
$hash = strtolower(hash('ripemd128', md5($this->getRandomGenerator()->string(100))));
$guid = substr($hash, 0, 8) . '-' . substr($hash, 8, 4) . '-' . substr($hash, 12, 4);
$guid .= '-' . substr($hash, 16, 4) . '-' . substr($hash, 20, 12);
if ($braces) {
return '{' . $guid . '}';
}
return $guid;
}
/**
* Replace Xero's item manager with a test version that keeps track of calls.
*
* Possibly this could be replaced by a standard PHPUnit mock.
*/
protected function mockItemManager() {
$typed_data_manager = \Drupal::service('typed_data_manager');
$factory = \Drupal::service('xero.query.factory');
$logger = \Drupal::service('logger.factory');
$item_manager = new MockItemManager($typed_data_manager, $factory, $logger);
$this->container->set('xero.item_manager', $item_manager);
}
/**
* Disable the Xero Sync entity handler.
*
* As entity CRUD hooks typically invoke the entity handler to initiate
* syncing this allows us to setup tests that entities without needing to
* consider the effect of hooks on syncing.
*/
protected function disableEntityHandler() {
$this->entityHandler = $this->createMock(XeroSyncEntityHandler::class);
$this->entityHandler->expects($this->any())
->method('getFieldName')
->willReturn(XeroSyncEntityHandler::DEFAULT_FIELD_NAME);
$this->container->set('xero_sync.entity_handler', $this->entityHandler);
}
/**
* Set an expectation that there will not be further entity updates.
*
* @param int $count
* How many entity updates to expect.
*/
protected function expectEntityUpdates($count) {
$this->entityHandler->expects($this->exactly($count))
->method('handleUpdate');
}
/**
* Disable the use of queues, so that syncing happens immediately.
*/
protected function bypassQueues() {
$logger = \Drupal::service('logger.channel.xero_sync');
$entity_type_manager = \Drupal::service('entity_type.manager');
$entity_type_bundle_info = \Drupal::service('entity_type.bundle.info');
$synchronizer = \Drupal::service('xero_sync.synchronizer');
$typed_data_manager = \Drupal::service('typed_data_manager');
$job_manager = \Drupal::service('xero_sync.job_manager');
$serializer = \Drupal::service('serializer');
$time = \Drupal::service('datetime.time');
$entity_handler = \Drupal::service('xero_sync.entity_handler');
$queueless = new QueuelessJobManager($logger, $entity_type_manager, $entity_type_bundle_info, $synchronizer, $typed_data_manager, $job_manager, $serializer, $time, $entity_handler);
$this->container->set('xero_sync.job_manager', $queueless);
// We have to reset the entity handler also, because the job manager is a
// dependency of it and it may already be instantiated.
$entity_handler = new XeroSyncEntityHandler($queueless, $synchronizer, $typed_data_manager, $entity_type_manager, \Drupal::service('event_dispatcher'));
$this->container->set('xero_sync.entity_handler', $entity_handler);
}
}
