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'))
74 ->addArgument(new Reference('settings'));
80 protected function setUp() {
83 // Determine what database backend is running, and set the skip flag.
84 $this->skipTests = Database::getConnection()->databaseType() !== 'mysql';
86 // Create some schemas so our export contains tables.
87 $this->installSchema('system', [
91 $this->installSchema('dblog', ['watchdog']);
92 $this->installEntitySchema('block_content');
93 $this->installEntitySchema('user');
94 $this->installEntitySchema('file');
95 $this->installEntitySchema('menu_link_content');
96 $this->installSchema('system', 'sequences');
98 // Place some sample config to test for in the export.
100 'foo' => $this->randomMachineName(),
101 'bar' => $this->randomMachineName(),
103 $storage = new DatabaseStorage(Database::getConnection(), 'config');
104 $storage->write('test_config', $this->data);
106 // Create user account with some potential syntax issues.
107 $account = User::create(['mail' => 'q\'uote$dollar@example.com', 'name' => '$dollar']);
110 // Create url_alias (this will create 'url_alias').
111 $this->container->get('path.alias_storage')->save('/user/' . $account->id(), '/user/example');
113 // Create a cache table (this will create 'cache_discovery').
114 \Drupal::cache('discovery')->set('test', $this->data);
116 // These are all the tables that should now be in place.
119 'block_content_field_data',
120 'block_content_field_revision',
121 'block_content_revision',
132 'menu_link_content_data',
144 * Test the command directly.
146 public function testDbDumpCommand() {
147 if ($this->skipTests) {
148 $this->pass("Skipping test since the DbDumpCommand is currently only compatible with MySql");
152 $application = new DbDumpApplication();
153 $command = $application->find('dump-database-d8-mysql');
154 $command_tester = new CommandTester($command);
155 $command_tester->execute([]);
157 // Tables that are schema-only should not have data exported.
158 $pattern = preg_quote("\$connection->insert('sessions')");
159 $this->assertFalse(preg_match('/' . $pattern . '/', $command_tester->getDisplay()), 'Tables defined as schema-only do not have data exported to the script.');
161 // Table data is exported.
162 $pattern = preg_quote("\$connection->insert('config')");
163 $this->assertTrue(preg_match('/' . $pattern . '/', $command_tester->getDisplay()), 'Table data is properly exported to the script.');
165 // The test data are in the dump (serialized).
166 $pattern = preg_quote(serialize($this->data));
167 $this->assertTrue(preg_match('/' . $pattern . '/', $command_tester->getDisplay()), 'Generated data is found in the exported script.');
169 // Check that the user account name and email address was properly escaped.
170 $pattern = preg_quote('"q\'uote\$dollar@example.com"');
171 $this->assertTrue(preg_match('/' . $pattern . '/', $command_tester->getDisplay()), 'The user account email address was properly escaped in the exported script.');
172 $pattern = preg_quote('\'$dollar\'');
173 $this->assertTrue(preg_match('/' . $pattern . '/', $command_tester->getDisplay()), 'The user account name was properly escaped in the exported script.');
177 * Test loading the script back into the database.
179 public function testScriptLoad() {
180 if ($this->skipTests) {
181 $this->pass("Skipping test since the DbDumpCommand is currently only compatible with MySql");
185 // Generate the script.
186 $application = new DbDumpApplication();
187 $command = $application->find('dump-database-d8-mysql');
188 $command_tester = new CommandTester($command);
189 $command_tester->execute([]);
190 $script = $command_tester->getDisplay();
192 // Store original schemas and drop tables to avoid errors.
193 foreach ($this->tables as $table) {
194 $this->originalTableSchemas[$table] = $this->getTableSchema($table);
195 $this->originalTableIndexes[$table] = $this->getTableIndexes($table);
196 Database::getConnection()->schema()->dropTable($table);
199 // This will load the data.
200 $file = sys_get_temp_dir() . '/' . $this->randomMachineName();
201 file_put_contents($file, $script);
204 // The tables should now exist and the schemas should match the originals.
205 foreach ($this->tables as $table) {
206 $this->assertTrue(Database::getConnection()
208 ->tableExists($table), SafeMarkup::format('Table @table created by the database script.', ['@table' => $table]));
209 $this->assertSame($this->originalTableSchemas[$table], $this->getTableSchema($table), SafeMarkup::format('The schema for @table was properly restored.', ['@table' => $table]));
210 $this->assertSame($this->originalTableIndexes[$table], $this->getTableIndexes($table), SafeMarkup::format('The indexes for @table were properly restored.', ['@table' => $table]));
213 // Ensure the test config has been replaced.
214 $config = unserialize(db_query("SELECT data FROM {config} WHERE name = 'test_config'")->fetchField());
215 $this->assertIdentical($config, $this->data, 'Script has properly restored the config table data.');
217 // Ensure the cache data was not exported.
218 $this->assertFalse(\Drupal::cache('discovery')
219 ->get('test'), 'Cache data was not exported to the script.');
223 * Helper function to get a simplified schema for a given table.
225 * @param string $table
228 * Array keyed by field name, with the values being the field type.
230 protected function getTableSchema($table) {
231 // Verify the field type on the data column in the cache table.
232 // @todo this is MySQL specific.
233 $query = db_query("SHOW COLUMNS FROM {" . $table . "}");
235 while ($row = $query->fetchAssoc()) {
236 $definition[$row['Field']] = $row['Type'];
242 * Returns indexes for a given table.
244 * @param string $table
245 * The table to find indexes for.
248 * The 'primary key', 'unique keys', and 'indexes' portion of the Drupal
251 protected function getTableIndexes($table) {
252 $query = db_query("SHOW INDEX FROM {" . $table . "}");
254 while ($row = $query->fetchAssoc()) {
255 $index_name = $row['Key_name'];
256 $column = $row['Column_name'];
257 // Key the arrays by the index sequence for proper ordering (start at 0).
258 $order = $row['Seq_in_index'] - 1;
260 // If specified, add length to the index.
261 if ($row['Sub_part']) {
262 $column = [$column, $row['Sub_part']];
265 if ($index_name === 'PRIMARY') {
266 $definition['primary key'][$order] = $column;
268 elseif ($row['Non_unique'] == 0) {
269 $definition['unique keys'][$index_name][$order] = $column;
272 $definition['indexes'][$index_name][$order] = $column;