1<?php
2
3namespace Drupal\Tests\migrate_drupal_ui\Functional;
4
5use Drupal\Core\Database\Database;
6use Drupal\migrate\Plugin\MigrateIdMapInterface;
7use Drupal\migrate_drupal\MigrationConfigurationTrait;
8use Drupal\Tests\BrowserTestBase;
9use Drupal\Tests\migrate_drupal\Traits\CreateTestContentEntitiesTrait;
10use Drupal\Tests\WebAssert;
11
12/**
13 * Provides a base class for testing migration upgrades in the UI.
14 */
15abstract class MigrateUpgradeTestBase extends BrowserTestBase {
16
17  use MigrationConfigurationTrait;
18  use CreateTestContentEntitiesTrait;
19
20  /**
21   * Use the Standard profile to test help implementations of many core modules.
22   *
23   * @var string
24   */
25  protected $profile = 'standard';
26
27  /**
28   * The source database connection.
29   *
30   * @var \Drupal\Core\Database\Connection
31   */
32  protected $sourceDatabase;
33
34  /**
35   * {@inheritdoc}
36   */
37  protected function setUp() {
38    parent::setUp();
39    $this->createMigrationConnection();
40    $this->sourceDatabase = Database::getConnection('default', 'migrate_drupal_ui');
41
42    // Log in as user 1. Migrations in the UI can only be performed as user 1.
43    $this->drupalLogin($this->rootUser);
44  }
45
46  /**
47   * Loads a database fixture into the source database connection.
48   *
49   * @param string $path
50   *   Path to the dump file.
51   */
52  protected function loadFixture($path) {
53    $default_db = Database::getConnection()->getKey();
54    Database::setActiveConnection($this->sourceDatabase->getKey());
55
56    if (substr($path, -3) == '.gz') {
57      $path = 'compress.zlib://' . $path;
58    }
59    require $path;
60
61    Database::setActiveConnection($default_db);
62  }
63
64  /**
65   * Changes the database connection to the prefixed one.
66   *
67   * @todo Remove when we don't use global. https://www.drupal.org/node/2552791
68   */
69  protected function createMigrationConnection() {
70    $connection_info = Database::getConnectionInfo('default')['default'];
71    if ($connection_info['driver'] === 'sqlite') {
72      // Create database file in the test site's public file directory so that
73      // \Drupal\simpletest\TestBase::restoreEnvironment() will delete this once
74      // the test is complete.
75      $file = $this->publicFilesDirectory . '/' . $this->testId . '-migrate.db.sqlite';
76      touch($file);
77      $connection_info['database'] = $file;
78      $connection_info['prefix'] = '';
79    }
80    else {
81      $prefix = is_array($connection_info['prefix']) ? $connection_info['prefix']['default'] : $connection_info['prefix'];
82      // Simpletest uses fixed length prefixes. Create a new prefix for the
83      // source database. Adding to the end of the prefix ensures that
84      // \Drupal\simpletest\TestBase::restoreEnvironment() will remove the
85      // additional tables.
86      $connection_info['prefix'] = $prefix . '0';
87    }
88
89    Database::addConnectionInfo('migrate_drupal_ui', 'default', $connection_info);
90  }
91
92  /**
93   * {@inheritdoc}
94   */
95  protected function tearDown() {
96    Database::removeConnection('migrate_drupal_ui');
97    parent::tearDown();
98  }
99
100  /**
101   * Tests the displayed upgrade paths.
102   *
103   * @param \Drupal\Tests\WebAssert $session
104   *   The web-assert session.
105   * @param array $available_paths
106   *   An array of modules that will be upgraded.
107   * @param array $missing_paths
108   *   An array of modules that will not be upgraded.
109   */
110  protected function assertUpgradePaths(WebAssert $session, array $available_paths, array $missing_paths) {
111    // Test the available migration paths.
112    foreach ($available_paths as $available) {
113      $session->elementExists('xpath', "//span[contains(@class, 'checked') and text() = '$available']");
114      $session->elementNotExists('xpath', "//span[contains(@class, 'error') and text() = '$available']");
115    }
116
117    // Test the missing migration paths.
118    foreach ($missing_paths as $missing) {
119      $session->elementExists('xpath', "//span[contains(@class, 'error') and text() = '$missing']");
120      $session->elementNotExists('xpath', "//span[contains(@class, 'checked') and text() = '$missing']");
121    }
122
123    // Test the total count of missing and available paths.
124    $session->elementsCount('xpath', "//span[contains(@class, 'upgrade-analysis-report__status-icon--error')]", count($missing_paths));
125    $session->elementsCount('xpath', "//span[contains(@class, 'upgrade-analysis-report__status-icon--checked')]", count($available_paths));
126  }
127
128  /**
129   * Gets the source base path for the concrete test.
130   *
131   * @return string
132   *   The source base path.
133   */
134  abstract protected function getSourceBasePath();
135
136  /**
137   * Gets the expected number of entities per entity type after migration.
138   *
139   * @return int[]
140   *   An array of expected counts keyed by entity type ID.
141   */
142  abstract protected function getEntityCounts();
143
144  /**
145   * Gets the available upgrade paths.
146   *
147   * @return string[]
148   *   An array of available upgrade paths.
149   */
150  abstract protected function getAvailablePaths();
151
152  /**
153   * Gets the missing upgrade paths.
154   *
155   * @return string[]
156   *   An array of missing upgrade paths.
157   */
158  abstract protected function getMissingPaths();
159
160  /**
161   * Gets expected number of entities per entity after incremental migration.
162   *
163   * @return int[]
164   *   An array of expected counts keyed by entity type ID.
165   */
166  abstract protected function getEntityCountsIncremental();
167
168  /**
169   * Helper method to assert the text on the 'Upgrade analysis report' page.
170   *
171   * @param \Drupal\Tests\WebAssert $session
172   *   The web-assert session.
173   * @param array $available_paths
174   *   An array of modules that will be upgraded.
175   * @param array $missing_paths
176   *   An array of modules that will not be upgraded.
177   */
178  protected function assertReviewPage(WebAssert $session, array $available_paths, array $missing_paths) {
179    $this->assertText('What will be upgraded?');
180
181    // Ensure there are no errors about the missing modules from the test module.
182    $session->pageTextNotContains(t('Source module not found for migration_provider_no_annotation.'));
183    $session->pageTextNotContains(t('Source module not found for migration_provider_test.'));
184    $session->pageTextNotContains(t('Destination module not found for migration_provider_test'));
185    // Ensure there are no errors about any other missing migration providers.
186    $session->pageTextNotContains(t('module not found'));
187
188    $this->assertUpgradePaths($session, $available_paths, $missing_paths);
189  }
190
191  /**
192   * Helper method that asserts text on the ID conflict form.
193   *
194   * @param \Drupal\Tests\WebAssert $session
195   *   The current session.
196   * @param array $entity_types
197   *   An array of entity types
198   */
199  protected function assertIdConflict(WebAssert $session, array $entity_types) {
200    /** @var \Drupal\ $entity_type_manager */
201    $entity_type_manager = \Drupal::service('entity_type.manager');
202
203    $session->pageTextContains('WARNING: Content may be overwritten on your new site.');
204    $session->pageTextContains('There is conflicting content of these types:');
205    $this->assertNotEmpty($entity_types, 'No entity types provided to \Drupal\Tests\migrate_drupal_ui\Functional\MigrateUpgradeTestBase::assertIdConflict()');
206    foreach ($entity_types as $entity_type) {
207      $label = $entity_type_manager->getDefinition($entity_type)->getPluralLabel();
208      $session->pageTextContains($label);
209    }
210    $session->pageTextContainsOnce('content items');
211    $session->pageTextContains('There is translated content of these types:');
212  }
213
214  /**
215   * Checks that migrations have been performed successfully.
216   *
217   * @param array $expected_counts
218   *   The expected counts of each entity type.
219   * @param int $version
220   *   The Drupal version.
221   */
222  protected function assertMigrationResults(array $expected_counts, $version) {
223    // Have to reset all the statics after migration to ensure entities are
224    // loadable.
225    $this->resetAll();
226    // Check that the expected number of entities is the same as the actual
227    // number of entities.
228    $entity_definitions = array_keys(\Drupal::entityTypeManager()->getDefinitions());
229    $expected_count_keys = array_keys($expected_counts);
230    sort($entity_definitions);
231    sort($expected_count_keys);
232    $this->assertSame($expected_count_keys, $entity_definitions);
233
234    // Assert the correct number of entities exist.
235    foreach ($entity_definitions as $entity_type) {
236      $real_count = (int) \Drupal::entityQuery($entity_type)->count()->execute();
237      $expected_count = $expected_counts[$entity_type];
238      $this->assertSame($expected_count, $real_count, "Found $real_count $entity_type entities, expected $expected_count.");
239    }
240
241    $plugin_manager = \Drupal::service('plugin.manager.migration');
242    /** @var \Drupal\migrate\Plugin\Migration[] $all_migrations */
243    $all_migrations = $plugin_manager->createInstancesByTag('Drupal ' . $version);
244    foreach ($all_migrations as $migration) {
245      $id_map = $migration->getIdMap();
246      foreach ($id_map as $source_id => $map) {
247        // Convert $source_id into a keyless array so that
248        // \Drupal\migrate\Plugin\migrate\id_map\Sql::getSourceHash() works as
249        // expected.
250        $source_id_values = array_values(unserialize($source_id));
251        $row = $id_map->getRowBySource($source_id_values);
252        $destination = serialize($id_map->currentDestination());
253        $message = "Migration of $source_id to $destination as part of the {$migration->id()} migration. The source row status is " . $row['source_row_status'];
254        // A completed migration should have maps with
255        // MigrateIdMapInterface::STATUS_IGNORED or
256        // MigrateIdMapInterface::STATUS_IMPORTED.
257        $this->assertNotSame(MigrateIdMapInterface::STATUS_FAILED, $row['source_row_status'], $message);
258        $this->assertNotSame(MigrateIdMapInterface::STATUS_NEEDS_UPDATE, $row['source_row_status'], $message);
259      }
260    }
261  }
262
263}
264