3 namespace Drupal\KernelTests\Core\Command;
5 use Drupal\Component\Utility\SafeMarkup;
6 use Drupal\Core\Command\DbDumpApplication;
7 use Drupal\Core\Config\DatabaseStorage;
8 use Drupal\Core\Database\Database;
9 use Drupal\Core\DependencyInjection\ContainerBuilder;
10 use Drupal\KernelTests\KernelTestBase;
11 use Drupal\user\Entity\User;
12 use Symfony\Component\Console\Tester\CommandTester;
13 use Symfony\Component\DependencyInjection\Reference;
16 * Tests for the database dump commands.
20 class DbDumpTest extends KernelTestBase {
25 public static $modules = ['system', 'config', 'dblog', 'menu_link_content', 'link', 'block_content', 'file', 'user'];
28 * Test data to write into config.
35 * Flag to skip these tests, which are database-backend dependent (MySQL).
37 * @see \Drupal\Core\Command\DbDumpCommand
41 protected $skipTests = FALSE;
44 * An array of original table schemas.
48 protected $originalTableSchemas = [];
51 * An array of original table indexes (including primary and unique keys).
55 protected $originalTableIndexes = [];
58 * Tables that should be part of the exported script.
67 * Register a database cache backend rather than memory-based.
69 public function register(ContainerBuilder $container) {
70 parent::register($container);
71 $container->register('cache_factory', 'Drupal\Core\Cache\DatabaseBackendFactory')
72 ->addArgument(new Reference('database'))
73 ->addArgument(new Reference('cache_tags.invalidator.checksum'));
79 protected function setUp() {
82 // Determine what database backend is running, and set the skip flag.
83 $this->skipTests = Database::getConnection()->databaseType() !== 'mysql';
85 // Create some schemas so our export contains tables.
86 $this->installSchema('system', [
90 $this->installSchema('dblog', ['watchdog']);
91 $this->installEntitySchema('block_content');
92 $this->installEntitySchema('user');
93 $this->installEntitySchema('file');
94 $this->installEntitySchema('menu_link_content');
95 $this->installSchema('system', 'sequences');
97 // Place some sample config to test for in the export.
99 'foo' => $this->randomMachineName(),
100 'bar' => $this->randomMachineName(),
102 $storage = new DatabaseStorage(Database::getConnection(), 'config');
103 $storage->write('test_config', $this->data);
105 // Create user account with some potential syntax issues.
106 $account = User::create(['mail' => 'q\'uote$dollar@example.com', 'name' => '$dollar']);
109 // Create url_alias (this will create 'url_alias').
110 $this->container->get('path.alias_storage')->save('/user/' . $account->id(), '/user/example');
112 // Create a cache table (this will create 'cache_discovery').
113 \Drupal::cache('discovery')->set('test', $this->data);
115 // These are all the tables that should now be in place.
118 'block_content_field_data',
119 'block_content_field_revision',
120 'block_content_revision',
131 'menu_link_content_data',
143 * Test the command directly.
145 public function testDbDumpCommand() {
146 if ($this->skipTests) {
147 $this->pass("Skipping test since the DbDumpCommand is currently only compatible with MySql");
151 $application = new DbDumpApplication();
152 $command = $application->find('dump-database-d8-mysql');
153 $command_tester = new CommandTester($command);
154 $command_tester->execute([]);
156 // Tables that are schema-only should not have data exported.
157 $pattern = preg_quote("\$connection->insert('sessions')");
158 $this->assertFalse(preg_match('/' . $pattern . '/', $command_tester->getDisplay()), 'Tables defined as schema-only do not have data exported to the script.');
160 // Table data is exported.
161 $pattern = preg_quote("\$connection->insert('config')");
162 $this->assertTrue(preg_match('/' . $pattern . '/', $command_tester->getDisplay()), 'Table data is properly exported to the script.');
164 // The test data are in the dump (serialized).
165 $pattern = preg_quote(serialize($this->data));
166 $this->assertTrue(preg_match('/' . $pattern . '/', $command_tester->getDisplay()), 'Generated data is found in the exported script.');
168 // Check that the user account name and email address was properly escaped.
169 $pattern = preg_quote('"q\'uote\$dollar@example.com"');
170 $this->assertTrue(preg_match('/' . $pattern . '/', $command_tester->getDisplay()), 'The user account email address was properly escaped in the exported script.');
171 $pattern = preg_quote('\'$dollar\'');
172 $this->assertTrue(preg_match('/' . $pattern . '/', $command_tester->getDisplay()), 'The user account name was properly escaped in the exported script.');
176 * Test loading the script back into the database.
178 public function testScriptLoad() {
179 if ($this->skipTests) {
180 $this->pass("Skipping test since the DbDumpCommand is currently only compatible with MySql");
184 // Generate the script.
185 $application = new DbDumpApplication();
186 $command = $application->find('dump-database-d8-mysql');
187 $command_tester = new CommandTester($command);
188 $command_tester->execute([]);
189 $script = $command_tester->getDisplay();
191 // Store original schemas and drop tables to avoid errors.
192 foreach ($this->tables as $table) {
193 $this->originalTableSchemas[$table] = $this->getTableSchema($table);
194 $this->originalTableIndexes[$table] = $this->getTableIndexes($table);
195 Database::getConnection()->schema()->dropTable($table);
198 // This will load the data.
199 $file = sys_get_temp_dir() . '/' . $this->randomMachineName();
200 file_put_contents($file, $script);
203 // The tables should now exist and the schemas should match the originals.
204 foreach ($this->tables as $table) {
205 $this->assertTrue(Database::getConnection()
207 ->tableExists($table), SafeMarkup::format('Table @table created by the database script.', ['@table' => $table]));
208 $this->assertIdentical($this->originalTableSchemas[$table], $this->getTableSchema($table), SafeMarkup::format('The schema for @table was properly restored.', ['@table' => $table]));
209 $this->assertIdentical($this->originalTableIndexes[$table], $this->getTableIndexes($table), SafeMarkup::format('The indexes for @table were properly restored.', ['@table' => $table]));
212 // Ensure the test config has been replaced.
213 $config = unserialize(db_query("SELECT data FROM {config} WHERE name = 'test_config'")->fetchField());
214 $this->assertIdentical($config, $this->data, 'Script has properly restored the config table data.');
216 // Ensure the cache data was not exported.
217 $this->assertFalse(\Drupal::cache('discovery')
218 ->get('test'), 'Cache data was not exported to the script.');
222 * Helper function to get a simplified schema for a given table.
224 * @param string $table
227 * Array keyed by field name, with the values being the field type.
229 protected function getTableSchema($table) {
230 // Verify the field type on the data column in the cache table.
231 // @todo this is MySQL specific.
232 $query = db_query("SHOW COLUMNS FROM {" . $table . "}");
234 while ($row = $query->fetchAssoc()) {
235 $definition[$row['Field']] = $row['Type'];
241 * Returns indexes for a given table.
243 * @param string $table
244 * The table to find indexes for.
247 * The 'primary key', 'unique keys', and 'indexes' portion of the Drupal
250 protected function getTableIndexes($table) {
251 $query = db_query("SHOW INDEX FROM {" . $table . "}");
253 while ($row = $query->fetchAssoc()) {
254 $index_name = $row['Key_name'];
255 $column = $row['Column_name'];
256 // Key the arrays by the index sequence for proper ordering (start at 0).
257 $order = $row['Seq_in_index'] - 1;
259 // If specified, add length to the index.
260 if ($row['Sub_part']) {
261 $column = [$column, $row['Sub_part']];
264 if ($index_name === 'PRIMARY') {
265 $definition['primary key'][$order] = $column;
267 elseif ($row['Non_unique'] == 0) {
268 $definition['unique keys'][$index_name][$order] = $column;
271 $definition['indexes'][$index_name][$order] = $column;