Upgraded drupal core with security updates
[yaffs-website] / web / core / tests / Drupal / KernelTests / Core / Command / DbDumpTest.php
1 <?php
2
3 namespace Drupal\KernelTests\Core\Command;
4
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;
14
15 /**
16  * Tests for the database dump commands.
17  *
18  * @group Update
19  */
20 class DbDumpTest extends KernelTestBase {
21
22   /**
23    * {@inheritdoc}
24    */
25   public static $modules = ['system', 'config', 'dblog', 'menu_link_content', 'link', 'block_content', 'file', 'user'];
26
27   /**
28    * Test data to write into config.
29    *
30    * @var array
31    */
32   protected $data;
33
34   /**
35    * Flag to skip these tests, which are database-backend dependent (MySQL).
36    *
37    * @see \Drupal\Core\Command\DbDumpCommand
38    *
39    * @var bool
40    */
41   protected $skipTests = FALSE;
42
43   /**
44    * An array of original table schemas.
45    *
46    * @var array
47    */
48   protected $originalTableSchemas = [];
49
50   /**
51    * An array of original table indexes (including primary and unique keys).
52    *
53    * @var array
54    */
55   protected $originalTableIndexes = [];
56
57   /**
58    * Tables that should be part of the exported script.
59    *
60    * @var array
61    */
62   protected $tables;
63
64   /**
65    * {@inheritdoc}
66    *
67    * Register a database cache backend rather than memory-based.
68    */
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   }
75
76   /**
77    * {@inheritdoc}
78    */
79   protected function setUp() {
80     parent::setUp();
81
82     // Determine what database backend is running, and set the skip flag.
83     $this->skipTests = Database::getConnection()->databaseType() !== 'mysql';
84
85     // Create some schemas so our export contains tables.
86     $this->installSchema('system', [
87       'key_value_expire',
88       'sessions',
89     ]);
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');
96
97     // Place some sample config to test for in the export.
98     $this->data = [
99       'foo' => $this->randomMachineName(),
100       'bar' => $this->randomMachineName(),
101     ];
102     $storage = new DatabaseStorage(Database::getConnection(), 'config');
103     $storage->write('test_config', $this->data);
104
105     // Create user account with some potential syntax issues.
106     $account = User::create(['mail' => 'q\'uote$dollar@example.com', 'name' => '$dollar']);
107     $account->save();
108
109     // Create url_alias (this will create 'url_alias').
110     $this->container->get('path.alias_storage')->save('/user/' . $account->id(), '/user/example');
111
112     // Create a cache table (this will create 'cache_discovery').
113     \Drupal::cache('discovery')->set('test', $this->data);
114
115     // These are all the tables that should now be in place.
116     $this->tables = [
117       'block_content',
118       'block_content_field_data',
119       'block_content_field_revision',
120       'block_content_revision',
121       'cachetags',
122       'config',
123       'cache_bootstrap',
124       'cache_config',
125       'cache_data',
126       'cache_discovery',
127       'cache_entity',
128       'file_managed',
129       'key_value_expire',
130       'menu_link_content',
131       'menu_link_content_data',
132       'sequences',
133       'sessions',
134       'url_alias',
135       'user__roles',
136       'users',
137       'users_field_data',
138       'watchdog',
139     ];
140   }
141
142   /**
143    * Test the command directly.
144    */
145   public function testDbDumpCommand() {
146     if ($this->skipTests) {
147       $this->pass("Skipping test since the DbDumpCommand is currently only compatible with MySql");
148       return;
149     }
150
151     $application = new DbDumpApplication();
152     $command = $application->find('dump-database-d8-mysql');
153     $command_tester = new CommandTester($command);
154     $command_tester->execute([]);
155
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.');
159
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.');
163
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.');
167
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.');
173   }
174
175   /**
176    * Test loading the script back into the database.
177    */
178   public function testScriptLoad() {
179     if ($this->skipTests) {
180       $this->pass("Skipping test since the DbDumpCommand is currently only compatible with MySql");
181       return;
182     }
183
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();
190
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);
196     }
197
198     // This will load the data.
199     $file = sys_get_temp_dir() . '/' . $this->randomMachineName();
200     file_put_contents($file, $script);
201     require_once $file;
202
203     // The tables should now exist and the schemas should match the originals.
204     foreach ($this->tables as $table) {
205       $this->assertTrue(Database::getConnection()
206         ->schema()
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]));
210     }
211
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.');
215
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.');
219   }
220
221   /**
222    * Helper function to get a simplified schema for a given table.
223    *
224    * @param string $table
225    *
226    * @return array
227    *   Array keyed by field name, with the values being the field type.
228    */
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 . "}");
233     $definition = [];
234     while ($row = $query->fetchAssoc()) {
235       $definition[$row['Field']] = $row['Type'];
236     }
237     return $definition;
238   }
239
240   /**
241    * Returns indexes for a given table.
242    *
243    * @param string $table
244    *   The table to find indexes for.
245    *
246    * @return array
247    *   The 'primary key', 'unique keys', and 'indexes' portion of the Drupal
248    *   table schema.
249    */
250   protected function getTableIndexes($table) {
251     $query = db_query("SHOW INDEX FROM {" . $table . "}");
252     $definition = [];
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;
258
259       // If specified, add length to the index.
260       if ($row['Sub_part']) {
261         $column = [$column, $row['Sub_part']];
262       }
263
264       if ($index_name === 'PRIMARY') {
265         $definition['primary key'][$order] = $column;
266       }
267       elseif ($row['Non_unique'] == 0) {
268         $definition['unique keys'][$index_name][$order] = $column;
269       }
270       else {
271         $definition['indexes'][$index_name][$order] = $column;
272       }
273     }
274     return $definition;
275   }
276
277 }