Initial Drupal 11 with DDEV setup

This commit is contained in:
gluebox
2025-10-08 11:39:17 -04:00
commit 89ef74b305
25344 changed files with 2599172 additions and 0 deletions

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Functional;
use Drupal\Tests\system\Functional\Module\GenericModuleTestBase;
/**
* Generic module test for migrate.
*
* @group migrate
*/
class GenericTest extends GenericModuleTestBase {}

View File

@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Functional;
/**
* Tests for the MigrateController class.
*
* @group migrate
*/
class MigrateMessageControllerTest extends MigrateMessageTestBase {
/**
* Tests the overview page for migrate messages.
*
* Tests the overview page with the following scenarios;
* - No message tables.
* - With message tables.
*/
public function testOverview(): void {
$session = $this->assertSession();
// First, test with no source database or message tables.
$this->drupalGet('/admin/reports/migration-messages');
$session->titleEquals('Migration messages | Drupal');
$session->pageTextContainsOnce('There are no migration message tables.');
// Create map and message tables.
$this->createTables($this->migrationIds);
// Now, test with message tables.
$this->drupalGet('/admin/reports/migration-messages');
foreach ($this->migrationIds as $migration_id) {
$session->pageTextContains($migration_id);
}
}
/**
* Tests the detail pages for migrate messages.
*
* Tests the detail page with the following scenarios;
* - No source database connection or message tables with a valid and an
* invalid migration.
* - A source database connection with message tables with a valid and an
* invalid migration.
* - A source database connection with message tables and a source plugin
* that does not have a description for a source ID in the values returned
* from fields().
*/
public function testDetail(): void {
$session = $this->assertSession();
// Details page with invalid migration.
$this->drupalGet('/admin/reports/migration-messages/invalid');
$session->statusCodeEquals(404);
// Details page with valid migration.
$this->drupalGet('/admin/reports/migration-messages/custom_test');
$session->statusCodeEquals(404);
// Create map and message tables.
$this->createTables($this->migrationIds);
$not_available_text = "When there is an error processing a row, the migration system saves the error message but not the source ID(s) of the row. That is why some messages in this table have 'Not available' in the source ID column(s).";
// Test details page for each migration.
foreach ($this->migrationIds as $migration_id) {
$this->drupalGet("/admin/reports/migration-messages/$migration_id");
$session->pageTextContains($migration_id);
if ($migration_id == 'custom_test') {
$session->pageTextContains('Not available');
$session->pageTextContains($not_available_text);
}
}
// Details page with invalid migration.
$this->drupalGet('/admin/reports/migration-messages/invalid');
$session->statusCodeEquals(404);
// Details page for a migration without a map table.
$this->database->schema()->dropTable('migrate_map_custom_test');
$this->drupalGet('/admin/reports/migration-messages/custom_test');
$session->statusCodeEquals(404);
// Details page for a migration with a map table but no message table.
$this->createTables($this->migrationIds);
$this->database->schema()->dropTable('migrate_message_custom_test');
$this->drupalGet('/admin/reports/migration-messages/custom_test');
$session->pageTextContains('The message table is missing for this migration.');
}
}

View File

@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Functional;
use Drupal\migrate\Plugin\MigrationInterface;
/**
* Tests for the MessageForm class.
*
* @group migrate
*/
class MigrateMessageFormTest extends MigrateMessageTestBase {
/**
* Tests the message form.
*/
public function testFilter(): void {
$session = $this->assertSession();
// Create map and message tables.
$this->createTables($this->migrationIds);
// Expected counts for each error level.
$expected = [
MigrationInterface::MESSAGE_ERROR => 3,
MigrationInterface::MESSAGE_WARNING => 0,
MigrationInterface::MESSAGE_NOTICE => 0,
MigrationInterface::MESSAGE_INFORMATIONAL => 1,
];
// Confirm that all the entries are displayed.
$this->drupalGet('/admin/reports/migration-messages/custom_test');
$session->statusCodeEquals(200);
$messages = $this->getMessages();
$this->assertCount(4, $messages);
// Set the filter to match each of the two filter-type attributes and
// confirm the correct number of entries are displayed.
foreach ($expected as $level => $expected_count) {
$edit['severity[]'] = $level;
$this->submitForm($edit, 'Filter');
$count = $this->getLevelCounts($expected);
$this->assertEquals($expected_count, $count[$level], sprintf('Count for level %s failed', $level));
}
// Reset the filter
$this->submitForm([], 'Reset');
$messages = $this->getMessages();
$this->assertCount(4, $messages);
}
/**
* Gets the count of migration messages by level.
*
* @param array $levels
* The error levels to check.
*
* @return array
* The count of each error level keyed by the error level.
*/
protected function getLevelCounts(array $levels): array {
$entries = $this->getMessages();
$count = array_fill(1, count($levels), 0);
foreach ($entries as $entry) {
if (array_key_exists($entry['severity'], $levels)) {
$count[$entry['severity']]++;
}
}
return $count;
}
/**
* Gets the migrate messages.
*
* @return array[]
* List of log events where each event is an array with following keys:
* - msg_id: (string) A message id.
* - severity: (int) The MigrationInterface error level.
* - message: (string) The migration message.
*/
protected function getMessages(): array {
$levels = [
'Error' => MigrationInterface::MESSAGE_ERROR,
'Warning' => MigrationInterface::MESSAGE_WARNING,
'Notice' => MigrationInterface::MESSAGE_NOTICE,
'Info' => MigrationInterface::MESSAGE_INFORMATIONAL,
];
$entries = [];
$table = $this->xpath('.//table[@id="admin-migrate-msg"]/tbody/tr');
foreach ($table as $row) {
$cells = $row->findAll('css', 'td');
if (count($cells) === 3) {
$entries[] = [
'msg_id' => $cells[0]->getText(),
'severity' => $levels[$cells[1]->getText()],
'message' => $cells[2]->getText(),
];
}
}
return $entries;
}
}

View File

@ -0,0 +1,239 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Functional;
use Drupal\Tests\BrowserTestBase;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Plugin\MigrationInterface;
// cspell:ignore destid sourceid
/**
* Provides base class for testing migrate messages.
*/
abstract class MigrateMessageTestBase extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'message_test',
'migrate',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* Migration IDs.
*
* @var string[]
*/
protected $migrationIds = ['custom_test'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$user = $this->createUser(['view migration messages']);
$this->drupalLogin($user);
$this->database = \Drupal::database();
}
/**
* Creates map and message tables for testing.
*
* @see \Drupal\migrate\Plugin\migrate\id_map\Sql::ensureTables
*/
protected function createTables($migration_ids): void {
foreach ($migration_ids as $migration_id) {
$map_table_name = "migrate_map_$migration_id";
$message_table_name = "migrate_message_$migration_id";
if (!$this->database->schema()->tableExists($map_table_name)) {
$fields = [];
$fields['source_ids_hash'] = [
'type' => 'varchar',
'length' => '64',
'not null' => TRUE,
];
$fields['sourceid1'] = [
'type' => 'varchar',
'length' => '255',
'not null' => TRUE,
];
$fields['destid1'] = [
'type' => 'varchar',
'length' => '255',
'not null' => FALSE,
];
$fields['source_row_status'] = [
'type' => 'int',
'size' => 'tiny',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => MigrateIdMapInterface::STATUS_IMPORTED,
];
$fields['rollback_action'] = [
'type' => 'int',
'size' => 'tiny',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => MigrateIdMapInterface::ROLLBACK_DELETE,
];
$fields['last_imported'] = [
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
];
$fields['hash'] = [
'type' => 'varchar',
'length' => '64',
'not null' => FALSE,
];
$schema = [
'description' => '',
'fields' => $fields,
'primary key' => ['source_ids_hash'],
];
$this->database->schema()->createTable($map_table_name, $schema);
$rows = [
[
'source_ids_hash' => '37c655d',
'sourceid1' => 'navigation',
'destid1' => 'tools',
'source_row_status' => '0',
'rollback_action' => '1',
'last_imported' => '0',
'hash' => '',
],
[
'source_ids_hash' => '3a34190',
'sourceid1' => 'menu-fixed-lang',
'destid1' => 'menu-fixed-lang',
'source_row_status' => '0',
'rollback_action' => '0',
'last_imported' => '0',
'hash' => '',
],
[
'source_ids_hash' => '3e51f67',
'sourceid1' => 'management',
'destid1' => 'admin',
'source_row_status' => '0',
'rollback_action' => '1',
'last_imported' => '0',
'hash' => '',
],
[
'source_ids_hash' => '94a5caa',
'sourceid1' => 'user-menu',
'destid1' => 'account',
'source_row_status' => '0',
'rollback_action' => '1',
'last_imported' => '0',
'hash' => '',
],
[
'source_ids_hash' => 'c0efbcca',
'sourceid1' => 'main-menu',
'destid1' => 'main',
'source_row_status' => '0',
'rollback_action' => '1',
'last_imported' => '0',
'hash' => '',
],
[
'source_ids_hash' => 'f64cb72f',
'sourceid1' => 'menu-test-menu',
'destid1' => 'menu-test-menu',
'source_row_status' => '0',
'rollback_action' => '0',
'last_imported' => '0',
'hash' => '',
],
];
foreach ($rows as $row) {
$this->database->insert($map_table_name)->fields($row)->execute();
}
}
if (!$this->database->schema()->tableExists($message_table_name)) {
$fields = [];
$fields['msgid'] = [
'type' => 'serial',
'unsigned' => TRUE,
'not null' => TRUE,
];
$fields['source_ids_hash'] = [
'type' => 'varchar',
'length' => '64',
'not null' => TRUE,
];
$fields['level'] = [
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 1,
];
$fields['message'] = [
'type' => 'text',
'size' => 'medium',
'not null' => TRUE,
];
$schema = [
'description' => '',
'fields' => $fields,
'primary key' => ['msgid'],
];
$this->database->schema()->createTable($message_table_name, $schema);
$rows = [
[
'msgid' => '1',
'source_ids_hash' => '28cfb3d1',
'level' => (string) MigrationInterface::MESSAGE_ERROR,
'message' => 'Config entities can not be stubbed.',
],
[
'msgid' => '2',
'source_ids_hash' => '28cfb3d1',
'level' => (string) MigrationInterface::MESSAGE_ERROR,
'message' => 'Config entities can not be stubbed.',
],
[
'msgid' => '3',
'source_ids_hash' => '05914d93',
'level' => (string) MigrationInterface::MESSAGE_ERROR,
'message' => 'Config entities can not be stubbed.',
],
[
'msgid' => '4',
'source_ids_hash' => '05914d93',
'level' => (string) MigrationInterface::MESSAGE_INFORMATIONAL,
'message' => 'Config entities can not be stubbed.',
],
];
foreach ($rows as $row) {
$this->database->insert($message_table_name)->fields($row)->execute();
}
}
}
}
}

View File

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Functional;
use Drupal\node\Entity\Node;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
/**
* Execute migration.
*
* This is intentionally a Functional test instead of a Kernel test because
* Kernel tests have proven to not catch all edge cases that are encountered
* via a Functional test.
*
* @group migrate
*/
class MigrateNoMigrateDrupalTest extends BrowserTestBase {
use ContentTypeCreationTrait;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $modules = [
'migrate',
'migrate_no_migrate_drupal_test',
'node',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->createContentType(['type' => 'no_migrate_drupal']);
}
/**
* Tests execution of a migration.
*/
public function testExecutionNoMigrateDrupal(): void {
$this->drupalGet('/migrate_no_migrate_drupal_test/execute');
$this->assertSession()->pageTextContains('Migration was successful.');
$node_1 = Node::load(1);
$node_2 = Node::load(2);
$this->assertEquals('Node 1', $node_1->label());
$this->assertEquals('Node 2', $node_2->label());
}
}

View File

@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Functional\process;
use Drupal\migrate\MigrateExecutable;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\Tests\BrowserTestBase;
// cspell:ignore destid
/**
* Tests the 'download' process plugin.
*
* @group migrate
*/
class DownloadFunctionalTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate', 'file'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests that an exception is thrown bu migration continues with the next row.
*/
public function testExceptionThrow(): void {
$invalid_url = "{$this->baseUrl}/not-existent-404";
$valid_url = "{$this->baseUrl}/core/misc/favicon.ico";
$definition = [
'source' => [
'plugin' => 'embedded_data',
'data_rows' => [
['url' => $invalid_url, 'uri' => 'public://first.txt'],
['url' => $valid_url, 'uri' => 'public://second.ico'],
],
'ids' => [
'url' => ['type' => 'string'],
],
],
'process' => [
'uri' => [
'plugin' => 'download',
'source' => ['url', 'uri'],
],
],
'destination' => [
'plugin' => 'entity:file',
],
];
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition);
$executable = new MigrateExecutable($migration);
$result = $executable->import();
// Check that the migration has completed.
$this->assertEquals(MigrationInterface::RESULT_COMPLETED, $result);
/** @var \Drupal\migrate\Plugin\MigrateIdMapInterface $id_map_plugin */
$id_map_plugin = $migration->getIdMap();
// Check that the first row was marked as failed in the id map table.
$map_row = $id_map_plugin->getRowBySource(['url' => $invalid_url]);
$this->assertEquals(MigrateIdMapInterface::STATUS_FAILED, $map_row['source_row_status']);
$this->assertNull($map_row['destid1']);
// Check that a message with the thrown exception has been logged.
$messages = $id_map_plugin->getMessages(['url' => $invalid_url])->fetchAll();
$this->assertCount(1, $messages);
$message = reset($messages);
// Assert critical parts of the error message, but not the exact message,
// since it depends on Guzzle's internal implementation of PSR-7.
$id = $migration->getPluginId();
$this->assertStringContainsString("$id:uri:download:", $message->message);
$this->assertStringContainsString($invalid_url, $message->message);
$this->assertEquals(MigrationInterface::MESSAGE_ERROR, $message->level);
// Check that the second row was migrated successfully.
$map_row = $id_map_plugin->getRowBySource(['url' => $valid_url]);
$this->assertEquals(MigrateIdMapInterface::STATUS_IMPORTED, $map_row['source_row_status']);
$this->assertEquals(1, $map_row['destid1']);
}
}

View File

@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
/**
* Tests the high water handling.
*
* @covers \Drupal\migrate_high_water_test\Plugin\migrate\source\HighWaterTest
* @group migrate
*/
class HighWaterNotJoinableTest extends MigrateSqlSourceTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'migrate',
'migrate_drupal',
'migrate_high_water_test',
];
/**
* {@inheritdoc}
*/
public static function providerSource() {
$tests = [];
// Test high water when the map is not joinable.
// The source data.
$tests[0]['source_data']['high_water_node'] = [
[
'id' => 1,
'title' => 'Item 1',
'changed' => 1,
],
[
'id' => 2,
'title' => 'Item 2',
'changed' => 2,
],
[
'id' => 3,
'title' => 'Item 3',
'changed' => 3,
],
];
// The expected results.
$tests[0]['expected_data'] = [
[
'id' => 2,
'title' => 'Item 2',
'changed' => 2,
],
[
'id' => 3,
'title' => 'Item 3',
'changed' => 3,
],
];
// The expected count is the count returned by the query before the query
// is modified by SqlBase::initializeIterator().
$tests[0]['expected_count'] = 3;
$tests[0]['configuration'] = [
'high_water_property' => [
'name' => 'changed',
],
];
$tests[0]['high_water'] = $tests[0]['source_data']['high_water_node'][0]['changed'];
// Test high water initialized to NULL.
$tests[1]['source_data'] = $tests[0]['source_data'];
$tests[1]['expected_data'] = [
[
'id' => 1,
'title' => 'Item 1',
'changed' => 1,
],
[
'id' => 2,
'title' => 'Item 2',
'changed' => 2,
],
[
'id' => 3,
'title' => 'Item 3',
'changed' => 3,
],
];
$tests[1]['expected_count'] = $tests[0]['expected_count'];
$tests[1]['configuration'] = $tests[0]['configuration'];
$tests[1]['high_water'] = NULL;
// Test high water initialized to an empty string.
$tests[2]['source_data'] = $tests[0]['source_data'];
$tests[2]['expected_data'] = [
[
'id' => 1,
'title' => 'Item 1',
'changed' => 1,
],
[
'id' => 2,
'title' => 'Item 2',
'changed' => 2,
],
[
'id' => 3,
'title' => 'Item 3',
'changed' => 3,
],
];
$tests[2]['expected_count'] = $tests[0]['expected_count'];
$tests[2]['configuration'] = $tests[0]['configuration'];
$tests[2]['high_water'] = '';
return $tests;
}
}

View File

@ -0,0 +1,310 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
// cspell:ignore Highwater
/**
* Tests migration high water property.
*
* @group migrate
*/
class HighWaterTest extends MigrateTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'user',
'node',
'migrate',
'migrate_high_water_test',
'field',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create source test table.
$this->sourceDatabase->schema()->createTable('high_water_node', [
'fields' => [
'id' => [
'description' => 'Serial',
'type' => 'serial',
'unsigned' => TRUE,
'not null' => TRUE,
],
'changed' => [
'description' => 'Highwater',
'type' => 'int',
'unsigned' => TRUE,
],
'title' => [
'description' => 'Title',
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'default' => '',
],
],
'primary key' => [
'id',
],
'description' => 'Contains nodes to import',
]);
// Add 3 items to source table.
$this->sourceDatabase->insert('high_water_node')
->fields([
'title',
'changed',
])
->values([
'title' => 'Item 1',
'changed' => 1,
])
->values([
'title' => 'Item 2',
'changed' => 2,
])
->values([
'title' => 'Item 3',
'changed' => 3,
])
->execute();
$this->installEntitySchema('node');
$this->installEntitySchema('user');
$this->installSchema('node', 'node_access');
$this->executeMigration('high_water_test');
}
/**
* Tests high water property of SqlBase.
*/
public function testHighWater(): void {
// Assert all of the nodes have been imported.
$this->assertNodeExists('Item 1');
$this->assertNodeExists('Item 2');
$this->assertNodeExists('Item 3');
// Update Item 1 setting its high_water_property to value that is below
// current high water mark.
$this->sourceDatabase->update('high_water_node')
->fields([
'title' => 'Item 1 updated',
'changed' => 2,
])
->condition('title', 'Item 1')
->execute();
// Update Item 2 setting its high_water_property to value equal to
// current high water mark.
$this->sourceDatabase->update('high_water_node')
->fields([
'title' => 'Item 2 updated',
'changed' => 3,
])
->condition('title', 'Item 2')
->execute();
// Update Item 3 setting its high_water_property to value that is above
// current high water mark.
$this->sourceDatabase->update('high_water_node')
->fields([
'title' => 'Item 3 updated',
'changed' => 4,
])
->condition('title', 'Item 3')
->execute();
// Execute migration again.
$this->executeMigration('high_water_test');
// Item with lower high water should not be updated.
$this->assertNodeExists('Item 1');
$this->assertNodeDoesNotExist('Item 1 updated');
// Item with equal high water should not be updated.
$this->assertNodeExists('Item 2');
$this->assertNodeDoesNotExist('Item 2 updated');
// Item with greater high water should be updated.
$this->assertNodeExists('Item 3 updated');
$this->assertNodeDoesNotExist('Item 3');
}
/**
* Tests that the high water value can be 0.
*/
public function testZeroHighwater(): void {
// Assert all of the nodes have been imported.
$this->assertNodeExists('Item 1');
$this->assertNodeExists('Item 2');
$this->assertNodeExists('Item 3');
$migration = $this->container->get('plugin.manager.migration')->CreateInstance('high_water_test', []);
$source = $migration->getSourcePlugin();
$source->rewind();
$count = 0;
while ($source->valid()) {
$count++;
$source->next();
}
// Expect no rows as everything is below the high water mark.
$this->assertSame(0, $count);
// Test resetting the high water mark to 0.
$this->container->get('keyvalue')->get('migrate:high_water')->set('high_water_test', 0);
$migration = $this->container->get('plugin.manager.migration')->CreateInstance('high_water_test', []);
$source = $migration->getSourcePlugin();
$source->rewind();
$count = 0;
while ($source->valid()) {
$count++;
$source->next();
}
$this->assertSame(3, $count);
}
/**
* Tests that deleting the high water value causes all rows to be reimported.
*/
public function testNullHighwater(): void {
// Assert all of the nodes have been imported.
$this->assertNodeExists('Item 1');
$this->assertNodeExists('Item 2');
$this->assertNodeExists('Item 3');
$migration = $this->container->get('plugin.manager.migration')->CreateInstance('high_water_test', []);
$source = $migration->getSourcePlugin();
$source->rewind();
$count = 0;
while ($source->valid()) {
$count++;
$source->next();
}
// Expect no rows as everything is below the high water mark.
$this->assertSame(0, $count);
// Test resetting the high water mark.
$this->container->get('keyvalue')->get('migrate:high_water')->delete('high_water_test');
$migration = $this->container->get('plugin.manager.migration')->CreateInstance('high_water_test', []);
$source = $migration->getSourcePlugin();
$source->rewind();
$count = 0;
while ($source->valid()) {
$count++;
$source->next();
}
$this->assertSame(3, $count);
}
/**
* Tests high water property of SqlBase when rows marked for update.
*/
public function testHighWaterUpdate(): void {
// Assert all of the nodes have been imported.
$this->assertNodeExists('Item 1');
$this->assertNodeExists('Item 2');
$this->assertNodeExists('Item 3');
// Update Item 1 setting its high_water_property to value that is below
// current high water mark.
$this->sourceDatabase->update('high_water_node')
->fields([
'title' => 'Item 1 updated',
'changed' => 2,
])
->condition('title', 'Item 1')
->execute();
// Update Item 2 setting its high_water_property to value equal to
// current high water mark.
$this->sourceDatabase->update('high_water_node')
->fields([
'title' => 'Item 2 updated',
'changed' => 3,
])
->condition('title', 'Item 2')
->execute();
// Update Item 3 setting its high_water_property to value that is above
// current high water mark.
$this->sourceDatabase->update('high_water_node')
->fields([
'title' => 'Item 3 updated',
'changed' => 4,
])
->condition('title', 'Item 3')
->execute();
// Set all rows as needing an update.
$id_map = $this->getMigration('high_water_test')->getIdMap();
$id_map->prepareUpdate();
$this->executeMigration('high_water_test');
// Item with lower high water should be updated.
$this->assertNodeExists('Item 1 updated');
$this->assertNodeDoesNotExist('Item 1');
// Item with equal high water should be updated.
$this->assertNodeExists('Item 2 updated');
$this->assertNodeDoesNotExist('Item 2');
// Item with greater high water should be updated.
$this->assertNodeExists('Item 3 updated');
$this->assertNodeDoesNotExist('Item 3');
}
/**
* Assert that node with given title exists.
*
* @param string $title
* Title of the node.
*
* @internal
*/
protected function assertNodeExists(string $title): void {
self::assertTrue($this->nodeExists($title));
}
/**
* Assert that node with given title does not exist.
*
* @param string $title
* Title of the node.
*
* @internal
*/
protected function assertNodeDoesNotExist(string $title): void {
self::assertFalse($this->nodeExists($title));
}
/**
* Checks if node with given title exists.
*
* @param string $title
* Title of the node.
*
* @return bool
* TRUE if node exists, FALSE otherwise.
*/
protected function nodeExists($title): bool {
$query = \Drupal::entityQuery('node')->accessCheck(FALSE);
$result = $query
->condition('title', $title)
->range(0, 1)
->execute();
return !empty($result);
}
}

View File

@ -0,0 +1,157 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\migrate\MigrateExecutable;
use Drupal\taxonomy\Entity\Term;
use Drupal\taxonomy\Entity\Vocabulary;
/**
* Tests setting of bundles on content entity migrations.
*
* @group migrate
*/
class MigrateBundleTest extends MigrateTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['taxonomy', 'text', 'user', 'system'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
$this->installEntitySchema('taxonomy_term');
$this->installConfig(['taxonomy']);
// Set up two vocabularies (taxonomy bundles).
Vocabulary::create(['vid' => 'tags', 'name' => 'Tags']);
Vocabulary::create(['vid' => 'categories', 'name' => 'Categories']);
}
/**
* Tests setting the bundle in the destination.
*/
public function testDestinationBundle(): void {
$term_data_rows = [
['id' => 1, 'name' => 'Category 1'],
];
$ids = ['id' => ['type' => 'integer']];
$definition = [
'id' => 'terms',
'migration_tags' => ['Bundle test'],
'source' => [
'plugin' => 'embedded_data',
'data_rows' => $term_data_rows,
'ids' => $ids,
],
'process' => [
'tid' => 'id',
'name' => 'name',
],
'destination' => [
'plugin' => 'entity:taxonomy_term',
'default_bundle' => 'categories',
],
'migration_dependencies' => [],
];
$term_migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition);
// Import and validate the term entity was created with the correct bundle.
$term_executable = new MigrateExecutable($term_migration, $this);
$term_executable->import();
/** @var \Drupal\taxonomy\Entity\Term $term */
$term = Term::load(1);
$this->assertEquals('categories', $term->bundle());
}
/**
* Tests setting the bundle in the process pipeline.
*/
public function testProcessBundle(): void {
$term_data_rows = [
['id' => 1, 'vocab' => 'categories', 'name' => 'Category 1'],
['id' => 2, 'vocab' => 'tags', 'name' => 'Tag 1'],
];
$ids = ['id' => ['type' => 'integer']];
$definition = [
'id' => 'terms',
'migration_tags' => ['Bundle test'],
'source' => [
'plugin' => 'embedded_data',
'data_rows' => $term_data_rows,
'ids' => $ids,
],
'process' => [
'tid' => 'id',
'vid' => 'vocab',
'name' => 'name',
],
'destination' => [
'plugin' => 'entity:taxonomy_term',
],
'migration_dependencies' => [],
];
$term_migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition);
// Import and validate the term entities were created with the correct
// bundle.
$term_executable = new MigrateExecutable($term_migration, $this);
$term_executable->import();
/** @var \Drupal\taxonomy\Entity\Term $term */
$term = Term::load(1);
$this->assertEquals('categories', $term->bundle());
$term = Term::load(2);
$this->assertEquals('tags', $term->bundle());
}
/**
* Tests setting bundles both in process and destination.
*/
public function testMixedBundles(): void {
$term_data_rows = [
['id' => 1, 'vocab' => 'categories', 'name' => 'Category 1'],
['id' => 2, 'name' => 'Tag 1'],
];
$ids = ['id' => ['type' => 'integer']];
$definition = [
'id' => 'terms',
'migration_tags' => ['Bundle test'],
'source' => [
'plugin' => 'embedded_data',
'data_rows' => $term_data_rows,
'ids' => $ids,
],
'process' => [
'tid' => 'id',
'vid' => 'vocab',
'name' => 'name',
],
'destination' => [
'plugin' => 'entity:taxonomy_term',
// When no vocab is provided, the destination bundle is applied.
'default_bundle' => 'tags',
],
'migration_dependencies' => [],
];
$term_migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition);
// Import and validate the term entities were created with the correct
// bundle.
$term_executable = new MigrateExecutable($term_migration, $this);
$term_executable->import();
/** @var \Drupal\taxonomy\Entity\Term $term */
$term = Term::load(1);
$this->assertEquals('categories', $term->bundle());
$term = Term::load(2);
$this->assertEquals('tags', $term->bundle());
}
}

View File

@ -0,0 +1,169 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\migrate\MigrateExecutable;
/**
* Tests rolling back of configuration objects.
*
* @group migrate
*/
class MigrateConfigRollbackTest extends MigrateTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['system', 'language', 'config_translation'];
/**
* Tests rolling back configuration.
*/
public function testConfigRollback(): void {
// Use system.site configuration to demonstrate importing and rolling back
// configuration.
$variable = [
[
'id' => 'site_name',
'site_name' => 'Some site',
'site_slogan' => 'Awesome slogan',
],
];
$ids = [
'id' =>
[
'type' => 'string',
],
];
$definition = [
'id' => 'config',
'migration_tags' => ['Import and rollback test'],
'source' => [
'plugin' => 'embedded_data',
'data_rows' => $variable,
'ids' => $ids,
],
'process' => [
'name' => 'site_name',
'slogan' => 'site_slogan',
],
'destination' => [
'plugin' => 'config',
'config_name' => 'system.site',
],
];
/** @var \Drupal\migrate\Plugin\Migration $config_migration */
$config_migration = \Drupal::service('plugin.manager.migration')
->createStubMigration($definition);
$config_id_map = $config_migration->getIdMap();
// Rollback is not enabled for configuration translations.
$this->assertFalse($config_migration->getDestinationPlugin()->supportsRollback());
// Import and validate config entities were created.
$config_executable = new MigrateExecutable($config_migration, $this);
$config_executable->import();
$config = $this->config('system.site');
$this->assertSame('Some site', $config->get('name'));
$this->assertSame('Awesome slogan', $config->get('slogan'));
$map_row = $config_id_map->getRowBySource(['id' => $variable[0]['id']]);
$this->assertNotNull($map_row['destid1']);
// Rollback and verify the configuration changes are still there.
$config_executable->rollback();
$config = $this->config('system.site');
$this->assertSame('Some site', $config->get('name'));
$this->assertSame('Awesome slogan', $config->get('slogan'));
// Confirm the map row is deleted.
$this->assertFalse($config_id_map->getRowBySource(['id' => $variable[0]['id']]));
// We use system configuration to demonstrate importing and rolling back
// configuration translations.
$i18n_variable = [
[
'id' => 'site_name',
'language' => 'fr',
'site_name' => 'fr - Some site',
'site_slogan' => 'fr - Awesome slogan',
],
[
'id' => 'site_name',
'language' => 'is',
'site_name' => 'is - Some site',
'site_slogan' => 'is - Awesome slogan',
],
];
$ids = [
'id' =>
[
'type' => 'string',
],
'language' =>
[
'type' => 'string',
],
];
$definition = [
'id' => 'i18n_config',
'migration_tags' => ['Import and rollback test'],
'source' => [
'plugin' => 'embedded_data',
'data_rows' => $i18n_variable,
'ids' => $ids,
],
'process' => [
'langcode' => 'language',
'name' => 'site_name',
'slogan' => 'site_slogan',
],
'destination' => [
'plugin' => 'config',
'config_name' => 'system.site',
'translations' => 'true',
],
];
$config_migration = \Drupal::service('plugin.manager.migration')
->createStubMigration($definition);
$config_id_map = $config_migration->getIdMap();
// Rollback is enabled for configuration translations.
$this->assertTrue($config_migration->getDestinationPlugin()->supportsRollback());
// Import and validate config entities were created.
$config_executable = new MigrateExecutable($config_migration, $this);
$config_executable->import();
$language_manager = \Drupal::service('language_manager');
foreach ($i18n_variable as $row) {
$langcode = $row['language'];
/** @var \Drupal\language\Config\LanguageConfigOverride $config_translation */
$config_translation = $language_manager->getLanguageConfigOverride($langcode, 'system.site');
$this->assertSame($row['site_name'], $config_translation->get('name'));
$this->assertSame($row['site_slogan'], $config_translation->get('slogan'));
$map_row = $config_id_map->getRowBySource(['id' => $row['id'], 'language' => $row['language']]);
$this->assertNotNull($map_row['destid1']);
}
// Rollback and verify the translation have been removed.
$config_executable->rollback();
foreach ($i18n_variable as $row) {
$langcode = $row['language'];
$config_translation = $language_manager->getLanguageConfigOverride($langcode, 'system.site');
$this->assertNull($config_translation->get('name'));
$this->assertNull($config_translation->get('slogan'));
// Confirm the map row is deleted.
$map_row = $config_id_map->getRowBySource(['id' => $row['id'], 'language' => $langcode]);
$this->assertFalse($map_row);
}
// Test that the configuration is still present.
$config = $this->config('system.site');
$this->assertSame('Some site', $config->get('name'));
$this->assertSame('Awesome slogan', $config->get('slogan'));
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\KernelTests\KernelTestBase;
/**
* Allows tests to alter dumps after they have loaded.
*
* @see \Drupal\migrate_drupal\Tests\d6\MigrateFileTest
*/
interface MigrateDumpAlterInterface {
/**
* Allows tests to alter dumps after they have loaded.
*
* @param \Drupal\KernelTests\KernelTestBase $test
* The test that is being run.
*/
public static function migrateDumpAlter(KernelTestBase $test);
}

View File

@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests the EmbeddedDataSource plugin.
*
* @group migrate
*/
class MigrateEmbeddedDataTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate'];
/**
* Tests the embedded_data source plugin.
*/
public function testEmbeddedData(): void {
$data_rows = [
['key' => '1', 'field1' => 'f1value1', 'field2' => 'f2value1'],
['key' => '2', 'field1' => 'f1value2', 'field2' => 'f2value2'],
];
$ids = ['key' => ['type' => 'integer']];
$definition = [
'migration_tags' => ['Embedded data test'],
'source' => [
'plugin' => 'embedded_data',
'data_rows' => $data_rows,
'ids' => $ids,
],
'process' => [],
'destination' => ['plugin' => 'null'],
];
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition);
$source = $migration->getSourcePlugin();
// Validate the plugin returns the source data that was provided.
$results = [];
/** @var \Drupal\migrate\Row $row */
foreach ($source as $row) {
$this->assertFalse($row->isStub());
$data_row = $row->getSource();
// The "data" row returned by getSource() also includes all source
// configuration - we remove it so we see only the data itself.
unset($data_row['plugin']);
unset($data_row['data_rows']);
unset($data_row['ids']);
$results[] = $data_row;
}
$this->assertSame($data_rows, $results);
// Validate the public APIs.
$this->assertSameSize($data_rows, $source);
$this->assertSame($ids, $source->getIds());
$expected_fields = [
'key' => 'key',
'field1' => 'field1',
'field2' => 'field2',
];
$this->assertSame($expected_fields, $source->fields());
}
}

View File

@ -0,0 +1,447 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\Core\Entity\EntityFieldManager;
use Drupal\entity_test\Entity\EntityTestMul;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\KernelTests\KernelTestBase;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\migrate\MigrateExecutable;
use Drupal\migrate\Plugin\migrate\destination\EntityContentBase;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Tests\StubTestTrait;
use Drupal\migrate_entity_test\Entity\StringIdEntityTest;
/**
* Tests the EntityContentBase destination.
*
* @group migrate
*/
class MigrateEntityContentBaseTest extends KernelTestBase {
use StubTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'entity_test',
'field',
'language',
'migrate',
'user',
];
/**
* The storage for entity_test_mul.
*
* @var \Drupal\Core\Entity\ContentEntityStorageInterface
*/
protected $storage;
/**
* A content migrate destination.
*
* @var \Drupal\migrate\Plugin\MigrateDestinationInterface
*/
protected $destination;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Enable two required fields with default values: a single-value field and
// a multi-value field.
\Drupal::state()->set('entity_test.required_default_field', TRUE);
\Drupal::state()->set('entity_test.required_multi_default_field', TRUE);
$this->installEntitySchema('entity_test_mul');
$this->installEntitySchema('entity_test_with_bundle');
$this->installEntitySchema('entity_test_no_bundle');
ConfigurableLanguage::createFromLangcode('en')->save();
ConfigurableLanguage::createFromLangcode('fr')->save();
$this->storage = $this->container->get('entity_type.manager')->getStorage('entity_test_mul');
}
/**
* Check the existing translations of an entity.
*
* @param int $id
* The entity ID.
* @param string $default
* The expected default translation language code.
* @param string[] $others
* The expected other translation language codes.
*
* @internal
*/
protected function assertTranslations(int $id, string $default, array $others = []): void {
$entity = $this->storage->load($id);
$this->assertNotEmpty($entity, "Entity exists");
$this->assertEquals($default, $entity->language()->getId(), "Entity default translation");
$translations = array_keys($entity->getTranslationLanguages(FALSE));
sort($others);
sort($translations);
$this->assertEquals($others, $translations, "Entity translations");
}
/**
* Create the destination plugin to test.
*
* @param array $configuration
* The plugin configuration.
*/
protected function createDestination(array $configuration): void {
$this->destination = new EntityContentBase(
$configuration,
'fake_plugin_id',
[],
$this->createMock(MigrationInterface::class),
$this->storage,
[],
$this->container->get('entity_field.manager'),
$this->container->get('plugin.manager.field.field_type'),
$this->container->get('account_switcher'),
$this->container->get('entity_type.bundle.info'),
);
}
/**
* Tests importing and rolling back translated entities.
*/
public function testTranslated(): void {
// Create a destination.
$this->createDestination(['translations' => TRUE]);
// Create some pre-existing entities.
$this->storage->create(['id' => 1, 'langcode' => 'en'])->save();
$this->storage->create(['id' => 2, 'langcode' => 'fr'])->save();
$translated = $this->storage->create(['id' => 3, 'langcode' => 'en']);
$translated->save();
$translated->addTranslation('fr')->save();
// Pre-assert that things are as expected.
$this->assertTranslations(1, 'en');
$this->assertTranslations(2, 'fr');
$this->assertTranslations(3, 'en', ['fr']);
$this->assertNull($this->storage->load(4));
$destination_rows = [
// Existing default translation.
['id' => 1, 'langcode' => 'en', 'action' => MigrateIdMapInterface::ROLLBACK_PRESERVE],
// New translation.
['id' => 2, 'langcode' => 'en', 'action' => MigrateIdMapInterface::ROLLBACK_DELETE],
// Existing non-default translation.
['id' => 3, 'langcode' => 'fr', 'action' => MigrateIdMapInterface::ROLLBACK_PRESERVE],
// Brand new row.
['id' => 4, 'langcode' => 'fr', 'action' => MigrateIdMapInterface::ROLLBACK_DELETE],
];
$rollback_actions = [];
// Import some rows.
foreach ($destination_rows as $idx => $destination_row) {
$row = new Row();
foreach ($destination_row as $key => $value) {
$row->setDestinationProperty($key, $value);
}
$this->destination->import($row);
// Check that the rollback action is correct, and save it.
$this->assertEquals($destination_row['action'], $this->destination->rollbackAction());
$rollback_actions[$idx] = $this->destination->rollbackAction();
}
$this->assertTranslations(1, 'en');
$this->assertTranslations(2, 'fr', ['en']);
$this->assertTranslations(3, 'en', ['fr']);
$this->assertTranslations(4, 'fr');
// Rollback the rows.
foreach ($destination_rows as $idx => $destination_row) {
if ($rollback_actions[$idx] == MigrateIdMapInterface::ROLLBACK_DELETE) {
$this->destination->rollback($destination_row);
}
}
// No change, update of existing translation.
$this->assertTranslations(1, 'en');
// Remove added translation.
$this->assertTranslations(2, 'fr');
// No change, update of existing translation.
$this->assertTranslations(3, 'en', ['fr']);
// No change, can't remove default translation.
$this->assertTranslations(4, 'fr');
}
/**
* Tests creation of ID columns table with definitions taken from entity type.
*/
public function testEntityWithStringId(): void {
$this->enableModules(['migrate_entity_test']);
$this->installEntitySchema('migrate_string_id_entity_test');
$definition = [
'source' => [
'plugin' => 'embedded_data',
'data_rows' => [
['id' => 123, 'version' => 'foo'],
// This integer needs an 'int' schema with 'big' size. If 'destid1'
// is not correctly taking the definition from the destination entity
// type, the import will fail with a SQL exception.
['id' => 123456789012, 'version' => 'bar'],
],
'ids' => [
'id' => ['type' => 'integer', 'size' => 'big'],
'version' => ['type' => 'string'],
],
],
'process' => [
'id' => 'id',
'version' => 'version',
],
'destination' => [
'plugin' => 'entity:migrate_string_id_entity_test',
],
];
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition);
$executable = new MigrateExecutable($migration);
$result = $executable->import();
$this->assertEquals(MigrationInterface::RESULT_COMPLETED, $result);
/** @var \Drupal\migrate\Plugin\MigrateIdMapInterface $id_map_plugin */
$id_map_plugin = $migration->getIdMap();
// Check that the destination has been stored.
$map_row = $id_map_plugin->getRowBySource(['id' => 123, 'version' => 'foo']);
$this->assertEquals(123, $map_row['destid1']);
$map_row = $id_map_plugin->getRowBySource(['id' => 123456789012, 'version' => 'bar']);
$this->assertEquals(123456789012, $map_row['destid1']);
}
/**
* Tests empty destinations.
*/
public function testEmptyDestinations(): void {
$this->enableModules(['migrate_entity_test']);
$this->installEntitySchema('migrate_string_id_entity_test');
$definition = [
'source' => [
'plugin' => 'embedded_data',
'data_rows' => [
['id' => 123, 'version' => 'foo'],
// This integer needs an 'int' schema with 'big' size. If 'destid1'
// is not correctly taking the definition from the destination entity
// type, the import will fail with an SQL exception.
['id' => 123456789012, 'version' => 'bar'],
],
'ids' => [
'id' => ['type' => 'integer', 'size' => 'big'],
'version' => ['type' => 'string'],
],
'constants' => ['null' => NULL],
],
'process' => [
'id' => 'id',
'version' => 'version',
],
'destination' => [
'plugin' => 'entity:migrate_string_id_entity_test',
],
];
$migration = \Drupal::service('plugin.manager.migration')
->createStubMigration($definition);
$executable = new MigrateExecutable($migration);
$executable->import();
/** @var \Drupal\migrate_entity_test\Entity\StringIdEntityTest $entity */
$entity = StringIdEntityTest::load('123');
$this->assertSame('foo', $entity->version->value);
$entity = StringIdEntityTest::load('123456789012');
$this->assertSame('bar', $entity->version->value);
// Rerun the migration forcing the version to NULL.
$definition['process'] = [
'id' => 'id',
'version' => 'constants/null',
];
$migration = \Drupal::service('plugin.manager.migration')
->createStubMigration($definition);
$executable = new MigrateExecutable($migration);
$executable->import();
/** @var \Drupal\migrate_entity_test\Entity\StringIdEntityTest $entity */
$entity = StringIdEntityTest::load('123');
$this->assertNull($entity->version->value);
$entity = StringIdEntityTest::load('123456789012');
$this->assertNull($entity->version->value);
}
/**
* Tests stub rows.
*/
public function testStubRows(): void {
// Create a destination.
$this->createDestination([]);
// Import a stub row.
$row = new Row([], [], TRUE);
$row->setDestinationProperty('type', 'test');
$ids = $this->destination->import($row);
$this->assertCount(1, $ids);
// Make sure the entity was saved.
$entity = EntityTestMul::load(reset($ids));
$this->assertInstanceOf(EntityTestMul::class, $entity);
// Make sure the default value was applied to the required fields.
$single_field_name = 'required_default_field';
$single_default_value = $entity->getFieldDefinition($single_field_name)->getDefaultValueLiteral();
$this->assertSame($single_default_value, $entity->get($single_field_name)->getValue());
$multi_field_name = 'required_multi_default_field';
$multi_default_value = $entity->getFieldDefinition($multi_field_name)->getDefaultValueLiteral();
$count = 3;
$this->assertCount($count, $multi_default_value);
for ($i = 0; $i < $count; ++$i) {
$this->assertSame($multi_default_value[$i], $entity->get($multi_field_name)->get($i)->getValue());
}
}
/**
* Tests bundle is properly provided for stubs without bundle support.
*
* @todo Remove this test in when native PHP type-hints will be added for
* EntityFieldManagerInterface::getFieldDefinitions(). See
* https://www.drupal.org/project/drupal/issues/3050720.
*/
public function testBundleFallbackForStub(): void {
$this->enableModules(['migrate_entity_test']);
$this->installEntitySchema('migrate_string_id_entity_test');
$entity_type_manager = $this->container->get('entity_type.manager');
$entity_type_bundle_info = $this->container->get('entity_type.bundle.info');
$entity_display_repository = $this
->container
->get('entity_display.repository');
$typed_data_manager = $this->container->get('typed_data_manager');
$language_manager = $this->container->get('language_manager');
$keyvalue = $this->container->get('keyvalue');
$module_handler = $this->container->get('module_handler');
$cache_discovery = $this->container->get('cache.discovery');
$entity_last_installed_schema_repository = $this
->container
->get('entity.last_installed_schema.repository');
$decorated_entity_field_manager = new class ($entity_type_manager, $entity_type_bundle_info, $entity_display_repository, $typed_data_manager, $language_manager, $keyvalue, $module_handler, $cache_discovery, $entity_last_installed_schema_repository) extends EntityFieldManager {
/**
* {@inheritdoc}
*/
public function getFieldDefinitions($entity_type_id, $bundle) {
if (\is_null($bundle)) {
throw new \Exception("Bundle value shouldn't be NULL.");
}
return parent::getFieldDefinitions($entity_type_id, $bundle);
}
};
$this->container->set('entity_field.manager', $decorated_entity_field_manager);
$this->createEntityStub('migrate_string_id_entity_test');
}
/**
* Test destination fields() method.
*/
public function testFields(): void {
$entity_type_manager = $this->container->get('entity_type.manager');
// Create two bundles for the entity_test_with_bundle entity type.
$bundle_storage = $entity_type_manager->getStorage('entity_test_bundle');
$bundle_storage->create([
'id' => 'test_bundle_no_fields',
'label' => 'Test bundle without fields',
])->save();
$bundle_storage->create([
'id' => 'test_bundle_with_fields',
'label' => 'Test bundle with fields',
])->save();
// Create a mock migration and get the destination plugin manager.
$migration = $this->prophesize(MigrationInterface::class)->reveal();
/** @var \Drupal\migrate\Plugin\MigrateDestinationPluginManager $manager */
$manager = \Drupal::service('plugin.manager.migrate.destination');
// Test with an entity type with no bundles.
$destination_plugin = $manager->createInstance('entity:entity_test_no_bundle', [], $migration);
$fields = $destination_plugin->fields();
$this->assertArrayHasKey('id', $fields);
// Confirm the test field is not found.
$this->assertArrayNotHasKey('field_text', $fields);
// Create a text field attached to the entity with no bundles.
FieldStorageConfig::create([
'type' => 'string',
'entity_type' => 'entity_test_no_bundle',
'field_name' => 'field_text',
])->save();
FieldConfig::create([
'entity_type' => 'entity_test_no_bundle',
'bundle' => 'entity_test_no_bundle',
'field_name' => 'field_text',
])->save();
// Confirm that the 'field_text' is now found.
$destination_plugin = $manager->createInstance('entity:entity_test_no_bundle', [], $migration);
$fields = $destination_plugin->fields();
$this->assertArrayHasKey('id', $fields);
$this->assertArrayHasKey('field_text', $fields);
// Repeat the test with an entity with bundles.
$destination_plugin = $manager->createInstance('entity:entity_test_with_bundle', [], $migration);
$fields = $destination_plugin->fields();
$this->assertArrayHasKey('id', $fields);
$this->assertArrayNotHasKey('field_text', $fields);
// Create a text field attached to the entity with bundles.
FieldStorageConfig::create([
'type' => 'string',
'entity_type' => 'entity_test_with_bundle',
'field_name' => 'field_text',
])->save();
FieldConfig::create([
'entity_type' => 'entity_test_with_bundle',
'bundle' => 'test_bundle_with_fields',
'field_name' => 'field_text',
])->save();
// Confirm that the 'field_text' is found when the default bundle is set.
$destination_plugin = $manager->createInstance('entity:entity_test_with_bundle', ['default_bundle' => 'test_bundle_with_fields'], $migration);
$fields = $destination_plugin->fields();
$this->assertArrayHasKey('id', $fields);
$this->assertArrayHasKey('field_text', $fields);
// Confirm that the 'field_text' is not found when the default bundle is not
// set.
$destination_plugin = $manager->createInstance('entity:entity_test_with_bundle', [], $migration);
$fields = $destination_plugin->fields();
$this->assertArrayHasKey('id', $fields);
$this->assertArrayNotHasKey('field_text', $fields);
}
}

View File

@ -0,0 +1,281 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\Component\Utility\Html;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\filter\Entity\FilterFormat;
use Drupal\filter\FilterFormatInterface;
use Drupal\KernelTests\KernelTestBase;
use Drupal\migrate\Event\MigrateEvents;
use Drupal\migrate\Event\MigrateIdMapMessageEvent;
use Drupal\migrate\MigrateExecutable;
use Drupal\user\Entity\Role;
use Drupal\user\Entity\User;
use Drupal\user\Plugin\Validation\Constraint\UserNameConstraint;
use Drupal\user\RoleInterface;
/**
* Tests validation of an entity during migration.
*
* @group migrate
*/
class MigrateEntityContentValidationTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'entity_test',
'field',
'filter',
'filter_test',
'migrate',
'system',
'text',
'user',
];
/**
* Messages accumulated during the migration run.
*
* @var string[]
*/
protected $messages = [];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
$this->installEntitySchema('user_role');
$this->installEntitySchema('entity_test');
$this->installConfig(['field', 'filter_test', 'system', 'user']);
$this->container
->get('event_dispatcher')
->addListener(MigrateEvents::IDMAP_MESSAGE, [$this, 'mapMessageRecorder']);
}
/**
* Tests an import with invalid data and checks error messages.
*/
public function test1(): void {
// Make sure that a user with uid 2 exists.
$this->container
->get('entity_type.manager')
->getStorage('user')
->create([
'uid' => 2,
'name' => $this->randomMachineName(),
'status' => 1,
])
->save();
$this->runImport([
'source' => [
'plugin' => 'embedded_data',
'data_rows' => [
[
'id' => '1',
'name' => $this->randomString(256),
'user_id' => '1',
],
[
'id' => '2',
'name' => $this->randomString(64),
'user_id' => '1',
],
[
'id' => '3',
'name' => $this->randomString(64),
'user_id' => '2',
],
],
'ids' => [
'id' => ['type' => 'integer'],
],
],
'process' => [
'id' => 'id',
'name' => 'name',
'user_id' => 'user_id',
],
'destination' => [
'plugin' => 'entity:entity_test',
'validate' => TRUE,
],
]);
$this->assertSame('1: [entity_test: 1]: name.0.value=<em class="placeholder">Name</em>: may not be longer than 64 characters.||user_id.0.target_id=The referenced entity (<em class="placeholder">user</em>: <em class="placeholder">1</em>) does not exist.', $this->messages[0], 'First message should have 2 validation errors.');
$this->assertSame('2: [entity_test: 2]: user_id.0.target_id=The referenced entity (<em class="placeholder">user</em>: <em class="placeholder">1</em>) does not exist.', $this->messages[1], 'Second message should have 1 validation error.');
$this->assertArrayNotHasKey(2, $this->messages, 'Third message should not exist.');
}
/**
* Tests an import with invalid data and checks error messages.
*/
public function test2(): void {
$long_username = $this->randomString(61);
$username_constraint = new UserNameConstraint();
$this->runImport([
'source' => [
'plugin' => 'embedded_data',
'data_rows' => [
[
'id' => 1,
'name' => $long_username,
],
[
'id' => 2,
'name' => $this->randomString(32),
],
[
'id' => 3,
'name' => $this->randomString(32),
],
],
'ids' => [
'id' => ['type' => 'integer'],
],
],
'process' => [
'name' => 'name',
],
'destination' => [
'plugin' => 'entity:user',
'validate' => TRUE,
],
]);
$message = strtr($username_constraint->tooLongMessage, [
'%name' => '<em class="placeholder">' . Html::escape($long_username) . '</em>',
'%max' => '<em class="placeholder">' . 60 . '</em>',
]);
$this->assertSame(sprintf('1: [user]: name=%s||name=%s||mail=Email field is required.', $username_constraint->illegalMessage, $message), $this->messages[0], 'First message should have 3 validation errors.');
$this->assertSame(sprintf('2: [user]: name=%s||mail=Email field is required.', $username_constraint->illegalMessage), $this->messages[1], 'Second message should have 2 validation errors.');
$this->assertSame(sprintf('3: [user]: name=%s||mail=Email field is required.', $username_constraint->illegalMessage), $this->messages[2], 'Third message should have 2 validation errors.');
$this->assertArrayNotHasKey(3, $this->messages, 'Fourth message should not exist.');
}
/**
* Tests validation for entities that are instances of EntityOwnerInterface.
*/
public function testEntityOwnerValidation(): void {
// Text format access is impacted by user permissions.
$filter_test_format = FilterFormat::load('filter_test');
assert($filter_test_format instanceof FilterFormatInterface);
// Create 2 users, an admin user who has permission to use this text format
// and another who does not have said access.
$role = Role::create([
'id' => 'admin',
'label' => 'admin',
'is_admin' => TRUE,
]);
assert($role instanceof RoleInterface);
$role->grantPermission($filter_test_format->getPermissionName());
$role->save();
$admin_user = User::create([
'name' => 'foobar',
'mail' => 'foobar@example.com',
]);
$admin_user->addRole($role->id())->save();
$normal_user = User::create([
'name' => 'normal user',
'mail' => 'normal@example.com',
]);
$normal_user->save();
// Add a "body" field with the text format.
$field_name = $this->randomMachineName();
$field_storage = FieldStorageConfig::create([
'field_name' => $field_name,
'entity_type' => 'entity_test',
'type' => 'text',
]);
$field_storage->save();
FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => 'entity_test',
])->save();
// Attempt to migrate entities. The first record is owned by an admin user.
$definition = [
'source' => [
'plugin' => 'embedded_data',
'data_rows' => [
[
'id' => 1,
'uid' => $admin_user->id(),
'body' => [
'value' => 'foo',
'format' => 'filter_test',
],
],
[
'id' => 2,
'uid' => $normal_user->id(),
'body' => [
'value' => 'bar',
'format' => 'filter_test',
],
],
],
'ids' => [
'id' => ['type' => 'integer'],
],
],
'process' => [
'id' => 'id',
'user_id' => 'uid',
"$field_name/value" => 'body/value',
"$field_name/format" => 'body/format',
],
'destination' => [
'plugin' => 'entity:entity_test',
'validate' => TRUE,
],
];
$this->container->get('current_user')->setAccount($normal_user);
$this->runImport($definition);
// The second user import should fail validation because they do not have
// access to use "filter_test" filter.
$this->assertSame(sprintf('2: [entity_test: 2]: user_id.0.target_id=This entity (<em class="placeholder">user</em>: <em class="placeholder">%s</em>) cannot be referenced.||%s.0.format=The value you selected is not a valid choice.', $normal_user->id(), $field_name), $this->messages[0]);
$this->assertArrayNotHasKey(1, $this->messages);
}
/**
* Reacts to map message event.
*
* @param \Drupal\migrate\Event\MigrateIdMapMessageEvent $event
* The migration event.
*/
public function mapMessageRecorder(MigrateIdMapMessageEvent $event): void {
$this->messages[] = implode(',', $event->getSourceIdValues()) . ': ' . $event->getMessage();
}
/**
* Runs an import of a migration.
*
* @param array $definition
* The migration definition.
*
* @throws \Exception
* @throws \Drupal\migrate\MigrateException
*/
protected function runImport(array $definition): void {
// Reset the list of messages from a previous migration.
$this->messages = [];
(new MigrateExecutable($this->container->get('plugin.manager.migration')->createStubMigration($definition)))->import();
}
}

View File

@ -0,0 +1,217 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\migrate\Event\MigrateImportEvent;
use Drupal\migrate\Event\MigrateMapDeleteEvent;
use Drupal\migrate\Event\MigrateMapSaveEvent;
use Drupal\migrate\Event\MigratePostRowSaveEvent;
use Drupal\migrate\Event\MigratePreRowSaveEvent;
use Drupal\migrate\Event\MigrateEvents;
use Drupal\migrate\MigrateExecutable;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests events fired on migrations.
*
* @group migrate
*/
class MigrateEventsTest extends KernelTestBase {
/**
* State service for recording information received by event listeners.
*
* @var \Drupal\Core\State\State
*/
protected $state;
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate', 'migrate_events_test'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->state = \Drupal::state();
\Drupal::service('event_dispatcher')->addListener(MigrateEvents::MAP_SAVE,
[$this, 'mapSaveEventRecorder']);
\Drupal::service('event_dispatcher')->addListener(MigrateEvents::MAP_DELETE,
[$this, 'mapDeleteEventRecorder']);
\Drupal::service('event_dispatcher')->addListener(MigrateEvents::PRE_IMPORT,
[$this, 'preImportEventRecorder']);
\Drupal::service('event_dispatcher')->addListener(MigrateEvents::POST_IMPORT,
[$this, 'postImportEventRecorder']);
\Drupal::service('event_dispatcher')->addListener(MigrateEvents::PRE_ROW_SAVE,
[$this, 'preRowSaveEventRecorder']);
\Drupal::service('event_dispatcher')->addListener(MigrateEvents::POST_ROW_SAVE,
[$this, 'postRowSaveEventRecorder']);
}
/**
* Tests migration events.
*/
public function testMigrateEvents(): void {
// Run a simple little migration, which should trigger one of each event
// other than map_delete.
$definition = [
'migration_tags' => ['Event test'],
'source' => [
'plugin' => 'embedded_data',
'data_rows' => [
['data' => 'dummy value'],
],
'ids' => [
'data' => ['type' => 'string'],
],
],
'process' => ['value' => 'data'],
'destination' => ['plugin' => 'dummy'],
];
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition);
$executable = new MigrateExecutable($migration);
// As the import runs, events will be dispatched, recording the received
// information in state.
$executable->import();
// Validate from the recorded state that the events were received.
$event = $this->state->get('migrate_events_test.pre_import_event', []);
$this->assertSame(MigrateEvents::PRE_IMPORT, $event['event_name']);
$this->assertSame($migration->id(), $event['migration']->id());
$event = $this->state->get('migrate_events_test.post_import_event', []);
$this->assertSame(MigrateEvents::POST_IMPORT, $event['event_name']);
$this->assertSame($migration->id(), $event['migration']->id());
$event = $this->state->get('migrate_events_test.map_save_event', []);
$this->assertSame(MigrateEvents::MAP_SAVE, $event['event_name']);
// Validating the last row processed.
$this->assertSame('dummy value', $event['fields']['sourceid1']);
$this->assertSame('dummy value', $event['fields']['destid1']);
$this->assertSame(0, $event['fields']['source_row_status']);
$event = $this->state->get('migrate_events_test.map_delete_event', []);
$this->assertSame([], $event);
$event = $this->state->get('migrate_events_test.pre_row_save_event', []);
$this->assertSame(MigrateEvents::PRE_ROW_SAVE, $event['event_name']);
$this->assertSame($migration->id(), $event['migration']->id());
// Validating the last row processed.
$this->assertSame('dummy value', $event['row']->getSourceProperty('data'));
$event = $this->state->get('migrate_events_test.post_row_save_event', []);
$this->assertSame(MigrateEvents::POST_ROW_SAVE, $event['event_name']);
$this->assertSame($migration->id(), $event['migration']->id());
// Validating the last row processed.
$this->assertSame('dummy value', $event['row']->getSourceProperty('data'));
$this->assertSame('dummy value', $event['destination_id_values']['value']);
// Generate a map delete event.
$migration->getIdMap()->delete(['data' => 'dummy value']);
$event = $this->state->get('migrate_events_test.map_delete_event', []);
$this->assertSame(MigrateEvents::MAP_DELETE, $event['event_name']);
$this->assertSame(['data' => 'dummy value'], $event['source_id']);
}
/**
* Reacts to map save event.
*
* @param \Drupal\migrate\Event\MigrateMapSaveEvent $event
* The migration event.
* @param string $name
* The event name.
*/
public function mapSaveEventRecorder(MigrateMapSaveEvent $event, $name): void {
$this->state->set('migrate_events_test.map_save_event', [
'event_name' => $name,
'map' => $event->getMap(),
'fields' => $event->getFields(),
]);
}
/**
* Reacts to map delete event.
*
* @param \Drupal\migrate\Event\MigrateMapDeleteEvent $event
* The migration event.
* @param string $name
* The event name.
*/
public function mapDeleteEventRecorder(MigrateMapDeleteEvent $event, $name): void {
$this->state->set('migrate_events_test.map_delete_event', [
'event_name' => $name,
'map' => $event->getMap(),
'source_id' => $event->getSourceId(),
]);
}
/**
* Reacts to pre-import event.
*
* @param \Drupal\migrate\Event\MigrateImportEvent $event
* The migration event.
* @param string $name
* The event name.
*/
public function preImportEventRecorder(MigrateImportEvent $event, $name): void {
$this->state->set('migrate_events_test.pre_import_event', [
'event_name' => $name,
'migration' => $event->getMigration(),
]);
}
/**
* Reacts to post-import event.
*
* @param \Drupal\migrate\Event\MigrateImportEvent $event
* The migration event.
* @param string $name
* The event name.
*/
public function postImportEventRecorder(MigrateImportEvent $event, $name): void {
$this->state->set('migrate_events_test.post_import_event', [
'event_name' => $name,
'migration' => $event->getMigration(),
]);
}
/**
* Reacts to pre-row-save event.
*
* @param \Drupal\migrate\Event\MigratePreRowSaveEvent $event
* The migration event.
* @param string $name
* The event name.
*/
public function preRowSaveEventRecorder(MigratePreRowSaveEvent $event, $name): void {
$this->state->set('migrate_events_test.pre_row_save_event', [
'event_name' => $name,
'migration' => $event->getMigration(),
'row' => $event->getRow(),
]);
}
/**
* Reacts to post-row-save event.
*
* @param \Drupal\migrate\Event\MigratePostRowSaveEvent $event
* The migration event.
* @param string $name
* The event name.
*/
public function postRowSaveEventRecorder(MigratePostRowSaveEvent $event, $name): void {
$this->state->set('migrate_events_test.post_row_save_event', [
'event_name' => $name,
'migration' => $event->getMigration(),
'row' => $event->getRow(),
'destination_id_values' => $event->getDestinationIdValues(),
]);
}
}

View File

@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\migrate\Plugin\MigrationInterface;
/**
* Tests the MigrateExecutable class.
*
* @group migrate
*/
class MigrateExecutableTest extends MigrateTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'entity_test',
'user',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
$this->installEntitySchema('entity_test');
}
/**
* Tests the MigrateExecutable class.
*/
public function testMigrateExecutable(): void {
$data_rows = [
['key' => '1', 'field1' => 'f1value1', 'field2' => 'f2value1'],
['key' => '2', 'field1' => 'f1value2', 'field2' => 'f2value2'],
];
$ids = ['key' => ['type' => 'integer']];
$definition = [
'migration_tags' => ['Embedded data test'],
'source' => [
'plugin' => 'embedded_data',
'data_rows' => $data_rows,
'ids' => $ids,
],
'process' => [],
'destination' => ['plugin' => 'entity:entity_test'],
];
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition);
$executable = new TestMigrateExecutable($migration);
$this->assertEquals(MigrationInterface::RESULT_COMPLETED, $executable->import());
// Test the exception message when a process plugin throws a
// MigrateSkipRowException. Change the definition to have one data row and a
// process that will throw a MigrateSkipRowException on every row.
$definition['source']['data_rows'] = [
[
'key' => '1',
'field1' => 'f1value1',
],
];
$definition['process'] = [
'foo' => [
'plugin' => 'skip_row_if_not_set',
'index' => 'foo',
'source' => 'field1',
'message' => 'test message',
],
];
$migration = \Drupal::service('plugin.manager.migration')
->createStubMigration($definition);
$executable = new TestMigrateExecutable($migration);
$executable->import();
$messages = iterator_to_array($migration->getIdMap()->getMessages());
$this->assertCount(1, $messages);
$expected = $migration->getPluginId() . ':foo: test message';
$this->assertEquals($expected, $messages[0]->message);
}
}

View File

@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\migrate\MigrateExecutable;
use Drupal\node\Entity\NodeType;
/**
* Tests migrating non-Drupal translated content.
*
* Ensure it's possible to migrate in translations, even if there's no nid or
* tnid property on the source.
*
* @group migrate
*/
class MigrateExternalTranslatedTest extends MigrateTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'user',
'language',
'node',
'field',
'migrate_external_translated_test',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installSchema('node', ['node_access']);
$this->installEntitySchema('user');
$this->installEntitySchema('node');
// Create some languages.
ConfigurableLanguage::createFromLangcode('en')->save();
ConfigurableLanguage::createFromLangcode('fr')->save();
ConfigurableLanguage::createFromLangcode('es')->save();
// Create a content type.
NodeType::create([
'type' => 'external_test',
'name' => 'Test node type',
])->save();
}
/**
* Tests importing and rolling back our data.
*/
public function testMigrations(): void {
/** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
$storage = $this->container->get('entity_type.manager')->getStorage('node');
$this->assertCount(0, $storage->loadMultiple());
// Run the migrations.
$migration_ids = ['external_translated_test_node', 'external_translated_test_node_translation'];
$this->executeMigrations($migration_ids);
$this->assertCount(3, $storage->loadMultiple());
$node = $storage->load(1);
$this->assertEquals('en', $node->language()->getId());
$this->assertEquals('Cat', $node->title->value);
$this->assertEquals('Chat', $node->getTranslation('fr')->title->value);
$this->assertEquals('es - Cat', $node->getTranslation('es')->title->value);
$node = $storage->load(2);
$this->assertEquals('en', $node->language()->getId());
$this->assertEquals('Dog', $node->title->value);
$this->assertEquals('fr - Dog', $node->getTranslation('fr')->title->value);
$this->assertFalse($node->hasTranslation('es'), "No spanish translation for node 2");
$node = $storage->load(3);
$this->assertEquals('en', $node->language()->getId());
$this->assertEquals('Monkey', $node->title->value);
$this->assertFalse($node->hasTranslation('fr'), "No french translation for node 3");
$this->assertFalse($node->hasTranslation('es'), "No spanish translation for node 3");
$this->assertNull($storage->load(4), "No node 4 migrated");
// Roll back the migrations.
foreach ($migration_ids as $migration_id) {
$migration = $this->getMigration($migration_id);
$executable = new MigrateExecutable($migration, $this);
$executable->rollback();
}
$this->assertCount(0, $storage->loadMultiple());
}
}

View File

@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\migrate\Event\MigratePostRowSaveEvent;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Event\MigrateEvents;
use Drupal\migrate\MigrateExecutable;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests interruptions triggered during migrations.
*
* @group migrate
*/
class MigrateInterruptionTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate', 'migrate_events_test'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
\Drupal::service('event_dispatcher')->addListener(MigrateEvents::POST_ROW_SAVE,
[$this, 'postRowSaveEventRecorder']);
}
/**
* Tests migration interruptions.
*/
public function testMigrateEvents(): void {
// Run a simple little migration, which should trigger one of each event
// other than map_delete.
$definition = [
'migration_tags' => ['Interruption test'],
'source' => [
'plugin' => 'embedded_data',
'data_rows' => [
['data' => 'dummy value'],
['data' => 'dummy value2'],
],
'ids' => [
'data' => ['type' => 'string'],
],
],
'process' => ['value' => 'data'],
'destination' => ['plugin' => 'dummy'],
];
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition);
$executable = new MigrateExecutable($migration);
// When the import runs, the first row imported will trigger an
// interruption.
$result = $executable->import();
$this->assertEquals(MigrationInterface::RESULT_INCOMPLETE, $result);
// The status should have been reset to IDLE.
$this->assertEquals(MigrationInterface::STATUS_IDLE, $migration->getStatus());
}
/**
* Reacts to post-row-save event.
*
* @param \Drupal\migrate\Event\MigratePostRowSaveEvent $event
* The migration event.
* @param string $name
* The event name.
*/
public function postRowSaveEventRecorder(MigratePostRowSaveEvent $event, $name): void {
$event->getMigration()->interruptMigration(MigrationInterface::RESULT_INCOMPLETE);
}
}

View File

@ -0,0 +1,172 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\migrate\MigrateException;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
/**
* Tests the Migrate Lookup service.
*
* @group migrate
*/
class MigrateLookupTest extends MigrateTestBase {
use ContentTypeCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'node',
'field',
'user',
'text',
'migrate_lookup_test',
];
/**
* The migration lookup service.
*
* @var \Drupal\migrate\MigrateLookupInterface
*/
protected $migrateLookup;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->setTestLogger();
$this->migrateLookup = $this->container->get('migrate.lookup');
$this->installEntitySchema('node');
$this->installEntitySchema('user');
$this->installConfig(['node', 'user']);
$this->createContentType(['type' => 'node_lookup']);
}
/**
* Tests scenarios around single id lookups.
*/
public function testSingleLookup(): void {
$this->executeMigration('sample_lookup_migration');
// Test numerically indexed source id.
$result = $this->migrateLookup->lookup('sample_lookup_migration', [17]);
$this->assertSame('1', $result[0]['nid']);
// Test associatively indexed source id.
$result = $this->migrateLookup->lookup('sample_lookup_migration', ['id' => 25]);
$this->assertSame('2', $result[0]['nid']);
// Test lookup not found.
$result = $this->migrateLookup->lookup('sample_lookup_migration', [1337]);
$this->assertSame([], $result);
}
/**
* Tests an invalid lookup.
*/
public function testInvalidIdLookup(): void {
$this->executeMigration('sample_lookup_migration');
$this->expectException(MigrateException::class);
$this->expectExceptionMessage("Extra unknown items for map migrate_map_sample_lookup_migration in source IDs: array (\n 'invalid_id' => 25,\n)");
// Test invalidly indexed source id.
$this->migrateLookup->lookup('sample_lookup_migration', ['invalid_id' => 25]);
}
/**
* Tests lookups with multiple source ids.
*/
public function testMultipleSourceIds(): void {
$this->executeMigration('sample_lookup_migration_multiple_source_ids');
// Test with full set of numerically indexed source ids.
$result = $this->migrateLookup->lookup('sample_lookup_migration_multiple_source_ids', [
25,
26,
]);
$this->assertCount(1, $result);
$this->assertSame('3', $result[0]['nid']);
// Test with full set of associatively indexed source ids.
$result = $this->migrateLookup->lookup('sample_lookup_migration_multiple_source_ids', [
'id' => 17,
'version_id' => 17,
]);
$this->assertCount(1, $result);
$this->assertSame('1', $result[0]['nid']);
// Test with full set of associatively indexed source ids in the wrong
// order.
$result = $this->migrateLookup->lookup('sample_lookup_migration_multiple_source_ids', [
'version_id' => 26,
'id' => 25,
]);
$this->assertCount(1, $result);
$this->assertSame('3', $result[0]['nid']);
// Test with a partial set of numerically indexed ids.
$result = $this->migrateLookup->lookup('sample_lookup_migration_multiple_source_ids', [25]);
$this->assertCount(2, $result);
$this->assertSame('2', $result[0]['nid']);
$this->assertSame('3', $result[1]['nid']);
// Test with a partial set of associatively indexed ids.
$result = $this->migrateLookup->lookup('sample_lookup_migration_multiple_source_ids', ['version_id' => 25]);
$this->assertCount(1, $result);
$this->assertSame('2', $result[0]['nid']);
}
/**
* Tests looking up against multiple migrations at once.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
* @throws \Drupal\migrate\MigrateException
*/
public function testMultipleMigrationLookup(): void {
$migrations = [
'sample_lookup_migration',
'sample_lookup_migration_2',
];
foreach ($migrations as $migration) {
$this->executeMigration($migration);
}
// Test numerically indexed source id.
$result = $this->migrateLookup->lookup($migrations, [17]);
$this->assertSame('1', $result[0]['nid']);
// Test associatively indexed source id.
$result = $this->migrateLookup->lookup($migrations, ['id' => 35]);
$this->assertSame('4', $result[0]['nid']);
// Test lookup not found.
$result = $this->migrateLookup->lookup($migrations, [1337]);
$this->assertSame([], $result);
}
/**
* Tests a lookup with string source ids.
*/
public function testLookupWithStringIds(): void {
$this->executeMigration('sample_lookup_migration_string_ids');
// Test numerically indexed source id.
$result = $this->migrateLookup->lookup('sample_lookup_migration_string_ids', ['node1']);
$this->assertSame('10', $result[0]['nid']);
// Test associatively indexed source id.
$result = $this->migrateLookup->lookup('sample_lookup_migration_string_ids', ['id' => 'node2']);
$this->assertSame('11', $result[0]['nid']);
// Test lookup not found.
$result = $this->migrateLookup->lookup('sample_lookup_migration_string_ids', ['node1337']);
$this->assertSame([], $result);
}
}

View File

@ -0,0 +1,154 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Event\MigrateEvents;
use Drupal\migrate\Event\MigrateIdMapMessageEvent;
use Drupal\migrate\MigrateExecutable;
use Drupal\migrate\MigrateMessageInterface;
use Drupal\migrate\Plugin\migrate\id_map\Sql;
/**
* Tests whether idmap messages are sent to message interface when requested.
*
* @group migrate
*/
class MigrateMessageTest extends KernelTestBase implements MigrateMessageInterface {
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate', 'system'];
/**
* Migration to run.
*
* @var \Drupal\migrate\Plugin\MigrationInterface
*/
protected $migration;
/**
* Messages accumulated during the migration run.
*
* @var array
*/
protected $messages = [];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig(['system']);
// A simple migration, which will generate a message to the ID map because
// the concat plugin throws an exception if its source is not an array.
$definition = [
'migration_tags' => ['Message test'],
'source' => [
'plugin' => 'embedded_data',
'data_rows' => [
['name' => 'source_message', 'value' => 'a message'],
],
'ids' => [
'name' => ['type' => 'string'],
],
],
'process' => [
'message' => [
'plugin' => 'concat',
'source' => 'value',
],
],
'destination' => [
'plugin' => 'config',
'config_name' => 'system.maintenance',
],
];
$this->migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition);
}
/**
* Tests migration interruptions.
*/
public function testMessagesNotTeed(): void {
// We don't ask for messages to be teed, so don't expect any.
$executable = new MigrateExecutable($this->migration, $this);
$executable->import();
$this->assertCount(0, $this->messages);
}
/**
* Tests migration interruptions.
*/
public function testMessagesTeed(): void {
// Ask to receive any messages sent to the idmap.
\Drupal::service('event_dispatcher')->addListener(MigrateEvents::IDMAP_MESSAGE,
[$this, 'mapMessageRecorder']);
$executable = new MigrateExecutable($this->migration, $this);
$executable->import();
$this->assertCount(1, $this->messages);
$id = $this->migration->getPluginId();
$this->assertSame("source_message: $id:message:concat: 'a message' is not an array", reset($this->messages));
}
/**
* Tests the return value of getMessages().
*
* This method returns an iterator of StdClass objects. Check that these
* objects have the expected keys.
*/
public function testGetMessages(): void {
$id = $this->migration->getPluginId();
$expected_message = (object) [
'src_name' => 'source_message',
'dest_config_name' => NULL,
'msgid' => '1',
Sql::SOURCE_IDS_HASH => '170cde81762e22552d1b1578cf3804c89afefe9efbc7cc835185d7141060b032',
'level' => '1',
'message' => "$id:message:concat: 'a message' is not an array",
];
$executable = new MigrateExecutable($this->migration, $this);
$executable->import();
$count = 0;
foreach ($this->migration->getIdMap()->getMessages() as $message) {
++$count;
$this->assertEquals($expected_message, $message);
}
$this->assertEquals(1, $count);
}
/**
* Reacts to map message event.
*
* @param \Drupal\migrate\Event\MigrateIdMapMessageEvent $event
* The migration event.
* @param string $name
* The event name.
*/
public function mapMessageRecorder(MigrateIdMapMessageEvent $event, $name): void {
if ($event->getLevel() == MigrationInterface::MESSAGE_NOTICE ||
$event->getLevel() == MigrationInterface::MESSAGE_INFORMATIONAL) {
$type = 'status';
}
else {
$type = 'error';
}
$source_id_string = implode(',', $event->getSourceIdValues());
$this->display($source_id_string . ': ' . $event->getMessage(), $type);
}
/**
* {@inheritdoc}
*/
public function display($message, $type = 'status'): void {
$this->messages[] = $message;
}
}

View File

@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\Core\Database\Database;
use Drupal\KernelTests\KernelTestBase;
use Drupal\migrate\Exception\RequirementsException;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Plugin\MigrationInterface;
/**
* Tests that a SQL migration can be instantiated without a database connection.
*
* @group migrate
*/
class MigrateMissingDatabaseTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate', 'migrate_missing_database_test'];
/**
* The migration plugin manager.
*
* @var \Drupal\migrate\Plugin\MigrationPluginManager
*/
protected $migrationPluginManager;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->migrationPluginManager = \Drupal::service('plugin.manager.migration');
// Set the 'migrate' database connection to use a missing database.
$info = Database::getConnectionInfo('default')['default'];
$info['database'] = 'godot';
Database::addConnectionInfo('migrate', 'default', $info);
}
/**
* Tests a SQL migration without the database connection.
*
* - The migration can be instantiated.
* - The checkRequirements() method throws a RequirementsException.
*/
public function testMissingDatabase(): void {
if (Database::getConnection()->driver() === 'sqlite') {
$this->markTestSkipped('Not compatible with sqlite');
}
$migration = $this->migrationPluginManager->createInstance('missing_database');
$this->assertInstanceOf(MigrationInterface::class, $migration);
$this->assertInstanceOf(MigrateIdMapInterface::class, $migration->getIdMap());
$this->expectException(RequirementsException::class);
$this->expectExceptionMessage('No database connection available for source plugin migrate_missing_database_test');
$migration->checkRequirements();
}
}

View File

@ -0,0 +1,173 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateExecutable;
use Drupal\migrate\Plugin\migrate\process\Get;
use Drupal\migrate\Plugin\migrate\process\SubProcess;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Plugin\MigratePluginManagerInterface;
use Drupal\migrate\Plugin\MigrateProcessInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
/**
* Tests the format of messages from process plugin exceptions.
*
* @group migrate
*/
class MigrateProcessErrorMessagesTest extends MigrateTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'migrate_events_test',
'migrate',
];
/**
* A mock Process Plugin Manager.
*
* @var \Prophecy\Prophecy\ObjectProphecy
*/
protected ObjectProphecy $processPluginManager;
/**
* A mock ID Map Plugin Manager.
*
* @var \Prophecy\Prophecy\ObjectProphecy
*/
protected ObjectProphecy $idMapPluginManager;
/**
* A mock ID Map.
*
* @var \Prophecy\Prophecy\ObjectProphecy
*/
protected ObjectProphecy $idMap;
/**
* The default stub migration definition.
*
* @var array
*/
protected array $definition = [
'id' => 'process_errors_migration',
'idMap' => [
'plugin' => 'idmap_prophecy',
],
'source' => [
'plugin' => 'embedded_data',
'data_rows' => [
[
'id' => 1,
'my_property' => [
'subfield' => [
42,
],
],
],
],
'ids' => ['id' => ['type' => 'integer']],
],
'process' => [
'id' => 'id',
],
'destination' => [
'plugin' => 'dummy',
],
'migration_dependencies' => [],
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->processPluginManager = $this->prophesize(MigratePluginManagerInterface::class);
$this->idMapPluginManager = $this->prophesize(MigratePluginManagerInterface::class);
$this->idMap = $this->prophesize(MigrateIdMapInterface::class);
}
/**
* Tests format of map messages saved from plugin exceptions.
*/
public function testProcessErrorMessage(): void {
$this->definition['process']['error']['plugin'] = 'test_error';
$this->idMap->saveMessage(['id' => 1], "process_errors_migration:error:test_error: Process exception.", MigrationInterface::MESSAGE_ERROR)->shouldBeCalled();
$this->setPluginManagers();
/** @var \Drupal\migrate\Plugin\MigrationInterface $migration */
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration($this->definition);
$executable = new MigrateExecutable($migration, $this);
$executable->import();
}
/**
* Tests format of map messages saved from sub_process exceptions.
*
* This checks the format of messages that are thrown from normal process
* plugins while being executed inside a sub_process pipeline as they
* bubble up to the main migration.
*/
public function testSubProcessErrorMessage(): void {
$this->definition['process']['subprocess_error'] = [
'plugin' => 'sub_process',
'source' => 'my_property',
'process' => [
'subfield' => [
[
'plugin' => 'test_error',
'value' => 'subfield',
],
],
],
];
$this->processPluginManager->createInstance('sub_process', Argument::cetera())
->will(fn($x) => new SubProcess($x[1], 'sub_process', ['handle_multiples' => TRUE]));
$this->idMap->saveMessage(['id' => 1], "process_errors_migration:subprocess_error:sub_process: test_error: Process exception.", MigrationInterface::MESSAGE_ERROR)->shouldBeCalled();
$this->setPluginManagers();
/** @var \Drupal\migrate\Plugin\MigrationInterface $migration */
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration($this->definition);
$executable = new MigrateExecutable($migration, $this);
$executable->import();
}
/**
* Prepares and sets the prophesized plugin managers.
*/
protected function setPluginManagers(): void {
$error_plugin_prophecy = $this->prophesize(MigrateProcessInterface::class);
$error_plugin_prophecy->getPluginDefinition()->willReturn(['plugin_id' => 'test_error']);
$error_plugin_prophecy->getPluginId()->willReturn('test_error');
$error_plugin_prophecy->reset()->shouldBeCalled();
$error_plugin_prophecy->transform(Argument::cetera())->willThrow(new MigrateException('Process exception.'));
$this->processPluginManager->createInstance('get', Argument::cetera())
->will(fn($x) => new Get($x[1], 'get', ['handle_multiples' => TRUE]));
$this->processPluginManager->createInstance('test_error', Argument::cetera())->willReturn($error_plugin_prophecy->reveal());
$this->idMap->setMessage(Argument::any())->willReturn();
$this->idMap->getRowBySource(Argument::any())->willReturn([]);
$this->idMap->delete(Argument::cetera())->willReturn();
$this->idMap->saveIdMapping(Argument::cetera())->willReturn();
$this->idMapPluginManager->createInstance('idmap_prophecy', Argument::cetera())->willReturn($this->idMap->reveal());
$this->container->set('plugin.manager.migrate.process', $this->processPluginManager->reveal());
$this->container->set('plugin.manager.migrate.id_map', $this->idMapPluginManager->reveal());
}
}

View File

@ -0,0 +1,176 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\migrate\MigrateExecutable;
use Drupal\taxonomy\Entity\Vocabulary;
/**
* Tests rolling back of imports.
*
* @group migrate
*/
class MigrateRollbackEntityConfigTest extends MigrateTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'field',
'taxonomy',
'text',
'language',
'config_translation',
'user',
'system',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
$this->installEntitySchema('taxonomy_term');
$this->installConfig(['taxonomy']);
}
/**
* Tests rolling back configuration entity translations.
*/
public function testConfigEntityRollback(): void {
// We use vocabularies to demonstrate importing and rolling back
// configuration entities with translations. First, import vocabularies.
$vocabulary_data_rows = [
['id' => '1', 'name' => 'categories', 'weight' => '2'],
['id' => '2', 'name' => 'tags', 'weight' => '1'],
];
$ids = ['id' => ['type' => 'integer']];
$definition = [
'id' => 'vocabularies',
'migration_tags' => ['Import and rollback test'],
'source' => [
'plugin' => 'embedded_data',
'data_rows' => $vocabulary_data_rows,
'ids' => $ids,
],
'process' => [
'vid' => 'id',
'name' => 'name',
'weight' => 'weight',
],
'destination' => ['plugin' => 'entity:taxonomy_vocabulary'],
];
/** @var \Drupal\migrate\Plugin\Migration $vocabulary_migration */
$vocabulary_migration = \Drupal::service('plugin.manager.migration')
->createStubMigration($definition);
$vocabulary_id_map = $vocabulary_migration->getIdMap();
$this->assertTrue($vocabulary_migration->getDestinationPlugin()
->supportsRollback());
// Import and validate vocabulary config entities were created.
$vocabulary_executable = new MigrateExecutable($vocabulary_migration, $this);
$vocabulary_executable->import();
foreach ($vocabulary_data_rows as $row) {
/** @var \Drupal\taxonomy\Entity\Vocabulary $vocabulary */
$vocabulary = Vocabulary::load($row['id']);
$this->assertNotEmpty($vocabulary);
$map_row = $vocabulary_id_map->getRowBySource(['id' => $row['id']]);
$this->assertNotNull($map_row['destid1']);
}
// Second, import translations of the vocabulary name property.
$vocabulary_i18n_data_rows = [
[
'id' => '1',
'name' => '1',
'language' => 'fr',
'property' => 'name',
'translation' => 'fr - categories',
],
[
'id' => '2',
'name' => '2',
'language' => 'fr',
'property' => 'name',
'translation' => 'fr - tags',
],
];
$ids = [
'id' => ['type' => 'integer'],
'language' => ['type' => 'string'],
];
$definition = [
'id' => 'i18n_vocabularies',
'migration_tags' => ['Import and rollback test'],
'source' => [
'plugin' => 'embedded_data',
'data_rows' => $vocabulary_i18n_data_rows,
'ids' => $ids,
'constants' => [
'name' => 'name',
],
],
'process' => [
'vid' => 'id',
'langcode' => 'language',
'property' => 'constants/name',
'translation' => 'translation',
],
'destination' => [
'plugin' => 'entity:taxonomy_vocabulary',
'translations' => 'true',
],
];
$vocabulary_i18n__migration = \Drupal::service('plugin.manager.migration')
->createStubMigration($definition);
$vocabulary_i18n_id_map = $vocabulary_i18n__migration->getIdMap();
$this->assertTrue($vocabulary_i18n__migration->getDestinationPlugin()
->supportsRollback());
// Import and validate vocabulary config entities were created.
$vocabulary_i18n_executable = new MigrateExecutable($vocabulary_i18n__migration, $this);
$vocabulary_i18n_executable->import();
$language_manager = \Drupal::service('language_manager');
foreach ($vocabulary_i18n_data_rows as $row) {
$langcode = $row['language'];
$id = 'taxonomy.vocabulary.' . $row['id'];
/** @var \Drupal\language\Config\LanguageConfigOverride $config_translation */
$config_translation = $language_manager->getLanguageConfigOverride($langcode, $id);
$this->assertSame($row['translation'], $config_translation->get('name'));
$map_row = $vocabulary_i18n_id_map->getRowBySource(['id' => $row['id'], 'language' => $row['language']]);
$this->assertNotNull($map_row['destid1']);
}
// Perform the rollback and confirm the translation was deleted and the map
// table row removed.
$vocabulary_i18n_executable->rollback();
foreach ($vocabulary_i18n_data_rows as $row) {
$langcode = $row['language'];
$id = 'taxonomy.vocabulary.' . $row['id'];
/** @var \Drupal\language\Config\LanguageConfigOverride $config_translation */
$config_translation = $language_manager->getLanguageConfigOverride($langcode, $id);
$this->assertNull($config_translation->get('name'));
$map_row = $vocabulary_i18n_id_map->getRowBySource(['id' => $row['id'], 'language' => $row['language']]);
$this->assertFalse($map_row);
}
// Confirm the original vocabulary still exists.
foreach ($vocabulary_data_rows as $row) {
/** @var \Drupal\taxonomy\Entity\Vocabulary $vocabulary */
$vocabulary = Vocabulary::load($row['id']);
$this->assertNotEmpty($vocabulary);
$map_row = $vocabulary_id_map->getRowBySource(['id' => $row['id']]);
$this->assertNotNull($map_row['destid1']);
}
}
}

View File

@ -0,0 +1,183 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\migrate\MigrateExecutable;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Row;
use Drupal\taxonomy\Entity\Term;
use Drupal\taxonomy\Entity\Vocabulary;
/**
* Tests rolling back of imports.
*
* @group migrate
*/
class MigrateRollbackTest extends MigrateTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['field', 'taxonomy', 'text', 'user', 'system'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
$this->installEntitySchema('taxonomy_term');
$this->installConfig(['taxonomy']);
}
/**
* Tests rolling back configuration and content entities.
*/
public function testRollback(): void {
// We use vocabularies to demonstrate importing and rolling back
// configuration entities.
$vocabulary_data_rows = [
['id' => '1', 'name' => 'categories', 'weight' => '2'],
['id' => '2', 'name' => 'tags', 'weight' => '1'],
];
$ids = ['id' => ['type' => 'integer']];
$definition = [
'id' => 'vocabularies',
'migration_tags' => ['Import and rollback test'],
'source' => [
'plugin' => 'embedded_data',
'data_rows' => $vocabulary_data_rows,
'ids' => $ids,
],
'process' => [
'vid' => 'id',
'name' => 'name',
'weight' => 'weight',
],
'destination' => ['plugin' => 'entity:taxonomy_vocabulary'],
];
$vocabulary_migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition);
$vocabulary_id_map = $vocabulary_migration->getIdMap();
$this->assertTrue($vocabulary_migration->getDestinationPlugin()->supportsRollback());
// Import and validate vocabulary config entities were created.
$vocabulary_executable = new MigrateExecutable($vocabulary_migration, $this);
$vocabulary_executable->import();
foreach ($vocabulary_data_rows as $row) {
/** @var \Drupal\taxonomy\Entity\Vocabulary $vocabulary */
$vocabulary = Vocabulary::load($row['id']);
$this->assertNotEmpty($vocabulary);
$map_row = $vocabulary_id_map->getRowBySource(['id' => $row['id']]);
$this->assertNotNull($map_row['destid1']);
}
// We use taxonomy terms to demonstrate importing and rolling back content
// entities.
$term_data_rows = [
['id' => '1', 'vocab' => '1', 'name' => 'music'],
['id' => '2', 'vocab' => '2', 'name' => 'Bach'],
['id' => '3', 'vocab' => '2', 'name' => 'Beethoven'],
];
$ids = ['id' => ['type' => 'integer']];
$definition = [
'id' => 'terms',
'migration_tags' => ['Import and rollback test'],
'source' => [
'plugin' => 'embedded_data',
'data_rows' => $term_data_rows,
'ids' => $ids,
],
'process' => [
'tid' => 'id',
'vid' => 'vocab',
'name' => 'name',
],
'destination' => ['plugin' => 'entity:taxonomy_term'],
'migration_dependencies' => ['required' => ['vocabularies']],
];
$term_migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition);
$term_id_map = $term_migration->getIdMap();
$this->assertTrue($term_migration->getDestinationPlugin()->supportsRollback());
// Pre-create a term, to make sure it isn't deleted on rollback.
$preserved_term_ids[] = 1;
$new_term = Term::create(['tid' => 1, 'vid' => 1, 'name' => 'music']);
$new_term->save();
// Import and validate term entities were created.
$term_executable = new MigrateExecutable($term_migration, $this);
$term_executable->import();
// Also explicitly mark one row to be preserved on rollback.
$preserved_term_ids[] = 2;
$map_row = $term_id_map->getRowBySource(['id' => 2]);
$dummy_row = new Row(['id' => 2], $ids);
$term_id_map->saveIdMapping($dummy_row, [$map_row['destid1']],
$map_row['source_row_status'], MigrateIdMapInterface::ROLLBACK_PRESERVE);
foreach ($term_data_rows as $row) {
/** @var \Drupal\taxonomy\Entity\Term $term */
$term = Term::load($row['id']);
$this->assertNotEmpty($term);
$map_row = $term_id_map->getRowBySource(['id' => $row['id']]);
$this->assertNotNull($map_row['destid1']);
}
// Add a failed row to test if this can be rolled back without errors.
$this->mockFailure($term_migration, ['id' => '4', 'vocab' => '2', 'name' => 'FAIL']);
// Rollback and verify the entities are gone.
$term_executable->rollback();
foreach ($term_data_rows as $row) {
$term = Term::load($row['id']);
if (in_array($row['id'], $preserved_term_ids)) {
$this->assertNotNull($term);
}
else {
$this->assertNull($term);
}
$map_row = $term_id_map->getRowBySource(['id' => $row['id']]);
$this->assertFalse($map_row);
}
$vocabulary_executable->rollback();
foreach ($vocabulary_data_rows as $row) {
$term = Vocabulary::load($row['id']);
$this->assertNull($term);
$map_row = $vocabulary_id_map->getRowBySource(['id' => $row['id']]);
$this->assertFalse($map_row);
}
// Test that simple configuration is not rollbackable.
$term_setting_rows = [
['id' => 1, 'override_selector' => '0', 'terms_per_page_admin' => '10'],
];
$ids = ['id' => ['type' => 'integer']];
$definition = [
'id' => 'taxonomy_settings',
'migration_tags' => ['Import and rollback test'],
'source' => [
'plugin' => 'embedded_data',
'data_rows' => $term_setting_rows,
'ids' => $ids,
],
'process' => [
'override_selector' => 'override_selector',
'terms_per_page_admin' => 'terms_per_page_admin',
],
'destination' => [
'plugin' => 'config',
'config_name' => 'taxonomy.settings',
],
'migration_dependencies' => ['required' => ['vocabularies']],
];
$settings_migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition);
$this->assertFalse($settings_migration->getDestinationPlugin()->supportsRollback());
}
}

View File

@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\MigrateExecutable;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
/**
* Tests row skips triggered during hook_migrate_prepare_row().
*
* @group migrate
*/
class MigrateSkipRowTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate', 'migrate_prepare_row_test'];
/**
* Tests migration interruptions.
*/
public function testPrepareRowSkip(): void {
// Run a simple little migration with two data rows which should be skipped
// in different ways.
$definition = [
'migration_tags' => ['prepare_row test'],
'source' => [
'plugin' => 'embedded_data',
'data_rows' => [
['id' => '1', 'data' => 'skip_and_record'],
['id' => '2', 'data' => 'skip_and_do_not_record'],
],
'ids' => [
'id' => ['type' => 'string'],
],
],
'process' => ['value' => 'data'],
'destination' => [
'plugin' => 'config',
'config_name' => 'migrate_test.settings',
],
'load' => ['plugin' => 'null'],
];
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition);
$executable = new MigrateExecutable($migration);
$result = $executable->import();
$this->assertEquals(MigrationInterface::RESULT_COMPLETED, $result);
/** @var \Drupal\migrate\Plugin\MigrateIdMapInterface $id_map_plugin */
$id_map_plugin = $migration->getIdMap();
// The first row is recorded in the map as ignored.
$map_row = $id_map_plugin->getRowBySource(['id' => 1]);
$this->assertEquals(MigrateIdMapInterface::STATUS_IGNORED, $map_row['source_row_status']);
// Check that no message has been logged for the first exception.
$messages = $id_map_plugin->getMessages(['id' => 1])->fetchAll();
$this->assertEmpty($messages);
// The second row is not recorded in the map.
$map_row = $id_map_plugin->getRowBySource(['id' => 2]);
$this->assertFalse($map_row);
// Check that the correct message has been logged for the second exception.
$messages = $id_map_plugin->getMessages(['id' => 2])->fetchAll();
$this->assertCount(1, $messages);
$message = reset($messages);
$this->assertEquals('skip_and_do_not_record message', $message->message);
$this->assertEquals(MigrationInterface::MESSAGE_INFORMATIONAL, $message->level);
// Insert a custom processor in the process flow.
$definition['process']['value'] = [
'source' => 'data',
'plugin' => 'test_skip_row_process',
];
// Change data to avoid triggering again hook_migrate_prepare_row().
$definition['source']['data_rows'] = [
['id' => '1', 'data' => 'skip_and_record (use plugin)'],
['id' => '2', 'data' => 'skip_and_do_not_record (use plugin)'],
];
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition);
$executable = new MigrateExecutable($migration);
$result = $executable->import();
$this->assertEquals(MigrationInterface::RESULT_COMPLETED, $result);
$id_map_plugin = $migration->getIdMap();
// The first row is recorded in the map as ignored.
$map_row = $id_map_plugin->getRowBySource(['id' => 1]);
$this->assertEquals(MigrateIdMapInterface::STATUS_IGNORED, $map_row['source_row_status']);
// The second row is not recorded in the map.
$map_row = $id_map_plugin->getRowBySource(['id' => 2]);
$this->assertFalse($map_row);
}
}

View File

@ -0,0 +1,218 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
/**
* Base class for tests of Migrate source plugins.
*
* Implementing classes must declare a providerSource() method for this class
* to work, defined as follows:
*
* @code
* abstract public static function providerSource(): array;
* @endcode
*
* The returned array should be as follows:
*
* @code
* Array of data sets to test, each of which is a numerically indexed array
* with the following elements:
* - An array of source data, which can be optionally processed and set up
* by subclasses.
* - An array of expected result rows.
* - (optional) The number of result rows the plugin under test is expected
* to return. If this is not a numeric value, the plugin will not be
* counted.
* - (optional) Array of configuration options for the plugin under test.
* @endcode
*
* @see \Drupal\Tests\migrate\Kernel\MigrateSourceTestBase::testSource
*/
abstract class MigrateSourceTestBase extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate', 'migrate_skip_all_rows_test'];
/**
* The mocked migration.
*
* @var \Drupal\migrate\Plugin\MigrationInterface|\Prophecy\Prophecy\ObjectProphecy
*/
protected $migration;
/**
* The source plugin under test.
*
* @var \Drupal\migrate\Plugin\MigrateSourceInterface
*/
protected $plugin;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create a mock migration. This will be injected into the source plugin
// under test.
$this->migration = $this->prophesize(MigrationInterface::class);
$this->migration->id()->willReturn(
$this->randomMachineName(16)
);
// Prophesize a useless ID map plugin and an empty set of destination IDs.
// Calling code can override these prophecies later and set up different
// behaviors.
$this->migration->getIdMap()->willReturn(
$this->prophesize(MigrateIdMapInterface::class)->reveal()
);
$this->migration->getDestinationIds()->willReturn([]);
}
/**
* Determines the plugin to be tested by reading the class @covers annotation.
*
* @return string
* The fully qualified class name of the plugin to be tested.
*/
protected function getPluginClass() {
$covers = $this->valueObjectForEvents()->metadata()->isCovers()->isClassLevel()->asArray();
if (isset($covers[0])) {
return $covers[0]->target();
}
else {
$this->fail('No plugin class was specified');
}
}
/**
* Instantiates the source plugin under test.
*
* @param array $configuration
* The source plugin configuration.
*
* @return \Drupal\migrate\Plugin\MigrateSourceInterface|object
* The fully configured source plugin.
*/
protected function getPlugin(array $configuration) {
// Only create the plugin once per test.
if ($this->plugin) {
return $this->plugin;
}
$class = ltrim($this->getPluginClass(), '\\');
/** @var \Drupal\migrate\Plugin\MigratePluginManager $plugin_manager */
$plugin_manager = $this->container->get('plugin.manager.migrate.source');
foreach ($plugin_manager->getDefinitions() as $id => $definition) {
if (ltrim($definition['class'], '\\') == $class) {
$this->plugin = $plugin_manager
->createInstance($id, $configuration, $this->migration->reveal());
$this->migration
->getSourcePlugin()
->willReturn($this->plugin);
return $this->plugin;
}
}
$this->fail('No plugin found for class ' . $class);
}
/**
* Tests the source plugin against a particular data set.
*
* @param array $source_data
* The source data that the source plugin will read.
* @param array $expected_data
* The result rows the source plugin is expected to return.
* @param mixed $expected_count
* (optional) How many rows the source plugin is expected to return.
* Defaults to count($expected_data). If set to a non-null, non-numeric
* value (like FALSE or 'nope'), the source plugin will not be counted.
* @param array $configuration
* (optional) Configuration for the source plugin.
* @param mixed $high_water
* (optional) The value of the high water field.
*
* @dataProvider providerSource
*/
public function testSource(array $source_data, array $expected_data, $expected_count = NULL, array $configuration = [], $high_water = NULL): void {
$plugin = $this->getPlugin($configuration);
$clone_plugin = clone $plugin;
// All source plugins must define IDs.
$this->assertNotEmpty($plugin->getIds());
// If there is a high water mark, set it in the high water storage.
if (isset($high_water)) {
$this->container
->get('keyvalue')
->get('migrate:high_water')
->set($this->migration->reveal()->id(), $high_water);
}
if (is_null($expected_count)) {
$expected_count = count($expected_data);
}
// If an expected count was given, assert it only if the plugin is
// countable.
if (is_numeric($expected_count)) {
$this->assertInstanceOf('\Countable', $plugin);
$this->assertCount($expected_count, $plugin);
}
$i = 0;
/** @var \Drupal\migrate\Row $row */
foreach ($plugin as $row) {
$this->assertInstanceOf(Row::class, $row);
$expected = $expected_data[$i++];
$actual = $row->getSource();
foreach ($expected as $key => $value) {
$this->assertArrayHasKey($key, $actual);
$msg = sprintf("Value at 'array[%s][%s]' is not correct.", $i - 1, $key);
if (is_array($value)) {
ksort($value);
ksort($actual[$key]);
$this->assertEquals($value, $actual[$key], $msg);
}
else {
$this->assertEquals((string) $value, (string) $actual[$key], $msg);
}
}
}
// False positives occur if the foreach is not entered. So, confirm the
// foreach loop was entered if the expected count is greater than 0.
if ($expected_count > 0) {
$this->assertGreaterThan(0, $i);
// Test that we can skip all rows.
\Drupal::state()->set('migrate_skip_all_rows_test_migrate_prepare_row', TRUE);
foreach ($clone_plugin as $row) {
$this->fail('Row not skipped');
}
}
}
/**
* Provides source data for ::testSource.
*
* @return iterable
* The source data.
*/
abstract public static function providerSource();
}

View File

@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\Core\Cache\MemoryCounterBackendFactory;
use Drupal\sqlite\Driver\Database\sqlite\Connection;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
/**
* Base class for tests of Migrate source plugins that use a database.
*/
abstract class MigrateSqlSourceTestBase extends MigrateSourceTestBase {
/**
* {@inheritdoc}
*/
public function register(ContainerBuilder $container) {
parent::register($container);
$container
->register('cache_factory', MemoryCounterBackendFactory::class)
->addArgument(new Reference('datetime.time'));
}
/**
* Builds an in-memory SQLite database from a set of source data.
*
* @param array $source_data
* The source data, keyed by table name. Each table is an array containing
* the rows in that table.
*
* @return \Drupal\sqlite\Driver\Database\sqlite\Connection
* The SQLite database connection.
*/
protected function getDatabase(array $source_data) {
// Create an in-memory SQLite database. Plugins can interact with it like
// any other database, and it will cease to exist when the connection is
// closed.
$connection_options = ['database' => ':memory:'];
$pdo = Connection::open($connection_options);
$connection = new Connection($pdo, $connection_options);
// Create the tables and fill them with data.
foreach ($source_data as $table => $rows) {
// Use the biggest row to build the table schema.
$counts = array_map('count', $rows);
asort($counts);
$pilot = $rows[array_key_last($counts)];
$connection->schema()
->createTable($table, [
// SQLite uses loose affinity typing, so it's OK for every field to
// be a text field.
'fields' => array_map(function () {
return ['type' => 'text'];
}, $pilot),
]);
$fields = array_keys($pilot);
$insert = $connection->insert($table)->fields($fields);
array_walk($rows, [$insert, 'values']);
$insert->execute();
}
return $connection;
}
/**
* Tests the source plugin against a particular data set.
*
* @param array $source_data
* The source data that the plugin will read. See getDatabase() for the
* expected format.
* @param array $expected_data
* The result rows the plugin is expected to return.
* @param int $expected_count
* (optional) How many rows the source plugin is expected to return.
* @param array $configuration
* (optional) Configuration for the source plugin.
* @param mixed $high_water
* (optional) The value of the high water field.
* @param string|null $expected_cache_key
* (optional) The expected cache key.
*
* @dataProvider providerSource
*
* @requires extension pdo_sqlite
*/
public function testSource(array $source_data, array $expected_data, $expected_count = NULL, array $configuration = [], $high_water = NULL, $expected_cache_key = NULL): void {
$plugin = $this->getPlugin($configuration);
// Since we don't yet inject the database connection, we need to use a
// reflection hack to set it in the plugin instance.
$reflector = new \ReflectionObject($plugin);
$property = $reflector->getProperty('database');
$property->setValue($plugin, $this->getDatabase($source_data));
/** @var MemoryCounterBackend $cache **/
$cache = \Drupal::cache('migrate');
if ($expected_cache_key) {
// Verify the computed cache key.
$property = $reflector->getProperty('cacheKey');
$this->assertSame($expected_cache_key, $property->getValue($plugin));
// Cache miss prior to calling ::count().
$this->assertFalse($cache->get($expected_cache_key, 'cache'));
$this->assertSame([], $cache->getCounter('set'));
$count = $plugin->count();
$this->assertSame($expected_count, $count);
$this->assertSame([$expected_cache_key => 1], $cache->getCounter('set'));
// Cache hit afterwards.
$cache_item = $cache->get($expected_cache_key, 'cache');
$this->assertNotSame(FALSE, $cache_item, 'This is not a cache hit.');
$this->assertSame($expected_count, $cache_item->data);
}
else {
$this->assertSame([], $cache->getCounter('set'));
$plugin->count();
$this->assertSame([], $cache->getCounter('set'));
}
parent::testSource($source_data, $expected_data, $expected_count, $configuration, $high_water);
}
}

View File

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\migrate\Plugin\MigrationInterface;
/**
* Tests migration status tracking.
*
* @group migrate
*/
class MigrateStatusTest extends MigrateTestBase {
/**
* Tests different connection types.
*/
public function testStatus(): void {
// Create a minimally valid migration.
$definition = [
'id' => 'migrate_status_test',
'migration_tags' => ['Testing'],
'source' => ['plugin' => 'empty'],
'destination' => [
'plugin' => 'config',
'config_name' => 'migrate_test.settings',
],
'process' => ['foo' => 'bar'],
];
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition);
// Default status is idle.
$status = $migration->getStatus();
$this->assertSame(MigrationInterface::STATUS_IDLE, $status);
// Test setting and retrieving all known status values.
$status_list = [
MigrationInterface::STATUS_IDLE,
MigrationInterface::STATUS_IMPORTING,
MigrationInterface::STATUS_ROLLING_BACK,
MigrationInterface::STATUS_STOPPING,
MigrationInterface::STATUS_DISABLED,
];
foreach ($status_list as $status) {
$migration->setStatus($status);
$this->assertSame($status, $migration->getStatus());
}
}
}

View File

@ -0,0 +1,142 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\field\Entity\FieldConfig;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
/**
* Tests the migrate.stub Service.
*
* @group migrate
*/
class MigrateStubTest extends MigrateTestBase {
use ContentTypeCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'node',
'field',
'user',
'text',
'filter',
'migrate_stub_test',
];
/**
* The migrate stub service.
*
* @var \Drupal\migrate\MigrateStubInterface
*/
protected $migrateStub;
/**
* The migration lookup service.
*
* @var \Drupal\migrate\MigrateLookupInterface
*/
protected $migrateLookup;
/**
* The migration plugin manager.
*
* @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface
*/
protected $migrationPluginManager;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->setTestLogger();
$this->migrateStub = $this->container->get('migrate.stub');
$this->migrateLookup = $this->container->get('migrate.lookup');
$this->migrationPluginManager = $this->container->get('plugin.manager.migration');
$this->installEntitySchema('node');
$this->installEntitySchema('user');
$this->installSchema('node', 'node_access');
$this->installConfig(['node', 'user']);
$this->createContentType(['type' => 'node_lookup']);
}
/**
* Tests stub creation.
*/
public function testCreateStub(): void {
$this->assertSame([], $this->migrateLookup->lookup('sample_stubbing_migration', [17]));
$ids = $this->migrateStub->createStub('sample_stubbing_migration', [17]);
$this->assertSame([$ids], $this->migrateLookup->lookup('sample_stubbing_migration', [17]));
$this->assertNotNull(\Drupal::entityTypeManager()->getStorage('node')->load($ids['nid']));
}
/**
* Tests raw stub creation.
*/
public function testCreateStubRawReturn(): void {
$this->assertSame([], $this->migrateLookup->lookup('sample_stubbing_migration', [17]));
$ids = $this->migrateStub->createStub('sample_stubbing_migration', [17], [], FALSE);
$this->assertSame($ids, [$this->migrateLookup->lookup('sample_stubbing_migration', [17])[0]['nid']]);
$this->assertNotNull(\Drupal::entityTypeManager()->getStorage('node')->load($ids[0]));
}
/**
* Tests stub creation with default values.
*/
public function testStubWithDefaultValues(): void {
$this->assertSame([], $this->migrateLookup->lookup('sample_stubbing_migration', [17]));
$ids = $this->migrateStub->createStub('sample_stubbing_migration', [17], ['title' => "Placeholder for source id 17"]);
$this->assertSame([$ids], $this->migrateLookup->lookup('sample_stubbing_migration', [17]));
$node = \Drupal::entityTypeManager()->getStorage('node')->load($ids['nid']);
$this->assertNotNull($node);
// Test that our default value was set as the node title.
$this->assertSame("Placeholder for source id 17", $node->label());
// Test that running the migration replaces the node title.
$this->executeMigration('sample_stubbing_migration');
$node = \Drupal::entityTypeManager()->getStorage('node')->loadUnchanged($ids['nid']);
$this->assertSame("Sample 1", $node->label());
}
/**
* Tests stub creation with bundle fields.
*/
public function testStubWithBundleFields(): void {
$this->createContentType(['type' => 'node_stub']);
// Make "Body" field required to make stubbing populate field value.
$body_field = FieldConfig::loadByName('node', 'node_stub', 'body');
$body_field->setRequired(TRUE)->save();
$this->assertSame([], $this->migrateLookup->lookup('sample_stubbing_migration', [33]));
$ids = $this->migrateStub->createStub('sample_stubbing_migration', [33], []);
$this->assertSame([$ids], $this->migrateLookup->lookup('sample_stubbing_migration', [33]));
$node = \Drupal::entityTypeManager()->getStorage('node')->load($ids['nid']);
$this->assertNotNull($node);
// Make sure the "Body" field value was populated.
$this->assertNotEmpty($node->get('body')->value);
}
/**
* Tests invalid source id count.
*/
public function testInvalidSourceIdCount(): void {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Expected and provided source id counts do not match.');
$this->migrateStub->createStub('sample_stubbing_migration_with_multiple_source_ids', [17]);
}
/**
* Tests invalid source ids keys.
*/
public function testInvalidSourceIdKeys(): void {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage("'version_id' is defined as a source ID but has no value.");
$this->migrateStub->createStub('sample_stubbing_migration_with_multiple_source_ids', ['id' => 17, 'not_a_key' => 17]);
}
}

View File

@ -0,0 +1,273 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\Core\Database\Database;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\KernelTests\KernelTestBase;
use Drupal\migrate\MigrateExecutable;
use Drupal\migrate\MigrateMessageInterface;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
/**
* Creates abstract base class for migration tests.
*/
abstract class MigrateTestBase extends KernelTestBase implements MigrateMessageInterface {
/**
* TRUE to collect messages instead of displaying them.
*
* @var bool
*/
protected $collectMessages = FALSE;
/**
* A two dimensional array of messages.
*
* The first key is the type of message, the second is just numeric. Values
* are the messages.
*
* @var array
*/
protected $migrateMessages;
/**
* The primary migration being tested.
*
* @var \Drupal\migrate\Plugin\MigrationInterface
*/
protected $migration;
/**
* The source database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $sourceDatabase;
/**
* A logger prophecy object.
*
* Using ::setTestLogger(), this prophecy will be configured and injected into
* the container. Using $this->logger->function(args)->shouldHaveBeenCalled()
* you can assert that the logger was called.
*
* @var \Prophecy\Prophecy\ObjectProphecy
*/
protected $logger;
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->createMigrationConnection();
$this->sourceDatabase = Database::getConnection('default', 'migrate');
// Attach the original test prefix as a database, for SQLite to attach its
// database file.
$this->sourceDatabase->attachDatabase(substr($this->sourceDatabase->getConnectionOptions()['prefix'], 0, -1));
}
/**
* Changes the database connection to the prefixed one.
*
* @todo Remove when we don't use global. https://www.drupal.org/node/2552791
*/
private function createMigrationConnection() {
// If the backup already exists, something went terribly wrong.
// This case is possible, because database connection info is a static
// global state construct on the Database class, which at least persists
// for all test methods executed in one PHP process.
if (Database::getConnectionInfo('simpletest_original_migrate')) {
throw new \RuntimeException("Bad Database connection state: 'simpletest_original_migrate' connection key already exists. Broken test?");
}
// Clone the current connection and replace the current prefix.
$connection_info = Database::getConnectionInfo('migrate');
if ($connection_info) {
Database::renameConnection('migrate', 'simpletest_original_migrate');
}
$connection_info = Database::getConnectionInfo('default');
foreach ($connection_info as $target => $value) {
$prefix = $value['prefix'];
// Tests use 7 character prefixes at most so this can't cause collisions.
$connection_info[$target]['prefix'] = $prefix . '0';
}
Database::addConnectionInfo('migrate', 'default', $connection_info['default']);
}
/**
* {@inheritdoc}
*/
protected function tearDown(): void {
$this->cleanupMigrateConnection();
parent::tearDown();
$this->collectMessages = FALSE;
unset($this->migration, $this->migrateMessages);
}
/**
* Cleans up the test migrate connection.
*
* @todo Remove when we don't use global. https://www.drupal.org/node/2552791
*/
private function cleanupMigrateConnection() {
Database::removeConnection('migrate');
$original_connection_info = Database::getConnectionInfo('simpletest_original_migrate');
if ($original_connection_info) {
Database::renameConnection('simpletest_original_migrate', 'migrate');
}
}
/**
* Prepare any dependent migrations.
*
* @param array $id_mappings
* A list of ID mappings keyed by migration IDs. Each ID mapping is a list
* of two arrays, the first are source IDs and the second are destination
* IDs.
*/
protected function prepareMigrations(array $id_mappings) {
$manager = $this->container->get('plugin.manager.migration');
foreach ($id_mappings as $migration_id => $data) {
foreach ($manager->createInstances($migration_id) as $migration) {
$id_map = $migration->getIdMap();
$id_map->setMessage($this);
$source_ids = $migration->getSourcePlugin()->getIds();
foreach ($data as $id_mapping) {
$row = new Row(array_combine(array_keys($source_ids), $id_mapping[0]), $source_ids);
$id_map->saveIdMapping($row, $id_mapping[1]);
}
}
}
}
/**
* Modify a migration's configuration before executing it.
*
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration to execute.
*/
protected function prepareMigration(MigrationInterface $migration) {
// Default implementation for test classes not requiring modification.
}
/**
* Executes a single migration.
*
* @param string|\Drupal\migrate\Plugin\MigrationInterface $migration
* The migration to execute, or its ID.
*/
protected function executeMigration($migration) {
if (is_string($migration)) {
$this->migration = $this->getMigration($migration);
}
else {
$this->migration = $migration;
}
if ($this instanceof MigrateDumpAlterInterface) {
$this->migrateDumpAlter($this);
}
$this->prepareMigration($this->migration);
(new MigrateExecutable($this->migration, $this))->import();
}
/**
* Executes a set of migrations in dependency order.
*
* @param string[] $ids
* Array of migration IDs, in any order. If any of these migrations use a
* deriver, the derivatives will be made before execution.
*/
protected function executeMigrations(array $ids) {
$manager = $this->container->get('plugin.manager.migration');
$instances = $manager->createInstances($ids);
array_walk($instances, [$this, 'executeMigration']);
}
/**
* {@inheritdoc}
*/
public function display($message, $type = 'status') {
if ($this->collectMessages) {
$this->migrateMessages[$type][] = $message;
}
else {
$this->assertEquals('status', $type, $message);
}
}
/**
* Start collecting messages and erase previous messages.
*/
public function startCollectingMessages() {
$this->collectMessages = TRUE;
$this->migrateMessages = [];
}
/**
* Stop collecting messages.
*/
public function stopCollectingMessages() {
$this->collectMessages = FALSE;
}
/**
* Records a failure in the map table of a specific migration.
*
* This is done in order to test scenarios which require a failed row.
*
* @param string|\Drupal\migrate\Plugin\MigrationInterface $migration
* The migration entity, or its ID.
* @param array $row
* The raw source row which "failed".
* @param int $status
* (optional) The failure status. Should be one of the
* MigrateIdMapInterface::STATUS_* constants. Defaults to
* MigrateIdMapInterface::STATUS_FAILED.
*/
protected function mockFailure($migration, array $row, $status = MigrateIdMapInterface::STATUS_FAILED) {
if (is_string($migration)) {
$migration = $this->getMigration($migration);
}
/** @var \Drupal\migrate\Plugin\MigrationInterface $migration */
$destination = array_map(function () {
return NULL;
}, $migration->getDestinationPlugin()->getIds());
$row = new Row($row, $migration->getSourcePlugin()->getIds());
$migration->getIdMap()->saveIdMapping($row, $destination, $status);
}
/**
* Gets the migration plugin.
*
* @param string $plugin_id
* The plugin ID of the migration to get.
*
* @return \Drupal\migrate\Plugin\Migration
* The migration plugin.
*/
protected function getMigration($plugin_id) {
return $this->container->get('plugin.manager.migration')->createInstance($plugin_id);
}
/**
* Injects the test logger into the container.
*/
protected function setTestLogger() {
$this->logger = $this->prophesize(LoggerChannelInterface::class);
$this->container->set('logger.channel.migrate', $this->logger->reveal());
\Drupal::setContainer($this->container);
}
}

View File

@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
/**
* Tests the migration plugin manager.
*
* @group migrate
*
* @coversDefaultClass \Drupal\migrate\Plugin\MigrationPluginManager
*/
class MigrationPluginManagerTest extends MigrateTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate', 'migrate_tag_test'];
/**
* The migration plugin manager.
*
* @var \Drupal\migrate\Plugin\MigrationPluginManager
*/
protected $migrationPluginManager;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->migrationPluginManager = \Drupal::service('plugin.manager.migration');
}
/**
* Tests Migration::createInstancesByTag().
*
* @covers ::createInstancesByTag
*
* @dataProvider providerCreateInstanceByTag
*/
public function testCreateInstancesByTag($tags, $expected): void {
// The test module includes a migration that does not use the migration_tags
// property. It is there to confirm that it is not included in the results.
// We create it to ensure it is a valid migration.
$migration = $this->migrationPluginManager->createInstances(['tag_test_no_tag']);
$this->assertArrayHasKey('tag_test_no_tag', $migration);
$migrations = $this->migrationPluginManager->createInstancesByTag($tags);
$actual = array_keys($migrations);
$this->assertSame($expected, $actual);
}
/**
* Data provider for testCreateInstancesByTag.
*/
public static function providerCreateInstanceByTag() {
return [
'get test' => [
'test',
['tag_test_0', 'tag_test_1'],
],
'get tag_test_1' => [
'tag_test_1',
['tag_test_1'],
],
'get no tags' => [
'',
[],
],
];
}
}

View File

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests the migration plugin.
*
* @group migrate
*
* @coversDefaultClass \Drupal\migrate\Plugin\Migration
*/
class MigrationTest extends KernelTestBase {
/**
* Enable field because we are using one of its source plugins.
*
* @var array
*/
protected static $modules = ['migrate', 'field'];
/**
* Tests Migration::set().
*
* @covers ::set
*/
public function testSetInvalidation(): void {
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration([
'source' => ['plugin' => 'empty'],
'destination' => ['plugin' => 'entity:entity_view_mode'],
]);
$this->assertEquals('empty', $migration->getSourcePlugin()->getPluginId());
$this->assertEquals('entity:entity_view_mode', $migration->getDestinationPlugin()->getPluginId());
// Test the source plugin is invalidated.
$migration->set('source', ['plugin' => 'embedded_data', 'data_rows' => [], 'ids' => []]);
$this->assertEquals('embedded_data', $migration->getSourcePlugin()->getPluginId());
// Test the destination plugin is invalidated.
$migration->set('destination', ['plugin' => 'null']);
$this->assertEquals('null', $migration->getDestinationPlugin()->getPluginId());
}
}

View File

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel\Plugin;
use Drupal\KernelTests\KernelTestBase;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Row;
use Drupal\user\Entity\User;
/**
* Tests the EntityExists process plugin.
*
* @group migrate
*/
class EntityExistsTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate', 'system', 'user'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
}
/**
* Tests the EntityExists plugin.
*/
public function testEntityExists(): void {
$user = User::create([
'name' => $this->randomString(),
]);
$user->save();
$uid = $user->id();
$plugin = \Drupal::service('plugin.manager.migrate.process')
->createInstance('entity_exists', [
'entity_type' => 'user',
]);
$executable = $this->prophesize(MigrateExecutableInterface::class)->reveal();
$row = new Row();
// Ensure that the entity ID is returned if it really exists.
$value = $plugin->transform($uid, $executable, $row, 'buffalo');
$this->assertSame($uid, $value);
// Ensure that the plugin returns FALSE if the entity doesn't exist.
$value = $plugin->transform(420, $executable, $row, 'buffalo');
$this->assertFalse($value);
// Make sure the plugin can gracefully handle an array as input.
$value = $plugin->transform([$uid, 420], $executable, $row, 'buffalo');
$this->assertSame($uid, $value);
}
}

View File

@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel\Plugin;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\node\Entity\Node;
use Drupal\Tests\migrate\Kernel\MigrateTestBase;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
// cspell:ignore tabarnak
/**
* Tests the EntityRevision destination plugin.
*
* @group migrate
*/
class EntityRevisionTest extends MigrateTestBase {
use ContentTypeCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'content_translation',
'field',
'filter',
'language',
'node',
'system',
'text',
'user',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('node');
$this->installEntitySchema('user');
$this->installConfig('node');
$this->installSchema('node', ['node_access']);
}
/**
* Tests that EntityRevision correctly handles revision translations.
*/
public function testRevisionTranslation(): void {
ConfigurableLanguage::createFromLangcode('fr')->save();
/** @var \Drupal\node\NodeInterface $node */
$node = Node::create([
'type' => $this->createContentType()->id(),
'title' => 'Default 1',
]);
$node->addTranslation('fr', [
'title' => 'French 1',
]);
$node->save();
$node->setNewRevision();
$node->setTitle('Default 2');
$node->getTranslation('fr')->setTitle('French 2');
$node->save();
$migration = [
'source' => [
'plugin' => 'embedded_data',
'data_rows' => [
[
'nid' => $node->id(),
'vid' => $node->getRevisionId(),
'langcode' => 'fr',
'title' => 'Titre nouveau, tabarnak!',
],
],
'ids' => [
'nid' => [
'type' => 'integer',
],
'vid' => [
'type' => 'integer',
],
'langcode' => [
'type' => 'string',
],
],
],
'process' => [
'nid' => 'nid',
'vid' => 'vid',
'langcode' => 'langcode',
'title' => 'title',
],
'destination' => [
'plugin' => 'entity_revision:node',
'translations' => TRUE,
],
];
/** @var \Drupal\migrate\Plugin\MigrationInterface $migration */
$migration = $this->container
->get('plugin.manager.migration')
->createStubMigration($migration);
$this->executeMigration($migration);
// The entity_revision destination uses the revision ID and langcode as its
// keys (the langcode is only used if the destination is configured for
// translation), so we should be able to look up the source IDs by revision
// ID and langcode.
$source_ids = $migration->getIdMap()->lookupSourceID([
'vid' => $node->getRevisionId(),
'langcode' => 'fr',
]);
$this->assertNotEmpty($source_ids);
$this->assertSame($node->id(), $source_ids['nid']);
$this->assertSame($node->getRevisionId(), $source_ids['vid']);
$this->assertSame('fr', $source_ids['langcode']);
// Confirm the french revision was used in the migration, instead of the
// default revision.
/** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
$entity_type_manager = \Drupal::entityTypeManager();
$revision = $entity_type_manager->getStorage('node')->loadRevision(1);
$this->assertSame('Default 1', $revision->label());
$this->assertSame('French 1', $revision->getTranslation('fr')->label());
$revision = $entity_type_manager->getStorage('node')->loadRevision(2);
$this->assertSame('Default 2', $revision->label());
$this->assertSame('Titre nouveau, tabarnak!', $revision->getTranslation('fr')->label());
}
}

View File

@ -0,0 +1,211 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel\Plugin;
use Drupal\Core\Url;
use Drupal\KernelTests\KernelTestBase;
use Drupal\migrate\MigrateExecutable;
use Drupal\migrate\Row;
use Drupal\node\Entity\Node;
/**
* Tests the Log process plugin.
*
* @group migrate
*/
class LogTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['node', 'migrate'];
/**
* The Log process plugin.
*
* @var \Drupal\migrate\Plugin\migrate\process\Log
*/
protected $logPlugin;
/**
* Migrate executable.
*
* @var \Drupal\Tests\migrate\Kernel\Plugin\TestMigrateExecutable
*/
protected $executable;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$definition = [
'source' => [
'plugin' => 'embedded_data',
'data_rows' => [
['id' => '1'],
],
'ids' => [
'id' => ['type' => 'integer'],
],
],
'destination' => [
'plugin' => 'null',
],
];
/** @var \Drupal\migrate\Plugin\migration $migration */
$migration = \Drupal::service('plugin.manager.migration')
->createStubMigration($definition);
$this->executable = new TestMigrateExecutable($migration);
// Plugin being tested.
$this->logPlugin = \Drupal::service('plugin.manager.migrate.process')
->createInstance('log');
}
/**
* Tests the Log plugin.
*/
public function testLog(): void {
$values = [
'nid' => 2,
'type' => 'page',
'title' => 'page',
];
$node = new Node($values, 'node', 'test');
$node_array = <<< NODE
Array
(
[nid] => Array
(
)
[uuid] => Array
(
)
[vid] => Array
(
)
[langcode] => Array
(
)
[type] => Array
(
)
[revision_timestamp] => Array
(
)
[revision_uid] => Array
(
)
[revision_log] => Array
(
)
[status] => Array
(
)
[uid] => Array
(
)
[title] => Array
(
)
[created] => Array
(
)
[changed] => Array
(
)
[promote] => Array
(
)
[sticky] => Array
(
)
[default_langcode] => Array
(
)
[revision_default] => Array
(
)
[revision_translation_affected] => Array
(
)
)
NODE;
$data = [
'node' => [
'value' => $node,
'expected_message' => "'foo' value is Drupal\\node\Entity\Node:\n'$node_array'",
],
'url' => [
'value' => Url::fromUri('https://en.wikipedia.org/wiki/Drupal#Community'),
'expected_message' => "'foo' value is Drupal\Core\Url:\n'https://en.wikipedia.org/wiki/Drupal#Community'",
],
];
$i = 1;
foreach ($data as $datum) {
$this->executable->sourceIdValues = ['id' => $i++];
// Test the input value is not altered.
$new_value = $this->logPlugin->transform($datum['value'], $this->executable, new Row(), 'foo');
$this->assertSame($datum['value'], $new_value);
// Test the stored message.
$message = $this->executable->getIdMap()
->getMessages($this->executable->sourceIdValues)
->fetchAllAssoc('message');
$actual_message = key($message);
$this->assertSame($datum['expected_message'], $actual_message);
}
}
}
/**
* MigrateExecutable test class.
*/
class TestMigrateExecutable extends MigrateExecutable {
/**
* The configuration values of the source.
*
* @var array
*/
public $sourceIdValues;
/**
* Get the ID map from the current migration.
*
* @return \Drupal\migrate\Plugin\MigrateIdMapInterface
* The ID map.
*/
public function getIdMap() {
return parent::getIdMap();
}
}

View File

@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel\Plugin;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests the migration plugin manager.
*
* @coversDefaultClass \Drupal\migrate\Plugin\MigratePluginManager
* @group migrate
*/
class MigrationPluginConfigurationTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'migrate',
'migrate_drupal',
// Test with a simple migration.
'migrate_plugin_config_test',
'locale',
];
/**
* Tests merging configuration into a plugin through the plugin manager.
*
* @dataProvider mergeProvider
*/
public function testConfigurationMerge($id, $configuration, $expected): void {
/** @var \Drupal\migrate\Plugin\MigrationInterface $migration */
$migration = $this->container->get('plugin.manager.migration')
->createInstance($id, $configuration);
$source_configuration = $migration->getSourceConfiguration();
$this->assertEquals($expected, $source_configuration);
}
/**
* Provide configuration data for testing.
*/
public static function mergeProvider() {
return [
// Tests adding new configuration to a migration.
[
// New configuration.
'simple_migration',
[
'source' => [
'constants' => [
'added_setting' => 'Ban them all!',
],
],
],
// Expected final source configuration.
[
'plugin' => 'simple_source',
'constants' => [
'added_setting' => 'Ban them all!',
],
],
],
// Tests overriding pre-existing configuration in a migration.
[
// New configuration.
'simple_migration',
[
'source' => [
'plugin' => 'a_different_plugin',
],
],
// Expected final source configuration.
[
'plugin' => 'a_different_plugin',
],
],
// New configuration.
[
'locale_settings',
[
'source' => [
'plugin' => 'variable',
'variables' => [
'locale_cache_strings',
'locale_js_directory',
],
'source_module' => 'locale',
],
],
// Expected final source and process configuration.
[
'plugin' => 'variable',
'variables' => [
'locale_cache_strings',
'locale_js_directory',
],
'source_module' => 'locale',
],
],
];
}
}

View File

@ -0,0 +1,161 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel\Plugin;
use Drupal\Core\Database\Database;
use Drupal\KernelTests\KernelTestBase;
use Drupal\migrate\Exception\RequirementsException;
use Drupal\migrate\Plugin\migrate\source\SqlBase;
use Drupal\migrate\Plugin\RequirementsInterface;
use Drupal\Tests\field\Traits\EntityReferenceFieldCreationTrait;
/**
* Tests the migration plugin manager.
*
* @coversDefaultClass \Drupal\migrate\Plugin\MigratePluginManager
* @group migrate
*/
class MigrationPluginListTest extends KernelTestBase {
use EntityReferenceFieldCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'migrate',
// Test with all modules containing Drupal migrations.
// @todo Remove Ban in https://www.drupal.org/project/drupal/issues/3488827
'ban',
'block',
'block_content',
'comment',
'contact',
'content_translation',
'dblog',
'field',
'file',
'filter',
'image',
'language',
'locale',
'menu_link_content',
'menu_ui',
'node',
'options',
'path',
'search',
'shortcut',
'syslog',
'system',
'taxonomy',
'text',
'update',
'user',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
}
/**
* @covers ::getDefinitions
*/
public function testGetDefinitions(): void {
// Create an entity reference field to make sure that migrations derived by
// EntityReferenceTranslationDeriver do not get discovered without
// migrate_drupal enabled.
$this->createEntityReferenceField('user', 'user', 'field_entity_reference', 'Entity Reference', 'node');
// Make sure retrieving all the core migration plugins does not throw any
// errors.
$migration_plugins = $this->container->get('plugin.manager.migration')->getDefinitions();
// All the plugins provided by core depend on migrate_drupal.
$this->assertEmpty($migration_plugins);
// Enable a module that provides migrations that do not depend on
// migrate_drupal.
$this->enableModules(['migrate_external_translated_test']);
$migration_plugins = $this->container->get('plugin.manager.migration')->getDefinitions();
// All the plugins provided by migrate_external_translated_test do not
// depend on migrate_drupal.
$this::assertArrayHasKey('external_translated_test_node', $migration_plugins);
$this::assertArrayHasKey('external_translated_test_node_translation', $migration_plugins);
// Disable the test module and the list should be empty again.
$this->disableModules(['migrate_external_translated_test']);
$migration_plugins = $this->container->get('plugin.manager.migration')->getDefinitions();
// All the plugins provided by core depend on migrate_drupal.
$this->assertEmpty($migration_plugins);
// Enable migrate_drupal to test that the plugins can now be discovered.
$this->enableModules(['migrate_drupal']);
$this->installConfig(['migrate_drupal']);
// Make sure retrieving these migration plugins in the absence of a database
// connection does not throw any errors.
$migration_plugins = $this->container->get('plugin.manager.migration')->createInstances([]);
// Any database-based source plugins should fail a requirements test in the
// absence of a source database connection (e.g., a connection with the
// 'migrate' key).
$source_plugins = array_map(function ($migration_plugin) {
return $migration_plugin->getSourcePlugin();
}, $migration_plugins);
foreach ($source_plugins as $id => $source_plugin) {
if ($source_plugin instanceof RequirementsInterface) {
try {
$source_plugin->checkRequirements();
}
catch (RequirementsException) {
unset($source_plugins[$id]);
}
}
}
// Without a connection defined, no database-based plugins should be
// returned.
foreach ($source_plugins as $id => $source_plugin) {
$this->assertNotInstanceOf(SqlBase::class, $source_plugin);
}
// Set up a migrate database connection so that plugin discovery works.
// Clone the current connection and replace the current prefix.
$connection_info = Database::getConnectionInfo('migrate');
if ($connection_info) {
Database::renameConnection('migrate', 'simpletest_original_migrate');
}
$connection_info = Database::getConnectionInfo('default');
foreach ($connection_info as $target => $value) {
$prefix = $value['prefix'];
// Tests use 7 character prefixes at most so this can't cause collisions.
$connection_info[$target]['prefix'] = $prefix . '0';
}
Database::addConnectionInfo('migrate', 'default', $connection_info['default']);
// Make sure source plugins can be serialized.
foreach ($migration_plugins as $migration_plugin) {
$source_plugin = $migration_plugin->getSourcePlugin();
if ($source_plugin instanceof SqlBase) {
$source_plugin->getDatabase();
}
$this->assertNotEmpty(serialize($source_plugin));
}
$migration_plugins = $this->container->get('plugin.manager.migration')->getDefinitions();
// All the plugins provided by core depend on migrate_drupal.
$this->assertNotEmpty($migration_plugins);
// Test that migrations derived by EntityReferenceTranslationDeriver are
// discovered now that migrate_drupal is enabled.
$this->assertArrayHasKey('d6_entity_reference_translation:user__user', $migration_plugins);
$this->assertArrayHasKey('d7_entity_reference_translation:user__user', $migration_plugins);
}
}

View File

@ -0,0 +1,162 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel\Plugin;
use Drupal\KernelTests\KernelTestBase;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateSkipRowException;
/**
* Tests the migration plugin.
*
* @coversDefaultClass \Drupal\migrate\Plugin\Migration
* @group migrate
*/
class MigrationTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate', 'migrate_expected_migrations_test'];
/**
* Tests Migration::getProcessPlugins()
*
* @covers ::getProcessPlugins
*/
public function testGetProcessPlugins(): void {
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration([]);
$this->assertEquals([], $migration->getProcessPlugins([]));
}
/**
* Tests Migration::getProcessPlugins() throws an exception.
*
* @covers ::getProcessPlugins
*/
public function testGetProcessPluginsException(): void {
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration([]);
$this->expectException(MigrateException::class);
$this->expectExceptionMessage('Invalid process configuration for foobar');
$migration->getProcessPlugins(['foobar' => ['plugin' => 'get']]);
}
/**
* Tests Migration::getProcessPlugins()
*
* @param array $process
* The migration process pipeline.
*
* @covers ::getProcessPlugins
*
* @dataProvider getProcessPluginsExceptionMessageProvider
*/
public function testGetProcessPluginsExceptionMessage(array $process): void {
// Test with an invalid process pipeline.
$plugin_definition = [
'id' => 'foo',
'process' => $process,
];
$destination = array_key_first(($process));
$migration = \Drupal::service('plugin.manager.migration')
->createStubMigration($plugin_definition);
$this->expectException(MigrateException::class);
$this->expectExceptionMessage("Invalid process for destination '$destination' in migration 'foo'");
$migration->getProcessPlugins();
}
/**
* Provides data for testing invalid process pipeline.
*/
public static function getProcessPluginsExceptionMessageProvider(): \Generator {
yield 'null' => ['process' => ['dest' => NULL]];
yield 'boolean' => ['process' => ['dest' => TRUE]];
yield 'integer' => ['process' => ['dest' => 2370]];
yield 'float' => ['process' => ['dest' => 1.61]];
}
/**
* Tests Migration::getMigrationDependencies()
*
* @covers ::getMigrationDependencies
*/
public function testGetMigrationDependencies(): void {
$plugin_manager = \Drupal::service('plugin.manager.migration');
$plugin_definition = [
'id' => 'foo',
'deriver' => 'fooDeriver',
'process' => [
'f1' => 'bar',
'f2' => [
'plugin' => 'migration',
'migration' => 'm1',
],
'f3' => [
'plugin' => 'sub_process',
'process' => [
'target_id' => [
'plugin' => 'migration',
'migration' => 'm2',
],
],
],
'f4' => [
'plugin' => 'migration_lookup',
'migration' => 'm3',
],
'f5' => [
'plugin' => 'sub_process',
'process' => [
'target_id' => [
'plugin' => 'migration_lookup',
'migration' => 'm4',
],
],
],
'f6' => [
'plugin' => 'iterator',
'process' => [
'target_id' => [
'plugin' => 'migration_lookup',
'migration' => 'm5',
],
],
],
'f7' => [
'plugin' => 'migration_lookup',
'migration' => 'foo',
],
],
];
$migration = $plugin_manager->createStubMigration($plugin_definition);
$this->assertSame(['required' => [], 'optional' => ['m1', 'm2', 'm3', 'm4', 'm5']], $migration->getMigrationDependencies());
}
/**
* Tests Migration::getDestinationIds()
*
* @covers ::getDestinationIds
*/
public function testGetDestinationIds(): void {
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration(['destinationIds' => ['foo' => 'bar']]);
$destination_ids = $migration->getDestinationIds();
$this->assertNotEmpty($destination_ids, 'Destination ids are not empty');
$this->assertEquals(['foo' => 'bar'], $destination_ids, 'Destination ids match the expected values.');
}
/**
* Tests Migration::getDestinationPlugin()
*
* @covers ::getDestinationPlugin
*/
public function testGetDestinationPlugin(): void {
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration(['destination' => ['no_stub' => TRUE]]);
$this->expectException(MigrateSkipRowException::class);
$this->expectExceptionMessage("Stub requested but not made because no_stub configuration is set.");
$migration->getDestinationPlugin(TRUE);
}
}

View File

@ -0,0 +1,206 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel\Plugin\id_map;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\Exception\SchemaTableColumnSizeTooLargeException;
use Drupal\Tests\migrate\Kernel\MigrateTestBase;
use Drupal\Tests\migrate\Unit\TestSqlIdMap;
use Drupal\migrate\MigrateException;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* Tests that the migrate map table is created.
*
* @group migrate
*/
class SqlTest extends MigrateTestBase {
/**
* Database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* A mock event dispatcher.
*
* @var object|\Prophecy\Prophecy\ProphecySubjectInterface|\Symfony\Contracts\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* Definition of a test migration.
*
* @var array
*/
protected $migrationDefinition;
/**
* The migration plugin manager.
*
* @var \Drupal\migrate\Plugin\MigrationPluginManager
*/
protected $migrationPluginManager;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->database = \Drupal::database();
$this->eventDispatcher = $this->prophesize(EventDispatcherInterface::class)
->reveal();
$this->migrationPluginManager = \Drupal::service('plugin.manager.migration');
$this->migrationDefinition = [
'id' => 'test',
'source' => [
'plugin' => 'embedded_data',
'data_rows' => [
[
'alpha' => '1',
'bravo' => '2',
'charlie' => '3',
'delta' => '4',
'echo' => '5',
],
],
'ids' => [],
],
'process' => [],
'destination' => [
'plugin' => 'null',
],
];
}
/**
* Tests that ensureTables creates the migrate map table.
*
* @dataProvider providerTestEnsureTables
*/
public function testEnsureTables($ids): void {
$this->migrationDefinition['source']['ids'] = $ids;
$migration = $this->migrationPluginManager->createStubMigration($this->migrationDefinition);
$map = new TestSqlIdMap($this->database, [], 'test', [], $migration, $this->eventDispatcher, $this->migrationPluginManager);
$map->ensureTables();
// Checks that the map table was created.
$exists = $this->database->schema()->tableExists('migrate_map_test');
$this->assertTrue($exists);
}
/**
* Provides data for testEnsureTables.
*/
public static function providerTestEnsureTables() {
return [
'no ids' => [
[],
],
'one id' => [
[
'alpha' => [
'type' => 'string',
],
],
],
'too many' => [
[
'alpha' => [
'type' => 'string',
],
'bravo' => [
'type' => 'string',
],
'charlie' => [
'type' => 'string',
],
'delta' => [
'type' => 'string',
],
'echo ' => [
'type' => 'string',
],
],
],
];
}
/**
* Tests exception is thrown in ensureTables fails.
*
* @dataProvider providerTestFailEnsureTables
*/
public function testFailEnsureTables($ids): void {
// This just tests mysql, as other PDO integrations allow longer indexes.
if (Database::getConnection()->databaseType() !== 'mysql') {
$this->markTestSkipped("This test only runs for MySQL");
}
$this->migrationDefinition['source']['ids'] = $ids;
$migration = $this->container
->get('plugin.manager.migration')
->createStubMigration($this->migrationDefinition);
// Use local id map plugin to force an error.
$map = new SqlIdMapTest($this->database, [], 'test', [], $migration, $this->eventDispatcher, $this->migrationPluginManager);
$this->expectException(SchemaTableColumnSizeTooLargeException::class);
$map->ensureTables();
}
/**
* Provides data for testFailEnsureTables.
*/
public static function providerTestFailEnsureTables() {
return [
'one id' => [
[
'alpha' => [
'type' => 'string',
],
],
],
];
}
}
/**
* Defines a test SQL ID map for use in tests.
*/
class SqlIdMapTest extends TestSqlIdMap implements \Iterator {
/**
* {@inheritdoc}
*/
protected function getFieldSchema(array $id_definition) {
if (!isset($id_definition['type'])) {
return [];
}
switch ($id_definition['type']) {
case 'integer':
return [
'type' => 'int',
'not null' => TRUE,
];
case 'string':
return [
'type' => 'varchar',
'length' => 65536,
'not null' => FALSE,
];
default:
throw new MigrateException($id_definition['type'] . ' not supported');
}
}
}

View File

@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel\Plugin\source;
use Drupal\Tests\migrate\Kernel\MigrateSqlSourceTestBase;
/**
* Tests the config source plugin.
*
* @covers \Drupal\migrate\Plugin\migrate\source\ConfigEntity
* @group migrate
*/
class ConfigEntityTest extends MigrateSqlSourceTestBase {
/**
* {@inheritdoc}
*/
public static function providerSource() {
$data = [];
// The source database tables.
$data[0]['source_data'] = [
'config' => [
[
'collection' => 'language.af',
'name' => 'user.settings',
'data' => 'a:1:{s:9:"anonymous";s:14:"af - Anonymous";}',
],
[
'collection' => '',
'name' => 'user.settings',
'data' => 'a:1:{s:9:"anonymous";s:9:"Anonymous";}',
],
[
'collection' => 'language.de',
'name' => 'user.settings',
'data' => 'a:1:{s:9:"anonymous";s:14:"de - Anonymous";}',
],
[
'collection' => 'language.af',
'name' => 'bar',
'data' => 'b:0;',
],
],
];
// The expected results.
$data[0]['expected_data'] = [
[
'collection' => 'language.af',
'name' => 'user.settings',
'data' => [
'anonymous' => 'af - Anonymous',
],
],
[
'collection' => 'language.af',
'name' => 'bar',
'data' => FALSE,
],
];
$data[0]['expected_count'] = NULL;
$data[0]['configuration'] = [
'names' => [
'user.settings',
'bar',
],
'collections' => [
'language.af',
],
];
// Test with name and no collection in configuration.
$data[1]['source_data'] = $data[0]['source_data'];
$data[1]['expected_data'] = [
[
'collection' => 'language.af',
'name' => 'bar',
'data' => FALSE,
],
];
$data[1]['expected_count'] = NULL;
$data[1]['configuration'] = [
'names' => [
'bar',
],
];
// Test with collection and no name in configuration.
$data[2]['source_data'] = $data[0]['source_data'];
$data[2]['expected_data'] = [
[
'collection' => 'language.de',
'name' => 'user.settings',
'data' => [
'anonymous' => 'de - Anonymous',
],
],
];
$data[2]['expected_count'] = NULL;
$data[2]['configuration'] = [
'collections' => [
'language.de',
],
];
return $data;
}
}

View File

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel\Plugin\source;
use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
use Drupal\KernelTests\KernelTestBase;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Plugin\migrate\source\ContentEntity;
/**
* Tests the constructor of the entity content source plugin.
*
* @group migrate
*/
class ContentEntityConstructorTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'migrate',
'node',
'system',
'user',
];
/**
* Tests the constructor.
*
* @dataProvider providerTestConstructor
*/
public function testConstructor(array $configuration, array $plugin_definition, string $exception_class, string $expected): void {
$migration = $this->prophesize(MigrationInterface::class)->reveal();
$this->expectException($exception_class);
$this->expectExceptionMessage($expected);
ContentEntity::create($this->container, $configuration, 'content_entity', $plugin_definition, $migration);
}
/**
* Provides data for constructor tests.
*/
public static function providerTestConstructor(): array {
return [
'entity type missing' => [
[],
['entity_type' => ''],
InvalidPluginDefinitionException::class,
'Missing required "entity_type" definition.',
],
'non content entity' => [
[],
['entity_type' => 'node_type'],
InvalidPluginDefinitionException::class,
'The entity type (node_type) is not supported. The "content_entity" source plugin only supports content entities.',
],
'not bundleable' => [
['bundle' => 'foo'],
['entity_type' => 'user'],
\InvalidArgumentException::class,
'A bundle was provided but the entity type (user) is not bundleable.',
],
'invalid bundle' => [
['bundle' => 'foo'],
['entity_type' => 'node'],
\InvalidArgumentException::class,
'The provided bundle (foo) is not valid for the (node) entity type.',
],
];
}
}

View File

@ -0,0 +1,474 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel\Plugin\source;
use Drupal\Component\Plugin\PluginBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\file\Entity\File;
use Drupal\KernelTests\KernelTestBase;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\media\Entity\Media;
use Drupal\migrate\Plugin\MigrateSourceInterface;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\taxonomy\Entity\Term;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\Tests\field\Traits\EntityReferenceFieldCreationTrait;
use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
use Drupal\user\Entity\User;
/**
* Tests the entity content source plugin.
*
* @group migrate
* @group #slow
*/
class ContentEntityTest extends KernelTestBase {
use EntityReferenceFieldCreationTrait;
use MediaTypeCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'user',
'migrate',
'system',
'node',
'taxonomy',
'field',
'file',
'image',
'media',
'media_test_source',
'text',
'filter',
'language',
'content_translation',
];
/**
* The bundle used in this test.
*
* @var string
*/
protected $bundle = 'article';
/**
* The name of the field used in this test.
*
* @var string
*/
protected $fieldName = 'field_entity_reference';
/**
* The vocabulary ID.
*
* @var string
*/
protected $vocabulary = 'fruit';
/**
* The test user.
*
* @var \Drupal\user\Entity\User
*/
protected $user;
/**
* The migration plugin manager.
*
* @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface
*/
protected $migrationPluginManager;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('node');
$this->installEntitySchema('file');
$this->installEntitySchema('media');
$this->installEntitySchema('taxonomy_term');
$this->installEntitySchema('user');
$this->installSchema('user', 'users_data');
$this->installSchema('file', 'file_usage');
$this->installSchema('node', ['node_access']);
$this->installConfig(static::$modules);
ConfigurableLanguage::createFromLangcode('fr')->save();
// Create article content type.
$node_type = NodeType::create(['type' => $this->bundle, 'name' => 'Article']);
$node_type->save();
// Create a vocabulary.
$vocabulary = Vocabulary::create([
'name' => $this->vocabulary,
'description' => $this->vocabulary,
'vid' => $this->vocabulary,
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
]);
$vocabulary->save();
// Create a term reference field on node.
$this->createEntityReferenceField(
'node',
$this->bundle,
$this->fieldName,
'Term reference',
'taxonomy_term',
'default',
['target_bundles' => [$this->vocabulary]],
FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
);
// Create a term reference field on user.
$this->createEntityReferenceField(
'user',
'user',
$this->fieldName,
'Term reference',
'taxonomy_term',
'default',
['target_bundles' => [$this->vocabulary]],
FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
);
// Create a node, with data in a term reference field, and then add a French
// translation of the node.
$this->user = User::create([
'name' => 'user123',
'uid' => 1,
'mail' => 'example@example.com',
]);
$this->user->save();
// Add the anonymous user so we can test later that it is not provided in a
// source row.
User::create([
'name' => 'anon',
'uid' => 0,
])->save();
$term = Term::create([
'vid' => $this->vocabulary,
'name' => 'Apples',
'uid' => $this->user->id(),
]);
$term->save();
$this->user->set($this->fieldName, $term->id());
$this->user->save();
$node = Node::create([
'type' => $this->bundle,
'title' => 'Apples',
$this->fieldName => $term->id(),
'uid' => $this->user->id(),
]);
$node->save();
$node->addTranslation('fr', [
'title' => 'fr - Apples',
$this->fieldName => $term->id(),
])->save();
$this->migrationPluginManager = $this->container->get('plugin.manager.migration');
}
/**
* Helper to assert IDs structure.
*
* @param \Drupal\migrate\Plugin\MigrateSourceInterface $source
* The source plugin.
* @param array $configuration
* The source plugin configuration (Nope, no getter available).
*
* @internal
*/
protected function assertIds(MigrateSourceInterface $source, array $configuration): void {
$ids = $source->getIds();
[, $entity_type_id] = explode(PluginBase::DERIVATIVE_SEPARATOR, $source->getPluginId());
$entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id);
$this->assertArrayHasKey($entity_type->getKey('id'), $ids);
$ids_count_expected = 1;
if ($entity_type->isTranslatable()) {
$ids_count_expected++;
$this->assertArrayHasKey($entity_type->getKey('langcode'), $ids);
}
if ($entity_type->isRevisionable() && $configuration['add_revision_id']) {
$ids_count_expected++;
$this->assertArrayHasKey($entity_type->getKey('revision'), $ids);
}
$this->assertCount($ids_count_expected, $ids);
}
/**
* Tests user source plugin.
*
* @dataProvider migrationConfigurationProvider
*/
public function testUserSource(array $configuration): void {
$migration = $this->migrationPluginManager
->createStubMigration($this->migrationDefinition('content_entity:user', $configuration));
$user_source = $migration->getSourcePlugin();
$this->assertSame('users', $user_source->__toString());
if (!$configuration['include_translations']) {
// Confirm that the anonymous user is in the source database but not
// included in the rows returned by the content_entity.
$this->assertNotNull(User::load(0));
$this->assertEquals(1, $user_source->count());
}
$this->assertIds($user_source, $configuration);
$fields = $user_source->fields();
$this->assertArrayHasKey('name', $fields);
$this->assertArrayHasKey('pass', $fields);
$this->assertArrayHasKey('mail', $fields);
$this->assertArrayHasKey('uid', $fields);
$this->assertArrayHasKey('roles', $fields);
$user_source->rewind();
$values = $user_source->current()->getSource();
$this->assertEquals('example@example.com', $values['mail'][0]['value']);
$this->assertEquals('user123', $values['name'][0]['value']);
$this->assertEquals(1, $values['uid']);
$this->assertEquals(1, $values['field_entity_reference'][0]['target_id']);
}
/**
* Tests file source plugin.
*
* @dataProvider migrationConfigurationProvider
*/
public function testFileSource(array $configuration): void {
$file = File::create([
'filename' => 'foo.txt',
'uid' => $this->user->id(),
'uri' => 'public://foo.txt',
]);
$file->save();
$migration = $this->migrationPluginManager
->createStubMigration($this->migrationDefinition('content_entity:file', $configuration));
$file_source = $migration->getSourcePlugin();
$this->assertSame('files', $file_source->__toString());
if (!$configuration['include_translations']) {
$this->assertEquals(1, $file_source->count());
}
$this->assertIds($file_source, $configuration);
$fields = $file_source->fields();
$this->assertArrayHasKey('fid', $fields);
$this->assertArrayHasKey('filemime', $fields);
$this->assertArrayHasKey('filename', $fields);
$this->assertArrayHasKey('uid', $fields);
$this->assertArrayHasKey('uri', $fields);
$file_source->rewind();
$values = $file_source->current()->getSource();
$this->assertEquals('text/plain', $values['filemime'][0]['value']);
$this->assertEquals('public://foo.txt', $values['uri'][0]['value']);
$this->assertEquals('foo.txt', $values['filename'][0]['value']);
$this->assertEquals(1, $values['fid']);
}
/**
* Tests node source plugin.
*
* @dataProvider migrationConfigurationProvider
*/
public function testNodeSource(array $configuration): void {
$configuration += ['bundle' => $this->bundle];
$migration = $this->migrationPluginManager
->createStubMigration($this->migrationDefinition('content_entity:node', $configuration));
$node_source = $migration->getSourcePlugin();
$this->assertSame('content items', $node_source->__toString());
$this->assertIds($node_source, $configuration);
$fields = $node_source->fields();
$this->assertArrayHasKey('nid', $fields);
$this->assertArrayHasKey('vid', $fields);
$this->assertArrayHasKey('title', $fields);
$this->assertArrayHasKey('uid', $fields);
$this->assertArrayHasKey('sticky', $fields);
$node_source->rewind();
$values = $node_source->current()->getSource();
$this->assertEquals($this->bundle, $values['type'][0]['target_id']);
$this->assertEquals(1, $values['nid']);
if ($configuration['add_revision_id']) {
$this->assertEquals(1, $values['vid']);
}
else {
$this->assertEquals([['value' => '1']], $values['vid']);
}
$this->assertEquals('en', $values['langcode']);
$this->assertEquals(1, $values['status'][0]['value']);
$this->assertEquals('Apples', $values['title'][0]['value']);
$this->assertEquals(1, $values['default_langcode'][0]['value']);
$this->assertEquals(1, $values['field_entity_reference'][0]['target_id']);
if ($configuration['include_translations']) {
$node_source->next();
$values = $node_source->current()->getSource();
$this->assertEquals($this->bundle, $values['type'][0]['target_id']);
$this->assertEquals(1, $values['nid']);
if ($configuration['add_revision_id']) {
$this->assertEquals(1, $values['vid']);
}
else {
$this->assertEquals([0 => ['value' => 1]], $values['vid']);
}
$this->assertEquals('fr', $values['langcode']);
$this->assertEquals(1, $values['status'][0]['value']);
$this->assertEquals('fr - Apples', $values['title'][0]['value']);
$this->assertEquals(0, $values['default_langcode'][0]['value']);
$this->assertEquals(1, $values['field_entity_reference'][0]['target_id']);
}
}
/**
* Tests media source plugin.
*
* @dataProvider migrationConfigurationProvider
*/
public function testMediaSource(array $configuration): void {
$values = [
'id' => 'image',
'label' => 'Image',
'source' => 'test',
'new_revision' => FALSE,
];
$media_type = $this->createMediaType('test', $values);
$media = Media::create([
'name' => 'Foo media',
'uid' => $this->user->id(),
'bundle' => $media_type->id(),
]);
$media->save();
$configuration += [
'bundle' => 'image',
];
$migration = $this->migrationPluginManager
->createStubMigration($this->migrationDefinition('content_entity:media', $configuration));
$media_source = $migration->getSourcePlugin();
$this->assertSame('media items', $media_source->__toString());
if (!$configuration['include_translations']) {
$this->assertEquals(1, $media_source->count());
}
$this->assertIds($media_source, $configuration);
$fields = $media_source->fields();
$this->assertArrayHasKey('bundle', $fields);
$this->assertArrayHasKey('mid', $fields);
$this->assertArrayHasKey('vid', $fields);
$this->assertArrayHasKey('name', $fields);
$this->assertArrayHasKey('status', $fields);
$media_source->rewind();
$values = $media_source->current()->getSource();
$this->assertEquals(1, $values['mid']);
if ($configuration['add_revision_id']) {
$this->assertEquals(1, $values['vid']);
}
else {
$this->assertEquals([['value' => 1]], $values['vid']);
}
$this->assertEquals('Foo media', $values['name'][0]['value']);
$this->assertNull($values['thumbnail'][0]['title']);
$this->assertEquals(1, $values['uid'][0]['target_id']);
$this->assertEquals('image', $values['bundle'][0]['target_id']);
}
/**
* Tests term source plugin.
*
* @dataProvider migrationConfigurationProvider
*/
public function testTermSource(array $configuration): void {
$term2 = Term::create([
'vid' => $this->vocabulary,
'name' => 'Granny Smith',
'uid' => $this->user->id(),
'parent' => 1,
]);
$term2->save();
$configuration += [
'bundle' => $this->vocabulary,
];
$migration = $this->migrationPluginManager
->createStubMigration($this->migrationDefinition('content_entity:taxonomy_term', $configuration));
$term_source = $migration->getSourcePlugin();
$this->assertSame('taxonomy terms', $term_source->__toString());
if (!$configuration['include_translations']) {
$this->assertEquals(2, $term_source->count());
}
$this->assertIds($term_source, $configuration);
$fields = $term_source->fields();
$this->assertArrayHasKey('vid', $fields);
$this->assertArrayHasKey('revision_id', $fields);
$this->assertArrayHasKey('tid', $fields);
$this->assertArrayHasKey('name', $fields);
$term_source->rewind();
$values = $term_source->current()->getSource();
$this->assertEquals($this->vocabulary, $values['vid'][0]['target_id']);
$this->assertEquals(1, $values['tid']);
$this->assertEquals('Apples', $values['name'][0]['value']);
$this->assertSame([['target_id' => '0']], $values['parent']);
$term_source->next();
$values = $term_source->current()->getSource();
$this->assertEquals($this->vocabulary, $values['vid'][0]['target_id']);
$this->assertEquals(2, $values['tid']);
$this->assertEquals('Granny Smith', $values['name'][0]['value']);
$this->assertSame([['target_id' => '1']], $values['parent']);
}
/**
* Data provider for several test methods.
*
* @see \Drupal\Tests\migrate\Kernel\Plugin\source\ContentEntityTest::testUserSource
* @see \Drupal\Tests\migrate\Kernel\Plugin\source\ContentEntityTest::testFileSource
* @see \Drupal\Tests\migrate\Kernel\Plugin\source\ContentEntityTest::testNodeSource
* @see \Drupal\Tests\migrate\Kernel\Plugin\source\ContentEntityTest::testMediaSource
* @see \Drupal\Tests\migrate\Kernel\Plugin\source\ContentEntityTest::testTermSource
*/
public static function migrationConfigurationProvider(): array {
$data = [];
foreach ([FALSE, TRUE] as $include_translations) {
foreach ([FALSE, TRUE] as $add_revision_id) {
$configuration = [
'include_translations' => $include_translations,
'add_revision_id' => $add_revision_id,
];
// Add an array key for this data set.
$data[http_build_query($configuration)] = [$configuration];
}
}
return $data;
}
/**
* Get a migration definition.
*
* @param string $plugin_id
* The plugin id.
* @param array $configuration
* The plugin configuration.
*
* @return array
* The definition.
*/
protected function migrationDefinition(string $plugin_id, array $configuration = []): array {
return [
'source' => [
'plugin' => $plugin_id,
] + $configuration,
'process' => [],
'destination' => [
'plugin' => 'null',
],
];
}
}

View File

@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel\Plugin\source;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests discovery of source plugins with annotations.
*
* Migrate source plugins use a specific discovery class to accommodate multiple
* providers. This tests that the backwards compatibility of discovery for
* plugin classes using annotations still works, even after all core plugins
* have been converted to attributes.
*
* @group migrate
*/
class MigrateSourceDiscoveryTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate'];
/**
* @covers \Drupal\migrate\Plugin\MigrateSourcePluginManager::getDefinitions
*/
public function testGetDefinitions(): void {
// First, check the expected plugins are provided by migrate only.
$expected = ['config_entity', 'embedded_data', 'empty'];
$source_plugins = \Drupal::service('plugin.manager.migrate.source')->getDefinitions();
ksort($source_plugins);
$this->assertSame($expected, array_keys($source_plugins));
// Next, install the file module, which has 4 migrate source plugins, all of
// which depend on migrate_drupal. Since migrate_drupal is not installed,
// none of the source plugins from file should be discovered. However, the
// content_entity source for the file entity type should be discovered.
$expected = ['config_entity', 'content_entity:file', 'embedded_data', 'empty'];
$this->enableModules(['file']);
$source_plugins = \Drupal::service('plugin.manager.migrate.source')->getDefinitions();
ksort($source_plugins);
$this->assertSame($expected, array_keys($source_plugins));
// Install migrate_drupal and now the source plugins from the file modules
// should be found.
$expected = [
'config_entity',
'd6_file',
'd6_upload',
'd6_upload_instance',
'd7_file',
'embedded_data',
'empty',
];
$this->enableModules(['migrate_drupal']);
$source_plugins = \Drupal::service('plugin.manager.migrate.source')->getDefinitions();
$this->assertSame(array_diff($expected, array_keys($source_plugins)), []);
}
/**
* @covers \Drupal\migrate\Plugin\MigrateSourcePluginManager::getDefinitions
*/
public function testAnnotationGetDefinitionsBackwardsCompatibility(): void {
// First, test attribute-only discovery.
$expected = ['config_entity', 'embedded_data', 'empty'];
$source_plugins = \Drupal::service('plugin.manager.migrate.source')->getDefinitions();
ksort($source_plugins);
$this->assertSame($expected, array_keys($source_plugins));
// Next, test discovery of both attributed and annotated plugins. The
// annotated plugin with multiple providers depends on migrate_drupal and
// should not be discovered with it uninstalled.
$expected = ['annotated', 'config_entity', 'embedded_data', 'empty'];
$this->enableModules(['migrate_source_annotation_bc_test']);
$source_plugins = \Drupal::service('plugin.manager.migrate.source')->getDefinitions();
ksort($source_plugins);
$this->assertSame($expected, array_keys($source_plugins));
// Install migrate_drupal and now the annotated plugin that depends on it
// should be discovered.
$expected = [
'annotated',
'annotated_multiple_providers',
'config_entity',
'embedded_data',
'empty',
];
$this->enableModules(['migrate_drupal']);
$source_plugins = \Drupal::service('plugin.manager.migrate.source')->getDefinitions();
// Confirming here the that the source plugins that migrate and
// migrate_source_annotation_bc_test are discovered. There are additional
// plugins provided by migrate_drupal, but they do not need to be enumerated
// here.
$this->assertSame(array_diff($expected, array_keys($source_plugins)), []);
}
}

View File

@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel\Plugin\source;
use Drupal\Tests\migrate\Kernel\MigrateSqlSourceTestBase;
/**
* Tests SqlBase source count caching.
*
* @covers \Drupal\migrate_sql_count_cache_test\Plugin\migrate\source\SqlCountCache
* @covers \Drupal\migrate\Plugin\migrate\source\SqlBase::doCount
* @covers \Drupal\migrate\Plugin\migrate\source\SourcePluginBase::count
*
* @group migrate
*/
class MigrateSqlSourceCountCacheTest extends MigrateSqlSourceTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate_sql_count_cache_test'];
/**
* {@inheritdoc}
*/
public static function providerSource() {
// All tests use the same source_data, expected_data, expected_count, and
// high_water. The high water is set later to maintain the order of the
// parameters.
$data = [
'source_data' => [
'source_table' => [
['id' => 1],
['id' => 2],
['id' => 3],
['id' => 4],
],
],
'expected_data' => [
['id' => 1],
['id' => 2],
['id' => 3],
['id' => 4],
],
'expected_count' => 4,
];
return [
'uncached source count' => $data,
'cached source count, auto-generated cache key' => $data + [
'configuration' => [
'cache_counts' => TRUE,
],
'high_water' => NULL,
'expected_cache_key' => 'sql_count_cache-dbed2396c230e025663091479993a206441bf1f9ae4e60ebf3b504e4a76ad471',
],
'cached source count, auto-generated cache key for alternative source configuration' => $data + [
'configuration' => [
'cache_counts' => TRUE,
'some_source_plugin_configuration_key' => 19920106,
],
'high_water' => NULL,
'expected_cache_key' => 'sql_count_cache-83c62856dd5afc011f32574bcdc11c595557d629e1d73045e9353df2441ec269',
],
'cached source count, provided cache key' => $data + [
'configuration' => [
'cache_counts' => TRUE,
'cache_key' => 'custom_cache_key_here',
],
'high_water' => NULL,
'expected_cache_key' => 'custom_cache_key_here',
],
];
}
}

View File

@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel\Plugin\source;
use Drupal\migrate\Plugin\migrate\source\SourcePluginBase;
use Drupal\Tests\migrate\Kernel\MigrateTestBase;
/**
* Test source counts are correctly cached.
*
* @group migrate
*/
class MigrationSourceCacheTest extends MigrateTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate_cache_counts_test'];
/**
* The migration plugin manager.
*
* @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface
*/
protected $migrationPluginManager;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->migrationPluginManager = $this->container->get('plugin.manager.migration');
}
/**
* Tests that counts for the same plugin ID are not crossed.
*/
public function testCacheCountsNotContaminated(): void {
$migration_1_definition = [
'source' => [
'plugin' => 'cacheable_embedded_data',
'cache_counts' => TRUE,
'ids' => [
'id' => [
'type' => 'integer',
],
],
'data_rows' => [
[
['id' => 1],
],
],
],
];
$migration_2_definition = [
'source' => [
'plugin' => 'cacheable_embedded_data',
'cache_counts' => TRUE,
'ids' => [
'id' => [
'type' => 'integer',
],
],
'data_rows' => [
['id' => 1],
['id' => 2],
],
],
];
$migration_1 = $this->migrationPluginManager->createStubMigration($migration_1_definition);
$migration_2 = $this->migrationPluginManager->createStubMigration($migration_2_definition);
$migration_1_source = $migration_1->getSourcePlugin();
$migration_2_source = $migration_2->getSourcePlugin();
// Verify correct counts when count is refreshed.
$this->assertSame(1, $migration_1_source->count(TRUE));
$this->assertSame(2, $migration_2_source->count(TRUE));
// Verify correct counts are cached.
$this->assertCount(1, $migration_1_source);
$this->assertCount(2, $migration_2_source);
// Verify the cache keys are different.
$cache_key_property = new \ReflectionProperty(SourcePluginBase::class, 'cacheKey');
$this->assertNotEquals($cache_key_property->getValue($migration_1_source), $cache_key_property->getValue($migration_2_source));
}
/**
* Test that values are pulled from the cache when appropriate.
*/
public function testCacheCountsUsed(): void {
$migration_definition = [
'source' => [
'plugin' => 'cacheable_embedded_data',
'cache_counts' => TRUE,
'ids' => [
'id' => [
'type' => 'integer',
],
],
'data_rows' => [
['id' => 1],
['id' => 2],
],
],
];
$migration = $this->migrationPluginManager->createStubMigration($migration_definition);
$migration_source = $migration->getSourcePlugin();
$this->assertCount(2, $migration_source);
// Pollute the cache.
$cache_key_property = new \ReflectionProperty($migration_source, 'cacheKey');
$cache_key = $cache_key_property->getValue($migration_source);
\Drupal::cache('migrate')->set($cache_key, 7);
$this->assertCount(7, $migration_source);
$this->assertSame(2, $migration_source->count(TRUE));
}
}

View File

@ -0,0 +1,262 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\sqlite\Driver\Database\sqlite\Connection;
use Drupal\TestTools\Random;
/**
* Tests query batching.
*
* @covers \Drupal\migrate_query_batch_test\Plugin\migrate\source\QueryBatchTest
* @group migrate
*/
class QueryBatchTest extends KernelTestBase {
/**
* The mocked migration.
*
* @var \Drupal\migrate\Plugin\MigrationInterface|\Prophecy\Prophecy\ObjectProphecy
*/
protected $migration;
/**
* {@inheritdoc}
*/
protected static $modules = [
'migrate',
'migrate_query_batch_test',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create a mock migration. This will be injected into the source plugin
// under test.
$this->migration = $this->prophesize(MigrationInterface::class);
$this->migration->id()->willReturn(
$this->randomMachineName(16)
);
// Prophesize a useless ID map plugin and an empty set of destination IDs.
// Calling code can override these prophecies later and set up different
// behaviors.
$this->migration->getIdMap()->willReturn(
$this->prophesize(MigrateIdMapInterface::class)->reveal()
);
$this->migration->getDestinationIds()->willReturn([]);
}
/**
* Tests a negative batch size throws an exception.
*/
public function testBatchSizeNegative(): void {
$this->expectException(MigrateException::class);
$this->expectExceptionMessage('batch_size must be greater than or equal to zero');
$plugin = $this->getPlugin(['batch_size' => -1]);
$plugin->next();
}
/**
* Tests a non integer batch size throws an exception.
*/
public function testBatchSizeNonInteger(): void {
$this->expectException(MigrateException::class);
$this->expectExceptionMessage('batch_size must be greater than or equal to zero');
$plugin = $this->getPlugin(['batch_size' => '1']);
$plugin->next();
}
/**
* {@inheritdoc}
*/
public static function queryDataProvider() {
// Define the parameters for building the data array. The first element is
// the number of source data rows, the second is the batch size to set on
// the plugin configuration.
$test_parameters = [
// Test when batch size is 0.
[200, 0],
// Test when rows mod batch size is 0.
[200, 20],
// Test when rows mod batch size is > 0.
[200, 30],
// Test when batch size = row count.
[200, 200],
// Test when batch size > row count.
[200, 300],
];
// Build the data provider array. The provider array consists of the source
// data rows, the expected result data, the expected count, the plugin
// configuration, the expected batch size and the expected batch count.
$table = 'query_batch_test';
$tests = [];
$data_set = 0;
foreach ($test_parameters as $data) {
[$num_rows, $batch_size] = $data;
for ($i = 0; $i < $num_rows; $i++) {
$tests[$data_set]['source_data'][$table][] = [
'id' => $i,
'data' => Random::string(),
];
}
$tests[$data_set]['expected_data'] = $tests[$data_set]['source_data'][$table];
$tests[$data_set]['num_rows'] = $num_rows;
// Plugin configuration array.
$tests[$data_set]['configuration'] = ['batch_size' => $batch_size];
// Expected batch size.
$tests[$data_set]['expected_batch_size'] = $batch_size;
// Expected batch count is 0 unless a batch size is set.
$expected_batch_count = 0;
if ($batch_size > 0) {
$expected_batch_count = (int) ($num_rows / $batch_size);
if ($num_rows % $batch_size) {
// If there is a remainder an extra batch is needed to get the
// remaining rows.
$expected_batch_count++;
}
}
$tests[$data_set]['expected_batch_count'] = $expected_batch_count;
$data_set++;
}
return $tests;
}
/**
* Tests query batch size.
*
* @param array $source_data
* The source data, keyed by table name. Each table is an array containing
* the rows in that table.
* @param array $expected_data
* The result rows the plugin is expected to return.
* @param int $num_rows
* How many rows the source plugin is expected to return.
* @param array $configuration
* Configuration for the source plugin specifying the batch size.
* @param int $expected_batch_size
* The expected batch size, will be set to zero for invalid batch sizes.
* @param int $expected_batch_count
* The total number of batches.
*
* @dataProvider queryDataProvider
*/
public function testQueryBatch($source_data, $expected_data, $num_rows, $configuration, $expected_batch_size, $expected_batch_count): void {
$plugin = $this->getPlugin($configuration);
// Since we don't yet inject the database connection, we need to use a
// reflection hack to set it in the plugin instance.
$reflector = new \ReflectionObject($plugin);
$property = $reflector->getProperty('database');
$connection = $this->getDatabase($source_data);
$property->setValue($plugin, $connection);
// Test the results.
$i = 0;
/** @var \Drupal\migrate\Row $row */
foreach ($plugin as $row) {
$expected = $expected_data[$i++];
$actual = $row->getSource();
foreach ($expected as $key => $value) {
$this->assertArrayHasKey($key, $actual);
$this->assertSame((string) $value, (string) $actual[$key]);
}
}
// Test that all rows were retrieved.
self::assertSame($num_rows, $i);
// Test the batch size.
if (is_null($expected_batch_size)) {
$expected_batch_size = $configuration['batch_size'];
}
$property = $reflector->getProperty('batchSize');
self::assertSame($expected_batch_size, $property->getValue($plugin));
// Test the batch count.
if (is_null($expected_batch_count)) {
$expected_batch_count = intdiv($num_rows, $expected_batch_size);
if ($num_rows % $configuration['batch_size']) {
$expected_batch_count++;
}
}
$property = $reflector->getProperty('batch');
self::assertSame($expected_batch_count, $property->getValue($plugin));
}
/**
* Instantiates the source plugin under test.
*
* @param array $configuration
* The source plugin configuration.
*
* @return \Drupal\migrate\Plugin\MigrateSourceInterface|object
* The fully configured source plugin.
*/
protected function getPlugin($configuration) {
/** @var \Drupal\migrate\Plugin\MigratePluginManager $plugin_manager */
$plugin_manager = $this->container->get('plugin.manager.migrate.source');
$plugin = $plugin_manager->createInstance('query_batch_test', $configuration, $this->migration->reveal());
$this->migration
->getSourcePlugin()
->willReturn($plugin);
return $plugin;
}
/**
* Builds an in-memory SQLite database from a set of source data.
*
* @param array $source_data
* The source data, keyed by table name. Each table is an array containing
* the rows in that table.
*
* @return \Drupal\sqlite\Driver\Database\sqlite\Connection
* The SQLite database connection.
*/
protected function getDatabase(array $source_data) {
// Create an in-memory SQLite database. Plugins can interact with it like
// any other database, and it will cease to exist when the connection is
// closed.
$connection_options = ['database' => ':memory:'];
$pdo = Connection::open($connection_options);
$connection = new Connection($pdo, $connection_options);
// Create the tables and fill them with data.
foreach ($source_data as $table => $rows) {
// Use the biggest row to build the table schema.
$counts = array_map('count', $rows);
asort($counts);
$pilot = $rows[array_key_last($counts)];
$connection->schema()
->createTable($table, [
// SQLite uses loose affinity typing, so it's OK for every field to
// be a text field.
'fields' => array_map(function () {
return ['type' => 'text'];
}, $pilot),
]);
$fields = array_keys($pilot);
$insert = $connection->insert($table)->fields($fields);
array_walk($rows, [$insert, 'values']);
$insert->execute();
}
return $connection;
}
}

View File

@ -0,0 +1,327 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\Core\Database\Query\ConditionInterface;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\Core\Database\Statement\FetchAs;
use Drupal\Core\Database\StatementInterface;
use Drupal\migrate\Exception\RequirementsException;
use Drupal\Core\Database\Database;
use Drupal\migrate\MigrateExecutable;
use Drupal\migrate\MigrateMessage;
use Drupal\migrate\Plugin\migrate\source\SqlBase;
use Drupal\migrate\Plugin\MigrationInterface;
/**
* Tests the functionality of SqlBase.
*
* @group migrate
*/
class SqlBaseTest extends MigrateTestBase {
/**
* The (probably mocked) migration under test.
*
* @var \Drupal\migrate\Plugin\MigrationInterface
*/
protected $migration;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->migration = $this->createMock(MigrationInterface::class);
$this->migration->method('id')->willReturn('foo');
}
/**
* Tests different connection types.
*/
public function testConnectionTypes(): void {
$sql_base = new TestSqlBase([], $this->migration);
// Verify that falling back to the default 'migrate' connection (defined in
// the base class) works.
$this->assertSame('default', $sql_base->getDatabase()->getTarget());
$this->assertSame('migrate', $sql_base->getDatabase()->getKey());
// Verify the fallback state key overrides the 'migrate' connection.
$target = 'test_fallback_target';
$key = 'test_fallback_key';
$config = ['target' => $target, 'key' => $key];
$database_state_key = 'test_fallback_state';
\Drupal::state()->set($database_state_key, $config);
\Drupal::state()->set('migrate.fallback_state_key', $database_state_key);
// Create a test connection using the default database configuration.
Database::addConnectionInfo($key, $target, Database::getConnectionInfo('default')['default']);
$this->assertSame($sql_base->getDatabase()->getTarget(), $target);
$this->assertSame($sql_base->getDatabase()->getKey(), $key);
// Verify that setting explicit connection information overrides fallbacks.
$target = 'test_db_target';
$key = 'test_migrate_connection';
$config = ['target' => $target, 'key' => $key];
$sql_base->setConfiguration($config);
Database::addConnectionInfo($key, $target, Database::getConnectionInfo('default')['default']);
// Validate we have injected our custom key and target.
$this->assertSame($sql_base->getDatabase()->getTarget(), $target);
$this->assertSame($sql_base->getDatabase()->getKey(), $key);
// Now test we can have SqlBase create the connection from an info array.
$sql_base = new TestSqlBase([], $this->migration);
$target = 'test_db_target2';
$key = 'test_migrate_connection2';
$database = Database::getConnectionInfo('default')['default'];
$config = ['target' => $target, 'key' => $key, 'database' => $database];
$sql_base->setConfiguration($config);
// Call getDatabase() to get the connection defined.
$sql_base->getDatabase();
// Validate the connection has been created with the right values.
$this->assertSame(Database::getConnectionInfo($key)[$target], $database);
// Now, test this all works when using state to store db info.
$target = 'test_state_db_target';
$key = 'test_state_migrate_connection';
$config = ['target' => $target, 'key' => $key];
$database_state_key = 'migrate_sql_base_test';
\Drupal::state()->set($database_state_key, $config);
$sql_base->setConfiguration(['database_state_key' => $database_state_key]);
Database::addConnectionInfo($key, $target, Database::getConnectionInfo('default')['default']);
// Validate we have injected our custom key and target.
$this->assertSame($sql_base->getDatabase()->getTarget(), $target);
$this->assertSame($sql_base->getDatabase()->getKey(), $key);
// Now test we can have SqlBase create the connection from an info array.
$sql_base = new TestSqlBase([], $this->migration);
$target = 'test_state_db_target2';
$key = 'test_state_migrate_connection2';
$database = Database::getConnectionInfo('default')['default'];
$config = ['target' => $target, 'key' => $key, 'database' => $database];
$database_state_key = 'migrate_sql_base_test2';
\Drupal::state()->set($database_state_key, $config);
$sql_base->setConfiguration(['database_state_key' => $database_state_key]);
// Call getDatabase() to get the connection defined.
$sql_base->getDatabase();
// Validate the connection has been created with the right values.
$this->assertSame(Database::getConnectionInfo($key)[$target], $database);
// Verify that falling back to 'migrate' when the connection is not defined
// throws a RequirementsException.
\Drupal::state()->delete('migrate.fallback_state_key');
$sql_base->setConfiguration([]);
Database::renameConnection('migrate', 'fallback_connection');
$this->expectException(RequirementsException::class);
$this->expectExceptionMessage('No database connection configured for source plugin');
$sql_base->getDatabase();
}
/**
* Tests the exception when a connection is defined but not available.
*/
public function testBrokenConnection(): void {
if (Database::getConnection()->driver() === 'sqlite') {
$this->markTestSkipped('Not compatible with sqlite');
}
$sql_base = new TestSqlBase([], $this->migration);
$target = 'test_state_db_target2';
$key = 'test_state_migrate_connection2';
$database = Database::getConnectionInfo('default')['default'];
$database['database'] = 'godot';
$config = ['target' => $target, 'key' => $key, 'database' => $database];
$database_state_key = 'migrate_sql_base_test2';
\Drupal::state()->set($database_state_key, $config);
$sql_base->setConfiguration(['database_state_key' => $database_state_key]);
// Call checkRequirements(): it will call getDatabase() and convert the
// exception to a RequirementsException.
$this->expectException(RequirementsException::class);
$this->expectExceptionMessage('No database connection available for source plugin sql_base');
$sql_base->checkRequirements();
}
/**
* Tests that SqlBase respects high-water values.
*
* @param mixed $high_water
* (optional) The high-water value to set.
* @param array $query_result
* (optional) The expected query results.
*
* @dataProvider highWaterDataProvider
*/
public function testHighWater($high_water = NULL, array $query_result = []): void {
$configuration = [
'high_water_property' => [
'name' => 'order',
],
];
$source = new TestSqlBase($configuration, $this->migration);
if ($high_water) {
\Drupal::keyValue('migrate:high_water')->set($this->migration->id(), $high_water);
}
$statement = $this->createMock(StatementInterface::class);
$statement->expects($this->atLeastOnce())->method('setFetchMode')->with(FetchAs::Associative);
$query = $this->createMock(SelectInterface::class);
$query->method('execute')->willReturn($statement);
$query->expects($this->atLeastOnce())->method('orderBy')->with('order', 'ASC');
$condition_group = $this->createMock(ConditionInterface::class);
$query->method('orConditionGroup')->willReturn($condition_group);
$source->setQuery($query);
$source->rewind();
}
/**
* Data provider for ::testHighWater().
*
* @return array
* The scenarios to test.
*/
public static function highWaterDataProvider() {
return [
'no high-water value set' => [],
'high-water value set' => [33],
];
}
/**
* Tests prepare query method.
*/
public function testPrepareQuery(): void {
$this->prepareSourceData();
$this->enableModules(['migrate_sql_prepare_query_test', 'entity_test']);
/** @var \Drupal\migrate\Plugin\MigrationInterface $migration */
$migration = $this->container->get('plugin.manager.migration')
->createStubMigration([
'source' => ['plugin' => 'test_sql_prepare_query'],
'process' => ['id' => 'id', 'name' => 'name'],
'destination' => ['plugin' => 'entity:entity_test'],
]);
// One item is excluded by the condition defined in the source plugin.
// @see \Drupal\migrate_sql_prepare_query_test\Plugin\migrate\source\TestSqlPrepareQuery
$count = $migration->getSourcePlugin()->count();
$this->assertEquals(2, $count);
// Run the migration and verify that the number of migrated items matches
// the initial source count.
(new MigrateExecutable($migration, new MigrateMessage()))->import();
$this->assertEquals(2, $migration->getIdMap()->processedCount());
}
/**
* Creates a custom source table and some sample data.
*/
protected function prepareSourceData(): void {
$this->sourceDatabase->schema()->createTable('migrate_source_test', [
'fields' => [
'id' => ['type' => 'int'],
'name' => ['type' => 'varchar', 'length' => 32],
],
]);
// Add some data in the table.
$this->sourceDatabase->insert('migrate_source_test')
->fields(['id', 'name'])
->values(['id' => 1, 'name' => 'foo'])
->values(['id' => 2, 'name' => 'bar'])
->values(['id' => 3, 'name' => 'baz'])
->execute();
}
}
/**
* A dummy source to help with testing SqlBase.
*
* @package Drupal\migrate\Plugin\migrate\source
*/
class TestSqlBase extends SqlBase {
/**
* The query to execute.
*
* @var \Drupal\Core\Database\Query\SelectInterface
*/
protected $query;
/**
* Overrides the constructor so we can create one easily.
*
* @param array $configuration
* The plugin instance configuration.
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* (optional) The migration being run.
*/
public function __construct(array $configuration = [], ?MigrationInterface $migration = NULL) {
parent::__construct($configuration, 'sql_base', ['requirements_met' => TRUE], $migration, \Drupal::state());
}
/**
* Gets the database without caching it.
*/
public function getDatabase() {
$this->database = NULL;
return parent::getDatabase();
}
/**
* Allows us to set the configuration from a test.
*
* @param array $config
* The config array.
*/
public function setConfiguration($config): void {
$this->configuration = $config;
}
/**
* {@inheritdoc}
*/
public function getIds() {
return [];
}
/**
* {@inheritdoc}
*/
public function fields() {
throw new \RuntimeException(__METHOD__ . " not implemented for " . __CLASS__);
}
/**
* {@inheritdoc}
*/
public function query() {
return $this->query;
}
/**
* Sets the query to execute.
*
* @param \Drupal\Core\Database\Query\SelectInterface $query
* The query to execute.
*/
public function setQuery(SelectInterface $query): void {
$this->query = $query;
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
/**
* Class to test FilterIterators.
*/
class TestFilterIterator extends \FilterIterator {
/**
* {@inheritdoc}
*/
public function accept(): bool {
return TRUE;
}
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\migrate\MigrateExecutable;
/**
* Tests MigrateExecutable.
*/
class TestMigrateExecutable extends MigrateExecutable {
/**
* {@inheritdoc}
*/
protected function getIdMap() {
// This adds test coverage that this works.
return new TestFilterIterator(parent::getIdMap());
}
/**
* {@inheritdoc}
*/
protected function getSource() {
// This adds test coverage that this works.
return new TestFilterIterator(parent::getSource());
}
}

View File

@ -0,0 +1,241 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
/**
* Tests migration track changes property.
*
* @group migrate
*/
class TrackChangesTest extends MigrateTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'user',
'taxonomy',
'migrate',
'migrate_track_changes_test',
'text',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create source test table.
$this->sourceDatabase->schema()->createTable('track_changes_term', [
'fields' => [
'tid' => [
'description' => 'Serial',
'type' => 'serial',
'unsigned' => TRUE,
'not null' => TRUE,
],
'name' => [
'description' => 'Name',
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'default' => '',
],
'description' => [
'description' => 'Name',
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
'default' => '',
],
],
'primary key' => [
'tid',
],
'description' => 'Contains taxonomy terms to import',
]);
// Add 4 items to source table.
$this->sourceDatabase->insert('track_changes_term')
->fields([
'name',
'description',
])
->values([
'name' => 'Item 1',
'description' => 'Text item 1',
])
->values([
'name' => 'Item 2',
'description' => 'Text item 2',
])
->values([
'name' => 'Item 3',
'description' => 'Text item 3',
])
->values([
'name' => 'Item 4',
'description' => 'Text item 4',
])
->execute();
$this->installEntitySchema('taxonomy_term');
$this->installEntitySchema('user');
$this->executeMigration('track_changes_test');
}
/**
* Tests track changes property of SqlBase.
*/
public function testTrackChanges(): void {
// Assert all of the terms have been imported.
$this->assertTermExists('name', 'Item 1');
$this->assertTermExists('name', 'Item 2');
$this->assertTermExists('description', 'Text item 3');
$this->assertTermExists('description', 'Text item 4');
// Save the original hash, rerun the migration and check that the hashes
// are the same.
$id_map = $this->migration->getIdMap();
for ($i = 1; $i < 5; $i++) {
$row = $id_map->getRowBySource(['tid' => $i]);
$original_hash[$i] = $row['hash'];
}
$this->executeMigration($this->migration);
for ($i = 1; $i < 5; $i++) {
$row = $id_map->getRowBySource(['tid' => $i]);
$new_hash[$i] = $row['hash'];
}
$this->assertEquals($original_hash, $new_hash);
// Update Item 1 triggering its track_changes by name.
$this->sourceDatabase->update('track_changes_term')
->fields([
'name' => 'Item 1 updated',
])
->condition('name', 'Item 1')
->execute();
// Update Item 2 keeping it's track_changes name the same.
$this->sourceDatabase->update('track_changes_term')
->fields([
'name' => 'Item 2',
])
->condition('name', 'Item 2')
->execute();
// Update Item 3 triggering its track_changes by field.
$this->sourceDatabase->update('track_changes_term')
->fields([
'description' => 'Text item 3 updated',
])
->condition('name', 'Item 3')
->execute();
// Update Item 2 keeping it's track_changes field the same.
$this->sourceDatabase->update('track_changes_term')
->fields([
'description' => 'Text item 4',
])
->condition('name', 'Item 4')
->execute();
// Execute migration again.
$this->executeMigration($this->migration);
// Check that the all the hashes except for 'Item 2'and 'Item 4' have
// changed.
for ($i = 1; $i < 5; $i++) {
$row = $id_map->getRowBySource(['tid' => $i]);
$new_hash[$i] = $row['hash'];
}
$this->assertNotEquals($original_hash[1], $new_hash[1]);
$this->assertEquals($original_hash[2], $new_hash[2]);
$this->assertNotEquals($original_hash[3], $new_hash[3]);
$this->assertEquals($original_hash[4], $new_hash[4]);
// Item with name changes should be updated.
$this->assertTermExists('name', 'Item 1 updated');
$this->assertTermDoesNotExist('name', 'Item 1');
// Item without name changes should not be updated.
$this->assertTermExists('name', 'Item 2');
// Item with field changes should be updated.
$this->assertTermExists('description', 'Text item 3 updated');
$this->assertTermDoesNotExist('description', 'Text item 3');
// Item without field changes should not be updated.
$this->assertTermExists('description', 'Text item 4');
// Test hashes again after forcing all rows to be re-imported.
$id_map->prepareUpdate();
// Execute migration again.
$this->executeMigration('track_changes_test');
for ($i = 1; $i < 5; $i++) {
$row = $id_map->getRowBySource(['tid' => $i]);
$newer_hash[$i] = $row['hash'];
}
$this->assertEquals($new_hash[1], $newer_hash[1]);
$this->assertEquals($new_hash[2], $newer_hash[2]);
$this->assertEquals($new_hash[3], $newer_hash[3]);
$this->assertEquals($new_hash[4], $newer_hash[4]);
}
/**
* Assert that term with given name exists.
*
* @param string $property
* Property to evaluate.
* @param string $value
* Value to evaluate.
*
* @internal
*/
protected function assertTermExists(string $property, string $value): void {
self::assertTrue($this->termExists($property, $value));
}
/**
* Assert that term with given title does not exist.
*
* @param string $property
* Property to evaluate.
* @param string $value
* Value to evaluate.
*
* @internal
*/
protected function assertTermDoesNotExist(string $property, string $value): void {
self::assertFalse($this->termExists($property, $value));
}
/**
* Checks if term with given name exists.
*
* @param string $property
* Property to evaluate.
* @param string $value
* Value to evaluate.
*
* @return bool
* TRUE if term exists, FALSE otherwise.
*/
protected function termExists($property, $value): bool {
$property = $property === 'description' ? 'description__value' : $property;
$query = \Drupal::entityQuery('taxonomy_term')->accessCheck(FALSE);
$result = $query
->condition($property, $value)
->range(0, 1)
->execute();
return !empty($result);
}
}

View File

@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel\process;
use Drupal\Core\StreamWrapper\StreamWrapperInterface;
use Drupal\KernelTests\Core\File\FileTestBase;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Plugin\migrate\process\Download;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Row;
use GuzzleHttp\Client;
/**
* Tests the download process plugin.
*
* @group migrate
*/
class DownloadTest extends FileTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['system'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->container->get('stream_wrapper_manager')->registerWrapper('temporary', 'Drupal\Core\StreamWrapper\TemporaryStream', StreamWrapperInterface::LOCAL_NORMAL);
}
/**
* Tests a download that overwrites an existing local file.
*/
public function testOverwritingDownload(): void {
// Create a pre-existing file at the destination.
$destination_uri = $this->createUri('existing_file.txt');
// Test destructive download.
$actual_destination = $this->doTransform($destination_uri);
$this->assertSame($destination_uri, $actual_destination, 'Import returned a destination that was not renamed');
$this->assertFileDoesNotExist('public://existing_file_0.txt');
}
/**
* Tests a download that renames the downloaded file if there's a collision.
*/
public function testNonDestructiveDownload(): void {
// Create a pre-existing file at the destination.
$destination_uri = $this->createUri('another_existing_file.txt');
// Test non-destructive download.
$actual_destination = $this->doTransform($destination_uri, ['file_exists' => 'rename']);
$this->assertSame('public://another_existing_file_0.txt', $actual_destination, 'Import returned a renamed destination');
$this->assertFileExists($actual_destination);
}
/**
* Tests that an exception is thrown if the destination URI is not writable.
*/
public function testWriteProtectedDestination(): void {
// Create a pre-existing file at the destination.
$destination_uri = $this->createUri('not-writable.txt');
// Make the destination non-writable.
$this->container
->get('file_system')
->chmod($destination_uri, 0444);
// Pass or fail, we'll need to make the file writable again so the test
// can clean up after itself.
$fix_permissions = function () use ($destination_uri) {
$this->container
->get('file_system')
->chmod($destination_uri, 0755);
};
try {
$this->doTransform($destination_uri);
$fix_permissions();
$this->fail('MigrateException was not thrown for non-writable destination URI.');
}
catch (MigrateException) {
$this->assertTrue(TRUE, 'MigrateException was thrown for non-writable destination URI.');
$fix_permissions();
}
}
/**
* Runs an input value through the download plugin.
*
* @param string $destination_uri
* The destination URI to download to.
* @param array $configuration
* (optional) Configuration for the download plugin.
*
* @return string
* The local URI of the downloaded file.
*/
protected function doTransform($destination_uri, $configuration = []) {
// Prepare a mock HTTP client.
$this->container->set('http_client', $this->createMock(Client::class));
// Instantiate the plugin statically so it can pull dependencies out of
// the container.
$plugin = Download::create($this->container, $configuration, 'download', []);
// Execute the transformation.
$executable = $this->createMock(MigrateExecutableInterface::class);
$row = new Row([], []);
// Return the downloaded file's local URI.
$value = [
'http://drupal.org/favicon.ico',
$destination_uri,
];
// Assert that number of stream resources in use is the same before and
// after the download.
$initial_count = count(get_resources('stream'));
$return = $plugin->transform($value, $executable, $row, 'foo');
$this->assertCount($initial_count, get_resources('stream'));
return $return;
}
}

View File

@ -0,0 +1,115 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel\process;
use Drupal\KernelTests\KernelTestBase;
use Drupal\migrate\MigrateExecutable;
use Drupal\migrate\Plugin\MigrationInterface;
/**
* Tests the extract process plugin.
*
* @group migrate
*/
class ExtractTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate'];
/**
* Returns test migration definition.
*
* @return array
* The test migration definition.
*/
public function getDefinition() {
return [
'source' => [
'plugin' => 'embedded_data',
'data_rows' => [],
'ids' => [
'id' => ['type' => 'string'],
],
],
'process' => [
'first' => [
'plugin' => 'extract',
'index' => [0],
'source' => 'simple_array',
],
'second' => [
'plugin' => 'extract',
'index' => [1],
'source' => 'complex_array',
],
],
'destination' => [
'plugin' => 'config',
'config_name' => 'migrate_test.settings',
],
];
}
/**
* Tests multiple value handling.
*
* @param array $source_data
* The source data.
* @param array $expected_data
* The expected results.
*
* @dataProvider multipleValueProviderSource
*/
public function testMultipleValueExplode(array $source_data, array $expected_data): void {
$definition = $this->getDefinition();
$definition['source']['data_rows'] = [$source_data];
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition);
$executable = new MigrateExecutable($migration);
$result = $executable->import();
// Migration needs to succeed before further assertions are made.
$this->assertSame(MigrationInterface::RESULT_COMPLETED, $result);
// Compare with expected data.
$this->assertEquals($expected_data, \Drupal::config('migrate_test.settings')->get());
}
/**
* Provides multiple source data for "extract" process plugin test.
*/
public static function multipleValueProviderSource() {
$tests = [
[
'source_data' => [
'id' => '1',
'simple_array' => ['alpha', 'beta'],
'complex_array' => [['alpha', 'beta'], ['psi', 'omega']],
],
'expected_data' => [
'first' => 'alpha',
'second' => ['psi', 'omega'],
],
],
[
'source_data' => [
'id' => '2',
'simple_array' => ['one'],
'complex_array' => [0, 1],
],
'expected_data' => [
'first' => 'one',
'second' => 1,
],
],
];
return $tests;
}
}

View File

@ -0,0 +1,259 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel\process;
use Drupal\Core\StreamWrapper\StreamWrapperInterface;
use Drupal\KernelTests\Core\File\FileTestBase;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Plugin\migrate\process\FileCopy;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Plugin\MigrateProcessInterface;
use Drupal\migrate\Row;
use GuzzleHttp\Client;
/**
* Tests the file_copy process plugin.
*
* @coversDefaultClass \Drupal\migrate\Plugin\migrate\process\FileCopy
*
* @group migrate
*/
class FileCopyTest extends FileTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate', 'system'];
/**
* The file system service.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fileSystem;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->fileSystem = $this->container->get('file_system');
$this->container->get('stream_wrapper_manager')->registerWrapper('temporary', 'Drupal\Core\StreamWrapper\TemporaryStream', StreamWrapperInterface::LOCAL_NORMAL);
}
/**
* Tests successful imports/copies.
*/
public function testSuccessfulCopies(): void {
$file = $this->createUri(NULL, NULL, 'temporary');
$file_absolute = $this->fileSystem->realpath($file);
$data_sets = [
// Test a local to local copy.
[
$this->root . '/core/tests/fixtures/files/image-test.jpg',
'public://file1.jpg',
],
// Test a temporary file using an absolute path.
[
$file_absolute,
'temporary://test.jpg',
],
// Test a temporary file using a relative path.
[
$file_absolute,
'temporary://core/tests/fixtures/files/test.jpg',
],
];
foreach ($data_sets as $data) {
[$source_path, $destination_path] = $data;
$actual_destination = $this->doTransform($source_path, $destination_path);
$this->assertFileExists($destination_path);
// Make sure we didn't accidentally do a move.
$this->assertFileExists($source_path);
$this->assertSame($actual_destination, $destination_path, 'The import returned the copied filename.');
}
}
/**
* Tests successful file reuse.
*
* @param string $source_path
* Source path to copy from.
* @param string $destination_path
* The destination path to copy to.
*
* @dataProvider providerSuccessfulReuse
*/
public function testSuccessfulReuse($source_path, $destination_path): void {
$file_reuse = $this->doTransform($source_path, $destination_path);
clearstatcache(TRUE, $destination_path);
$timestamp = (new \SplFileInfo($file_reuse))->getMTime();
$this->assertIsInt($timestamp);
// We need to make sure the modified timestamp on the file is sooner than
// the attempted migration.
sleep(1);
$configuration = ['file_exists' => 'use existing'];
$this->doTransform($source_path, $destination_path, $configuration);
clearstatcache(TRUE, $destination_path);
$modified_timestamp = (new \SplFileInfo($destination_path))->getMTime();
$this->assertEquals($timestamp, $modified_timestamp);
$this->doTransform($source_path, $destination_path);
clearstatcache(TRUE, $destination_path);
$modified_timestamp = (new \SplFileInfo($destination_path))->getMTime();
$this->assertGreaterThan($timestamp, $modified_timestamp);
}
/**
* Provides the source and destination path files.
*/
public static function providerSuccessfulReuse() {
return [
[
'source_path' => static::getDrupalRoot() . '/core/tests/fixtures/files/image-test.jpg',
'destination_path' => 'public://file1.jpg',
],
[
'source_path' => 'https://www.drupal.org/favicon.ico',
'destination_path' => 'public://file2.jpg',
],
];
}
/**
* Tests successful moves.
*/
public function testSuccessfulMoves(): void {
$file_1 = $this->createUri(NULL, NULL, 'temporary');
$file_1_absolute = $this->fileSystem->realpath($file_1);
$file_2 = $this->createUri(NULL, NULL, 'temporary');
$file_2_absolute = $this->fileSystem->realpath($file_2);
$local_file = $this->createUri(NULL, NULL, 'public');
$data_sets = [
// Test a local to local copy.
[
$local_file,
'public://file1.jpg',
],
// Test a temporary file using an absolute path.
[
$file_1_absolute,
'temporary://test.jpg',
],
// Test a temporary file using a relative path.
[
$file_2_absolute,
'temporary://core/tests/fixtures/files/test.jpg',
],
];
foreach ($data_sets as $data) {
[$source_path, $destination_path] = $data;
$actual_destination = $this->doTransform($source_path, $destination_path, ['move' => TRUE]);
$this->assertFileExists($destination_path);
$this->assertFileDoesNotExist($source_path);
$this->assertSame($actual_destination, $destination_path, 'The importer returned the moved filename.');
}
}
/**
* Tests that non-existent files throw an exception.
*/
public function testNonExistentSourceFile(): void {
$source = '/non/existent/file';
$this->expectException(MigrateException::class);
$this->expectExceptionMessage("File '/non/existent/file' does not exist");
$this->doTransform($source, 'public://foo.jpg');
}
/**
* Tests that non-writable destination throw an exception.
*
* @covers ::transform
*/
public function testNonWritableDestination(): void {
$source = $this->createUri('file.txt', NULL, 'temporary');
// Create the parent location.
$this->createDirectory('public://dir');
// Copy the file under public://dir/subdir1/.
$this->doTransform($source, 'public://dir/subdir1/file.txt');
// Check that 'subdir1' was created and the file was successfully migrated.
$this->assertFileExists('public://dir/subdir1/file.txt');
// Remove all permissions from public://dir to trigger a failure when
// trying to create a subdirectory 'subdir2' inside public://dir.
$this->fileSystem->chmod('public://dir', 0);
// Check that the proper exception is raised.
$this->expectException(MigrateException::class);
$this->expectExceptionMessage("Could not create or write to directory 'public://dir/subdir2'");
$this->doTransform($source, 'public://dir/subdir2/file.txt');
}
/**
* Tests the 'rename' overwrite mode.
*/
public function testRenameFile(): void {
$source = $this->createUri(NULL, NULL, 'temporary');
$destination = $this->createUri('foo.txt', NULL, 'public');
$expected_destination = 'public://foo_0.txt';
$actual_destination = $this->doTransform($source, $destination, ['file_exists' => 'rename']);
$this->assertFileExists($expected_destination);
$this->assertSame($actual_destination, $expected_destination, 'The importer returned the renamed filename.');
}
/**
* Tests that remote URIs are delegated to the download plugin.
*/
public function testDownloadRemoteUri(): void {
$download_plugin = $this->createMock(MigrateProcessInterface::class);
$download_plugin->expects($this->once())->method('transform');
$plugin = new FileCopy(
[],
$this->randomMachineName(),
[],
$this->container->get('stream_wrapper_manager'),
$this->container->get('file_system'),
$download_plugin
);
$plugin->transform(
['http://drupal.org/favicon.ico', '/destination/path'],
$this->createMock(MigrateExecutableInterface::class),
new Row([], []),
$this->randomMachineName()
);
}
/**
* Do an import using the destination.
*
* @param string $source_path
* Source path to copy from.
* @param string $destination_path
* The destination path to copy to.
* @param array $configuration
* Process plugin configuration settings.
*
* @return string
* The URI of the copied file.
*/
protected function doTransform($source_path, $destination_path, $configuration = []) {
// Prepare a mock HTTP client.
$this->container->set('http_client', $this->createMock(Client::class));
$plugin = FileCopy::create($this->container, $configuration, 'file_copy', []);
$executable = $this->prophesize(MigrateExecutableInterface::class)->reveal();
$row = new Row([], []);
return $plugin->transform([$source_path, $destination_path], $executable, $row, 'foo');
}
}

View File

@ -0,0 +1,174 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel\process;
use Drupal\KernelTests\KernelTestBase;
use Drupal\migrate\MigrateExecutable;
use Drupal\migrate\Plugin\MigrationInterface;
/**
* Tests process pipelines with scalar and multiple values handling.
*
* @group migrate
*/
class HandleMultiplesTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate'];
/**
* Provides the test migration definition.
*
* @return array
* The test migration definition.
*/
public function getDefinition() {
return [
'source' => [
'plugin' => 'embedded_data',
'data_rows' => [],
'ids' => [
'id' => ['type' => 'string'],
],
],
'process' => [
// Process pipeline for testing values from string to array to string.
'first' => [
// Expects a string and returns an array.
[
'plugin' => 'explode',
'source' => 'scalar',
'delimiter' => '/',
],
// Expects an array and returns a string.
[
'plugin' => 'extract',
'index' => [1],
],
// Expects a string and returns a string.
[
'plugin' => 'callback',
'callable' => 'strtoupper',
],
],
// Process pipeline for testing values from array to string to array.
'second' => [
// Expects an array and returns a string.
[
'plugin' => 'extract',
'source' => 'multiple',
'index' => [1],
],
// Expects a string and returns a string.
[
'plugin' => 'callback',
'callable' => 'strtoupper',
],
// Expects a string and returns an array.
[
'plugin' => 'explode',
'delimiter' => '/',
],
],
// Process pipeline for testing 'get' overriding a single.
'get_from_single' => [
// Returns a string.
[
'plugin' => 'get',
'source' => 'scalar',
],
// Ignore previous and return an array.
[
'plugin' => 'get',
'source' => 'multiple',
],
],
// Process pipeline for testing 'get' overriding an array.
'get_from_multiple' => [
// Returns an array.
[
'plugin' => 'get',
'source' => 'multiple',
],
// Ignore previous and return a string.
[
'plugin' => 'get',
'source' => 'scalar',
],
],
],
'destination' => [
'plugin' => 'config',
'config_name' => 'migrate_test.settings',
],
];
}
/**
* Tests process pipelines with scalar and multiple values handling.
*
* @param array $source_data
* The source data.
* @param array $expected_data
* The expected results.
*
* @dataProvider scalarAndMultipleValuesProviderSource
*/
public function testScalarAndMultipleValues(array $source_data, array $expected_data): void {
$definition = $this->getDefinition();
$definition['source']['data_rows'] = [$source_data];
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition);
$executable = new MigrateExecutable($migration);
$result = $executable->import();
// Migration needs to succeed before further assertions are made.
$this->assertSame(MigrationInterface::RESULT_COMPLETED, $result);
// Compare with expected data.
$this->assertEquals($expected_data, \Drupal::config('migrate_test.settings')->get());
}
/**
* Provides the source data with scalar and multiple values.
*
* @return array
* An array of test cases.
*/
public static function scalarAndMultipleValuesProviderSource() {
return [
[
'source_data' => [
'id' => '1',
// Source value for the first pipeline.
'scalar' => 'foo/bar',
// Source value for the second pipeline.
'multiple' => [
'foo',
'bar/baz',
],
],
'expected_data' => [
// Expected value from the first pipeline.
'first' => 'BAR',
// Expected value from the second pipeline.
'second' => [
'BAR',
'BAZ',
],
'get_from_single' => [
'foo',
'bar/baz',
],
'get_from_multiple' => 'foo/bar',
],
],
];
}
}

View File

@ -0,0 +1,276 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel\process;
use Drupal\KernelTests\KernelTestBase;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Plugin\migrate\process\Route;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
use Drupal\Tests\user\Traits\UserCreationTrait;
// cspell:ignore nzdt
/**
* Tests the route process plugin.
*
* @coversDefaultClass \Drupal\migrate\Plugin\migrate\process\Route
*
* @group migrate
*/
class RouteTest extends KernelTestBase {
use UserCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['user', 'system'];
/**
* Tests Route plugin based on providerTestRoute() values.
*
* @param mixed $value
* Input value for the Route process plugin.
* @param array $expected
* The expected results from the Route transform process.
*
* @dataProvider providerTestRoute
*/
public function testRoute($value, $expected): void {
$actual = $this->doTransform($value);
$this->assertSame($expected, $actual);
}
/**
* Data provider for testRoute().
*
* @return array
* An array of arrays, where the first element is the input to the Route
* process plugin, and the second is the expected results.
*/
public static function providerTestRoute() {
// Internal link tests.
// Valid link path and options.
$values[0] = [
'user/login',
[
'attributes' => [
'title' => 'Test menu link 1',
],
],
];
$expected[0] = [
'route_name' => 'user.login',
'route_parameters' => [],
'options' => [
'query' => [],
'attributes' => [
'title' => 'Test menu link 1',
],
],
'url' => NULL,
];
// Valid link path and empty options.
$values[1] = [
'user/login',
[],
];
$expected[1] = [
'route_name' => 'user.login',
'route_parameters' => [],
'options' => [
'query' => [],
],
'url' => NULL,
];
// Valid link path and no options.
$values[2] = 'user/login';
$expected[2] = [
'route_name' => 'user.login',
'route_parameters' => [],
'options' => [
'query' => [],
],
'url' => NULL,
];
// Invalid link path.
$values[3] = 'users';
$expected[3] = [];
// Valid link path with parameter.
$values[4] = [
'system/timezone/nzdt',
[
'attributes' => [
'title' => 'Show NZDT',
],
],
];
$expected[4] = [
'route_name' => 'system.timezone',
'route_parameters' => [
'abbreviation' => 'nzdt',
'offset' => -1,
'is_daylight_saving_time' => NULL,
],
'options' => [
'query' => [],
'attributes' => [
'title' => 'Show NZDT',
],
],
'url' => NULL,
];
// External link tests.
// Valid external link path and options.
$values[5] = [
'https://www.drupal.org',
[
'attributes' => [
'title' => 'Drupal',
],
],
];
$expected[5] = [
'route_name' => NULL,
'route_parameters' => [],
'options' => [
'attributes' => [
'title' => 'Drupal',
],
],
'url' => 'https://www.drupal.org',
];
// Valid external link path and options.
$values[6] = [
'https://www.drupal.org/user/1/edit?pass-reset-token=QgtDKcRV4e4fjg6v2HTa6CbWx-XzMZ5XBZTufinqsM73qIhscIuU_BjZ6J2tv4dQI6N50ZJOag',
[
'attributes' => [
'title' => 'Drupal password reset',
],
],
];
$expected[6] = [
'route_name' => NULL,
'route_parameters' => [],
'options' => [
'attributes' => [
'title' => 'Drupal password reset',
],
],
'url' => 'https://www.drupal.org/user/1/edit?pass-reset-token=QgtDKcRV4e4fjg6v2HTa6CbWx-XzMZ5XBZTufinqsM73qIhscIuU_BjZ6J2tv4dQI6N50ZJOag',
];
return [
// Test data for internal paths.
// Test with valid link path and options.
[$values[0], $expected[0]],
// Test with valid link path and empty options.
[$values[1], $expected[1]],
// Test with valid link path and no options.
[$values[2], $expected[2]],
// Test with Invalid link path.
[$values[3], $expected[3]],
// Test with Valid link path with query options and parameters.
[$values[4], $expected[4]],
// Test data for external paths.
// Test with external link path and options.
[$values[5], $expected[5]],
// Test with valid link path and query options.
[$values[6], $expected[6]],
];
}
/**
* Tests Route plugin based on providerTestRoute() values.
*
* @param mixed $value
* Input value for the Route process plugin.
* @param array $expected
* The expected results from the Route transform process.
*
* @dataProvider providerTestRouteWithParamQuery
*/
public function testRouteWithParamQuery($value, $expected): void {
// Create a user so that user/1/edit is a valid path.
$this->setUpCurrentUser();
$this->installConfig(['user']);
$actual = $this->doTransform($value);
$this->assertSame($expected, $actual);
}
/**
* Data provider for testRouteWithParamQuery().
*
* @return array
* An array of arrays, where the first element is the input to the Route
* process plugin, and the second is the expected results.
*/
public static function providerTestRouteWithParamQuery() {
$values = [];
$expected = [];
// Valid link path with query options and parameters.
$values[0] = [
'user/1/edit',
[
'attributes' => [
'title' => 'Edit admin',
],
'query' => [
'destination' => '/admin/people',
],
],
];
$expected[0] = [
'route_name' => 'entity.user.edit_form',
'route_parameters' => [
'user' => '1',
],
'options' => [
'attributes' => [
'title' => 'Edit admin',
],
'query' => [
'destination' => '/admin/people',
],
],
'url' => NULL,
];
return [
// Test with valid link path with parameters and options.
[$values[0], $expected[0]],
];
}
/**
* Transforms link path data to a route.
*
* @param array|string $value
* Source link path information.
*
* @return array
* The route information based on the source link_path.
*/
protected function doTransform($value) {
$pathValidator = $this->container->get('path.validator');
$row = new Row();
$migration = $this->prophesize(MigrationInterface::class)->reveal();
$executable = $this->prophesize(MigrateExecutableInterface::class)->reveal();
$plugin = new Route([], 'route', [], $migration, $pathValidator);
$actual = $plugin->transform($value, $executable, $row, 'destination_property');
return $actual;
}
}

View File

@ -0,0 +1,148 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel\process;
use Drupal\KernelTests\KernelTestBase;
use Drupal\migrate\MigrateExecutable;
use Drupal\migrate\Plugin\MigrationInterface;
/**
* Tests process pipelines when a sub_process skips a row or process.
*
* @group migrate
*/
class SubProcessWithSkipTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate'];
/**
* Provides the test migration definition.
*
* @return array
* The test migration definition.
*/
public function getDefinition() {
return [
'source' => [
'plugin' => 'embedded_data',
'data_rows' => [
[
'id' => 'skip_test',
'my_array_of_arrays' => [
[
'key_1' => 'foo',
'key_2' => 'bar',
],
[
'key_1' => NULL,
'key_2' => 'baz',
],
],
],
],
'ids' => [
'id' => ['type' => 'string'],
],
],
'process' => [
'first' => [
'plugin' => 'default_value',
'default_value' => 'something outside of sub_process',
],
'second' => [
'plugin' => 'sub_process',
'source' => 'my_array_of_arrays',
'process' => [
'prop_1' => [
[
'plugin' => 'skip_on_empty',
'source' => 'key_1',
],
// We put a process after skip_on_empty to better test skipping
// a process.
[
'plugin' => 'get',
'source' => 'key_2',
],
],
'prop_2' => 'key_2',
],
],
],
'destination' => [
'plugin' => 'config',
'config_name' => 'migrate_test.settings',
],
];
}
/**
* Test use of skip_on_empty within sub_process.
*
* @param string $method
* The method to use with skip_on_empty (row or process).
* @param array $expected_data
* The expected result of the migration.
*
* @dataProvider providerTestSubProcessSkip
*/
public function testSubProcessSkip(string $method, array $expected_data): void {
$definition = $this->getDefinition();
$definition['process']['second']['process']['prop_1'][0]['method'] = $method;
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition);
$executable = new MigrateExecutable($migration);
$result = $executable->import();
// Migration needs to succeed before further assertions are made.
$this->assertSame(MigrationInterface::RESULT_COMPLETED, $result);
// Compare with expected data.
$this->assertEquals($expected_data, \Drupal::config('migrate_test.settings')->get());
}
/**
* Data provider for testNotFoundSubProcess().
*
* @return array
* The data for the testNotFoundSubProcess() test.
*/
public static function providerTestSubProcessSkip(): array {
return [
'skip row' => [
'method' => 'row',
'expected_data' => [
'first' => 'something outside of sub_process',
'second' => [
[
'prop_1' => 'bar',
'prop_2' => 'bar',
],
],
],
],
'skip process' => [
'method' => 'process',
'expected_data' => [
'first' => 'something outside of sub_process',
'second' => [
[
'prop_1' => 'bar',
'prop_2' => 'bar',
],
[
'prop_2' => 'baz',
],
],
],
],
];
}
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit\Event;
use Drupal\migrate\Event\EventBase;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\migrate\Event\EventBase
* @group migrate
*/
class EventBaseTest extends UnitTestCase {
/**
* Tests getMigration method.
*
* @covers ::__construct
* @covers ::getMigration
*/
public function testGetMigration(): void {
$migration = $this->prophesize('\Drupal\migrate\Plugin\MigrationInterface')->reveal();
$message_service = $this->prophesize('\Drupal\migrate\MigrateMessageInterface')->reveal();
$event = new EventBase($migration, $message_service);
$this->assertSame($migration, $event->getMigration());
}
/**
* Tests logging a message.
*
* @covers ::__construct
* @covers ::logMessage
*/
public function testLogMessage(): void {
$migration = $this->prophesize('\Drupal\migrate\Plugin\MigrationInterface')->reveal();
$message_service = $this->prophesize('\Drupal\migrate\MigrateMessageInterface');
$event = new EventBase($migration, $message_service->reveal());
// Assert that the intended calls to the services happen.
$message_service->display('status message', 'status')->shouldBeCalledTimes(1);
$event->logMessage('status message');
$message_service->display('warning message', 'warning')->shouldBeCalledTimes(1);
$event->logMessage('warning message', 'warning');
}
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit\Event;
use Drupal\migrate\Event\MigrateImportEvent;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\migrate\Event\MigrateImportEvent
* @group migrate
*/
class MigrateImportEventTest extends UnitTestCase {
/**
* Tests getMigration method.
*
* @covers ::__construct
* @covers ::getMigration
*/
public function testGetMigration(): void {
$migration = $this->prophesize('\Drupal\migrate\Plugin\MigrationInterface')->reveal();
$message_service = $this->prophesize('\Drupal\migrate\MigrateMessageInterface')->reveal();
$event = new MigrateImportEvent($migration, $message_service);
$this->assertSame($migration, $event->getMigration());
}
/**
* Tests logging a message.
*
* @covers ::__construct
* @covers ::logMessage
*/
public function testLogMessage(): void {
$migration = $this->prophesize('\Drupal\migrate\Plugin\MigrationInterface');
$message_service = $this->prophesize('\Drupal\migrate\MigrateMessageInterface');
$event = new MigrateImportEvent($migration->reveal(), $message_service->reveal());
// Assert that the intended calls to the services happen.
$message_service->display('status message', 'status')->shouldBeCalledTimes(1);
$event->logMessage('status message');
$message_service->display('warning message', 'warning')->shouldBeCalledTimes(1);
$event->logMessage('warning message', 'warning');
}
}

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit\Event;
use Drupal\migrate\Event\MigratePostRowSaveEvent;
/**
* @coversDefaultClass \Drupal\migrate\Event\MigratePostRowSaveEvent
* @group migrate
*/
class MigratePostRowSaveEventTest extends EventBaseTest {
/**
* Tests getDestinationIdValues method.
*
* @covers ::__construct
* @covers ::getDestinationIdValues
*/
public function testGetDestinationIdValues(): void {
$migration = $this->prophesize('\Drupal\migrate\Plugin\MigrationInterface')->reveal();
$message_service = $this->prophesize('\Drupal\migrate\MigrateMessageInterface')->reveal();
$row = $this->prophesize('\Drupal\migrate\Row')->reveal();
$event = new MigratePostRowSaveEvent($migration, $message_service, $row, [1, 2, 3]);
$this->assertSame([1, 2, 3], $event->getDestinationIdValues());
}
/**
* Tests getRow method.
*
* @covers ::__construct
* @covers ::getRow
*/
public function testGetRow(): void {
$migration = $this->prophesize('\Drupal\migrate\Plugin\MigrationInterface')->reveal();
$message_service = $this->prophesize('\Drupal\migrate\MigrateMessageInterface');
$row = $this->prophesize('\Drupal\migrate\Row')->reveal();
$event = new MigratePostRowSaveEvent($migration, $message_service->reveal(), $row, [1, 2, 3]);
$this->assertSame($row, $event->getRow());
}
}

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit\Event;
use Drupal\migrate\Event\MigratePreRowSaveEvent;
/**
* @coversDefaultClass \Drupal\migrate\Event\MigratePreRowSaveEvent
* @group migrate
*/
class MigratePreRowSaveEventTest extends EventBaseTest {
/**
* Tests getRow method.
*
* @covers ::__construct
* @covers ::getRow
*/
public function testGetRow(): void {
$migration = $this->prophesize('\Drupal\migrate\Plugin\MigrationInterface')->reveal();
$message_service = $this->prophesize('\Drupal\migrate\MigrateMessageInterface')->reveal();
$row = $this->prophesize('\Drupal\migrate\Row')->reveal();
$event = new MigratePreRowSaveEvent($migration, $message_service, $row);
$this->assertSame($row, $event->getRow());
}
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit\Exception;
use Drupal\migrate\MigrateSkipProcessException;
use Drupal\Tests\UnitTestCase;
/**
* Tests deprecation error on MigrateSkipProcessException.
*
* @group legacy
*/
class MigrateSkipProcessExceptionTest extends UnitTestCase {
/**
* Tests a deprecation error is triggered on throw.
*/
public function testDeprecation(): void {
$this->expectException(MigrateSkipProcessException::class);
$this->expectDeprecation("Unsilenced deprecation: " . MigrateSkipProcessException::class . " is deprecated in drupal:10.3.0 and is removed from drupal:12.0.0. Return TRUE from a process plugin's isPipelineStopped() method to halt further processing on a pipeline. See https://www.drupal.org/node/3414511");
throw new MigrateSkipProcessException();
}
}

View File

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit\Exception;
use Drupal\migrate\Exception\RequirementsException;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\migrate\Exception\RequirementsException
* @group migrate
*/
class RequirementsExceptionTest extends UnitTestCase {
protected const MISSING_REQUIREMENTS = ['random_jackson_pivot', 'exoplanet'];
/**
* @covers ::getRequirements
*/
public function testGetRequirements(): void {
$exception = new RequirementsException('Missing requirements ', ['requirements' => static::MISSING_REQUIREMENTS]);
$this->assertEquals(['requirements' => static::MISSING_REQUIREMENTS], $exception->getRequirements());
}
/**
* @covers ::getRequirementsString
* @dataProvider getRequirementsProvider
*/
public function testGetExceptionString($expected, $message, $requirements): void {
$exception = new RequirementsException($message, $requirements);
$this->assertEquals($expected, $exception->getRequirementsString());
}
/**
* Provides a list of requirements to test.
*/
public static function getRequirementsProvider() {
return [
[
'requirements: random_jackson_pivot.',
'Single Requirement',
['requirements' => static::MISSING_REQUIREMENTS[0]],
],
[
'requirements: random_jackson_pivot. requirements: exoplanet.',
'Multiple Requirements',
['requirements' => static::MISSING_REQUIREMENTS],
],
];
}
}

View File

@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit;
use Prophecy\Argument;
/**
* Tests the \Drupal\migrate\MigrateExecutable::memoryExceeded() method.
*
* @group migrate
*/
class MigrateExecutableMemoryExceededTest extends MigrateTestCase {
/**
* The mocked migration entity.
*
* @var \Drupal\migrate\Plugin\MigrationInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected $migration;
/**
* The mocked migrate message.
*
* @var \Drupal\migrate\MigrateMessageInterface|\Prophecy\Prophecy\ObjectProphecy
*/
protected $message;
/**
* The tested migrate executable.
*
* @var \Drupal\Tests\migrate\Unit\TestMigrateExecutable
*/
protected $executable;
/**
* The migration configuration, initialized to set the ID to test.
*
* @var array
*/
protected $migrationConfiguration = [
'id' => 'test',
];
/**
* The php.ini memory_limit value.
*
* @var int
*/
protected $memoryLimit = 10000000;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->migration = $this->getMigration();
$this->message = $this->prophesize('Drupal\migrate\MigrateMessageInterface');
$this->executable = new TestMigrateExecutable($this->migration, $this->message->reveal());
$this->executable->setStringTranslation($this->getStringTranslationStub());
}
/**
* Runs the actual test.
*
* @param string $message
* The second message to assert.
* @param bool $memory_exceeded
* Whether to test the memory exceeded case.
* @param int|null $memory_usage_first
* (optional) The first memory usage value. Defaults to NULL.
* @param int|null $memory_usage_second
* (optional) The fake amount of memory usage reported after memory reclaim.
* Defaults to NULL.
* @param int|null $memory_limit
* (optional) The memory limit. Defaults to NULL.
*/
protected function runMemoryExceededTest($message, $memory_exceeded, $memory_usage_first = NULL, $memory_usage_second = NULL, $memory_limit = NULL): void {
$this->executable->setMemoryLimit($memory_limit ?: $this->memoryLimit);
$this->executable->setMemoryUsage($memory_usage_first ?: $this->memoryLimit, $memory_usage_second ?: $this->memoryLimit);
$this->executable->setMemoryThreshold(0.85);
if ($message) {
$this->message->display(Argument::that(fn(string $subject) => str_contains($subject, 'reclaiming memory')), 'warning')
->shouldBeCalledOnce();
$this->message->display(Argument::that(fn(string $subject) => str_contains($subject, $message)), 'warning')
->shouldBeCalledOnce();
}
else {
$this->message->display(Argument::cetera())
->shouldNotBeCalled();
}
$result = $this->executable->memoryExceeded();
$this->assertEquals($memory_exceeded, $result);
}
/**
* Tests memoryExceeded method when a new batch is needed.
*/
public function testMemoryExceededNewBatch(): void {
// First case try reset and then start new batch.
$this->runMemoryExceededTest('starting new batch', TRUE);
}
/**
* Tests memoryExceeded method when enough is cleared.
*/
public function testMemoryExceededClearedEnough(): void {
$this->runMemoryExceededTest('reclaimed enough', FALSE, $this->memoryLimit, $this->memoryLimit * 0.75);
}
/**
* Tests memoryExceeded when memory usage is not exceeded.
*/
public function testMemoryNotExceeded(): void {
$this->runMemoryExceededTest('', FALSE, floor($this->memoryLimit * 0.85) - 1);
}
}

View File

@ -0,0 +1,637 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit;
use Drupal\Component\Utility\Html;
use Drupal\migrate\Plugin\MigrateDestinationInterface;
use Drupal\migrate\Plugin\MigrateProcessInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Row;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
/**
* @coversDefaultClass \Drupal\migrate\MigrateExecutable
* @group migrate
*/
class MigrateExecutableTest extends MigrateTestCase {
/**
* Stores ID map records of the ID map plugin from ::getTestRollbackIdMap.
*
* @var string[][]
*/
protected static $idMapRecords;
/**
* The mocked migration entity.
*
* @var \Drupal\migrate\Plugin\MigrationInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected $migration;
/**
* The mocked migrate message.
*
* @var \Drupal\migrate\MigrateMessageInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected $message;
/**
* The tested migrate executable.
*
* @var \Drupal\Tests\migrate\Unit\TestMigrateExecutable
*/
protected $executable;
/**
* A mocked event dispatcher.
*
* @var \Symfony\Contracts\EventDispatcher\EventDispatcherInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected $eventDispatcher;
/**
* The migration's configuration values.
*
* @var array
*/
protected $migrationConfiguration = [
'id' => 'test',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
static::$idMapRecords = [];
$this->migration = $this->getMigration();
$this->message = $this->createMock('Drupal\migrate\MigrateMessageInterface');
$this->eventDispatcher = $this->createMock('Symfony\Contracts\EventDispatcher\EventDispatcherInterface');
$this->executable = new TestMigrateExecutable($this->migration, $this->message, $this->eventDispatcher);
$this->executable->setStringTranslation($this->getStringTranslationStub());
}
/**
* Tests an import with an incomplete rewinding.
*/
public function testImportWithFailingRewind(): void {
$exception_message = $this->getRandomGenerator()->string();
$source = $this->createMock('Drupal\migrate\Plugin\MigrateSourceInterface');
$source->expects($this->once())
->method('rewind')
->will($this->throwException(new \Exception($exception_message)));
// The exception message contains the line number where it is thrown. Save
// it for the testing the exception message.
$line = (__LINE__) - 3;
$this->migration->expects($this->any())
->method('getSourcePlugin')
->willReturn($source);
// Ensure that a message with the proper message was added.
$exception_message .= " in " . __FILE__ . " line $line";
$this->message->expects($this->once())
->method('display')
->with("Migration failed with source plugin exception: " . Html::escape($exception_message));
$result = $this->executable->import();
$this->assertEquals(MigrationInterface::RESULT_FAILED, $result);
}
/**
* Tests the import method with a valid row.
*/
public function testImportWithValidRow(): void {
$source = $this->getMockSource();
$this->executable->setSource($source);
$this->migration->expects($this->once())
->method('getProcessPlugins')
->willReturn([]);
$destination = $this->createMock('Drupal\migrate\Plugin\MigrateDestinationInterface');
$this->migration
->method('getDestinationPlugin')
->willReturn($destination);
$this->assertSame(MigrationInterface::RESULT_COMPLETED, $this->executable->import());
}
/**
* Tests the import method with a valid row.
*/
public function testImportWithValidRowWithoutDestinationId(): void {
$source = $this->getMockSource();
$this->executable->setSource($source);
$this->migration->expects($this->once())
->method('getProcessPlugins')
->willReturn([]);
$destination = $this->createMock('Drupal\migrate\Plugin\MigrateDestinationInterface');
$this->migration
->method('getDestinationPlugin')
->willReturn($destination);
$this->idMap->expects($this->never())
->method('saveIdMapping');
$this->assertSame(MigrationInterface::RESULT_COMPLETED, $this->executable->import());
}
/**
* Tests the import method with a valid row.
*/
public function testImportWithValidRowNoDestinationValues(): void {
$source = $this->getMockSource();
$this->executable->setSource($source);
$this->migration->expects($this->once())
->method('getProcessPlugins')
->willReturn([]);
$destination = $this->createMock('Drupal\migrate\Plugin\MigrateDestinationInterface');
$this->migration
->method('getDestinationPlugin')
->willReturn($destination);
$this->assertSame(MigrationInterface::RESULT_COMPLETED, $this->executable->import());
}
/**
* Tests the import method with a thrown MigrateException.
*
* The MigrationException in this case is being thrown from the destination.
*/
public function testImportWithValidRowWithDestinationMigrateException(): void {
$source = $this->getMockSource();
$this->executable->setSource($source);
$this->migration->expects($this->once())
->method('getProcessPlugins')
->willReturn([]);
$destination = $this->createMock('Drupal\migrate\Plugin\MigrateDestinationInterface');
$this->migration
->method('getDestinationPlugin')
->willReturn($destination);
$this->assertSame(MigrationInterface::RESULT_COMPLETED, $this->executable->import());
}
/**
* Tests the import method with a thrown MigrateException.
*
* The MigrationException in this case is being thrown from a process plugin.
*/
public function testImportWithValidRowWithProcesMigrateException(): void {
$exception_message = $this->getRandomGenerator()->string();
$source = $this->getMockSource();
$row = $this->getMockBuilder('Drupal\migrate\Row')
->disableOriginalConstructor()
->getMock();
$row->expects($this->once())
->method('getSourceIdValues')
->willReturn(['id' => 'test']);
$source->expects($this->once())
->method('current')
->willReturn($row);
$this->executable->setSource($source);
$this->migration->expects($this->once())
->method('getProcessPlugins')
->willThrowException(new MigrateException($exception_message));
$destination = $this->createMock('Drupal\migrate\Plugin\MigrateDestinationInterface');
$destination->expects($this->never())
->method('import');
$this->migration
->method('getDestinationPlugin')
->willReturn($destination);
$this->idMap->expects($this->once())
->method('saveIdMapping')
->with($row, [], MigrateIdMapInterface::STATUS_FAILED, NULL);
$this->idMap->expects($this->once())
->method('saveMessage');
$this->idMap->expects($this->never())
->method('lookupDestinationIds');
$this->assertSame(MigrationInterface::RESULT_COMPLETED, $this->executable->import());
}
/**
* Tests the import method with a regular Exception being thrown.
*/
public function testImportWithValidRowWithException(): void {
$source = $this->getMockSource();
$this->executable->setSource($source);
$this->migration->expects($this->once())
->method('getProcessPlugins')
->willReturn([]);
$destination = $this->createMock('Drupal\migrate\Plugin\MigrateDestinationInterface');
$this->migration
->method('getDestinationPlugin')
->willReturn($destination);
$this->assertSame(MigrationInterface::RESULT_COMPLETED, $this->executable->import());
}
/**
* Tests the processRow method.
*/
public function testProcessRow(): void {
$expected = [
'test' => 'test destination',
'test1' => 'test1 destination',
];
foreach ($expected as $key => $value) {
$plugins[$key][0] = $this->createMock('Drupal\migrate\Plugin\MigrateProcessInterface');
$plugins[$key][0]->expects($this->once())
->method('getPluginDefinition')
->willReturn([]);
$plugins[$key][0]->expects($this->once())
->method('transform')
->willReturn($value);
}
$this->migration->expects($this->once())
->method('getProcessPlugins')
->with(NULL)
->willReturn($plugins);
$row = new Row();
$this->executable->processRow($row);
foreach ($expected as $key => $value) {
$this->assertSame($row->getDestinationProperty($key), $value);
}
$this->assertSameSize($expected, $row->getDestination());
}
/**
* Tests the processRow method with an empty pipeline.
*/
public function testProcessRowEmptyPipeline(): void {
$this->migration->expects($this->once())
->method('getProcessPlugins')
->with(NULL)
->willReturn(['test' => []]);
$row = new Row();
$this->executable->processRow($row);
$this->assertSame($row->getDestination(), []);
}
/**
* Tests the processRow pipeline exception.
*/
public function testProcessRowPipelineException(): void {
$row = new Row();
$plugin = $this->prophesize(MigrateProcessInterface::class);
$plugin->getPluginDefinition()->willReturn(['handle_multiples' => FALSE]);
$plugin->transform(NULL, $this->executable, $row, 'destination_id')
->willReturn('transform_return_string');
$plugin->multiple()->willReturn(TRUE);
$plugin->getPluginId()->willReturn('plugin_id');
$plugin->reset()->shouldBeCalled();
$plugin->isPipelineStopped()->willReturn(FALSE);
$plugin = $plugin->reveal();
$plugins['destination_id'] = [$plugin, $plugin];
$this->migration->method('getProcessPlugins')->willReturn($plugins);
$this->expectException(MigrateException::class);
$this->expectExceptionMessage('Pipeline failed at plugin_id plugin for destination destination_id: transform_return_string received instead of an array,');
$this->executable->processRow($row);
}
/**
* Tests a plugin which stops the pipeline.
*/
public function testStopPipeline(): void {
$row = new Row();
// Prophesize a plugin that stops the pipeline and returns 'first_plugin'.
$stop_plugin = $this->prophesize(MigrateProcessInterface::class);
$stop_plugin->getPluginDefinition()->willReturn(['handle_multiples' => FALSE]);
$stop_plugin->transform(NULL, $this->executable, $row, 'destination_id')
->willReturn('first_plugin');
$stop_plugin->multiple()->willReturn(FALSE);
$stop_plugin->reset()->shouldBeCalled();
$stop_plugin->isPipelineStopped()->willReturn(TRUE);
// Prophesize a plugin that transforms 'first_plugin' to 'final_plugin'.
$final_plugin = $this->prophesize(MigrateProcessInterface::class);
$final_plugin->getPluginDefinition()->willReturn(['handle_multiples' => FALSE]);
$final_plugin->transform('first_plugin', $this->executable, $row, 'destination_id')
->willReturn('final_plugin');
$plugins['destination_id'] = [$stop_plugin->reveal(), $final_plugin->reveal()];
$this->migration->method('getProcessPlugins')->willReturn($plugins);
// Process the row and confirm that destination value is 'first_plugin'.
$this->executable->processRow($row);
$this->assertEquals('first_plugin', $row->getDestinationProperty('destination_id'));
}
/**
* Tests a plugin which does not stop the pipeline.
*/
public function testContinuePipeline(): void {
$row = new Row();
// Prophesize a plugin that does not stop the pipeline.
$continue_plugin = $this->prophesize(MigrateProcessInterface::class);
$continue_plugin->getPluginDefinition()->willReturn(['handle_multiples' => FALSE]);
$continue_plugin->transform(NULL, $this->executable, $row, 'destination_id')
->willReturn('first_plugin');
$continue_plugin->multiple()->willReturn(FALSE);
$continue_plugin->reset()->shouldBeCalled();
$continue_plugin->isPipelineStopped()->willReturn(FALSE);
// Prophesize a plugin that transforms 'first_plugin' to 'final_plugin'.
$final_plugin = $this->prophesize(MigrateProcessInterface::class);
$final_plugin->getPluginDefinition()->willReturn(['handle_multiples' => FALSE]);
$final_plugin->transform('first_plugin', $this->executable, $row, 'destination_id')
->willReturn('final_plugin');
$final_plugin->multiple()->willReturn(FALSE);
$final_plugin->reset()->shouldBeCalled();
$final_plugin->isPipelineStopped()->willReturn(FALSE);
$plugins['destination_id'] = [$continue_plugin->reveal(), $final_plugin->reveal()];
$this->migration->method('getProcessPlugins')->willReturn($plugins);
// Process the row and confirm that the destination value is 'final_plugin'.
$this->executable->processRow($row);
$this->assertEquals('final_plugin', $row->getDestinationProperty('destination_id'));
}
/**
* Tests the processRow method.
*/
public function testProcessRowEmptyDestination(): void {
$expected = [
'test' => 'test destination',
'test1' => 'test1 destination',
'test2' => NULL,
];
$row = new Row();
$plugins = [];
foreach ($expected as $key => $value) {
$plugin = $this->prophesize(MigrateProcessInterface::class);
$plugin->getPluginDefinition()->willReturn([]);
$plugin->transform(NULL, $this->executable, $row, $key)->willReturn($value);
$plugin->multiple()->willReturn(TRUE);
$plugin->reset()->shouldBeCalled();
$plugin->isPipelineStopped()->willReturn(FALSE);
$plugins[$key][0] = $plugin->reveal();
}
$this->migration->method('getProcessPlugins')->willReturn($plugins);
$this->executable->processRow($row);
foreach ($expected as $key => $value) {
$this->assertSame($value, $row->getDestinationProperty($key));
}
$this->assertCount(2, $row->getDestination());
$this->assertSame(['test2'], $row->getEmptyDestinationProperties());
}
/**
* Returns a mock migration source instance.
*
* @return \Drupal\migrate\Plugin\MigrateSourceInterface|\PHPUnit\Framework\MockObject\MockObject
* The mocked migration source.
*/
protected function getMockSource() {
$source = $this->createMock(StubSourcePlugin::class);
$source->expects($this->once())
->method('rewind');
$source->expects($this->any())
->method('valid')
->willReturn(TRUE, FALSE);
return $source;
}
/**
* Tests rollback.
*
* @param array[] $id_map_records
* The ID map records to test with.
* @param bool $rollback_called
* Sets an expectation that the destination's rollback() will or will not be
* called.
* @param string[] $source_id_keys
* The keys of the source IDs. The provided source ID keys must be defined
* in the $id_map_records parameter. Optional, defaults to ['source'].
* @param string[] $destination_id_keys
* The keys of the destination IDs. The provided keys must be defined in the
* $id_map_records parameter. Optional, defaults to ['destination'].
* @param int $expected_result
* The expected result of the rollback action. Optional, defaults to
* MigrationInterface::RESULT_COMPLETED.
*
* @dataProvider providerTestRollback
*
* @covers ::rollback
*/
public function testRollback(array $id_map_records, bool $rollback_called = TRUE, array $source_id_keys = ['source'], array $destination_id_keys = ['destination'], int $expected_result = MigrationInterface::RESULT_COMPLETED): void {
$id_map = $this
->getTestRollbackIdMap($id_map_records, $source_id_keys, $destination_id_keys)
->reveal();
$migration = $this->getMigration($id_map);
$destination = $this->prophesize(MigrateDestinationInterface::class);
if ($rollback_called) {
$destination->rollback($id_map->currentDestination())->shouldBeCalled();
}
else {
$destination->rollback()->shouldNotBeCalled();
}
$migration
->method('getDestinationPlugin')
->willReturn($destination->reveal());
$executable = new TestMigrateExecutable($migration, $this->message, $this->eventDispatcher);
$this->assertEquals($expected_result, $executable->rollback());
}
/**
* Data provider for ::testRollback.
*
* @return array
* The test cases.
*/
public static function providerTestRollback() {
return [
'Rollback delete' => [
'id_map_records' => [
[
'source' => '1',
'destination' => '1',
'rollback_action' => MigrateIdMapInterface::ROLLBACK_DELETE,
],
],
],
'Rollback preserve' => [
'id_map_records' => [
[
'source' => '1',
'destination' => '1',
'rollback_action' => MigrateIdMapInterface::ROLLBACK_PRESERVE,
],
],
'rollback_called' => FALSE,
],
'Rolling back a failed row' => [
'id_map_records' => [
[
'source' => '1',
'destination' => NULL,
'source_row_status' => '2',
'rollback_action' => MigrateIdMapInterface::ROLLBACK_DELETE,
],
],
'rollback_called' => FALSE,
],
'Rolling back with ID map having records with duplicated destination ID' => [
'id_map_records' => [
[
'source_1' => '1',
'source_2' => '1',
'destination' => '1',
'rollback_action' => MigrateIdMapInterface::ROLLBACK_DELETE,
],
[
'source_1' => '2',
'source_2' => '2',
'destination' => '2',
'rollback_action' => MigrateIdMapInterface::ROLLBACK_PRESERVE,
],
[
'source_1' => '3',
'source_2' => '3',
'destination' => '1',
'rollback_action' => MigrateIdMapInterface::ROLLBACK_DELETE,
],
],
'rollback_called' => TRUE,
'source_id_keys' => ['source_1', 'source_2'],
],
'Rollback NULL' => [
'id_map_records' => [
[
'source' => '1',
'destination' => '1',
'rollback_action' => NULL,
],
],
],
'Rollback missing' => [
'id_map_records' => [
[
'source' => '1',
'destination' => '1',
],
],
],
];
}
/**
* Returns an ID map object prophecy used in ::testRollback.
*
* @return \Prophecy\Prophecy\ObjectProphecy<\Drupal\migrate\Plugin\MigrateIdMapInterface>
* An ID map object prophecy.
*/
public function getTestRollbackIdMap(array $items, array $source_id_keys, array $destination_id_keys): ObjectProphecy {
static::$idMapRecords = array_map(function (array $item) {
return $item + [
'source_row_status' => '0',
'rollback_action' => '0',
'last_imported' => '0',
'hash' => '',
];
}, $items);
$array_iterator = new \ArrayIterator(static::$idMapRecords);
$id_map = $this->prophesize(MigrateIdMapInterface::class);
$id_map->setMessage(Argument::cetera())->willReturn(NULL);
$id_map->rewind()->will(function () use ($array_iterator) {
$array_iterator->rewind();
});
$id_map->valid()->will(function () use ($array_iterator) {
return $array_iterator->valid();
});
$id_map->next()->will(function () use ($array_iterator) {
$array_iterator->next();
});
$id_map->currentDestination()->will(function () use ($array_iterator, $destination_id_keys) {
$current = $array_iterator->current();
$destination_values = array_filter($current, function ($key) use ($destination_id_keys) {
return in_array($key, $destination_id_keys, TRUE);
}, ARRAY_FILTER_USE_KEY);
return empty(array_filter($destination_values, 'is_null'))
? array_combine($destination_id_keys, array_values($destination_values))
: NULL;
});
$id_map->currentSource()->will(function () use ($array_iterator, $source_id_keys) {
$current = $array_iterator->current();
$source_values = array_filter($current, function ($key) use ($source_id_keys) {
return in_array($key, $source_id_keys, TRUE);
}, ARRAY_FILTER_USE_KEY);
return empty(array_filter($source_values, 'is_null'))
? array_combine($source_id_keys, array_values($source_values))
: NULL;
});
$id_map->getRowByDestination(Argument::type('array'))->will(function () {
$destination_ids = func_get_args()[0][0];
$return = array_reduce(self::$idMapRecords, function (array $carry, array $record) use ($destination_ids) {
if (array_merge($record, $destination_ids) === $record) {
$carry = $record;
}
return $carry;
}, []);
return $return;
});
$id_map->deleteDestination(Argument::type('array'))->will(function () {
$destination_ids = func_get_args()[0][0];
$matching_records = array_filter(self::$idMapRecords, function (array $record) use ($destination_ids) {
return array_merge($record, $destination_ids) === $record;
});
foreach (array_keys($matching_records) as $record_key) {
unset(self::$idMapRecords[$record_key]);
}
});
$id_map->delete(Argument::type('array'))->will(function () {
$source_ids = func_get_args()[0][0];
$matching_records = array_filter(self::$idMapRecords, function (array $record) use ($source_ids) {
return array_merge($record, $source_ids) === $record;
});
foreach (array_keys($matching_records) as $record_key) {
unset(self::$idMapRecords[$record_key]);
}
});
return $id_map;
}
}

View File

@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\migrate\MigrateLookup;
use Drupal\migrate\Plugin\MigrateDestinationInterface;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Plugin\MigrationPluginManagerInterface;
/**
* Provides unit testing for the migration lookup service.
*
* @group migrate
*
* @coversDefaultClass \Drupal\migrate\MigrateLookup
*/
class MigrateLookupTest extends MigrateTestCase {
/**
* Tests the lookup function.
*
* @covers ::lookup
*/
public function testLookup(): void {
$source_ids = ['id' => '1'];
$destination_ids = [[2]];
$id_map = $this->prophesize(MigrateIdMapInterface::class);
$id_map->lookupDestinationIds($source_ids)->willReturn($destination_ids);
$destination = $this->prophesize(MigrateDestinationInterface::class);
$destination->getIds()->willReturn(['id' => ['type' => 'integer']]);
$migration = $this->prophesize(MigrationInterface::class);
$migration->getIdMap()->willReturn($id_map->reveal());
$migration->getDestinationPlugin()->willReturn($destination->reveal());
$plugin_manager = $this->prophesize(MigrationPluginManagerInterface::class);
$plugin_manager->createInstances('test_migration')->willReturn([$migration->reveal()]);
$lookup = new MigrateLookup($plugin_manager->reveal());
$this->assertSame([['id' => 2]], $lookup->lookup('test_migration', $source_ids));
}
/**
* Tests message logged when a single migration is not found.
*
* @dataProvider providerExceptionOnMigrationNotFound
*/
public function testExceptionOnMigrationNotFound($migrations, $message): void {
$migration_plugin_manager = $this->prophesize(MigrationPluginManagerInterface::class);
$migration_plugin_manager->createInstances($migrations)->willReturn([]);
$this->expectException(PluginNotFoundException::class);
$this->expectExceptionMessage($message);
$lookup = new MigrateLookup($migration_plugin_manager->reveal());
$lookup->lookup($migrations, [1]);
}
/**
* Provides data for testExceptionOnMigrationNotFound.
*/
public static function providerExceptionOnMigrationNotFound() {
return [
'string' => [
'bad_plugin',
"Plugin ID 'bad_plugin' was not found.",
],
'array one item' => [
['bad_plugin'],
"Plugin ID 'bad_plugin' was not found.",
],
];
}
/**
* Tests message logged when multiple migrations are not found.
*
* @dataProvider providerExceptionOnMultipleMigrationsNotFound
*/
public function testExceptionOnMultipleMigrationsNotFound($migrations, $message): void {
$migration_plugin_manager = $this->prophesize(MigrationPluginManagerInterface::class);
$migration_plugin_manager->createInstances($migrations)->willReturn([]);
$this->expectException(PluginException::class);
$this->expectExceptionMessage($message);
$lookup = new MigrateLookup($migration_plugin_manager->reveal());
$lookup->lookup($migrations, [1]);
}
/**
* Provides data for testExceptionOnMultipleMigrationsNotFound.
*/
public static function providerExceptionOnMultipleMigrationsNotFound() {
return [
'array two items' => [
['foo', 'bar'],
"Plugin IDs 'foo', 'bar' were not found.",
],
'empty array' => [
[],
"Plugin IDs '' were not found.",
],
];
}
}

View File

@ -0,0 +1,533 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
use Drupal\migrate\Event\MigrateRollbackEvent;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateExecutable;
use Drupal\migrate\MigrateSkipRowException;
use Drupal\migrate\Plugin\migrate\source\SourcePluginBase;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Plugin\MigrateSourceInterface;
use Drupal\migrate\Row;
/**
* @coversDefaultClass \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
* @group migrate
*/
class MigrateSourceTest extends MigrateTestCase {
/**
* Override the migration config.
*
* @var array
*/
protected $defaultMigrationConfiguration = [
'id' => 'test_migration',
'source' => [],
];
/**
* Test row data.
*
* @var array
*/
protected $row = ['test_sourceid1' => '1', 'timestamp' => 500];
/**
* Test source ids.
*
* @var array
*/
protected $sourceIds = ['test_sourceid1' => 'test_sourceid1'];
/**
* The migration entity.
*
* @var \Drupal\migrate\Plugin\MigrationInterface
*/
protected $migration;
/**
* The migrate executable.
*
* @var \Drupal\migrate\MigrateExecutable
*/
protected $executable;
/**
* Gets the source plugin to test.
*
* @param array $configuration
* (optional) The source configuration. Defaults to an empty array.
* @param array $migrate_config
* (optional) The migration configuration to be used in
* parent::getMigration(). Defaults to an empty array.
* @param int $status
* (optional) The default status for the new rows to be imported. Defaults
* to MigrateIdMapInterface::STATUS_NEEDS_UPDATE.
* @param int $high_water_value
* (optional) The high water mark to start from, if set.
*
* @return \Drupal\migrate\Plugin\MigrateSourceInterface
* A mocked source plugin.
*/
protected function getSource($configuration = [], $migrate_config = [], $status = MigrateIdMapInterface::STATUS_NEEDS_UPDATE, $high_water_value = NULL) {
$container = new ContainerBuilder();
\Drupal::setContainer($container);
$key_value = $this->createMock(KeyValueStoreInterface::class);
$key_value_factory = $this->createMock(KeyValueFactoryInterface::class);
$key_value_factory
->method('get')
->with('migrate:high_water')
->willReturn($key_value);
$container->set('keyvalue', $key_value_factory);
$container->set('cache.migrate', $this->createMock(CacheBackendInterface::class));
$this->migrationConfiguration = $this->defaultMigrationConfiguration + $migrate_config;
$this->migration = parent::getMigration();
$this->executable = $this->getMigrateExecutable($this->migration);
// Update the idMap for Source so the default is that the row has already
// been imported. This allows us to use the highwater mark to decide on the
// outcome of whether we choose to import the row.
$id_map_array = ['original_hash' => '', 'hash' => '', 'source_row_status' => $status];
$this->idMap
->expects($this->any())
->method('getRowBySource')
->willReturn($id_map_array);
$constructor_args = [$configuration, 'd6_action', [], $this->migration];
$methods = ['getModuleHandler', 'fields', 'getIds', '__toString', 'prepareRow', 'initializeIterator'];
$source_plugin = $this->getMockBuilder(SourcePluginBase::class)
->onlyMethods($methods)
->setConstructorArgs($constructor_args)
->getMock();
$source_plugin
->method('fields')
->willReturn([]);
$source_plugin
->method('getIds')
->willReturn([]);
$source_plugin
->method('__toString')
->willReturn('');
$source_plugin
->method('prepareRow')
->willReturn(empty($migrate_config['prepare_row_false']));
$rows = [$this->row];
if (isset($configuration['high_water_property']) && isset($high_water_value)) {
$property = $configuration['high_water_property']['name'];
$rows = array_filter($rows, function (array $row) use ($property, $high_water_value) {
return $row[$property] >= $high_water_value;
});
}
$iterator = new \ArrayIterator($rows);
$source_plugin
->method('initializeIterator')
->willReturn($iterator);
$module_handler = $this->createMock(ModuleHandlerInterface::class);
$source_plugin
->method('getModuleHandler')
->willReturn($module_handler);
$this->migration
->method('getSourcePlugin')
->willReturn($source_plugin);
return $source_plugin;
}
/**
* @covers ::__construct
*/
public function testHighwaterTrackChangesIncompatible(): void {
$source_config = ['track_changes' => TRUE, 'high_water_property' => ['name' => 'something']];
$this->expectException(MigrateException::class);
$this->getSource($source_config);
}
/**
* Tests that the source count is correct.
*
* @covers ::count
*/
public function testCount(): void {
// Mock the cache to validate set() receives appropriate arguments.
$container = new ContainerBuilder();
$cache = $this->createMock(CacheBackendInterface::class);
$cache->expects($this->any())->method('set')
->with($this->isType('string'), $this->isType('int'), $this->isType('int'));
$container->set('cache.migrate', $cache);
\Drupal::setContainer($container);
// Test that the basic count works.
$source = $this->getSource();
$this->assertEquals(1, $source->count());
// Test caching the count works.
$source = $this->getSource(['cache_counts' => TRUE]);
$this->assertEquals(1, $source->count());
// Test the skip argument.
$source = $this->getSource(['skip_count' => TRUE]);
$this->assertEquals(MigrateSourceInterface::NOT_COUNTABLE, $source->count());
$this->migrationConfiguration['id'] = 'test_migration';
$migration = $this->getMigration();
$source = new StubSourceGeneratorPlugin([], '', [], $migration);
// Test the skipCount property's default value.
$this->assertEquals(MigrateSourceInterface::NOT_COUNTABLE, $source->count());
// Test the count value using a generator.
$source = new StubSourceGeneratorPlugin(['skip_count' => FALSE], '', [], $migration);
$this->assertEquals(3, $source->count());
}
/**
* Tests that the key can be set for the count cache.
*
* @covers ::count
*/
public function testCountCacheKey(): void {
// Mock the cache to validate set() receives appropriate arguments.
$container = new ContainerBuilder();
$cache = $this->createMock(CacheBackendInterface::class);
$cache->expects($this->any())->method('set')
->with('test_key', $this->isType('int'), $this->isType('int'));
$container->set('cache.migrate', $cache);
\Drupal::setContainer($container);
// Test caching the count with a configured key works.
$source = $this->getSource(['cache_counts' => TRUE, 'cache_key' => 'test_key']);
$this->assertEquals(1, $source->count());
}
/**
* Tests that we don't get a row if prepareRow() is false.
*/
public function testPrepareRowFalse(): void {
$source = $this->getSource([], ['prepare_row_false' => TRUE]);
$source->rewind();
$this->assertNull($source->current(), 'No row is available when prepareRow() is false.');
}
/**
* Tests that $row->needsUpdate() works as expected.
*/
public function testNextNeedsUpdate(): void {
$source = $this->getSource();
// $row->needsUpdate() === TRUE so we get a row.
$source->rewind();
$this->assertTrue(is_a($source->current(), 'Drupal\migrate\Row'), '$row->needsUpdate() is TRUE so we got a row.');
// Test that we don't get a row when the incoming row is marked as imported.
$source = $this->getSource([], [], MigrateIdMapInterface::STATUS_IMPORTED);
$source->rewind();
$this->assertNull($source->current(), 'Row was already imported, should be NULL');
}
/**
* Tests that an outdated highwater mark does not cause a row to be imported.
*/
public function testOutdatedHighwater(): void {
$configuration = [
'high_water_property' => [
'name' => 'timestamp',
],
];
$source = $this->getSource($configuration, [], MigrateIdMapInterface::STATUS_IMPORTED, $this->row['timestamp'] + 1);
// The current highwater mark is now higher than the row timestamp so no row
// is expected.
$source->rewind();
$this->assertNull($source->current(), 'Original highwater mark is higher than incoming row timestamp.');
}
/**
* Tests that a highwater mark newer than our saved one imports a row.
*
* @throws \Exception
*/
public function testNewHighwater(): void {
$configuration = [
'high_water_property' => [
'name' => 'timestamp',
],
];
// Set a highwater property field for source. Now we should have a row
// because the row timestamp is greater than the current highwater mark.
$source = $this->getSource($configuration, [], MigrateIdMapInterface::STATUS_IMPORTED, $this->row['timestamp'] - 1);
$source->rewind();
$this->assertInstanceOf(Row::class, $source->current());
}
/**
* Tests basic row preparation.
*
* @covers ::prepareRow
*/
public function testPrepareRow(): void {
$this->migrationConfiguration['id'] = 'test_migration';
// Get a new migration with an id.
$migration = $this->getMigration();
$source = new StubSourcePlugin([], '', [], $migration);
$row = new Row();
$module_handler = $this->prophesize(ModuleHandlerInterface::class);
$module_handler->invokeAll('migrate_prepare_row', [$row, $source, $migration])
->willReturn([TRUE, TRUE])
->shouldBeCalled();
$module_handler->invokeAll('migrate_' . $migration->id() . '_prepare_row', [$row, $source, $migration])
->willReturn([TRUE, TRUE])
->shouldBeCalled();
$source->setModuleHandler($module_handler->reveal());
// Ensure we don't log this to the mapping table.
$this->idMap->expects($this->never())
->method('saveIdMapping');
$this->assertTrue($source->prepareRow($row));
// Track_changes...
$source = new StubSourcePlugin(['track_changes' => TRUE], '', [], $migration);
$row2 = $this->prophesize(Row::class);
$row2->rehash()
->shouldBeCalled();
$module_handler->invokeAll('migrate_prepare_row', [$row2, $source, $migration])
->willReturn([TRUE, TRUE])
->shouldBeCalled();
$module_handler->invokeAll('migrate_' . $migration->id() . '_prepare_row', [$row2, $source, $migration])
->willReturn([TRUE, TRUE])
->shouldBeCalled();
$source->setModuleHandler($module_handler->reveal());
$this->assertTrue($source->prepareRow($row2->reveal()));
}
/**
* Tests that global prepare hooks can skip rows.
*
* @covers ::prepareRow
*/
public function testPrepareRowGlobalPrepareSkip(): void {
$this->migrationConfiguration['id'] = 'test_migration';
$migration = $this->getMigration();
$source = new StubSourcePlugin([], '', [], $migration);
$row = new Row();
$module_handler = $this->prophesize(ModuleHandlerInterface::class);
// Return a failure from a prepare row hook.
$module_handler->invokeAll('migrate_prepare_row', [$row, $source, $migration])
->willReturn([TRUE, FALSE, TRUE])
->shouldBeCalled();
$module_handler->invokeAll('migrate_' . $migration->id() . '_prepare_row', [$row, $source, $migration])
->willReturn([TRUE, TRUE])
->shouldBeCalled();
$source->setModuleHandler($module_handler->reveal());
$this->idMap->expects($this->once())
->method('saveIdMapping')
->with($row, [], MigrateIdMapInterface::STATUS_IGNORED);
$this->assertFalse($source->prepareRow($row));
}
/**
* Tests that migrate specific prepare hooks can skip rows.
*
* @covers ::prepareRow
*/
public function testPrepareRowMigratePrepareSkip(): void {
$this->migrationConfiguration['id'] = 'test_migration';
$migration = $this->getMigration();
$source = new StubSourcePlugin([], '', [], $migration);
$row = new Row();
$module_handler = $this->prophesize(ModuleHandlerInterface::class);
// Return a failure from a prepare row hook.
$module_handler->invokeAll('migrate_prepare_row', [$row, $source, $migration])
->willReturn([TRUE, TRUE])
->shouldBeCalled();
$module_handler->invokeAll('migrate_' . $migration->id() . '_prepare_row', [$row, $source, $migration])
->willReturn([TRUE, FALSE, TRUE])
->shouldBeCalled();
$source->setModuleHandler($module_handler->reveal());
$this->idMap->expects($this->once())
->method('saveIdMapping')
->with($row, [], MigrateIdMapInterface::STATUS_IGNORED);
$this->assertFalse($source->prepareRow($row));
}
/**
* Tests that a skip exception during prepare hooks correctly skips.
*
* @covers ::prepareRow
*/
public function testPrepareRowPrepareException(): void {
$this->migrationConfiguration['id'] = 'test_migration';
$migration = $this->getMigration();
$source = new StubSourcePlugin([], '', [], $migration);
$row = new Row();
$module_handler = $this->prophesize(ModuleHandlerInterface::class);
// Return a failure from a prepare row hook.
$module_handler->invokeAll('migrate_prepare_row', [$row, $source, $migration])
->willReturn([TRUE, TRUE])
->shouldBeCalled();
$module_handler->invokeAll('migrate_' . $migration->id() . '_prepare_row', [$row, $source, $migration])
->willThrow(new MigrateSkipRowException())
->shouldBeCalled();
$source->setModuleHandler($module_handler->reveal());
// This will only be called on the first prepare because the second
// explicitly avoids it.
$this->idMap->expects($this->once())
->method('saveIdMapping')
->with($row, [], MigrateIdMapInterface::STATUS_IGNORED);
$this->assertFalse($source->prepareRow($row));
// Throw an exception the second time that avoids mapping.
$e = new MigrateSkipRowException('', FALSE);
$module_handler->invokeAll('migrate_' . $migration->id() . '_prepare_row', [$row, $source, $migration])
->willThrow($e)
->shouldBeCalled();
$this->assertFalse($source->prepareRow($row));
}
/**
* Tests that default values are preserved for several source methods.
*/
public function testDefaultPropertiesValues(): void {
$this->migrationConfiguration['id'] = 'test_migration';
$migration = $this->getMigration();
$source = new StubSourceGeneratorPlugin([], '', [], $migration);
// Test the default value of the skipCount Value;
$this->assertTrue($source->getSkipCount());
$this->assertTrue($source->getCacheCounts());
$this->assertTrue($source->getTrackChanges());
}
/**
* Gets a mock executable for the test.
*
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration entity.
*
* @return \Drupal\migrate\MigrateExecutable
* The migrate executable.
*/
protected function getMigrateExecutable($migration) {
/** @var \Drupal\migrate\MigrateMessageInterface $message */
$message = $this->createMock('Drupal\migrate\MigrateMessageInterface');
/** @var \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $event_dispatcher */
$event_dispatcher = $this->createMock('Symfony\Contracts\EventDispatcher\EventDispatcherInterface');
return new MigrateExecutable($migration, $message, $event_dispatcher);
}
/**
* @covers ::preRollback
*/
public function testPreRollback(): void {
$this->migrationConfiguration['id'] = 'test_migration';
$plugin_id = 'test_migration';
$migration = $this->getMigration();
// Verify that preRollback() sets the high water mark to NULL.
$key_value = $this->createMock(KeyValueStoreInterface::class);
$key_value->expects($this->once())
->method('set')
->with($plugin_id, NULL);
$key_value_factory = $this->createMock(KeyValueFactoryInterface::class);
$key_value_factory->expects($this->once())
->method('get')
->with('migrate:high_water')
->willReturn($key_value);
$container = new ContainerBuilder();
$container->set('keyvalue', $key_value_factory);
\Drupal::setContainer($container);
$source = new StubSourceGeneratorPlugin([], $plugin_id, [], $migration);
$source->preRollback(new MigrateRollbackEvent($migration));
}
}
/**
* Defines a stubbed source plugin with a generator as iterator.
*
* This stub overwrites the $skipCount, $cacheCounts, and $trackChanges
* properties.
*/
class StubSourceGeneratorPlugin extends StubSourcePlugin {
/**
* {@inheritdoc}
*/
protected $skipCount = TRUE;
/**
* {@inheritdoc}
*/
protected $cacheCounts = TRUE;
/**
* {@inheritdoc}
*/
protected $trackChanges = TRUE;
/**
* Return the skipCount value.
*/
public function getSkipCount() {
return $this->skipCount;
}
/**
* Return the cacheCounts value.
*/
public function getCacheCounts() {
return $this->cacheCounts;
}
/**
* Return the trackChanges value.
*/
public function getTrackChanges() {
return $this->trackChanges;
}
/**
* {@inheritdoc}
*/
protected function initializeIterator(): \Generator {
yield 'foo';
yield 'bar';
yield 'iggy';
}
}

View File

@ -0,0 +1,212 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
/**
* Tests the SQL ID map plugin ensureTables() method.
*
* @group migrate
*/
class MigrateSqlIdMapEnsureTablesTest extends MigrateTestCase {
/**
* The migration configuration, initialized to set the ID and destination IDs.
*
* @var array
*/
protected $migrationConfiguration = [
'id' => 'sql_idmap_test',
];
/**
* Tests the ensureTables method when the tables do not exist.
*/
public function testEnsureTablesNotExist(): void {
$fields['source_ids_hash'] = [
'type' => 'varchar',
'length' => 64,
'not null' => 1,
'description' => 'Hash of source ids. Used as primary key',
];
$fields['sourceid1'] = [
'type' => 'int',
'not null' => TRUE,
];
$fields['sourceid2'] = [
'type' => 'int',
'not null' => TRUE,
];
$fields['destid1'] = [
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
];
$fields['source_row_status'] = [
'type' => 'int',
'size' => 'tiny',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => MigrateIdMapInterface::STATUS_IMPORTED,
'description' => 'Indicates current status of the source row',
];
$fields['rollback_action'] = [
'type' => 'int',
'size' => 'tiny',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => MigrateIdMapInterface::ROLLBACK_DELETE,
'description' => 'Flag indicating what to do for this item on rollback',
];
$fields['last_imported'] = [
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
'description' => 'UNIX timestamp of the last time this row was imported',
'size' => 'big',
];
$fields['hash'] = [
'type' => 'varchar',
'length' => '64',
'not null' => FALSE,
'description' => 'Hash of source row data, for detecting changes',
];
$map_table_schema = [
'description' => 'Mappings from source identifier value(s) to destination identifier value(s).',
'fields' => $fields,
'primary key' => ['source_ids_hash'],
'indexes' => [
'source' => ['sourceid1', 'sourceid2'],
],
];
// Now do the message table.
$fields = [];
$fields['msgid'] = [
'type' => 'serial',
'unsigned' => TRUE,
'not null' => TRUE,
];
$fields['source_ids_hash'] = [
'type' => 'varchar',
'length' => 64,
'not null' => 1,
'description' => 'Hash of source ids. Used as primary key',
];
$fields['level'] = [
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 1,
];
$fields['message'] = [
'type' => 'text',
'size' => 'medium',
'not null' => TRUE,
];
$table_schema = [
'description' => 'Messages generated during a migration process',
'fields' => $fields,
'primary key' => ['msgid'],
'indexes' => [
'source_ids_hash' => ['source_ids_hash'],
],
];
$schema = $this->prophesize('Drupal\Core\Database\Schema');
$schema->tableExists('migrate_map_sql_idmap_test')->willReturn(FALSE);
$schema->tableExists('migrate_message_sql_idmap_test')->willReturn(FALSE);
$schema->createTable('migrate_map_sql_idmap_test', $map_table_schema)->shouldBeCalled();
$schema->createTable('migrate_message_sql_idmap_test', $table_schema)->shouldBeCalled();
$this->runEnsureTablesTest($schema->reveal());
}
/**
* Tests the ensureTables method when the tables exist.
*/
public function testEnsureTablesExist(): void {
$schema = $this->prophesize('Drupal\Core\Database\Schema');
$schema->tableExists('migrate_map_sql_idmap_test')->willReturn(TRUE);
$schema->fieldExists('migrate_map_sql_idmap_test', 'rollback_action')->willReturn(FALSE);
$schema->fieldExists('migrate_map_sql_idmap_test', 'hash')->willReturn(FALSE);
$schema->fieldExists('migrate_map_sql_idmap_test', 'source_ids_hash')->willReturn(FALSE);
$schema->addField('migrate_map_sql_idmap_test', 'rollback_action', [
'type' => 'int',
'size' => 'tiny',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
'description' => 'Flag indicating what to do for this item on rollback',
])->shouldBeCalled();
$schema->addField('migrate_map_sql_idmap_test', 'hash', [
'type' => 'varchar',
'length' => '64',
'not null' => FALSE,
'description' => 'Hash of source row data, for detecting changes',
])->shouldBeCalled();
$schema->addField('migrate_map_sql_idmap_test', 'source_ids_hash', [
'type' => 'varchar',
'length' => '64',
'not null' => TRUE,
'description' => 'Hash of source ids. Used as primary key',
])->shouldBeCalled();
$this->runEnsureTablesTest($schema->reveal());
}
/**
* Actually run the test.
*
* @param array $schema
* The mock schema object with expectations set. The Sql constructor calls
* ensureTables() which in turn calls this object and the expectations on
* it are the actual test and there are no additional asserts added.
*/
protected function runEnsureTablesTest($schema): void {
$database = $this->getMockBuilder('Drupal\Core\Database\Connection')
->disableOriginalConstructor()
->getMock();
$database->expects($this->any())
->method('schema')
->willReturn($schema);
$database->expects($this->any())
->method('getPrefix')
->willReturn('');
$migration = $this->getMigration();
$plugin = $this->createMock('Drupal\migrate\Plugin\MigrateSourceInterface');
$plugin->expects($this->any())
->method('getIds')
->willReturn([
'source_id_property' => [
'type' => 'integer',
],
'source_id_property_2' => [
'type' => 'integer',
],
]);
$migration->expects($this->any())
->method('getSourcePlugin')
->willReturn($plugin);
$plugin = $this->createMock('Drupal\migrate\Plugin\MigrateSourceInterface');
$plugin->expects($this->any())
->method('getIds')
->willReturn([
'destination_id_property' => [
'type' => 'string',
],
]);
$migration->expects($this->any())
->method('getDestinationPlugin')
->willReturn($plugin);
/** @var \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $event_dispatcher */
$event_dispatcher = $this->createMock('Symfony\Contracts\EventDispatcher\EventDispatcherInterface');
$migration_manager = $this->createMock('Drupal\migrate\Plugin\MigrationPluginManagerInterface');
$map = new TestSqlIdMap($database, [], 'sql', [], $migration, $event_dispatcher, $migration_manager);
$map->getDatabase();
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Tests\UnitTestCase;
use Drupal\migrate\MigrateStub;
use Drupal\migrate\Plugin\MigrateDestinationInterface;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Plugin\MigrateSourceInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Plugin\MigrationPluginManagerInterface;
use Drupal\migrate\Row;
use Prophecy\Argument;
/**
* Tests the migrate stub service.
*
* @group migrate
*
* @coversDefaultClass \Drupal\migrate\MigrateStub
*/
class MigrateStubTest extends UnitTestCase {
/**
* The plugin manager prophecy.
*
* @var \Prophecy\Prophecy\ObjectProphecy
*/
protected $migrationPluginManager;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->migrationPluginManager = $this->prophesize(MigrationPluginManagerInterface::class);
}
/**
* Tests stubbing.
*
* @covers ::createStub
*/
public function testCreateStub(): void {
$destination_plugin = $this->prophesize(MigrateDestinationInterface::class);
$destination_plugin->import(Argument::type(Row::class))->willReturn(['id' => 2]);
$source_plugin = $this->prophesize(MigrateSourceInterface::class);
$source_plugin->getIds()->willReturn(['id' => ['type' => 'integer']]);
$id_map = $this->prophesize(MigrateIdMapInterface::class);
$migration = $this->prophesize(MigrationInterface::class);
$migration->getIdMap()->willReturn($id_map->reveal());
$migration->getDestinationPlugin(TRUE)->willReturn($destination_plugin->reveal());
$migration->getProcessPlugins([])->willReturn([]);
$migration->getProcess()->willReturn([]);
$migration->getSourceConfiguration()->willReturn([]);
$migration->getSourcePlugin()->willReturn($source_plugin->reveal());
$this->migrationPluginManager->createInstances(['test_migration'])->willReturn([$migration->reveal()]);
$stub = new MigrateStub($this->migrationPluginManager->reveal());
$this->assertSame(['id' => 2], $stub->createStub('test_migration', ['id' => 1], []));
}
/**
* Tests that an error is logged if the plugin manager throws an exception.
*/
public function testExceptionOnPluginNotFound(): void {
$this->migrationPluginManager->createInstances(['test_migration'])->willReturn([]);
$this->expectException(PluginNotFoundException::class);
$this->expectExceptionMessage("Plugin ID 'test_migration' was not found.");
$stub = new MigrateStub($this->migrationPluginManager->reveal());
$stub->createStub('test_migration', [1]);
}
/**
* Tests that an error is logged on derived migrations.
*/
public function testExceptionOnDerivedMigration(): void {
$this->migrationPluginManager->createInstances(['test_migration'])->willReturn([
'test_migration:d1' => $this->prophesize(MigrationInterface::class)->reveal(),
'test_migration:d2' => $this->prophesize(MigrationInterface::class)->reveal(),
]);
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Cannot stub derivable migration "test_migration". You must specify the id of a specific derivative to stub.');
$stub = new MigrateStub($this->migrationPluginManager->reveal());
$stub->createStub('test_migration', [1]);
}
}

View File

@ -0,0 +1,219 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit;
use Drupal\sqlite\Driver\Database\sqlite\Connection;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\Tests\UnitTestCase;
/**
* Provides setup and helper methods for Migrate module tests.
*/
abstract class MigrateTestCase extends UnitTestCase {
/**
* An array of migration configuration values.
*
* @var array
*/
protected $migrationConfiguration = [];
/**
* The migration ID map.
*
* @var \Drupal\migrate\Plugin\MigrateIdMapInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected $idMap;
/**
* Local store for mocking setStatus()/getStatus().
*
* @var int
*/
protected $migrationStatus = MigrationInterface::STATUS_IDLE;
/**
* Retrieves a mocked migration.
*
* @param \Drupal\migrate\Plugin\MigrateIdMapInterface|\PHPUnit\Framework\MockObject\MockObject|null $id_map
* An ID map plugin to use, or NULL for using a mocked one. Optional,
* defaults to NULL.
*
* @return \Drupal\migrate\Plugin\MigrationInterface|\PHPUnit\Framework\MockObject\MockObject
* The mocked migration.
*/
protected function getMigration($id_map = NULL) {
$this->migrationConfiguration += ['migrationClass' => 'Drupal\migrate\Plugin\Migration'];
$this->idMap = $id_map;
if (is_null($id_map)) {
$this->idMap = $this->createMock(MigrateIdMapInterface::class);
$this->idMap
->method('getQualifiedMapTableName')
->willReturn('test_map');
}
$migration = $this->getMockBuilder($this->migrationConfiguration['migrationClass'])
->disableOriginalConstructor()
->getMock();
$migration->method('checkRequirements')
->willReturn(TRUE);
$migration->method('getIdMap')
->willReturn($this->idMap);
// We need the state to be toggled throughout the test so we store the value
// on the test class and use a return callback.
$migration->expects($this->any())
->method('getStatus')
->willReturnCallback(function () {
return $this->migrationStatus;
});
$migration->expects($this->any())
->method('setStatus')
->willReturnCallback(function ($status) {
$this->migrationStatus = $status;
});
$migration->method('getMigrationDependencies')
->willReturn([
'required' => [],
'optional' => [],
]);
$configuration = &$this->migrationConfiguration;
$migration->method('set')
->willReturnCallback(function ($argument, $value) use (&$configuration) {
$configuration[$argument] = $value;
});
$migration->method('id')
->willReturn($configuration['id']);
return $migration;
}
/**
* Gets an SQLite database connection object for use in tests.
*
* @param array $database_contents
* The database contents faked as an array. Each key is a table name, each
* value is a list of table rows, an associative array of field => value.
* @param array $connection_options
* (optional) Options for the database connection. Defaults to an empty
* array.
*
* @return \Drupal\sqlite\Driver\Database\sqlite\Connection
* The database connection.
*/
protected function getDatabase(array $database_contents, $connection_options = []) {
if (extension_loaded('pdo_sqlite')) {
$connection_options['database'] = ':memory:';
$pdo = Connection::open($connection_options);
$connection = new Connection($pdo, $connection_options);
}
else {
$this->markTestSkipped('The pdo_sqlite extension is not available.');
}
// Initialize the DIC with a fake module handler for alterable queries.
$container = new ContainerBuilder();
$container->set('module_handler', $this->createMock('\Drupal\Core\Extension\ModuleHandlerInterface'));
\Drupal::setContainer($container);
// Create the tables and load them up with data, skipping empty ones.
foreach (array_filter($database_contents) as $table => $rows) {
$pilot_row = reset($rows);
$connection->schema()->createTable($table, $this->createSchemaFromRow($pilot_row));
$insert = $connection->insert($table)->fields(array_keys($pilot_row));
array_walk($rows, [$insert, 'values']);
$insert->execute();
}
return $connection;
}
/**
* Generates a table schema from a row.
*
* @param array $row
* The reference row on which to base the schema.
*
* @return array
* The Schema API-ready table schema.
*/
protected function createSchemaFromRow(array $row) {
// SQLite uses loose ("affinity") typing, so it is OK for every column to be
// a text field.
$fields = array_map(function () {
return ['type' => 'text'];
}, $row);
return ['fields' => $fields];
}
/**
* Tests a query.
*
* @param array|\Traversable $iter
* The countable. foreach-able actual results if a query is being run.
* @param array $expected_results
* An array of expected results.
*/
public function queryResultTest($iter, $expected_results) {
$this->assertSameSize($expected_results, $iter, 'Number of results match');
$count = 0;
foreach ($iter as $data_row) {
$expected_row = $expected_results[$count];
$count++;
foreach ($expected_row as $key => $expected_value) {
$this->retrievalAssertHelper($expected_value, $this->getValue($data_row, $key), sprintf('Value matches for key "%s"', $key));
}
}
$this->assertSame(count($expected_results), $count);
}
/**
* Gets the value on a row for a given key.
*
* @param array $row
* The row information.
* @param string $key
* The key identifier.
*
* @return mixed
* The value on a row for a given key.
*/
protected function getValue($row, $key) {
return $row[$key];
}
/**
* Asserts tested values during test retrieval.
*
* @param mixed $expected_value
* The incoming expected value to test.
* @param mixed $actual_value
* The incoming value itself.
* @param string $message
* The tested result as a formatted string.
*/
protected function retrievalAssertHelper($expected_value, $actual_value, $message) {
if (is_array($expected_value)) {
// If the expected and actual values are empty, no need to array compare.
if (empty($expected_value && $actual_value)) {
return;
}
$this->assertEquals($expected_value, $actual_value, $message);
}
else {
$this->assertSame((string) $expected_value, (string) $actual_value, $message);
}
}
}

View File

@ -0,0 +1,243 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\migrate\Plugin\Migration;
use Drupal\migrate\Plugin\MigrationPluginManager;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\migrate\Plugin\MigrationPluginManager
* @group migrate
*/
class MigrationPluginManagerTest extends UnitTestCase {
/**
* A plugin manager.
*
* @var \Drupal\migrate\Plugin\MigrationPluginManager
*/
protected $pluginManager;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Get a plugin manager for testing.
$module_handler = $this->createMock('Drupal\Core\Extension\ModuleHandlerInterface');
$cache_backend = $this->createMock('Drupal\Core\Cache\CacheBackendInterface');
$language_manager = $this->createMock('Drupal\Core\Language\LanguageManagerInterface');
$this->pluginManager = new MigrationPluginManager($module_handler, $cache_backend, $language_manager);
}
/**
* Tests building dependencies for multiple migrations.
*
* @dataProvider dependencyProvider
*/
public function testDependencyBuilding($migrations_data, $result_ids): void {
$migrations = [];
foreach ($migrations_data as $migration_id => $migration_data) {
$migrations[$migration_id] = new TestMigrationMock($migration_id, $migration_data['migration_dependencies']);
}
$ordered_migrations = $this->pluginManager->buildDependencyMigration($migrations, []);
// Verify results.
$this->assertEquals($result_ids, array_keys($ordered_migrations));
foreach ($migrations_data as $migration_id => $migration_data) {
$migration = $migrations[$migration_id];
$requirements = $migration_data['result_requirements'];
if (empty($requirements)) {
$this->assertEquals([], $migration->set);
}
else {
$requirements = array_combine($requirements, $requirements);
$this->assertCount(1, $migration->set);
[$set_prop, $set_requirements] = reset($migration->set);
$this->assertEquals('requirements', $set_prop);
$this->assertEquals($requirements, $set_requirements);
}
}
}
/**
* Tests that expandPluginIds returns all derivatives.
*/
public function testExpandPluginIds(): void {
$backend = $this->prophesize(CacheBackendInterface::class);
$cache = new \stdClass();
$cache->data = [
'a:a' => ['provider' => 'core'],
'a:b' => ['provider' => 'core'],
'b' => ['provider' => 'core'],
];
$backend->get('migration_plugins')->willReturn($cache);
$this->pluginManager->setCacheBackend($backend->reveal(), 'migration_plugins');
$plugin_ids = $this->pluginManager->expandPluginIds(['b', 'a']);
$this->assertContains('a:a', $plugin_ids);
$this->assertContains('a:b', $plugin_ids);
$this->assertContains('b', $plugin_ids);
}
/**
* Provide dependency data for testing.
*/
public static function dependencyProvider() {
return [
// Just one migration, with no dependencies.
[
[
'm1' => [
'migration_dependencies' => [],
'result_requirements' => [],
],
],
['m1'],
],
// Just one migration, with required dependencies.
[
[
'm1' => [
'migration_dependencies' => [
'required' => ['required1', 'required2'],
],
'result_requirements' => ['required1', 'required2'],
],
],
['m1'],
],
// Just one migration, with optional dependencies.
[
[
'm1' => [
'migration_dependencies' => [
'optional' => ['optional1'],
],
'result_requirements' => [],
],
],
['m1'],
],
// Multiple migrations.
[
[
'm1' => [
'migration_dependencies' => [
'required' => ['required1', 'required2'],
],
'result_requirements' => ['required1', 'required2'],
],
'm2' => [
'migration_dependencies' => [
'optional' => ['optional1'],
],
'result_requirements' => [],
],
],
['m1', 'm2'],
],
// Multiple migrations, reordered due to optional requirement.
[
[
'm1' => [
'migration_dependencies' => [
'optional' => ['m2'],
],
'result_requirements' => [],
],
'm2' => [
'migration_dependencies' => [
'optional' => ['optional1'],
],
'result_requirements' => [],
],
],
['m2', 'm1'],
],
// Ensure that optional requirements aren't turned into required ones,
// if the last migration has no optional deps.
[
[
'm1' => [
'migration_dependencies' => [
'optional' => ['m2'],
],
'result_requirements' => [],
],
'm2' => [
'migration_dependencies' => [],
'result_requirements' => [],
],
],
['m2', 'm1'],
],
];
}
}
/**
* A mock migration plugin.
*
* Why are we using a custom class here?
*
* 1. The function buildDependencyMigration() calls $migration->set(), which
* is not actually in MigrationInterface.
*
* 2. The function buildDependencyMigration() calls array_multisort on an
* array with mocks in it. PHPUnit mocks are really complex, and if PHP tries
* to compare them it will die with "Nesting level too deep".
*/
class TestMigrationMock extends Migration {
/**
* The values passed into set().
*
* @var array
*/
public $set = [];
/**
* TestMigrationMock constructor.
*/
public function __construct($id, $migration_dependencies) {
// Intentionally ignore parent constructor.
$this->id = $id;
$this->migration_dependencies = $migration_dependencies;
}
/**
* {@inheritdoc}
*/
public function id() {
return $this->id;
}
/**
* {@inheritdoc}
*/
public function getMigrationDependencies() {
// For the purpose of testing, do not expand dependencies.
return $this->migration_dependencies;
}
/**
* {@inheritdoc}
*/
public function set($prop, $value): void {
$this->set[] = func_get_args();
}
}

View File

@ -0,0 +1,522 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit;
use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Plugin\Migration;
use Drupal\migrate\Exception\RequirementsException;
use Drupal\migrate\Plugin\MigrateDestinationInterface;
use Drupal\migrate\Plugin\MigrateSourceInterface;
use Drupal\migrate\Plugin\MigrationPluginManagerInterface;
use Drupal\migrate\Plugin\RequirementsInterface;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\migrate\Plugin\Migration
*
* @group migrate
*/
class MigrationTest extends UnitTestCase {
/**
* Tests checking migration dependencies in the constructor.
*
* @param array $dependencies
* An array of migration dependencies.
*
* @covers ::__construct
*
* @dataProvider getInvalidMigrationDependenciesProvider
*/
public function testMigrationDependenciesInConstructor(array $dependencies): void {
$configuration = ['migration_dependencies' => $dependencies];
$plugin_id = 'test_migration';
$migration_plugin_manager = $this->createMock('\Drupal\migrate\Plugin\MigrationPluginManagerInterface');
$source_plugin_manager = $this->createMock('\Drupal\migrate\Plugin\MigratePluginManagerInterface');
$process_plugin_manager = $this->createMock('\Drupal\migrate\Plugin\MigratePluginManagerInterface');
$destination_plugin_manager = $this->createMock('\Drupal\migrate\Plugin\MigrateDestinationPluginManager');
$id_map_plugin_manager = $this->createMock('\Drupal\migrate\Plugin\MigratePluginManagerInterface');
$this->expectException(InvalidPluginDefinitionException::class);
$this->expectExceptionMessage("Invalid migration dependencies configuration for migration test_migration");
new Migration($configuration, $plugin_id, [], $migration_plugin_manager, $source_plugin_manager, $process_plugin_manager, $destination_plugin_manager, $id_map_plugin_manager);
}
/**
* Tests checking requirements for source plugins.
*
* @covers ::checkRequirements
*/
public function testRequirementsForSourcePlugin(): void {
$migration = new TestMigration();
$source_plugin = $this->createMock('Drupal\Tests\migrate\Unit\RequirementsAwareSourceInterface');
$source_plugin->expects($this->once())
->method('checkRequirements')
->willThrowException(new RequirementsException('Missing source requirement', ['key' => 'value']));
$destination_plugin = $this->createMock('Drupal\Tests\migrate\Unit\RequirementsAwareDestinationInterface');
$migration->setSourcePlugin($source_plugin);
$migration->setDestinationPlugin($destination_plugin);
$this->expectException(RequirementsException::class);
$this->expectExceptionMessage('Missing source requirement');
$migration->checkRequirements();
}
/**
* Tests checking requirements for destination plugins.
*
* @covers ::checkRequirements
*/
public function testRequirementsForDestinationPlugin(): void {
$migration = new TestMigration();
$source_plugin = $this->createMock('Drupal\migrate\Plugin\MigrateSourceInterface');
$destination_plugin = $this->createMock('Drupal\Tests\migrate\Unit\RequirementsAwareDestinationInterface');
$destination_plugin->expects($this->once())
->method('checkRequirements')
->willThrowException(new RequirementsException('Missing destination requirement', ['key' => 'value']));
$migration->setSourcePlugin($source_plugin);
$migration->setDestinationPlugin($destination_plugin);
$this->expectException(RequirementsException::class);
$this->expectExceptionMessage('Missing destination requirement');
$migration->checkRequirements();
}
/**
* Tests checking requirements for destination plugins.
*
* @covers ::checkRequirements
*/
public function testRequirementsForMigrations(): void {
$migration = new TestMigration();
// Setup source and destination plugins without any requirements.
$source_plugin = $this->createMock('Drupal\migrate\Plugin\MigrateSourceInterface');
$destination_plugin = $this->createMock('Drupal\migrate\Plugin\MigrateDestinationInterface');
$migration->setSourcePlugin($source_plugin);
$migration->setDestinationPlugin($destination_plugin);
$plugin_manager = $this->createMock('Drupal\migrate\Plugin\MigrationPluginManagerInterface');
$migration->setMigrationPluginManager($plugin_manager);
// We setup the requirements that test_a doesn't exist and test_c is not
// completed yet.
$migration->setRequirements(['test_a', 'test_b', 'test_c', 'test_d']);
$migration_b = $this->createMock(MigrationInterface::class);
$migration_c = $this->createMock(MigrationInterface::class);
$migration_d = $this->createMock(MigrationInterface::class);
$migration_b->expects($this->once())
->method('allRowsProcessed')
->willReturn(TRUE);
$migration_c->expects($this->once())
->method('allRowsProcessed')
->willReturn(FALSE);
$migration_d->expects($this->once())
->method('allRowsProcessed')
->willReturn(TRUE);
$plugin_manager->expects($this->once())
->method('createInstances')
->with(['test_a', 'test_b', 'test_c', 'test_d'])
->willReturn(['test_b' => $migration_b, 'test_c' => $migration_c, 'test_d' => $migration_d]);
$this->expectException(RequirementsException::class);
$this->expectExceptionMessage('Missing migrations test_a, test_c');
$migration->checkRequirements();
}
/**
* Tests getting requirement list.
*
* @covers ::getRequirements
*/
public function testGetMigrations(): void {
$migration = new TestMigration();
$requirements = ['test_a', 'test_b', 'test_c', 'test_d'];
$migration->setRequirements($requirements);
$this->assertEquals($requirements, $migration->getRequirements());
}
/**
* Tests valid migration dependencies configuration returns expected values.
*
* @param array|null $source
* The migration dependencies configuration being tested.
* @param array $expected_value
* The migration dependencies configuration array expected.
*
* @covers ::getMigrationDependencies
* @dataProvider getValidMigrationDependenciesProvider
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
*/
public function testMigrationDependenciesWithValidConfig($source, array $expected_value): void {
$migration = new TestMigration();
// Set the plugin manager to support getMigrationDependencies().
$plugin_manager = $this->createMock('Drupal\migrate\Plugin\MigrationPluginManagerInterface');
$migration->setMigrationPluginManager($plugin_manager);
$plugin_manager->expects($this->exactly(2))
->method('expandPluginIds')
->willReturnArgument(0);
if (!is_null($source)) {
$migration->set('migration_dependencies', $source);
}
$this->assertSame($migration->getMigrationDependencies(), $expected_value);
}
/**
* Tests that getting migration dependencies fails with invalid configuration.
*
* @param array $dependencies
* An array of migration dependencies.
*
* @covers ::getMigrationDependencies
*
* @dataProvider getInvalidMigrationDependenciesProvider
*
* @group legacy
*/
public function testMigrationDependenciesWithInvalidConfig(array $dependencies): void {
$migration = new TestMigration();
// Set the plugin ID to test the returned message.
$plugin_id = 'test_migration';
$migration->setPluginId($plugin_id);
// Migration dependencies expects ['optional' => []] or ['required' => []]].
$migration->set('migration_dependencies', $dependencies);
$this->expectException(InvalidPluginDefinitionException::class);
$this->expectExceptionMessage("Invalid migration dependencies configuration for migration {$plugin_id}");
$migration->getMigrationDependencies();
}
/**
* Provides data for valid migration configuration test.
*/
public static function getValidMigrationDependenciesProvider() {
return [
[
'source' => NULL,
'expected_value' => ['required' => [], 'optional' => []],
],
[
'source' => [],
'expected_value' => ['required' => [], 'optional' => []],
],
[
'source' => ['required' => ['test_migration']],
'expected_value' => ['required' => ['test_migration'], 'optional' => []],
],
[
'source' => ['optional' => ['test_migration']],
'expected_value' => ['optional' => ['test_migration'], 'required' => []],
],
[
'source' => ['required' => ['req_test_migration'], 'optional' => ['opt_test_migration']],
'expected_value' => ['required' => ['req_test_migration'], 'optional' => ['opt_test_migration']],
],
];
}
/**
* Provides invalid migration dependencies.
*/
public static function getInvalidMigrationDependenciesProvider() {
return [
'invalid key' => [
'dependencies' => ['bogus' => []],
],
'required not array' => [
'dependencies' => ['required' => 17, 'optional' => []],
],
'optional not array' => [
'dependencies' => ['required' => [], 'optional' => 17],
],
];
}
/**
* Tests the addition of required dependencies.
*
* @param string[]|null $initial_dependency
* The migration dependencies configuration being tested.
* @param string[] $addition
* Add array of additions.
* @param string[] $expected
* The migration dependencies configuration array expected.
*
* @covers ::addRequiredDependencies
* @dataProvider providerTestAddRequiredDependencies
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
*/
public function testAddRequiredDependencies(?array $initial_dependency, array $addition, array $expected): void {
$migration = new TestMigration($initial_dependency);
$migration->setMigrationPluginManager($this->getMockPluginManager());
$migration->addRequiredDependencies($addition);
$this->assertSame($expected, $migration->getMigrationDependencies());
}
/**
* Provides data for testAddRequiredDependencies.
*/
public static function providerTestAddRequiredDependencies(): array {
return [
'NULL' => [
NULL,
['foo'],
[
'required' => ['foo'],
'optional' => [],
],
],
'empty' => [
[],
['foo', 'bar'],
[
'required' => ['foo', 'bar'],
'optional' => [],
],
],
'add empty' => [
['required' => ['block']],
[],
[
'required' => ['block'],
'optional' => [],
],
],
'add one' => [
['required' => ['block']],
['foo'],
[
'required' => ['block', 'foo'],
'optional' => [],
],
],
'add two' => [
['required' => ['block']],
['foo', 'bar'],
[
'required' => ['block', 'foo', 'bar'],
'optional' => [],
],
],
'add existing' => [
['required' => ['foo']],
['foo', 'bar'],
[
'required' => [0 => 'foo', 2 => 'bar'],
'optional' => [],
],
],
'add two, with optional' => [
['required' => ['block'], 'optional' => ['foo']],
['foo', 'bar'],
[
'required' => ['block', 'foo', 'bar'],
'optional' => ['foo'],
],
],
];
}
/**
* Tests the addition of optional dependencies.
*
* @param string[]|null $initial_dependency
* The migration dependencies configuration being tested.
* @param string[] $addition
* Add array of additions.
* @param string[] $expected
* The migration dependencies configuration array expected.
*
* @covers ::addOptionalDependencies
* @dataProvider providerTestAddOptionalDependencies
*/
public function testAddOptionalDependencies(?array $initial_dependency, array $addition, array $expected): void {
$migration = new TestMigration($initial_dependency);
$migration->setMigrationPluginManager($this->getMockPluginManager());
$migration->addOptionalDependencies($addition);
$this->assertSame($expected, $migration->getMigrationDependencies());
}
/**
* Provides data for testAddOptionalDependencies.
*/
public static function providerTestAddOptionalDependencies(): array {
return [
'NULL' => [
NULL,
['foo'],
[
'required' => [],
'optional' => ['foo'],
],
],
'empty' => [
[],
['foo', 'bar'],
[
'required' => [],
'optional' => ['foo', 'bar'],
],
],
'add empty' => [
['optional' => ['block']],
[],
[
'optional' => ['block'],
'required' => [],
],
],
'add one' => [
['optional' => ['block']],
['foo'],
[
'optional' => ['block', 'foo'],
'required' => [],
],
],
'add two' => [
['optional' => ['block']],
['foo', 'bar'],
[
'optional' => ['block', 'foo', 'bar'],
'required' => [],
],
],
'add existing' => [
['optional' => ['foo']],
['foo', 'bar'],
[
'optional' => [0 => 'foo', 1 => 'bar'],
'required' => [],
],
],
'add two, with optional' => [
['optional' => ['block'], 'required' => ['foo']],
['foo', 'bar'],
[
'optional' => ['block', 'foo', 'bar'],
'required' => ['foo'],
],
],
];
}
// Set the plugin manager.
/**
* Returns a mock MigrationPluginManager.
*
* @return \Drupal\migrate\Plugin\MigrationPluginManagerInterface|\PHPUnit\Framework\MockObject\MockObject
* A configured MigrationPluginManager test mock.
*/
public function getMockPluginManager() {
$plugin_manager = $this->createMock('Drupal\migrate\Plugin\MigrationPluginManagerInterface');
$plugin_manager->expects($this->exactly(2))
->method('expandPluginIds')
->willReturnArgument(0);
return $plugin_manager;
}
}
/**
* Defines the TestMigration class.
*/
class TestMigration extends Migration {
/**
* Constructs an instance of TestMigration object.
*
* @param string[]|null $initial_dependency
* An associative array of required and optional migrations IDs, keyed by
* 'required' and 'optional'.
*/
public function __construct(?array $initial_dependency = NULL) {
$this->migration_dependencies = ($this->migration_dependencies ?: []) + ['required' => [], 'optional' => []];
if ($initial_dependency) {
$this->migration_dependencies = $initial_dependency;
}
$this->requirements = ['require1', 'require2'];
}
/**
* Sets the migration ID (machine name).
*
* @param string $plugin_id
* The plugin ID of the plugin instance.
*/
public function setPluginId($plugin_id): void {
$this->pluginId = $plugin_id;
}
/**
* Sets the requirements values.
*
* @param array $requirements
* The array of requirement values.
*/
public function setRequirements(array $requirements): void {
$this->requirements = $requirements;
}
/**
* Sets the source Plugin.
*
* @param \Drupal\migrate\Plugin\MigrateSourceInterface $source_plugin
* The source Plugin.
*/
public function setSourcePlugin(MigrateSourceInterface $source_plugin): void {
$this->sourcePlugin = $source_plugin;
}
/**
* Sets the destination Plugin.
*
* @param \Drupal\migrate\Plugin\MigrateDestinationInterface $destination_plugin
* The destination Plugin.
*/
public function setDestinationPlugin(MigrateDestinationInterface $destination_plugin): void {
$this->destinationPlugin = $destination_plugin;
}
/**
* Sets the plugin manager service.
*
* @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $plugin_manager
* The plugin manager service.
*/
public function setMigrationPluginManager(MigrationPluginManagerInterface $plugin_manager): void {
$this->migrationPluginManager = $plugin_manager;
}
}
/**
* Defines the RequirementsAwareSourceInterface.
*/
interface RequirementsAwareSourceInterface extends MigrateSourceInterface, RequirementsInterface {}
/**
* Defines the RequirementsAwareDestinationInterface.
*/
interface RequirementsAwareDestinationInterface extends MigrateDestinationInterface, RequirementsInterface {}

View File

@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit\Plugin;
use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\migrate\Plugin\MigrateSourcePluginManager;
use Drupal\migrate\Plugin\NoSourcePluginDecorator;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\migrate\Plugin\NoSourcePluginDecorator
* @group migrate
*/
class NoSourcePluginDecoratorTest extends UnitTestCase {
/**
* @covers ::getDefinitions
* @dataProvider providerGetDefinitions
*/
public function testGetDefinitions(array $definition, bool $source_exists): void {
$source_manager = $this->createMock(MigrateSourcePluginManager::class);
$source_manager->expects($this->any())
->method('hasDefinition')
->willReturn($source_exists);
$container = new ContainerBuilder();
$container->set('plugin.manager.migrate.source', $source_manager);
\Drupal::setContainer($container);
$discovery_interface = $this->createMock(DiscoveryInterface::class);
$discovery_interface->expects($this->once())
->method('getDefinitions')
->willReturn([$definition]);
$decorator = new NoSourcePluginDecorator($discovery_interface);
$results = $decorator->getDefinitions();
if ($source_exists) {
$this->assertEquals([$definition], $results);
}
else {
$this->assertEquals([], $results);
}
}
/**
* Provides data for testGetDefinitions().
*/
public static function providerGetDefinitions(): array {
return [
'source exists' => [
[
'source' => ['plugin' => 'valid_plugin'],
'process' => [],
'destination' => [],
],
TRUE,
],
'source does not exist' => [
[
'source' => ['plugin' => 'invalid_plugin'],
'process' => [],
'destination' => [],
],
FALSE,
],
'source is not defined' => [
[
'process' => [],
'destination' => [],
],
FALSE,
],
];
}
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit\Plugin\migrate\destination;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Tests\UnitTestCase;
use Drupal\migrate\Exception\RequirementsException;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Plugin\migrate\destination\Config;
/**
* Tests check requirements exception on DestinationBase.
*
* @group migrate
*/
class CheckRequirementsTest extends UnitTestCase {
/**
* Tests the check requirements exception message.
*/
public function testException(): void {
$destination = new Config(
['config_name' => 'test'],
'test',
[],
$this->prophesize(MigrationInterface::class)->reveal(),
$this->prophesize(ConfigFactoryInterface::class)->reveal(),
$this->prophesize(LanguageManagerInterface::class)->reveal(),
$this->prophesize(TypedConfigManagerInterface::class)->reveal(),
);
$this->expectException(RequirementsException::class);
$this->expectExceptionMessage("Destination plugin 'test' did not meet the requirements");
$destination->checkRequirements();
}
}

View File

@ -0,0 +1,146 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit\Plugin\migrate\destination;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use Drupal\Core\Session\AccountSwitcherInterface;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Plugin\migrate\destination\EntityContentBase;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Row;
use Prophecy\Argument;
/**
* Tests base entity migration destination functionality.
*
* @coversDefaultClass \Drupal\migrate\Plugin\migrate\destination\EntityContentBase
* @group migrate
*/
class EntityContentBaseTest extends EntityTestBase {
/**
* Tests basic entity save.
*
* @covers ::import
*/
public function testImport(): void {
$bundles = [];
$destination = new EntityTestDestination([], '', [],
$this->migration->reveal(),
$this->storage->reveal(),
$bundles,
$this->entityFieldManager->reveal(),
$this->prophesize(FieldTypePluginManagerInterface::class)->reveal(),
$this->prophesize(AccountSwitcherInterface::class)->reveal(),
$this->prophesize(EntityTypeBundleInfoInterface::class)->reveal(),
);
$entity = $this->prophesize(ContentEntityInterface::class);
$entity->isValidationRequired()
->shouldBeCalledTimes(1);
// Assert that save is called.
$entity->save()
->shouldBeCalledTimes(1);
// Syncing should be set once.
$entity->setSyncing(Argument::exact(TRUE))
->shouldBeCalledTimes(1);
// Set an id for the entity
$entity->id()
->willReturn(5);
$destination->setEntity($entity->reveal());
// Ensure the id is saved entity id is returned from import.
$this->assertEquals([5], $destination->import(new Row()));
// Assert that import set the rollback action.
$this->assertEquals(MigrateIdMapInterface::ROLLBACK_DELETE, $destination->rollbackAction());
}
/**
* Tests row skipping when we can't get an entity to save.
*
* @covers ::import
*/
public function testImportEntityLoadFailure(): void {
$bundles = [];
$destination = new EntityTestDestination([], '', [],
$this->migration->reveal(),
$this->storage->reveal(),
$bundles,
$this->entityFieldManager->reveal(),
$this->prophesize(FieldTypePluginManagerInterface::class)->reveal(),
$this->prophesize(AccountSwitcherInterface::class)->reveal(),
$this->prophesize(EntityTypeBundleInfoInterface::class)->reveal(),
);
$destination->setEntity(FALSE);
$this->expectException(MigrateException::class);
$this->expectExceptionMessage('Unable to get entity');
$destination->import(new Row());
}
/**
* Tests that translation destination fails for untranslatable entities.
*/
public function testUntranslatable(): void {
// An entity type without a language.
$this->entityType->getKey('langcode')->willReturn('');
$this->entityType->getKey('id')->willReturn('id');
$this->entityFieldManager->getBaseFieldDefinitions('foo')
->willReturn(['id' => BaseFieldDefinitionTest::create('integer')]);
$destination = new EntityTestDestination(
['translations' => TRUE],
'',
[],
$this->migration->reveal(),
$this->storage->reveal(),
[],
$this->entityFieldManager->reveal(),
$this->prophesize(FieldTypePluginManagerInterface::class)->reveal(),
$this->prophesize(AccountSwitcherInterface::class)->reveal(),
$this->prophesize(EntityTypeBundleInfoInterface::class)->reveal(),
);
$this->expectException(MigrateException::class);
$this->expectExceptionMessage('The "foo" entity type does not support translations.');
$destination->getIds();
}
}
/**
* Stub class for testing EntityContentBase methods.
*
* We want to test things without testing the base class implementations.
*/
class EntityTestDestination extends EntityContentBase {
/**
* The test entity.
*
* @var \Drupal\migrate\Plugin\migrate\destination\EntityContentBase|null
*/
private $entity = NULL;
/**
* Sets the test entity.
*/
public function setEntity($entity): void {
$this->entity = $entity;
}
/**
* Gets the test entity.
*/
protected function getEntity(Row $row, array $old_destination_id_values) {
return $this->entity;
}
/**
* Gets the test entity ID.
*/
public static function getEntityTypeId($plugin_id) {
return 'foo';
}
}

View File

@ -0,0 +1,134 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit\Plugin\migrate\destination;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use Drupal\Core\Session\AccountSwitcherInterface;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Plugin\migrate\destination\EntityRevision;
use Drupal\migrate\Row;
/**
* Tests entity revision destination functionality.
*
* @coversDefaultClass \Drupal\migrate\Plugin\migrate\destination\EntityRevision
* @group migrate
*/
class EntityRevisionTest extends EntityTestBase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->migration = $this->prophesize(MigrationInterface::class);
$this->storage = $this->prophesize(EntityStorageInterface::class);
$this->entityType = $this->prophesize(EntityTypeInterface::class);
$this->entityType->getSingularLabel()->willReturn('foo');
$this->entityType->getPluralLabel()->willReturn('bar');
$this->storage->getEntityType()->willReturn($this->entityType->reveal());
$this->storage->getEntityTypeId()->willReturn('foo');
}
/**
* Tests entities that do not support revisions.
*/
public function testNoRevisionSupport(): void {
$this->entityType->getKey('id')->willReturn('id');
$this->entityType->getKey('revision')->willReturn('');
$this->entityFieldManager->getBaseFieldDefinitions('foo')
->willReturn([
'id' => BaseFieldDefinitionTest::create('integer'),
]);
$destination = new EntityRevisionTestDestination(
[],
'',
[],
$this->migration->reveal(),
$this->storage->reveal(),
[],
$this->entityFieldManager->reveal(),
$this->prophesize(FieldTypePluginManagerInterface::class)->reveal(),
$this->prophesize(AccountSwitcherInterface::class)->reveal(),
$this->prophesize(EntityTypeBundleInfoInterface::class)->reveal(),
);
$this->expectException(MigrateException::class);
$this->expectExceptionMessage('The "foo" entity type does not support revisions.');
$destination->getIds();
}
/**
* Tests that translation destination fails for untranslatable entities.
*/
public function testUntranslatable(): void {
$this->entityType->getKey('id')->willReturn('id');
$this->entityType->getKey('revision')->willReturn('vid');
$this->entityType->getKey('langcode')->willReturn('');
$this->entityFieldManager->getBaseFieldDefinitions('foo')
->willReturn([
'id' => BaseFieldDefinitionTest::create('integer'),
'vid' => BaseFieldDefinitionTest::create('integer'),
]);
$destination = new EntityRevisionTestDestination(
['translations' => TRUE],
'',
[],
$this->migration->reveal(),
$this->storage->reveal(),
[],
$this->entityFieldManager->reveal(),
$this->prophesize(FieldTypePluginManagerInterface::class)->reveal(),
$this->prophesize(AccountSwitcherInterface::class)->reveal(),
$this->prophesize(EntityTypeBundleInfoInterface::class)->reveal(),
);
$this->expectException(MigrateException::class);
$this->expectExceptionMessage('The "foo" entity type does not support translations.');
$destination->getIds();
}
}
/**
* Stub class for testing EntityRevision methods.
*/
class EntityRevisionTestDestination extends EntityRevision {
/**
* The test entity.
*
* @var \Drupal\migrate\Plugin\migrate\destination\EntityRevision|null
*/
private $entity = NULL;
/**
* Sets the test entity.
*/
public function setEntity($entity): void {
$this->entity = $entity;
}
/**
* Gets the test entity.
*/
protected function getEntity(Row $row, array $old_destination_id_values) {
return $this->entity;
}
/**
* Gets the test entity ID.
*/
public static function getEntityTypeId($plugin_id) {
return 'foo';
}
}

View File

@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit\Plugin\migrate\destination;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\Tests\UnitTestCase;
/**
* Base test class for entity migration destination functionality.
*/
abstract class EntityTestBase extends UnitTestCase {
/**
* The migration entity.
*
* @var \Drupal\migrate\Plugin\MigrationInterface
*/
protected $migration;
/**
* The entity storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $storage;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeInterface
*/
protected $entityType;
/**
* The entity field manager service.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->migration = $this->prophesize(MigrationInterface::class);
$this->storage = $this->prophesize(EntityStorageInterface::class);
$this->entityType = $this->prophesize(EntityTypeInterface::class);
$this->entityType->getPluralLabel()->willReturn('foo');
$this->storage->getEntityType()->willReturn($this->entityType->reveal());
$this->storage->getEntityTypeId()->willReturn('foo');
$this->entityFieldManager = $this->prophesize(EntityFieldManagerInterface::class);
}
}
/**
* Stub class for BaseFieldDefinition.
*/
class BaseFieldDefinitionTest extends BaseFieldDefinition {
/**
* {@inheritdoc}
*/
public static function create($type) {
return new static([]);
}
/**
* {@inheritdoc}
*/
public function getSettings() {
return [];
}
/**
* {@inheritdoc}
*/
public function getType() {
return 'integer';
}
}

View File

@ -0,0 +1,456 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Row;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\migrate\Row
* @group migrate
*/
class RowTest extends UnitTestCase {
/**
* The source IDs.
*
* @var array
*/
protected $testSourceIds = [
'nid' => 'Node ID',
];
/**
* The test values.
*
* @var array
*/
protected $testValues = [
'nid' => 1,
'title' => 'node 1',
];
/**
* Test source properties for testing get and getMultiple.
*
* @var array
*/
protected $testGetSourceProperties = [
'source_key_1' => 'source_value_1',
'source_key_2' => 'source_value_2',
'@source_key_3' => 'source_value_3',
'shared_key_1' => 'source_shared_value_1',
'@shared_key_2' => 'source_shared_value_2',
'@@@@shared_key_3' => 'source_shared_value_3',
];
/**
* Test source keys for testing get and getMultiple.
*
* @var array
*/
protected $testGetSourceIds = [
'source_key_1' => [],
];
/**
* Test destination properties for testing get and getMultiple.
*
* @var array
*/
protected $testGetDestinationProperties = [
'destination_key_1' => 'destination_value_1',
'destination_key_2' => 'destination_value_2',
'@destination_key_3' => 'destination_value_3',
'shared_key_1' => 'destination_shared_value_1',
'@shared_key_2' => 'destination_shared_value_2',
'@@@@shared_key_3' => 'destination_shared_value_3',
];
/**
* The test hash.
*
* @var string
*/
protected $testHash = '85795d4cde4a2425868b812cc88052ecd14fc912e7b9b4de45780f66750e8b1e';
/**
* The test hash after changing title value to 'new title'.
*
* @var string
*/
protected $testHashMod = '9476aab0b62b3f47342cc6530441432e5612dcba7ca84115bbab5cceaca1ecb3';
/**
* Tests object creation: empty.
*/
public function testRowWithoutData(): void {
$row = new Row();
$this->assertSame([], $row->getSource(), 'Empty row');
}
/**
* Tests object creation: basic.
*/
public function testRowWithBasicData(): void {
$row = new Row($this->testValues, $this->testSourceIds);
$this->assertSame($this->testValues, $row->getSource(), 'Row with data, simple id.');
}
/**
* Tests object creation: multiple source IDs.
*/
public function testRowWithMultipleSourceIds(): void {
$multi_source_ids = $this->testSourceIds + ['vid' => 'Node revision'];
$multi_source_ids_values = $this->testValues + ['vid' => 1];
$row = new Row($multi_source_ids_values, $multi_source_ids);
$this->assertSame($multi_source_ids_values, $row->getSource(), 'Row with data, multiple source id.');
}
/**
* Tests object creation: invalid values.
*/
public function testRowWithInvalidData(): void {
$invalid_values = [
'title' => 'node X',
];
$this->expectException(\Exception::class);
new Row($invalid_values, $this->testSourceIds);
}
/**
* Tests source immutability after freeze.
*/
public function testSourceFreeze(): void {
$row = new Row($this->testValues, $this->testSourceIds);
$row->rehash();
$this->assertSame($this->testHash, $row->getHash(), 'Correct hash.');
$row->setSourceProperty('title', 'new title');
$row->rehash();
$this->assertSame($this->testHashMod, $row->getHash(), 'Hash changed correctly.');
$row->freezeSource();
$this->expectException(\Exception::class);
$row->setSourceProperty('title', 'new title');
}
/**
* Tests setting on a frozen row.
*/
public function testSetFrozenRow(): void {
$row = new Row($this->testValues, $this->testSourceIds);
$row->freezeSource();
$this->expectException(\Exception::class);
$this->expectExceptionMessage("The source is frozen and can't be changed any more");
$row->setSourceProperty('title', 'new title');
}
/**
* Tests hashing.
*/
public function testHashing(): void {
$row = new Row($this->testValues, $this->testSourceIds);
$this->assertSame('', $row->getHash(), 'No hash at creation');
$row->rehash();
$this->assertSame($this->testHash, $row->getHash(), 'Correct hash.');
$row->rehash();
$this->assertSame($this->testHash, $row->getHash(), 'Correct hash even doing it twice.');
// Set the map to needs update.
$test_id_map = [
'original_hash' => '',
'hash' => '',
'source_row_status' => MigrateIdMapInterface::STATUS_NEEDS_UPDATE,
];
$row->setIdMap($test_id_map);
$this->assertTrue($row->needsUpdate());
$row->rehash();
$this->assertSame($this->testHash, $row->getHash(), 'Correct hash even if id_mpa have changed.');
$row->setSourceProperty('title', 'new title');
$row->rehash();
$this->assertSame($this->testHashMod, $row->getHash(), 'Hash changed correctly.');
// Check hash calculation algorithm.
$hash = hash('sha256', serialize($row->getSource()));
$this->assertSame($hash, $row->getHash());
// Check length of generated hash used for mapping schema.
$this->assertSame(64, strlen($row->getHash()));
// Set the map to successfully imported.
$test_id_map = [
'original_hash' => '',
'hash' => '',
'source_row_status' => MigrateIdMapInterface::STATUS_IMPORTED,
];
$row->setIdMap($test_id_map);
$this->assertFalse($row->needsUpdate());
// Set the same hash value and ensure it was not changed.
$random = $this->randomMachineName();
$test_id_map = [
'original_hash' => $random,
'hash' => $random,
'source_row_status' => MigrateIdMapInterface::STATUS_NEEDS_UPDATE,
];
$row->setIdMap($test_id_map);
$this->assertFalse($row->changed());
// Set different has values to ensure it is marked as changed.
$test_id_map = [
'original_hash' => $this->randomMachineName(),
'hash' => $this->randomMachineName(),
'source_row_status' => MigrateIdMapInterface::STATUS_NEEDS_UPDATE,
];
$row->setIdMap($test_id_map);
$this->assertTrue($row->changed());
}
/**
* Tests getting/setting the ID Map.
*
* @covers ::setIdMap
* @covers ::getIdMap
*/
public function testGetSetIdMap(): void {
$row = new Row($this->testValues, $this->testSourceIds);
$test_id_map = [
'original_hash' => '',
'hash' => '',
'source_row_status' => MigrateIdMapInterface::STATUS_NEEDS_UPDATE,
];
$row->setIdMap($test_id_map);
$this->assertEquals($test_id_map, $row->getIdMap());
}
/**
* Tests the source ID.
*/
public function testSourceIdValues(): void {
$row = new Row($this->testValues, $this->testSourceIds);
$this->assertSame(['nid' => $this->testValues['nid']], $row->getSourceIdValues());
}
/**
* Tests the multiple source IDs.
*/
public function testMultipleSourceIdValues(): void {
// Set values in same order as ids.
$multi_source_ids = $this->testSourceIds + [
'vid' => 'Node revision',
'type' => 'Node type',
'langcode' => 'Node language',
];
$multi_source_ids_values = $this->testValues + [
'vid' => 1,
'type' => 'page',
'langcode' => 'en',
];
$row = new Row($multi_source_ids_values, $multi_source_ids);
$this->assertSame(array_keys($multi_source_ids), array_keys($row->getSourceIdValues()));
// Set values in different order.
$multi_source_ids = $this->testSourceIds + [
'vid' => 'Node revision',
'type' => 'Node type',
'langcode' => 'Node language',
];
$multi_source_ids_values = $this->testValues + [
'langcode' => 'en',
'type' => 'page',
'vid' => 1,
];
$row = new Row($multi_source_ids_values, $multi_source_ids);
$this->assertSame(array_keys($multi_source_ids), array_keys($row->getSourceIdValues()));
}
/**
* Tests getting the source property.
*
* @covers ::getSourceProperty
*/
public function testGetSourceProperty(): void {
$row = new Row($this->testValues, $this->testSourceIds);
$this->assertSame($this->testValues['nid'], $row->getSourceProperty('nid'));
$this->assertSame($this->testValues['title'], $row->getSourceProperty('title'));
$this->assertNull($row->getSourceProperty('non_existing'));
}
/**
* Tests setting and getting the destination.
*/
public function testDestination(): void {
$row = new Row($this->testValues, $this->testSourceIds);
$this->assertEmpty($row->getDestination());
$this->assertFalse($row->hasDestinationProperty('nid'));
// Set a destination.
$row->setDestinationProperty('nid', 2);
$this->assertTrue($row->hasDestinationProperty('nid'));
$this->assertEquals(['nid' => 2], $row->getDestination());
}
/**
* Tests setting/getting multiple destination IDs.
*/
public function testMultipleDestination(): void {
$row = new Row($this->testValues, $this->testSourceIds);
// Set some deep nested values.
$row->setDestinationProperty('image/alt', 'alt text');
$row->setDestinationProperty('image/fid', 3);
$this->assertTrue($row->hasDestinationProperty('image'));
$this->assertFalse($row->hasDestinationProperty('alt'));
$this->assertFalse($row->hasDestinationProperty('fid'));
$destination = $row->getDestination();
$this->assertEquals('alt text', $destination['image']['alt']);
$this->assertEquals(3, $destination['image']['fid']);
$this->assertEquals('alt text', $row->getDestinationProperty('image/alt'));
$this->assertEquals(3, $row->getDestinationProperty('image/fid'));
}
/**
* Tests getting source and destination properties.
*
* @param string $key
* The key to look up.
* @param string $expected_value
* The expected value.
*
* @dataProvider getDataProvider
* @covers ::get
*/
public function testGet($key, $expected_value): void {
$row = $this->createRowWithDestinationProperties($this->testGetSourceProperties, $this->testGetSourceIds, $this->testGetDestinationProperties);
$this->assertSame($expected_value, $row->get($key));
}
/**
* Data Provider for testGet.
*
* @return array
* The keys and expected values.
*/
public static function getDataProvider() {
return [
['source_key_1', 'source_value_1'],
['source_key_2', 'source_value_2'],
['@@source_key_3', 'source_value_3'],
['shared_key_1', 'source_shared_value_1'],
['@@shared_key_2', 'source_shared_value_2'],
['@@@@@@@@shared_key_3', 'source_shared_value_3'],
['@destination_key_1', 'destination_value_1'],
['@destination_key_2', 'destination_value_2'],
['@@@destination_key_3', 'destination_value_3'],
['@shared_key_1', 'destination_shared_value_1'],
['@@@shared_key_2', 'destination_shared_value_2'],
['@@@@@@@@@shared_key_3', 'destination_shared_value_3'],
['destination_key_1', NULL],
['@shared_key_2', NULL],
['@source_key_1', NULL],
['random_source_key', NULL],
['@random_destination_key', NULL],
];
}
/**
* Tests getting multiple source and destination properties.
*
* @param array $keys
* An array of keys to look up.
* @param array $expected_values
* An array of expected values.
*
* @covers ::getMultiple
* @dataProvider getMultipleDataProvider
*/
public function testGetMultiple(array $keys, array $expected_values): void {
$row = $this->createRowWithDestinationProperties($this->testGetSourceProperties, $this->testGetSourceIds, $this->testGetDestinationProperties);
$this->assertEquals(array_combine($keys, $expected_values), $row->getMultiple($keys));
}
/**
* Data Provider for testGetMultiple.
*
* @return array
* The keys and expected values.
*/
public static function getMultipleDataProvider() {
return [
'Single Key' => [
'keys' => ['source_key_1'],
'expected_values' => ['source_value_1'],
],
'All Source Keys' => [
'keys' => [
'source_key_1',
'source_key_2',
'@@source_key_3',
],
'expected_values' => [
'source_value_1',
'source_value_2',
'source_value_3',
],
],
'All Destination Keys' => [
'keys' => [
'@destination_key_1',
'@destination_key_2',
'@@@destination_key_3',
],
'expected_values' => [
'destination_value_1',
'destination_value_2',
'destination_value_3',
],
],
'Mix of keys including non-existent' => [
'keys' => [
'shared_key_1',
'@shared_key_1',
'@@shared_key_2',
'@@@shared_key_2',
'@@@@@@@@@shared_key_3',
'non_existent_source_key',
'@non_existent_destination_key',
],
'expected_values' => [
'source_shared_value_1',
'destination_shared_value_1',
'source_shared_value_2',
'destination_shared_value_2',
'destination_shared_value_3',
NULL,
NULL,
],
],
];
}
/**
* Create a row and load it with destination properties.
*
* @param array $source_properties
* The source property array.
* @param array $source_ids
* The source ids array.
* @param array $destination_properties
* The destination properties to load.
* @param bool $is_stub
* Whether this row is a stub row, defaults to FALSE.
*
* @return \Drupal\migrate\Row
* The row, populated with destination properties.
*/
protected function createRowWithDestinationProperties(array $source_properties, array $source_ids, array $destination_properties, $is_stub = FALSE) {
$row = new Row($source_properties, $source_ids, $is_stub);
foreach ($destination_properties as $key => $property) {
$row->setDestinationProperty($key, $property);
}
return $row;
}
}

View File

@ -0,0 +1,238 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Plugin\migrate\source\SqlBase;
use Drupal\Tests\UnitTestCase;
/**
* Tests the SqlBase class.
*
* @group migrate
*/
class SqlBaseTest extends UnitTestCase {
/**
* Tests that the ID map is joinable.
*
* @param bool $expected_result
* The expected result.
* @param bool $id_map_is_sql
* TRUE if we want getIdMap() to return an instance of Sql.
* @param bool $with_id_map
* TRUE if we want the ID map to have a valid map of IDs.
* @param array $source_options
* (optional) An array of connection options for the source connection.
* Defaults to an empty array.
* @param array $id_map_options
* (optional) An array of connection options for the ID map connection.
* Defaults to an empty array.
*
* @dataProvider sqlBaseTestProvider
*/
public function testMapJoinable($expected_result, $id_map_is_sql, $with_id_map, $source_options = [], $id_map_options = []): void {
// Setup a connection object.
$source_connection = $this->getMockBuilder('Drupal\Core\Database\Connection')
->disableOriginalConstructor()
->getMock();
$source_connection->expects($id_map_is_sql && $with_id_map ? $this->once() : $this->never())
->method('getConnectionOptions')
->willReturn($source_options);
// Setup the ID map connection.
$id_map_connection = $this->getMockBuilder('Drupal\Core\Database\Connection')
->disableOriginalConstructor()
->getMock();
$id_map_connection->expects($id_map_is_sql && $with_id_map ? $this->once() : $this->never())
->method('getConnectionOptions')
->willReturn($id_map_options);
// Setup the Sql object.
$sql = $this->getMockBuilder('Drupal\migrate\Plugin\migrate\id_map\Sql')
->disableOriginalConstructor()
->getMock();
$sql->expects($id_map_is_sql && $with_id_map ? $this->once() : $this->never())
->method('getDatabase')
->willReturn($id_map_connection);
// Setup a migration entity.
$migration = $this->createMock(MigrationInterface::class);
$migration->expects($with_id_map ? $this->once() : $this->never())
->method('getIdMap')
->willReturn($id_map_is_sql ? $sql : NULL);
// Create our SqlBase test class.
$sql_base = new TestSqlBase();
$sql_base->setMigration($migration);
$sql_base->setDatabase($source_connection);
// Configure the idMap to make the check in mapJoinable() pass.
if ($with_id_map) {
$sql_base->setIds([
'uid' => ['type' => 'integer', 'alias' => 'u'],
]);
}
$this->assertEquals($expected_result, $sql_base->mapJoinable());
}
/**
* The data provider for SqlBase.
*
* @return array
* An array of data per test run.
*/
public static function sqlBaseTestProvider() {
return [
// Source ids are empty so mapJoinable() is false.
[
FALSE,
FALSE,
FALSE,
],
// Still false because getIdMap() is not a subclass of Sql.
[
FALSE,
FALSE,
TRUE,
],
// Test mapJoinable() returns false when source and id connection options
// differ.
[
FALSE,
TRUE,
TRUE,
['driver' => 'mysql', 'username' => 'different_from_map', 'password' => 'different_from_map'],
['driver' => 'mysql', 'username' => 'different_from_source', 'password' => 'different_from_source'],
],
// Returns false because driver is pgsql and the databases are not the
// same.
[
FALSE,
TRUE,
TRUE,
['driver' => 'pgsql', 'database' => '1.pgsql', 'username' => 'same_value', 'password' => 'same_value'],
['driver' => 'pgsql', 'database' => '2.pgsql', 'username' => 'same_value', 'password' => 'same_value'],
],
// Returns false because driver is sqlite and the databases are not the
// same.
[
FALSE,
TRUE,
TRUE,
['driver' => 'sqlite', 'database' => '1.sqlite', 'username' => '', 'password' => ''],
['driver' => 'sqlite', 'database' => '2.sqlite', 'username' => '', 'password' => ''],
],
// Returns false because driver is not the same.
[
FALSE,
TRUE,
TRUE,
['driver' => 'pgsql', 'username' => 'same_value', 'password' => 'same_value'],
['driver' => 'mysql', 'username' => 'same_value', 'password' => 'same_value'],
],
];
}
}
/**
* Creates a base source class for SQL migration testing.
*/
class TestSqlBase extends SqlBase {
/**
* The database object.
*
* @var object
*/
protected $database;
/**
* The migration IDs.
*
* @var array
*/
protected $ids;
/**
* Override the constructor so we can create one easily.
*/
public function __construct() {}
/**
* Allows us to set the database during tests.
*
* @param mixed $database
* The database mock object.
*/
public function setDatabase($database): void {
$this->database = $database;
}
/**
* {@inheritdoc}
*/
public function getDatabase() {
return $this->database;
}
/**
* Allows us to set the migration during the test.
*
* @param mixed $migration
* The migration mock.
*/
public function setMigration($migration): void {
$this->migration = $migration;
}
/**
* {@inheritdoc}
*/
public function mapJoinable() {
return parent::mapJoinable();
}
/**
* {@inheritdoc}
*/
public function getIds() {
return $this->ids;
}
/**
* Allows us to set the IDs during a test.
*
* @param array $ids
* An array of identifiers.
*/
public function setIds($ids): void {
$this->ids = $ids;
}
/**
* {@inheritdoc}
*/
public function fields() {
throw new \RuntimeException(__METHOD__ . " not implemented for " . __CLASS__);
}
/**
* {@inheritdoc}
*/
public function query() {
throw new \RuntimeException(__METHOD__ . " not implemented for " . __CLASS__);
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
return [];
}
}

View File

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\migrate\Plugin\migrate\source\SourcePluginBase;
/**
* Stubbed source plugin for testing base class implementations.
*/
class StubSourcePlugin extends SourcePluginBase {
/**
* Helper for setting internal module handler implementation.
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
*/
public function setModuleHandler(ModuleHandlerInterface $module_handler): void {
$this->moduleHandler = $module_handler;
}
/**
* {@inheritdoc}
*/
public function fields(): array {
return [];
}
/**
* {@inheritdoc}
*/
public function __toString(): string {
return '';
}
/**
* {@inheritdoc}
*/
public function getIds(): array {
return [];
}
/**
* {@inheritdoc}
*/
protected function initializeIterator(): \Iterator {
return [];
}
}

View File

@ -0,0 +1,128 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\migrate\MigrateExecutable;
/**
* Tests MigrateExecutable.
*/
class TestMigrateExecutable extends MigrateExecutable {
/**
* The fake memory usage in bytes.
*
* @var int
*/
protected $memoryUsage;
/**
* The cleared memory usage.
*
* @var int
*/
protected $clearedMemoryUsage;
/**
* Sets the string translation service.
*
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The translation manager.
*/
public function setStringTranslation(TranslationInterface $string_translation) {
$this->stringTranslation = $string_translation;
return $this;
}
/**
* Allows access to set protected source property.
*
* @param \Drupal\migrate\Plugin\MigrateSourceInterface $source
* The value to set.
*/
public function setSource($source) {
$this->source = $source;
}
/**
* Allows access to protected sourceIdValues property.
*
* @param array $source_id_values
* The values to set.
*/
public function setSourceIdValues($source_id_values) {
$this->sourceIdValues = $source_id_values;
}
/**
* {@inheritdoc}
*/
public function handleException(\Exception $exception, $save = TRUE) {
$message = $exception->getMessage();
if ($save) {
$this->saveMessage($message);
}
$this->message->display($message);
}
/**
* Allows access to the protected memoryExceeded method.
*
* @return bool
* The memoryExceeded value.
*/
public function memoryExceeded() {
return parent::memoryExceeded();
}
/**
* {@inheritdoc}
*/
protected function attemptMemoryReclaim() {
return $this->clearedMemoryUsage;
}
/**
* {@inheritdoc}
*/
protected function getMemoryUsage() {
return $this->memoryUsage;
}
/**
* Sets the fake memory usage.
*
* @param int $memory_usage
* The fake memory usage value.
* @param int $cleared_memory_usage
* (optional) The fake cleared memory value. Defaults to NULL.
*/
public function setMemoryUsage($memory_usage, $cleared_memory_usage = NULL) {
$this->memoryUsage = $memory_usage;
$this->clearedMemoryUsage = $cleared_memory_usage;
}
/**
* Sets the memory limit.
*
* @param int $memory_limit
* The memory limit.
*/
public function setMemoryLimit($memory_limit) {
$this->memoryLimit = $memory_limit;
}
/**
* Sets the memory threshold.
*
* @param float $threshold
* The new threshold.
*/
public function setMemoryThreshold($threshold) {
$this->memoryThreshold = $threshold;
}
}

View File

@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit;
use Drupal\Core\Database\Connection;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Plugin\migrate\id_map\Sql;
use Drupal\migrate\Plugin\MigrationPluginManagerInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* Defines a SQL ID map for use in tests.
*/
class TestSqlIdMap extends Sql implements \Iterator {
/**
* Constructs a TestSqlIdMap object.
*
* @param \Drupal\Core\Database\Connection $database
* The database.
* @param array $configuration
* The configuration.
* @param string $plugin_id
* The plugin ID for the migration process to do.
* @param mixed $plugin_definition
* The configuration for the plugin.
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration to do.
* @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $event_dispatcher
* The event dispatcher service.
* @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_manager
* The migration manager.
*/
public function __construct(Connection $database, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EventDispatcherInterface $event_dispatcher, MigrationPluginManagerInterface $migration_manager) {
$this->database = $database;
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $event_dispatcher, $migration_manager);
}
/**
* {@inheritdoc}
*/
public $message;
/**
* Gets the field schema.
*
* @param array $id_definition
* An array defining the field, with a key 'type'.
*
* @return array
* A field schema depending on value of key 'type'. An empty array is
* returned if 'type' is not defined.
*
* @throws \Drupal\migrate\MigrateException
*/
protected function getFieldSchema(array $id_definition) {
if (!isset($id_definition['type'])) {
return [];
}
switch ($id_definition['type']) {
case 'integer':
return [
'type' => 'int',
'not null' => TRUE,
];
case 'string':
return [
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
];
default:
throw new MigrateException($id_definition['type'] . ' not supported');
}
}
/**
* {@inheritdoc}
*/
// phpcs:ignore Generic.CodeAnalysis.UselessOverridingMethod, Drupal.Commenting.FunctionComment.Missing
public function ensureTables() {
parent::ensureTables();
}
}

View File

@ -0,0 +1,115 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit\destination;
use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Plugin\migrate\destination\Config;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\migrate\Plugin\migrate\destination\Config
* @group migrate
*/
class ConfigTest extends UnitTestCase {
/**
* Tests the import method.
*/
public function testImport(): void {
$source = [
'test' => 'x',
];
$migration = $this->getMockBuilder('Drupal\migrate\Plugin\Migration')
->disableOriginalConstructor()
->getMock();
$config = $this->getMockBuilder('Drupal\Core\Config\Config')
->disableOriginalConstructor()
->getMock();
foreach ($source as $key => $val) {
$config->expects($this->once())
->method('set')
->with($this->equalTo($key), $this->equalTo($val))
->willReturn($config);
}
$config->expects($this->once())
->method('save');
$config->expects($this->atLeastOnce())
->method('getName')
->willReturn('d8_config');
$config_factory = $this->createMock('Drupal\Core\Config\ConfigFactoryInterface');
$config_factory->expects($this->once())
->method('getEditable')
->with('d8_config')
->willReturn($config);
$row = $this->getMockBuilder('Drupal\migrate\Row')
->disableOriginalConstructor()
->getMock();
$row->expects($this->any())
->method('getRawDestination')
->willReturn($source);
$language_manager = $this->getMockBuilder('Drupal\language\ConfigurableLanguageManagerInterface')
->disableOriginalConstructor()
->getMock();
$language_manager->expects($this->never())
->method('getLanguageConfigOverride')
->with('fr', 'd8_config')
->willReturn($config);
$destination = new Config(['config_name' => 'd8_config'], 'd8_config', ['pluginId' => 'd8_config'], $migration, $config_factory, $language_manager, $this->createMock(TypedConfigManagerInterface::class));
$destination_id = $destination->import($row);
$this->assertEquals(['d8_config'], $destination_id);
}
/**
* Tests the import method.
*/
public function testLanguageImport(): void {
$source = [
'langcode' => 'mi',
];
$migration = $this->getMockBuilder(MigrationInterface::class)
->disableOriginalConstructor()
->getMock();
$config = $this->getMockBuilder('Drupal\Core\Config\Config')
->disableOriginalConstructor()
->getMock();
foreach ($source as $key => $val) {
$config->expects($this->once())
->method('set')
->with($this->equalTo($key), $this->equalTo($val))
->willReturn($config);
}
$config->expects($this->once())
->method('save');
$config->expects($this->any())
->method('getName')
->willReturn('d8_config');
$config_factory = $this->createMock('Drupal\Core\Config\ConfigFactoryInterface');
$config_factory->expects($this->once())
->method('getEditable')
->with('d8_config')
->willReturn($config);
$row = $this->getMockBuilder('Drupal\migrate\Row')
->disableOriginalConstructor()
->getMock();
$row->expects($this->any())
->method('getRawDestination')
->willReturn($source);
$row->expects($this->any())
->method('getDestinationProperty')
->willReturn($source['langcode']);
$language_manager = $this->getMockBuilder('Drupal\language\ConfigurableLanguageManagerInterface')
->disableOriginalConstructor()
->getMock();
$language_manager->expects($this->any())
->method('getLanguageConfigOverride')
->with('mi', 'd8_config')
->willReturn($config);
$destination = new Config(['config_name' => 'd8_config', 'translations' => 'true'], 'd8_config', ['pluginId' => 'd8_config'], $migration, $config_factory, $language_manager, $this->createMock(TypedConfigManagerInterface::class));
$destination_id = $destination->import($row);
$this->assertEquals(['d8_config', 'mi'], $destination_id);
}
}

View File

@ -0,0 +1,265 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit\destination;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\RevisionableInterface;
use Drupal\Core\Entity\RevisionableStorageInterface;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use Drupal\Core\Session\AccountSwitcherInterface;
use Drupal\migrate\Plugin\migrate\destination\EntityRevision as RealEntityRevision;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
use Drupal\Tests\UnitTestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
/**
* Tests entity revision destination.
*
* @group migrate
* @coversDefaultClass \Drupal\migrate\Plugin\migrate\destination\EntityRevision
*/
class EntityRevisionTest extends UnitTestCase {
/**
* The migration.
*
* @var \Drupal\migrate\Plugin\MigrationInterface
*/
protected MigrationInterface $migration;
/**
* The destination storage.
*
* @var \Prophecy\Prophecy\ObjectProphecy
*/
protected ObjectProphecy $storage;
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected EntityFieldManagerInterface $entityFieldManager;
/**
* The field type manager.
*
* @var \Drupal\Core\Field\FieldTypePluginManagerInterface
*/
protected FieldTypePluginManagerInterface $fieldTypeManager;
/**
* The account switcher.
*
* @var \Drupal\Core\Session\AccountSwitcherInterface
*/
protected AccountSwitcherInterface $accountSwitcher;
/**
* The entity type bundle information.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected EntityTypeBundleInfoInterface $entityTypeBundle;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Setup mocks to be used when creating a revision destination.
$this->migration = $this->prophesize(MigrationInterface::class)->reveal();
$this->storage = $this->prophesize(RevisionableStorageInterface::class);
$entity_type = $this->prophesize(EntityTypeInterface::class);
$entity_type->getSingularLabel()->willReturn('crazy');
$entity_type->getPluralLabel()->willReturn('craziness');
$entity_type->getKey('id')->willReturn('nid');
$entity_type->getKey('revision')->willReturn('vid');
$this->storage->getEntityType()->willReturn($entity_type->reveal());
$this->entityFieldManager = $this->prophesize(EntityFieldManagerInterface::class)->reveal();
$this->fieldTypeManager = $this->prophesize(FieldTypePluginManagerInterface::class)->reveal();
$this->accountSwitcher = $this->prophesize(AccountSwitcherInterface::class)->reveal();
$this->entityTypeBundle = $this->prophesize(EntityTypeBundleInfoInterface::class)->reveal();
}
/**
* Tests that passed old destination values are used by default.
*
* @covers ::getEntity
*/
public function testGetEntityDestinationValues(): void {
$destination = $this->getEntityRevisionDestination([]);
// Return a dummy because we don't care what gets called.
$entity = $this->prophesize(RevisionableInterface::class);
// Assert that the first ID from the destination values is used to load the
// entity.
$this->storage->loadRevision(12)
->shouldBeCalled()
->willReturn($entity->reveal());
$row = new Row();
$this->assertEquals($entity->reveal(), $destination->getEntity($row, [12, 13]));
}
/**
* Tests that revision updates update.
*
* @covers ::getEntity
*/
public function testGetEntityUpdateRevision(): void {
$destination = $this->getEntityRevisionDestination([]);
$entity = $this->prophesize(RevisionableInterface::class);
// Assert we load the correct revision.
$this->storage->loadRevision(2)
->shouldBeCalled()
->willReturn($entity->reveal());
// Make sure its set as an update and not the default revision.
$entity->setNewRevision(FALSE)->shouldBeCalled();
$entity->isDefaultRevision()->shouldNotBeCalled();
$row = new Row(['nid' => 1, 'vid' => 2], ['nid' => 1, 'vid' => 2]);
$row->setDestinationProperty('vid', 2);
$this->assertEquals($entity->reveal(), $destination->getEntity($row, []));
}
/**
* Tests that new revisions are flagged to be written as new.
*
* @covers ::getEntity
*/
public function testGetEntityNewRevision(): void {
$destination = $this->getEntityRevisionDestination([]);
$entity = $this->prophesize(RevisionableInterface::class);
// Enforce is new should be disabled.
$entity->enforceIsNew(FALSE)->shouldBeCalled();
// And toggle this as new revision but not the default revision.
$entity->setNewRevision(TRUE)->shouldBeCalled();
$entity->isDefaultRevision(FALSE)->shouldBeCalled();
// Assert we load the correct revision.
$this->storage->load(1)
->shouldBeCalled()
->willReturn($entity->reveal());
$row = new Row(['nid' => 1, 'vid' => 2], ['nid' => 1, 'vid' => 2]);
$row->setDestinationProperty('nid', 1);
$this->assertEquals($entity->reveal(), $destination->getEntity($row, []));
}
/**
* Tests entity load failure.
*
* @covers ::getEntity
*/
public function testGetEntityLoadFailure(): void {
$destination = $this->getEntityRevisionDestination([]);
// Return a failed load and make sure we don't fail and we return FALSE.
$this->storage->load(1)
->shouldBeCalled()
->willReturn(FALSE);
$row = new Row(['nid' => 1, 'vid' => 2], ['nid' => 1, 'vid' => 2]);
$row->setDestinationProperty('nid', 1);
$this->assertFalse($destination->getEntity($row, []));
}
/**
* Tests entity revision save.
*
* @covers ::save
*/
public function testSave(): void {
$entity = $this->prophesize(ContentEntityInterface::class);
$entity->save()
->shouldBeCalled();
// Syncing should be set once.
$entity->setSyncing(Argument::exact(TRUE))
->shouldBeCalledTimes(1);
$entity->getRevisionId()
->shouldBeCalled()
->willReturn(1234);
$destination = $this->getEntityRevisionDestination();
$this->assertEquals([1234], $destination->save($entity->reveal(), []));
}
/**
* Helper method to create an entity revision destination with mock services.
*
* @param array $configuration
* Configuration for the destination.
* @param string $plugin_id
* The plugin id.
* @param array $plugin_definition
* The plugin definition.
*
* @return \Drupal\Tests\migrate\Unit\destination\EntityRevision
* Mocked destination.
*
* @see \Drupal\Tests\migrate\Unit\Destination\EntityRevision
*/
protected function getEntityRevisionDestination(array $configuration = [], $plugin_id = 'entity_revision', array $plugin_definition = []) {
return new EntityRevision($configuration, $plugin_id, $plugin_definition,
$this->migration,
$this->storage->reveal(),
[],
$this->entityFieldManager,
$this->fieldTypeManager,
$this->accountSwitcher,
$this->entityTypeBundle,
);
}
}
/**
* Mock that exposes from internal methods for testing.
*/
class EntityRevision extends RealEntityRevision {
/**
* Allow public access for testing.
*/
public function getEntity(Row $row, array $old_destination_id_values) {
return parent::getEntity($row, $old_destination_id_values);
}
/**
* Allow public access for testing.
*/
public function save(ContentEntityInterface $entity, array $old_destination_id_values = []) {
return parent::save($entity, $old_destination_id_values);
}
/**
* Don't test method from base class.
*
* This method is from the parent and we aren't concerned with the inner
* workings of its implementation which would trickle into mock assertions. An
* empty implementation avoids this.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to update.
* @param \Drupal\migrate\Row $row
* The row object to update from.
*
* @return \Drupal\Core\Entity\EntityInterface
* An updated entity from row values.
*/
protected function updateEntity(EntityInterface $entity, Row $row) {
return $entity;
}
}

View File

@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit\destination;
use Drupal\migrate\Plugin\migrate\destination\ComponentEntityDisplayBase;
use Drupal\migrate\Row;
use Drupal\Tests\migrate\Unit\MigrateTestCase;
/**
* Tests the entity display destination plugin.
*
* @group migrate
*/
class PerComponentEntityDisplayTest extends MigrateTestCase {
/**
* Tests the entity display import method.
*/
public function testImport(): void {
$values = [
'entity_type' => 'entity_type_test',
'bundle' => 'bundle_test',
'view_mode' => 'view_mode_test',
'field_name' => 'field_name_test',
'options' => ['test setting'],
];
$row = new Row();
foreach ($values as $key => $value) {
$row->setDestinationProperty($key, $value);
}
$entity = $this->getMockBuilder('Drupal\Core\Entity\Entity\EntityViewDisplay')
->disableOriginalConstructor()
->getMock();
$entity->expects($this->once())
->method('setComponent')
->with('field_name_test', ['test setting'])
->willReturnSelf();
$entity->expects($this->once())
->method('save')
->with();
$plugin = new TestPerComponentEntityDisplay($entity);
$this->assertSame(['entity_type_test', 'bundle_test', 'view_mode_test', 'field_name_test'], $plugin->import($row));
$this->assertSame(['entity_type_test', 'bundle_test', 'view_mode_test'], $plugin->getTestValues());
}
}
/**
* Test class used for testing per component entity display.
*/
class TestPerComponentEntityDisplay extends ComponentEntityDisplayBase {
const MODE_NAME = 'view_mode';
/**
* The arguments of getEntity.
*
* @var string[]
*/
protected $testValues;
/**
* The test entity.
*
* @var \PHPUnit\Framework\MockObject\MockObject
*/
protected $entity;
public function __construct($entity) {
$this->entity = $entity;
}
/**
* Gets the test entity.
*/
protected function getEntity($entity_type, $bundle, $view_mode) {
$this->testValues = func_get_args();
return $this->entity;
}
/**
* Gets the test values.
*/
public function getTestValues() {
return $this->testValues;
}
}

View File

@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit\destination;
use Drupal\migrate\Plugin\migrate\destination\PerComponentEntityFormDisplay;
use Drupal\migrate\Row;
use Drupal\Tests\migrate\Unit\MigrateTestCase;
/**
* Tests the entity display destination plugin.
*
* @group migrate
*/
class PerComponentEntityFormDisplayTest extends MigrateTestCase {
/**
* Tests the entity display import method.
*/
public function testImport(): void {
$values = [
'entity_type' => 'entity_type_test',
'bundle' => 'bundle_test',
'form_mode' => 'form_mode_test',
'field_name' => 'field_name_test',
'options' => ['test setting'],
];
$row = new Row();
foreach ($values as $key => $value) {
$row->setDestinationProperty($key, $value);
}
$entity = $this->getMockBuilder('Drupal\Core\Entity\Entity\EntityFormDisplay')
->disableOriginalConstructor()
->getMock();
$entity->expects($this->once())
->method('setComponent')
->with('field_name_test', ['test setting'])
->willReturnSelf();
$entity->expects($this->once())
->method('save')
->with();
$plugin = new TestPerComponentEntityFormDisplay($entity);
$this->assertSame(['entity_type_test', 'bundle_test', 'form_mode_test', 'field_name_test'], $plugin->import($row));
$this->assertSame(['entity_type_test', 'bundle_test', 'form_mode_test'], $plugin->getTestValues());
}
}
/**
* Test class for testing per component entity form display.
*/
class TestPerComponentEntityFormDisplay extends PerComponentEntityFormDisplay {
const MODE_NAME = 'form_mode';
/**
* The test values.
*
* @var string[]
*/
protected $testValues;
/**
* The test entity.
*
* @var \PHPUnit\Framework\MockObject\MockObject
*/
protected $entity;
public function __construct($entity) {
$this->entity = $entity;
}
/**
* Gets the test entity.
*/
protected function getEntity($entity_type, $bundle, $form_mode) {
$this->testValues = func_get_args();
return $this->entity;
}
/**
* Gets the test values.
*/
public function getTestValues() {
return $this->testValues;
}
}

View File

@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit\process;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Plugin\migrate\process\ArrayBuild;
/**
* @coversDefaultClass \Drupal\migrate\Plugin\migrate\process\ArrayBuild
* @group migrate
*/
class ArrayBuildTest extends MigrateProcessTestCase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
$configuration = [
'key' => 'foo',
'value' => 'bar',
];
$this->plugin = new ArrayBuild($configuration, 'map', []);
parent::setUp();
}
/**
* Tests successful transformation.
*/
public function testTransform(): void {
$source = [
['foo' => 'Foo', 'bar' => 'Bar'],
['foo' => 'foo bar', 'bar' => 'bar foo'],
];
$expected = [
'Foo' => 'Bar',
'foo bar' => 'bar foo',
];
$value = $this->plugin->transform($source, $this->migrateExecutable, $this->row, 'destination_property');
$this->assertSame($value, $expected);
}
/**
* Tests non-existent key for the key configuration.
*/
public function testNonExistentKey(): void {
$source = [
['bar' => 'foo'],
];
$this->expectException(MigrateException::class);
$this->expectExceptionMessage("The key 'foo' does not exist");
$this->plugin->transform($source, $this->migrateExecutable, $this->row, 'destination_property');
}
/**
* Tests non-existent key for the value configuration.
*/
public function testNonExistentValue(): void {
$source = [
['foo' => 'bar'],
];
$this->expectException(MigrateException::class);
$this->expectExceptionMessage("The key 'bar' does not exist");
$this->plugin->transform($source, $this->migrateExecutable, $this->row, 'destination_property');
}
/**
* Tests one-dimensional array input.
*/
public function testOneDimensionalArrayInput(): void {
$source = ['foo' => 'bar'];
$this->expectException(MigrateException::class);
$this->expectExceptionMessage('The input should be an array of arrays');
$this->plugin->transform($source, $this->migrateExecutable, $this->row, 'destination_property');
}
/**
* Tests string input.
*/
public function testStringInput(): void {
$source = 'foo';
$this->expectException(MigrateException::class);
$this->expectExceptionMessage('The input should be an array of arrays');
$this->plugin->transform($source, $this->migrateExecutable, $this->row, 'destination_property');
}
}

View File

@ -0,0 +1,137 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit\process;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Plugin\migrate\process\Callback;
/**
* Tests the callback process plugin.
*
* @group migrate
*/
class CallbackTest extends MigrateProcessTestCase {
/**
* Tests callback with valid "callable".
*
* @dataProvider providerCallback
*/
public function testCallback($callable): void {
$configuration = ['callable' => $callable];
$this->plugin = new Callback($configuration, 'map', []);
$value = $this->plugin->transform('FooBar', $this->migrateExecutable, $this->row, 'destination_property');
$this->assertSame('foobar', $value);
}
/**
* Data provider for ::testCallback().
*/
public static function providerCallback() {
return [
'function' => ['strtolower'],
'class method' => [[self::class, 'strtolower']],
];
}
/**
* Test callback with valid "callable" and multiple arguments.
*
* @dataProvider providerCallbackArray
*/
public function testCallbackArray($callable, $args, $result): void {
$configuration = ['callable' => $callable, 'unpack_source' => TRUE];
$this->plugin = new Callback($configuration, 'map', []);
$value = $this->plugin->transform($args, $this->migrateExecutable, $this->row, 'destination_property');
$this->assertSame($result, $value);
}
/**
* Data provider for ::testCallbackArray().
*/
public static function providerCallbackArray() {
return [
'date format' => [
'date',
['Y-m-d', 995328000],
'2001-07-17',
],
'rtrim' => [
'rtrim',
['https://www.example.com/', '/'],
'https://www.example.com',
],
'str_replace' => [
'str_replace',
[['One', 'two'], ['1', '2'], 'One, two, three!'],
'1, 2, three!',
],
'pi' => [
'pi',
[],
pi(),
],
];
}
/**
* Tests callback exceptions.
*
* @param string $message
* The expected exception message.
* @param array $configuration
* The plugin configuration being tested.
* @param string $class
* (optional) The expected exception class.
* @param mixed $args
* (optional) Arguments to pass to the transform() method.
*
* @dataProvider providerCallbackExceptions
*/
public function testCallbackExceptions($message, array $configuration, $class = 'InvalidArgumentException', $args = NULL): void {
$this->expectException($class);
$this->expectExceptionMessage($message);
$this->plugin = new Callback($configuration, 'map', []);
$this->plugin->transform($args, $this->migrateExecutable, $this->row, 'destination_property');
}
/**
* Data provider for ::testCallbackExceptions().
*/
public static function providerCallbackExceptions() {
return [
'not set' => [
'message' => 'The "callable" must be set.',
'configuration' => [],
],
'invalid method' => [
'message' => 'The "callable" must be a valid function or method.',
'configuration' => ['callable' => 'nonexistent_callable'],
],
'array required' => [
'message' => "When 'unpack_source' is set, the source must be an array. Instead it was of type 'string'",
'configuration' => ['callable' => 'count', 'unpack_source' => TRUE],
'class' => MigrateException::class,
'args' => 'This string is not an array.',
],
];
}
/**
* Makes a string lowercase for testing purposes.
*
* @param string $string
* The input string.
*
* @return string
* The lowercased string.
*
* @see \Drupal\Tests\migrate\Unit\process\CallbackTest::providerCallback()
*/
public static function strToLower($string) {
return mb_strtolower($string);
}
}

View File

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit\process;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Plugin\migrate\process\Concat;
/**
* Tests the concat process plugin.
*
* @group migrate
*/
class ConcatTest extends MigrateProcessTestCase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
$this->plugin = new TestConcat();
parent::setUp();
}
/**
* Tests concat works without a delimiter.
*/
public function testConcatWithoutDelimiter(): void {
$value = $this->plugin->transform(['foo', 'bar'], $this->migrateExecutable, $this->row, 'destination_property');
$this->assertSame('foobar', $value);
}
/**
* Tests concat fails properly on non-arrays.
*/
public function testConcatWithNonArray(): void {
$this->expectException(MigrateException::class);
$this->plugin->transform('foo', $this->migrateExecutable, $this->row, 'destination_property');
}
/**
* Tests concat works without a delimiter.
*/
public function testConcatWithDelimiter(): void {
$this->plugin->setDelimiter('_');
$value = $this->plugin->transform(['foo', 'bar'], $this->migrateExecutable, $this->row, 'destination_property');
$this->assertSame('foo_bar', $value);
}
}
/**
* Mock class for the concat process plugin.
*/
class TestConcat extends Concat {
public function __construct() {
}
/**
* Set the delimiter.
*
* @param string $delimiter
* The new delimiter.
*/
public function setDelimiter($delimiter): void {
$this->configuration['delimiter'] = $delimiter;
}
}

View File

@ -0,0 +1,146 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit\process;
use Drupal\migrate\Plugin\migrate\process\DefaultValue;
/**
* Tests the default_value process plugin.
*
* @group migrate
* @coversDefaultClass \Drupal\migrate\Plugin\migrate\process\DefaultValue
*/
class DefaultValueTest extends MigrateProcessTestCase {
/**
* Tests the default_value process plugin.
*
* @covers ::transform
*
* @dataProvider defaultValueDataProvider
*/
public function testDefaultValue($configuration, $expected_value, $value): void {
$process = new DefaultValue($configuration, 'default_value', []);
$value = $process->transform($value, $this->migrateExecutable, $this->row, 'destination_property');
$this->assertSame($expected_value, $value);
}
/**
* Provides data for the successful lookup test.
*
* @return array
* An array of test cases.
*/
public static function defaultValueDataProvider() {
return [
'strict_true_value_populated_array' => [
'configuration' => [
'strict' => TRUE,
'default_value' => 1,
],
'expected_value' => [0, 1, 2],
'value' => [0, 1, 2],
],
'strict_true_value_empty_string' => [
'configuration' => [
'strict' => TRUE,
'default_value' => 1,
],
'expected_value' => '',
'value' => '',
],
'strict_true_value_false' => [
'configuration' => [
'strict' => TRUE,
'default_value' => 1,
],
'expected_value' => FALSE,
'value' => FALSE,
],
'strict_true_value_null' => [
'configuration' => [
'strict' => TRUE,
'default_value' => 1,
],
'expected_value' => 1,
'value' => NULL,
],
'strict_true_value_zero_string' => [
'configuration' => [
'strict' => TRUE,
'default_value' => 1,
],
'expected_value' => '0',
'value' => '0',
],
'strict_true_value_zero' => [
'configuration' => [
'strict' => TRUE,
'default_value' => 1,
],
'expected_value' => 0,
'value' => 0,
],
'strict_true_value_empty_array' => [
'configuration' => [
'strict' => TRUE,
'default_value' => 1,
],
'expected_value' => [],
'value' => [],
],
'array_populated' => [
'configuration' => [
'default_value' => 1,
],
'expected_value' => [0, 1, 2],
'value' => [0, 1, 2],
],
'empty_string' => [
'configuration' => [
'default_value' => 1,
],
'expected_value' => 1,
'value' => '',
],
'false' => [
'configuration' => [
'default_value' => 1,
],
'expected_value' => 1,
'value' => FALSE,
],
'null' => [
'configuration' => [
'default_value' => 1,
],
'expected_value' => 1,
'value' => NULL,
],
'string_zero' => [
'configuration' => [
'default_value' => 1,
],
'expected_value' => 1,
'value' => '0',
],
'int_zero' => [
'configuration' => [
'default_value' => 1,
],
'expected_value' => 1,
'value' => 0,
],
'empty_array' => [
'configuration' => [
'default_value' => 1,
],
'expected_value' => 1,
'value' => [],
],
];
}
}

View File

@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit\process;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Plugin\migrate\process\Explode;
use Drupal\migrate\Plugin\migrate\process\Concat;
/**
* Tests the Explode process plugin.
*
* @group migrate
*/
class ExplodeTest extends MigrateProcessTestCase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
$configuration = [
'delimiter' => ',',
];
$this->plugin = new Explode($configuration, 'map', []);
parent::setUp();
}
/**
* Tests explode transform process works.
*/
public function testTransform(): void {
$value = $this->plugin->transform('foo,bar,tik', $this->migrateExecutable, $this->row, 'destination_property');
$this->assertSame(['foo', 'bar', 'tik'], $value);
}
/**
* Tests explode transform process works with a limit.
*/
public function testTransformLimit(): void {
$plugin = new Explode(['delimiter' => '_', 'limit' => 2], 'map', []);
$value = $plugin->transform('foo_bar_tik', $this->migrateExecutable, $this->row, 'destination_property');
$this->assertSame(['foo', 'bar_tik'], $value);
}
/**
* Tests if the explode process can be chained with handles_multiple process.
*/
public function testChainedTransform(): void {
$exploded = $this->plugin->transform('One,Two,Three', $this->migrateExecutable, $this->row, 'destination_property');
$concat = new Concat([], 'map', []);
$concatenated = $concat->transform($exploded, $this->migrateExecutable, $this->row, 'destination_property');
$this->assertSame('OneTwoThree', $concatenated);
}
/**
* Tests explode fails properly on non-strings.
*/
public function testExplodeWithNonString(): void {
$this->expectException(MigrateException::class);
$this->expectExceptionMessage('is not a string');
$this->plugin->transform(['foo'], $this->migrateExecutable, $this->row, 'destination_property');
}
/**
* Tests that explode works on non-strings but with strict set to FALSE.
*
* @dataProvider providerExplodeWithNonStrictAndEmptySource
*/
public function testExplodeWithNonStrictAndEmptySource($value, $expected): void {
$plugin = new Explode(['delimiter' => '|', 'strict' => FALSE], 'map', []);
$processed = $plugin->transform($value, $this->migrateExecutable, $this->row, 'destination_property');
$this->assertSame($expected, $processed);
}
/**
* Data provider for ::testExplodeWithNonStrictAndEmptySource().
*/
public static function providerExplodeWithNonStrictAndEmptySource() {
return [
'normal_string' => ['a|b|c', ['a', 'b', 'c']],
'integer_cast_to_string' => [123, ['123']],
'zero_integer_cast_to_string' => [0, ['0']],
'true_cast_to_string' => [TRUE, ['1']],
'null_empty_array' => [NULL, []],
'false_empty_array' => [FALSE, []],
'empty_string_empty_array' => ['', []],
];
}
/**
* Tests Explode exception handling when string-cast fails.
*/
public function testExplodeWithNonStrictAndNonCastable(): void {
$plugin = new Explode(['delimiter' => '|', 'strict' => FALSE], 'map', []);
$this->expectException(MigrateException::class);
$this->expectExceptionMessage('cannot be casted to a string');
$processed = $plugin->transform(['foo'], $this->migrateExecutable, $this->row, 'destination_property');
$this->assertSame(['foo'], $processed);
}
/**
* Tests Explode return values with an empty string and strict check.
*/
public function testExplodeWithStrictAndEmptyString(): void {
$plugin = new Explode(['delimiter' => '|'], 'map', []);
$processed = $plugin->transform('', $this->migrateExecutable, $this->row, 'destination_property');
$this->assertSame([''], $processed);
}
/**
* Tests explode fails with empty delimiter.
*/
public function testExplodeWithEmptyDelimiter(): void {
$this->expectException(MigrateException::class);
$this->expectExceptionMessage('delimiter is empty');
$plugin = new Explode(['delimiter' => ''], 'map', []);
$plugin->transform('foo,bar', $this->migrateExecutable, $this->row, 'destination_property');
}
}

View File

@ -0,0 +1,183 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit\process;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Plugin\migrate\process\Extract;
/**
* @coversDefaultClass \Drupal\migrate\Plugin\migrate\process\Extract
* @group migrate
*/
class ExtractTest extends MigrateProcessTestCase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
$configuration['index'] = ['foo'];
$this->plugin = new Extract($configuration, 'map', []);
parent::setUp();
}
/**
* Tests successful extraction.
*/
public function testExtract(): void {
$value = $this->plugin->transform(['foo' => 'bar'], $this->migrateExecutable, $this->row, 'destination_property');
$this->assertSame('bar', $value);
}
/**
* Tests invalid input.
*
* @dataProvider providerTestExtractInvalid
*/
public function testExtractInvalid($value): void {
$this->expectException(MigrateException::class);
$type = gettype($value);
$this->expectExceptionMessage(sprintf("Input should be an array, instead it was of type '%s'", $type));
$this->plugin->transform($value, $this->migrateExecutable, $this->row, 'destination_property');
}
/**
* Tests unsuccessful extraction.
*/
public function testExtractFail(): void {
$this->expectException(MigrateException::class);
$this->expectExceptionMessage("Array index missing, extraction failed for '[\n 'bar' => 'foo',\n]'. Consider adding a `default` key to the configuration.");
$this->plugin->transform(['bar' => 'foo'], $this->migrateExecutable, $this->row, 'destination_property');
}
/**
* Tests unsuccessful extraction.
*/
public function testExtractFailDefault(): void {
$plugin = new Extract(['index' => ['foo'], 'default' => 'test'], 'map', []);
$value = $plugin->transform(['bar' => 'foo'], $this->migrateExecutable, $this->row, 'destination_property');
$this->assertSame('test', $value, '');
}
/**
* Test the extract plugin with default values.
*
* @param array $value
* The process plugin input value.
* @param array $configuration
* The plugin configuration.
* @param string|null $expected
* The expected transformed value.
*
* @throws \Drupal\migrate\MigrateException
*
* @dataProvider providerExtractDefault
*/
public function testExtractDefault(array $value, array $configuration, $expected): void {
$this->plugin = new Extract($configuration, 'map', []);
$value = $this->plugin->transform($value, $this->migrateExecutable, $this->row, 'destination_property');
$this->assertSame($expected, $value);
}
/**
* Data provider for testExtractDefault.
*/
public static function providerExtractDefault() {
return [
[
['foo' => 'bar'],
[
'index' => ['foo'],
'default' => 'one',
],
'bar',
],
[
['foo' => 'bar'],
[
'index' => ['not_key'],
'default' => 'two',
],
'two',
],
[
['foo' => 'bar'],
[
'index' => ['not_key'],
'default' => NULL,
],
NULL,
],
[
['foo' => 'bar'],
[
'index' => ['not_key'],
'default' => TRUE,
],
TRUE,
],
[
['foo' => 'bar'],
[
'index' => ['not_key'],
'default' => FALSE,
],
FALSE,
],
[
['foo' => ''],
[
'index' => ['foo'],
'default' => NULL,
],
'',
],
];
}
/**
* Provides data for the testExtractInvalid.
*/
public static function providerTestExtractInvalid() {
$xml_str = <<<XML
<xml version='1.0'?>
<authors>
<name>Test Extract Invalid</name>
</authors>
XML;
$object = (object) [
'one' => 'test1',
'two' => 'test2',
'three' => 'test3',
];
return [
'empty string' => [
'',
],
'string' => [
'Extract Test',
],
'integer' => [
1,
],
'float' => [
1.0,
],
'NULL' => [
NULL,
],
'boolean' => [
TRUE,
],
'xml' => [
$xml_str,
],
'object' => [
$object,
],
];
}
}

View File

@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit\process;
use Drupal\Core\File\FileExists;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface;
use Drupal\migrate\Plugin\migrate\process\FileCopy;
use Drupal\migrate\Plugin\MigrateProcessInterface;
/**
* Tests the file copy process plugin.
*
* @group migrate
*
* @coversDefaultClass \Drupal\migrate\Plugin\migrate\process\FileCopy
*/
class FileCopyTest extends MigrateProcessTestCase {
/**
* Tests that the plugin constructor correctly sets the configuration.
*
* @param array $configuration
* The plugin configuration.
* @param \Drupal\Core\File\FileExists $expected
* The expected value of the plugin configuration.
*
* @dataProvider providerFileProcessBaseConstructor
*/
public function testFileProcessBaseConstructor(array $configuration, FileExists $expected): void {
$this->assertPlugin($configuration, $expected);
}
/**
* Data provider for testFileProcessBaseConstructor.
*/
public static function providerFileProcessBaseConstructor() {
return [
[['file_exists' => 'replace'], FileExists::Replace],
[['file_exists' => 'rename'], FileExists::Rename],
[['file_exists' => 'use existing'], FileExists::Error],
[['file_exists' => 'foobar'], FileExists::Replace],
[[], FileExists::Replace],
];
}
/**
* Creates a TestFileCopy process plugin.
*
* @param array $configuration
* The plugin configuration.
* @param \Drupal\Core\File\FileExists $expected
* The expected value of the plugin configuration.
*
* @internal
*/
protected function assertPlugin(array $configuration, FileExists $expected): void {
$stream_wrapper_manager = $this->prophesize(StreamWrapperManagerInterface::class)->reveal();
$file_system = $this->prophesize(FileSystemInterface::class)->reveal();
$download_plugin = $this->prophesize(MigrateProcessInterface::class)->reveal();
$this->plugin = new TestFileCopy($configuration, 'test', [], $stream_wrapper_manager, $file_system, $download_plugin);
$plugin_config = $this->plugin->getConfiguration();
$this->assertArrayHasKey('file_exists', $plugin_config);
$this->assertSame($expected, $plugin_config['file_exists']);
}
}
/**
* Class for testing FileCopy.
*/
class TestFileCopy extends FileCopy {
/**
* Gets this plugin's configuration.
*
* @return array
* An array of this plugin's configuration.
*/
public function getConfiguration() {
return $this->configuration;
}
}

View File

@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit\process;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Plugin\migrate\process\Flatten;
/**
* Tests the flatten plugin.
*
* @group migrate
*/
class FlattenTest extends MigrateProcessTestCase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
$this->plugin = new Flatten([], 'flatten', []);
parent::setUp();
}
/**
* Tests that various array flatten operations work properly.
*
* @dataProvider providerTestFlatten
*/
public function testFlatten($value, $expected): void {
$flattened = $this->plugin->transform($value, $this->migrateExecutable, $this->row, 'destination_property');
$this->assertSame($expected, $flattened);
}
/**
* Provides data for the testFlatten.
*/
public static function providerTestFlatten() {
$object = (object) [
'a' => 'test',
'b' => '1.2',
'c' => 'NULL',
];
return [
'array' => [
[1, 2, [3, 4, [5]], [], [7, 8]],
[1, 2, 3, 4, 5, 7, 8],
],
'object' => [
$object,
['test', '1.2', 'NULL'],
],
];
}
/**
* Tests that Flatten throws a MigrateException.
*
* @dataProvider providerTestFlattenInvalid
*/
public function testFlattenInvalid($value): void {
$this->expectException(MigrateException::class);
$type = gettype($value);
$this->expectExceptionMessage(sprintf("Input should be an array or an object, instead it was of type '%s'", $type));
$this->plugin->transform($value, $this->migrateExecutable, $this->row, 'destination_property');
}
/**
* Provides data for the testFlattenInvalid.
*/
public static function providerTestFlattenInvalid() {
$xml_str = <<<XML
<xml version='1.0'?>
<authors>
<name>Ada Lovelace</name>
</authors>
XML;
return [
'empty string' => [
'',
],
'string' => [
'Kate Sheppard',
],
'integer' => [
1,
],
'float' => [
1.2,
],
'NULL' => [
NULL,
],
'boolean' => [
TRUE,
],
'xml' => [
$xml_str,
],
];
}
}

View File

@ -0,0 +1,218 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit\process;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Plugin\migrate\process\FormatDate;
/**
* Tests the format date process plugin.
*
* @group migrate
*
* @coversDefaultClass Drupal\migrate\Plugin\migrate\process\FormatDate
*/
class FormatDateTest extends MigrateProcessTestCase {
/**
* Tests that missing configuration will throw an exception.
*/
public function testMigrateExceptionMissingFromFormat(): void {
$configuration = [
'from_format' => '',
'to_format' => 'Y-m-d',
];
$this->expectException(MigrateException::class);
$this->expectExceptionMessage('Format date plugin is missing from_format configuration.');
$this->plugin = new FormatDate($configuration, 'test_format_date', []);
$this->plugin->transform('01/05/1955', $this->migrateExecutable, $this->row, 'field_date');
}
/**
* Tests that missing configuration will throw an exception.
*/
public function testMigrateExceptionMissingToFormat(): void {
$configuration = [
'from_format' => 'm/d/Y',
'to_format' => '',
];
$this->expectException(MigrateException::class);
$this->expectExceptionMessage('Format date plugin is missing to_format configuration.');
$this->plugin = new FormatDate($configuration, 'test_format_date', []);
$this->plugin->transform('01/05/1955', $this->migrateExecutable, $this->row, 'field_date');
}
/**
* Tests that date format mismatches will throw an exception.
*/
public function testMigrateExceptionBadFormat(): void {
$configuration = [
'from_format' => 'm/d/Y',
'to_format' => 'Y-m-d',
];
$this->expectException(MigrateException::class);
$this->expectExceptionMessage("Format date plugin could not transform 'January 5, 1955' using the format 'm/d/Y'. Error: The date cannot be created from a format.");
$this->plugin = new FormatDate($configuration, 'test_format_date', []);
$this->plugin->transform('January 5, 1955', $this->migrateExecutable, $this->row, 'field_date');
}
/**
* Tests that an unexpected date value will throw an exception.
*/
public function testMigrateExceptionUnexpectedValue(): void {
$configuration = [
'from_format' => 'm/d/Y',
'to_format' => 'Y-m-d',
];
$this->expectException(MigrateException::class);
$this->expectExceptionMessage("Format date plugin could not transform '01/05/55' using the format 'm/d/Y'. Error: The created date does not match the input value.");
$this->plugin = new FormatDate($configuration, 'test_format_date', []);
$this->plugin->transform('01/05/55', $this->migrateExecutable, $this->row, 'field_date');
}
/**
* Tests transformation.
*
* @param array $configuration
* The configuration of the migration process plugin.
* @param string $value
* The source value for the migration process plugin.
* @param string $expected
* The expected value of the migration process plugin.
*
* @covers ::transform
* @dataProvider datesDataProvider
*/
public function testTransform($configuration, $value, $expected): void {
$this->plugin = new FormatDate($configuration, 'test_format_date', []);
$actual = $this->plugin->transform($value, $this->migrateExecutable, $this->row, 'field_date');
$this->assertEquals($expected, $actual);
}
/**
* Data provider of test dates.
*
* @return array
* Array of date formats and actual/expected values.
*/
public static function datesDataProvider() {
return [
'datetime_date' => [
'configuration' => [
'from_format' => 'm/d/Y',
'to_format' => 'Y-m-d',
],
'value' => '01/05/1955',
'expected' => '1955-01-05',
],
'datetime_datetime' => [
'configuration' => [
'from_format' => 'm/d/Y H:i:s',
'to_format' => 'Y-m-d\TH:i:s e',
],
'value' => '01/05/1955 10:43:22',
'expected' => '1955-01-05T10:43:22 Australia/Sydney',
],
'empty_values' => [
'configuration' => [
'from_format' => 'm/d/Y',
'to_format' => 'Y-m-d',
],
'value' => '',
'expected' => '',
],
'timezone_from_to' => [
'configuration' => [
'from_format' => 'Y-m-d H:i:s',
'to_format' => 'Y-m-d H:i:s e',
'from_timezone' => 'America/Managua',
'to_timezone' => 'UTC',
],
'value' => '2004-12-19 10:19:42',
'expected' => '2004-12-19 16:19:42 UTC',
],
'timezone_from' => [
'configuration' => [
'from_format' => 'Y-m-d h:i:s',
'to_format' => 'Y-m-d h:i:s e',
'from_timezone' => 'America/Managua',
],
'value' => '2004-11-19 10:25:33',
// Unit tests use Australia/Sydney timezone, so date value will be
// converted from America/Managua to Australia/Sydney timezone.
'expected' => '2004-11-20 03:25:33 Australia/Sydney',
],
'timezone_to' => [
'configuration' => [
'from_format' => 'Y-m-d H:i:s',
'to_format' => 'Y-m-d H:i:s e',
'to_timezone' => 'America/Managua',
],
'value' => '2004-12-19 10:19:42',
// Unit tests use Australia/Sydney timezone, so date value will be
// converted from Australia/Sydney to America/Managua timezone.
'expected' => '2004-12-18 17:19:42 America/Managua',
],
'integer_0' => [
'configuration' => [
'from_format' => 'U',
'to_format' => 'Y-m-d',
],
'value' => 0,
'expected' => '1970-01-01',
],
'string_0' => [
'configuration' => [
'from_format' => 'U',
'to_format' => 'Y-m-d',
],
'value' => '0',
'expected' => '1970-01-01',
],
'zeros' => [
'configuration' => [
'from_format' => 'Y-m-d H:i:s',
'to_format' => 'Y-m-d H:i:s e',
'settings' => ['validate_format' => FALSE],
],
'value' => '0000-00-00 00:00:00',
'expected' => '-0001-11-30 00:00:00 Australia/Sydney',
],
'zeros_same_timezone' => [
'configuration' => [
'from_format' => 'Y-m-d H:i:s',
'to_format' => 'Y-m-d H:i:s',
'settings' => ['validate_format' => FALSE],
'from_timezone' => 'UTC',
'to_timezone' => 'UTC',
],
'value' => '0000-00-00 00:00:00',
'expected' => '-0001-11-30 00:00:00',
],
'collected_date_attributes_day' => [
'configuration' => [
'from_format' => 'Y-m-d\TH:i:s',
'to_format' => 'Y-m-d\TH:i:s',
],
'value' => '2012-01-00T00:00:00',
'expected' => '2012-01-01T00:00:00',
],
'collected_date_attributes_month' => [
'configuration' => [
'from_format' => 'Y-m-d\TH:i:s',
'to_format' => 'Y-m-d\TH:i:s',
],
'value' => '2012-00-00T00:00:00',
'expected' => '2012-01-01T00:00:00',
],
];
}
}

View File

@ -0,0 +1,127 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit\process;
use Drupal\migrate\Plugin\migrate\process\Get;
/**
* Tests the get process plugin.
*
* @group migrate
*/
class GetTest extends MigrateProcessTestCase {
/**
* Tests the Get plugin when source is a string.
*/
public function testTransformSourceString(): void {
$this->row->expects($this->once())
->method('get')
->with('test')
->willReturn('source_value');
$this->plugin = new Get(['source' => 'test'], '', []);
$value = $this->plugin->transform(NULL, $this->migrateExecutable, $this->row, 'destination_property');
$this->assertSame('source_value', $value);
}
/**
* Tests the Get plugin when source is an array.
*/
public function testTransformSourceArray(): void {
$map = [
'test1' => 'source_value1',
'test2' => 'source_value2',
];
$this->plugin = new Get(['source' => ['test1', 'test2']], '', []);
$this->row->expects($this->exactly(2))
->method('get')
->willReturnCallback(function ($argument) use ($map) {
return $map[$argument];
});
$value = $this->plugin->transform(NULL, $this->migrateExecutable, $this->row, 'destination_property');
$this->assertSame(['source_value1', 'source_value2'], $value);
}
/**
* Tests the Get plugin when source is a string pointing to destination.
*/
public function testTransformSourceStringAt(): void {
$this->row->expects($this->once())
->method('get')
->with('@@test')
->willReturn('source_value');
$this->plugin = new Get(['source' => '@@test'], '', []);
$value = $this->plugin->transform(NULL, $this->migrateExecutable, $this->row, 'destination_property');
$this->assertSame('source_value', $value);
}
/**
* Tests the Get plugin when source is an array pointing to destination.
*/
public function testTransformSourceArrayAt(): void {
$map = [
'test1' => 'source_value1',
'@@test2' => 'source_value2',
'@@test3' => 'source_value3',
'test4' => 'source_value4',
];
$this->plugin = new Get(['source' => ['test1', '@@test2', '@@test3', 'test4']], '', []);
$this->row->expects($this->exactly(4))
->method('get')
->willReturnCallback(function ($argument) use ($map) {
return $map[$argument];
});
$value = $this->plugin->transform(NULL, $this->migrateExecutable, $this->row, 'destination_property');
$this->assertSame(['source_value1', 'source_value2', 'source_value3', 'source_value4'], $value);
}
/**
* Tests the Get plugin when source has integer values.
*
* @dataProvider integerValuesDataProvider
*/
public function testIntegerValues($source, $expected_value): void {
$this->row->expects($this->atMost(2))
->method('get')
->willReturnOnConsecutiveCalls('val1', 'val2');
$this->plugin = new Get(['source' => $source], '', []);
$return = $this->plugin->transform(NULL, $this->migrateExecutable, $this->row, 'destination_property');
$this->assertSame($expected_value, $return);
}
/**
* Provides data for the successful lookup test.
*
* @return array
* An array of data for the test.
*/
public static function integerValuesDataProvider() {
return [
[
'source' => [0 => 0, 1 => 'test'],
'expected_value' => [0 => 'val1', 1 => 'val2'],
],
[
'source' => [FALSE],
'expected_value' => [NULL],
],
[
'source' => [NULL],
'expected_value' => [NULL],
],
];
}
/**
* Tests the Get plugin for syntax errors by creating a prophecy of the class.
*
* An example of a syntax error is "Invalid tag_line detected".
*/
public function testPluginSyntax(): void {
$this->assertNotNull($this->prophesize(Get::class));
}
}

View File

@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit\process;
use Drupal\migrate\Plugin\migrate\process\Log;
use Drupal\migrate\Row;
/**
* Tests the Log process plugin.
*
* @group migrate
*/
class LogTest extends MigrateProcessTestCase {
/**
* Tests the Log plugin.
*
* @dataProvider providerTestLog
*/
public function testLog($value, $expected_message): void {
// Test the expected log message.
$this->migrateExecutable->expects($this->once())
->method('saveMessage')
->with($expected_message);
$plugin = new Log([], 'log', []);
// Test the input value is not altered.
$new_value = $plugin->transform($value, $this->migrateExecutable, new Row(), 'foo');
$this->assertSame($value, $new_value);
}
/**
* Provides data for testLog.
*
* @return string[][]
* An array of test data arrays.
*/
public static function providerTestLog() {
$object = (object) [
'a' => 'test',
'b' => 'test2',
'c' => 'test3',
];
$xml_str = <<<XML
<?xml version='1.0'?>
<mathematician>
<name>Ada Lovelace</name>
</mathematician>
XML;
return [
'int zero' => [
'value' => 0,
'expected_message' => "'foo' value is '0'",
],
'string empty' => [
'value' => '',
'expected_message' => "'foo' value is ''",
],
'string' => [
'value' => 'Testing the log message',
'expected_message' => "'foo' value is 'Testing the log message'",
],
'array' => [
'value' => ['key' => 'value'],
'expected_message' => "'foo' value is 'Array\n(\n [key] => value\n)\n'",
],
'float' => [
'value' => 1.123,
'expected_message' => "'foo' value is '1.123000'",
],
'NULL' => [
'value' => NULL,
'expected_message' => "'foo' value is 'NULL'",
],
'boolean' => [
'value' => TRUE,
'expected_message' => "'foo' value is 'true'",
],
'object_with_to_String' => [
'value' => new ObjWithString(),
'expected_message' => "'foo' value is Drupal\Tests\migrate\Unit\process\ObjWithString:\n'a test string'",
],
'object_no_to_string' => [
'value' => $object,
'expected_message' => "Unable to log the value for 'foo'",
],
'simple_xml' => [
'value' => new \SimpleXMLElement($xml_str),
'expected_message' => "'foo' value is SimpleXMLElement:\n'\n \n'",
],
];
}
}
/**
* Test class with a __toString() method.
*/
class ObjWithString {
/**
* Returns a string.
*
* @return string
* A string.
*/
public function __toString() {
return 'a test string';
}
}

View File

@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit\process;
use Drupal\migrate\Plugin\migrate\process\MachineName;
use Drupal\migrate\MigrateException;
/**
* Tests the machine name process plugin.
*
* @group migrate
*/
class MachineNameTest extends MigrateProcessTestCase {
/**
* The mock transliteration.
*
* @var \Drupal\Component\Transliteration\TransliterationInterface
*/
protected $transliteration;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
$this->transliteration = $this->getMockBuilder('Drupal\Component\Transliteration\TransliterationInterface')
->disableOriginalConstructor()
->getMock();
$this->row = $this->getMockBuilder('Drupal\migrate\Row')
->disableOriginalConstructor()
->getMock();
$this->migrateExecutable = $this->getMockBuilder('Drupal\migrate\MigrateExecutable')
->disableOriginalConstructor()
->getMock();
parent::setUp();
}
/**
* Tests machine name transformation of non-alphanumeric characters.
*
* @param string $human_name
* The human-readable name that will be converted in the test.
* @param array $configuration
* The plugin configuration.
* @param string $expected_result
* The expected result of the transformation.
*
* @dataProvider providerTestMachineNames
*/
public function testMachineNames(string $human_name, array $configuration, string $expected_result): void {
// Test for calling transliterate on mock object.
$this->transliteration
->expects($this->once())
->method('transliterate')
->with($human_name)
->willReturnCallback(function (string $string): string {
return str_replace(['á', 'é', 'ő'], ['a', 'e', 'o'], $string);
});
$plugin = new MachineName($configuration, 'machine_name', [], $this->transliteration);
$value = $plugin->transform($human_name, $this->migrateExecutable, $this->row, 'destination_property');
$this->assertEquals($expected_result, $value);
}
/**
* Provides test cases for MachineNameTest::testMachineNames().
*
* @return array
* An array of test cases.
*/
public static function providerTestMachineNames(): array {
return [
// Tests the following transformations:
// - non-alphanumeric character (including spaces) -> underscore,
// - Uppercase -> lowercase,
// - Multiple consecutive underscore -> single underscore.
'default' => [
'human_name' => 'foo2, the.bar;2*&the%baz!YEE____HaW áéő',
'configuration' => [],
'expected_result' => 'foo2_the_bar_2_the_baz_yee_haw_aeo',
],
// Tests with a different pattern that allows periods.
'period_allowed' => [
'human_name' => '2*&the%baz!YEE____HaW áéő.jpg',
'configuration' => [
'replace_pattern' => '/[^a-z0-9_.]+/',
],
'expected_result' => '2_the_baz_yee_haw_aeo.jpg',
],
];
}
/**
* Tests that the replacement regular expression is a string.
*/
public function testInvalidConfiguration(): void {
$configuration['replace_pattern'] = 1;
$this->expectException(MigrateException::class);
$this->expectExceptionMessage('The replace pattern should be a string');
new MachineName($configuration, 'machine_name', [], $this->transliteration);
}
}

View File

@ -0,0 +1,222 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit\process;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\Query\QueryInterface;
use Drupal\migrate\Plugin\migrate\process\MakeUniqueEntityField;
/**
* @coversDefaultClass \Drupal\migrate\Plugin\migrate\process\MakeUniqueEntityField
* @group migrate
*/
class MakeUniqueEntityFieldTest extends MigrateProcessTestCase {
/**
* The mock entity query.
*
* @var \Drupal\Core\Entity\Query\QueryInterface
*/
protected $entityQuery;
/**
* The mocked entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected $entityTypeManager;
/**
* The migration configuration, initialized to set the ID to test.
*
* @var array
*/
protected $migrationConfiguration = [
'id' => 'test',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
$this->entityQuery = $this->getMockBuilder('Drupal\Core\Entity\Query\QueryInterface')
->disableOriginalConstructor()
->getMock();
$this->entityQuery->expects($this->any())
->method('accessCheck')
->willReturnSelf();
$this->entityTypeManager = $this->createMock(EntityTypeManagerInterface::class);
$storage = $this->createMock(EntityStorageInterface::class);
$storage->expects($this->any())
->method('getQuery')
->willReturn($this->entityQuery);
$this->entityTypeManager->expects($this->any())
->method('getStorage')
->with('test_entity_type')
->willReturn($storage);
parent::setUp();
}
/**
* Tests making an entity field value unique.
*
* @dataProvider providerTestMakeUniqueEntityField
*/
public function testMakeUniqueEntityField($count, $postfix = '', $start = NULL, $length = NULL): void {
$configuration = [
'entity_type' => 'test_entity_type',
'field' => 'test_field',
];
if ($postfix) {
$configuration['postfix'] = $postfix;
}
$configuration['start'] = $start;
$configuration['length'] = $length;
$plugin = new MakeUniqueEntityField($configuration, 'make_unique', [], $this->getMigration(), $this->entityTypeManager);
$this->entityQueryExpects($count);
$value = $this->randomMachineName(32);
$actual = $plugin->transform($value, $this->migrateExecutable, $this->row, 'foo');
$expected = mb_substr($value, $start ?? 0, $length);
$expected .= $count ? $postfix . $count : '';
$this->assertSame($expected, $actual);
}
/**
* Tests that invalid start position throws an exception.
*/
public function testMakeUniqueEntityFieldEntityInvalidStart(): void {
$configuration = [
'entity_type' => 'test_entity_type',
'field' => 'test_field',
'start' => 'foobar',
];
$plugin = new MakeUniqueEntityField($configuration, 'make_unique', [], $this->getMigration(), $this->entityTypeManager);
$this->expectException('Drupal\migrate\MigrateException');
$this->expectExceptionMessage('The start position configuration key should be an integer. Omit this key to capture from the beginning of the string.');
$plugin->transform('test_start', $this->migrateExecutable, $this->row, 'foo');
}
/**
* Tests that invalid length option throws an exception.
*/
public function testMakeUniqueEntityFieldEntityInvalidLength(): void {
$configuration = [
'entity_type' => 'test_entity_type',
'field' => 'test_field',
'length' => 'foobar',
];
$plugin = new MakeUniqueEntityField($configuration, 'make_unique', [], $this->getMigration(), $this->entityTypeManager);
$this->expectException('Drupal\migrate\MigrateException');
$this->expectExceptionMessage('The character length configuration key should be an integer. Omit this key to capture the entire string.');
$plugin->transform('test_length', $this->migrateExecutable, $this->row, 'foo');
}
/**
* Data provider for testMakeUniqueEntityField().
*/
public static function providerTestMakeUniqueEntityField() {
return [
// Tests no duplication.
[0],
// Tests no duplication and start position.
[0, NULL, 10],
// Tests no duplication, start position, and length.
[0, NULL, 5, 10],
// Tests no duplication and length.
[0, NULL, NULL, 10],
// Tests duplication.
[3],
// Tests duplication and start position.
[3, NULL, 10],
// Tests duplication, start position, and length.
[3, NULL, 5, 10],
// Tests duplication and length.
[3, NULL, NULL, 10],
// Tests no duplication and postfix.
[0, '_'],
// Tests no duplication, postfix, and start position.
[0, '_', 5],
// Tests no duplication, postfix, start position, and length.
[0, '_', 5, 10],
// Tests no duplication, postfix, and length.
[0, '_', NULL, 10],
// Tests duplication and postfix.
[2, '_'],
// Tests duplication, postfix, and start position.
[2, '_', 5],
// Tests duplication, postfix, start position, and length.
[2, '_', 5, 10],
// Tests duplication, postfix, and length.
[2, '_', NULL, 10],
];
}
/**
* Helper function to add expectations to the mock entity query object.
*
* @param int $count
* The number of unique values to be set up.
*/
protected function entityQueryExpects($count): void {
$this->entityQuery->expects($this->exactly($count + 1))
->method('condition')
->willReturn($this->entityQuery);
$this->entityQuery->expects($this->exactly($count + 1))
->method('count')
->willReturn($this->entityQuery);
$this->entityQuery->expects($this->exactly($count + 1))
->method('execute')
->willReturnCallback(function () use (&$count) {
return $count--;
});
}
/**
* Tests making an entity field value unique only for migrated entities.
*/
public function testMakeUniqueEntityFieldMigrated(): void {
$configuration = [
'entity_type' => 'test_entity_type',
'field' => 'test_field',
'migrated' => TRUE,
];
$plugin = new MakeUniqueEntityField($configuration, 'make_unique', [], $this->getMigration(), $this->entityTypeManager);
// Setup the entityQuery used in MakeUniqueEntityFieldEntity::exists. The
// map, $map, is an array consisting of the four input parameters to the
// query condition method and then the query to return. Both 'forum' and
// 'test_vocab' are existing entities. There is no 'test_vocab1'.
$map = [];
foreach (['forums', 'test_vocab', 'test_vocab1'] as $id) {
$query = $this->prophesize(QueryInterface::class);
$query->willBeConstructedWith([]);
$query->accessCheck()->willReturn($query);
$query->execute()->willReturn($id === 'test_vocab1' ? [] : [$id]);
$map[] = ['test_field', $id, NULL, NULL, $query->reveal()];
}
$this->entityQuery
->method('condition')
->willReturnMap($map);
// Entity 'forums' is pre-existing, entity 'test_vocab' was migrated.
$this->idMap
->method('lookupSourceId')
->willReturnMap([
[['test_field' => 'forums'], FALSE],
[['test_field' => 'test_vocab'], ['source_id' => 42]],
]);
// Existing entity 'forums' was not migrated, value should not be unique.
$actual = $plugin->transform('forums', $this->migrateExecutable, $this->row, 'foo');
$this->assertEquals('forums', $actual, 'Pre-existing name is re-used');
// Entity 'test_vocab' was migrated, value should be unique.
$actual = $plugin->transform('test_vocab', $this->migrateExecutable, $this->row, 'foo');
$this->assertEquals('test_vocab1', $actual, 'Migrated name is deduplicated');
}
}

View File

@ -0,0 +1,341 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Unit\process;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Menu\MenuLinkDefault;
use Drupal\Core\Menu\MenuLinkManagerInterface;
use Drupal\Core\Menu\StaticMenuLinkOverridesInterface;
use Drupal\Core\Path\PathValidatorInterface;
use Drupal\Core\Url;
use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\migrate\MigrateLookupInterface;
use Drupal\migrate\MigrateSkipRowException;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Plugin\migrate\process\MenuLinkParent;
// cspell:ignore plid
/**
* Tests the menu link parent process plugin.
*
* @coversDefaultClass \Drupal\migrate\Plugin\migrate\process\MenuLinkParent
* @group migrate
*/
class MenuLinkParentTest extends MigrateProcessTestCase {
/**
* A MigrationInterface prophecy.
*
* @var \Prophecy\Prophecy\ObjectProphecy
*/
protected $migration;
/**
* A MigrateLookupInterface prophecy.
*
* @var \Prophecy\Prophecy\ObjectProphecy
*/
protected $migrateLookup;
/**
* A Path validator prophecy.
*
* @var \Prophecy\Prophecy\ObjectProphecy
*/
protected $pathValidator;
/**
* The menu link entity storage handler.
*
* @var \Prophecy\Prophecy\ObjectProphecy
*/
protected $menuLinkStorage;
/**
* The menu link plugin manager.
*
* @var \Prophecy\Prophecy\ObjectProphecy
*/
protected $menuLinkManager;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->migration = $this->prophesize(MigrationInterface::class);
$this->migrateLookup = $this->prophesize(MigrateLookupInterface::class);
$this->menuLinkManager = $this->prophesize(MenuLinkManagerInterface::class);
$this->menuLinkStorage = $this->prophesize(EntityStorageInterface::class);
$container = new ContainerBuilder();
$container->set('migrate.lookup', $this->migrateLookup->reveal());
$this->pathValidator = $this->prophesize(PathValidatorInterface::class);
$container->set('path.validator', $this->pathValidator->reveal());
\Drupal::setContainer($container);
}
/**
* Tests that an exception is thrown for invalid options.
*
* @param array $configuration
* The plugin configuration being tested.
* @param bool $is_valid
* TRUE if the configuration is valid, FALSE if not.
*
* @dataProvider providerConstructorException
*/
public function testConstructorException(array $configuration, bool $is_valid): void {
if (!$is_valid) {
$this->expectException('TypeError');
$this->expectExceptionMessage('Cannot assign string to property ' . MenuLinkParent::class . '::$lookupMigrations of type array');
}
$plugin = new MenuLinkParent($configuration, 'map', [], $this->migrateLookup->reveal(), $this->menuLinkManager->reveal(), $this->menuLinkStorage->reveal(), $this->migration->reveal());
if ($is_valid) {
$this->assertInstanceOf(MenuLinkParent::class, $plugin);
}
}
/**
* Provides data for testConstructorException().
*/
public static function providerConstructorException(): array {
return [
'default configuration is valid' => [
'configuration' => [],
'is_valid' => TRUE,
],
'lookup_migrations = null is valid' => [
'configuration' => ['lookup_migrations' => NULL],
'is_valid' => TRUE,
],
'bypass migration lookup is valid' => [
'configuration' => ['lookup_migrations' => []],
'is_valid' => TRUE,
],
'a list of migrations is valid' => [
'configuration' => ['lookup_migrations' => ['this_migration', 'another_migration']],
'is_valid' => TRUE,
],
'a single string is not valid' => [
'configuration' => ['lookup_migrations' => 'this_migration'],
'is_valid' => FALSE,
],
];
}
/**
* Tests that an exception is thrown when the parent menu link is not found.
*
* @param string[] $source_value
* The source value(s) for the migration process plugin.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
* @throws \Drupal\migrate\MigrateException
* @throws \Drupal\migrate\MigrateSkipRowException
*
* @dataProvider providerTransformException
*/
public function testTransformException(array $source_value): void {
[$parent_id, $menu_name] = $source_value;
$this->migrateLookup->lookup(NULL, [1])->willReturn([]);
$plugin = new MenuLinkParent([], 'map', [], $this->migrateLookup->reveal(), $this->menuLinkManager->reveal(), $this->menuLinkStorage->reveal(), $this->migration->reveal());
$this->expectException(MigrateSkipRowException::class);
$this->expectExceptionMessage("No parent link found for plid '$parent_id' in menu '$menu_name'.");
$plugin->transform($source_value, $this->migrateExecutable, $this->row, 'destination');
}
/**
* Provides data for testTransformException().
*/
public static function providerTransformException() {
// The parent ID does not for the following tests.
return [
'parent link external and could not be loaded' => [
'source_value' => [1, 'admin', 'http://example.com'],
],
'parent link path/menu name not passed' => [
'source_value' => [1, NULL, NULL],
],
'parent link is an internal URI that does not exist' => [
'source_value' => [1, NULL, 'admin/structure'],
],
];
}
/**
* Tests the menu link content process plugin.
*
* @param string[] $source_value
* The source value(s) for the migration process plugin.
* @param string $lookup_result
* The ID value to be returned from migration_lookup.
* @param string $plugin_id
* The menu link plugin ID.
* @param string $route_name
* A route to create.
* @param string $expected_result
* The expected value(s) of the migration process plugin.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
* @throws \Drupal\migrate\MigrateException
* @throws \Drupal\migrate\MigrateSkipRowException
*
* @dataProvider providerMenuLinkParent
*/
public function testMenuLinkParent(array $source_value, $lookup_result, $plugin_id, $route_name, $expected_result): void {
[$parent_id, $menu_name, $parent_link_path] = $source_value;
$this->migrateLookup->lookup(NULL, [$parent_id])
->willReturn([['id' => $lookup_result]]);
if ($route_name) {
$plugin_definition = ['menu_name' => $menu_name];
$static_override = $this->prophesize(StaticMenuLinkOverridesInterface::class);
$static_override = $static_override->reveal();
$menu_link = new MenuLinkDefault([], $plugin_id, $plugin_definition, $static_override);
$this->menuLinkManager->loadLinksByRoute($route_name, [], 'admin')
->willReturn([$plugin_id => $menu_link]);
$url = new Url($route_name, [], []);
$this->pathValidator->getUrlIfValidWithoutAccessCheck($parent_link_path)
->willReturn($url);
}
$result = $this->doTransform($source_value, $plugin_id);
$this->assertSame($expected_result, $result);
}
/**
* Provides data for testMenuLinkParent().
*/
public static function providerMenuLinkParent() {
return [
'menu link is route item' => [
'source_value' => [0, NULL, NULL],
'lookup_result' => NULL,
'plugin_id' => NULL,
'route_name' => NULL,
'expected_result' => '',
],
'parent id exists' => [
'source_value' => [1, NULL, NULL],
'lookup_result' => 1,
'plugin_id' => 'menu_link_content:abc',
'route_name' => NULL,
'expected_result' => 'menu_link_content:abc',
],
'no parent id internal route' => [
'source_value' => [20, 'admin', 'admin/content'],
'lookup_result' => NULL,
'plugin_id' => 'system.admin_structure',
'route_name' => 'system.admin_content',
'expected_result' => 'system.admin_structure',
],
'external' => [
'source_value' => [9054, 'admin', 'http://example.com'],
'lookup_result' => 9054,
'plugin_id' => 'menu_link_content:fe151460-dfa2-4133-8864-c1746f28ab27',
'route_name' => NULL,
'expected_result' => 'menu_link_content:fe151460-dfa2-4133-8864-c1746f28ab27',
],
];
}
/**
* Helper to finish setup and run the test.
*
* @param string[] $source_value
* The source value(s) for the migration process plugin.
* @param string $plugin_id
* The menu link plugin ID.
*
* @return string
* The transformed menu link.
*
* @throws \Drupal\migrate\MigrateSkipRowException
*/
public function doTransform(array $source_value, $plugin_id) {
[$parent_id, $menu_name, $parent_link_path] = $source_value;
$menu_link_content = $this->prophesize(MenuLinkContent::class);
$menu_link_content->getPluginId()->willReturn($plugin_id);
$this->menuLinkStorage->load($parent_id)->willReturn($menu_link_content);
$this->menuLinkStorage->loadByProperties([
'menu_name' => $menu_name,
'link.uri' => $parent_link_path,
])->willReturn([
$parent_id => $menu_link_content,
]);
$plugin = new MenuLinkParent([], 'menu_link', [], $this->migrateLookup->reveal(), $this->menuLinkManager->reveal(), $this->menuLinkStorage->reveal(), $this->migration->reveal());
return $plugin->transform($source_value, $this->migrateExecutable, $this->row, 'destination');
}
/**
* Tests the lookup_migrations option.
*
* @param int $plid
* The ID of the parent menu link.
* @param array $configuration
* The plugin configuration being tested.
* @param string $expected_result
* The expected value(s) of the migration process plugin.
*
* @dataProvider providerLookupMigrations
*/
public function testLookupMigrations(int $plid, array $configuration, string $expected_result): void {
$source_value = [$plid, 'some_menu', 'https://www.example.com'];
$this->migration->id()
->willReturn('this_migration');
$this->migrateLookup->lookup('this_migration', [1])
->willReturn([['id' => 101]]);
$this->migrateLookup->lookup('some_migration', [2])
->willReturn([['id' => 202]]);
$this->migrateLookup->lookup('some_migration', [3])
->willReturn([]);
$this->migrateLookup->lookup('another_migration', [3])
->willReturn([['id' => 303]]);
$menu_link_content_this = $this->prophesize(MenuLinkContent::class);
$menu_link_content_this->getPluginId()->willReturn('menu_link_content:this_migration');
$this->menuLinkStorage->load(101)->willReturn($menu_link_content_this);
$menu_link_content_some = $this->prophesize(MenuLinkContent::class);
$menu_link_content_some->getPluginId()->willReturn('menu_link_content:some_migration');
$this->menuLinkStorage->load(202)->willReturn($menu_link_content_some);
$menu_link_content_another = $this->prophesize(MenuLinkContent::class);
$menu_link_content_another->getPluginId()->willReturn('menu_link_content:another_migration');
$this->menuLinkStorage->load(303)->willReturn($menu_link_content_another);
$plugin = new MenuLinkParent($configuration, 'menu_link', [], $this->migrateLookup->reveal(), $this->menuLinkManager->reveal(), $this->menuLinkStorage->reveal(), $this->migration->reveal());
$result = $plugin->transform($source_value, $this->migrateExecutable, $this->row, 'destination');
$this->assertSame($expected_result, $result);
}
/**
* Provides data for testLookupMigrations().
*/
public static function providerLookupMigrations(): array {
return [
'default configuration' => [
'plid' => 1,
'configuration' => [],
'expected_result' => 'menu_link_content:this_migration',
],
'some migration' => [
'plid' => 2,
'configuration' => ['lookup_migrations' => ['some_migration', 'another_migration']],
'expected_result' => 'menu_link_content:some_migration',
],
'another migration' => [
'plid' => 3,
'configuration' => ['lookup_migrations' => ['some_migration', 'another_migration']],
'expected_result' => 'menu_link_content:another_migration',
],
];
}
}

Some files were not shown because too many files have changed in this diff Show More