Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.96% covered (success)
98.96%
285 / 288
82.35% covered (warning)
82.35%
14 / 17
CRAP
0.00% covered (danger)
0.00%
0 / 1
phpbb_dbal_migrator_test
98.96% covered (success)
98.96%
285 / 288
82.35% covered (warning)
82.35%
14 / 17
56
0.00% covered (danger)
0.00%
0 / 1
 getDataSet
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setUp
100.00% covered (success)
100.00%
38 / 38
100.00% covered (success)
100.00%
1 / 1
1
 test_update
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
1
 test_unfulfillable
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 test_if
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
3
 test_recall
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 test_if_params
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
3
 test_recall_params
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 test_revert
96.55% covered (success)
96.55%
28 / 29
0.00% covered (danger)
0.00%
0 / 1
4
 test_revert_table
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
3
 test_fail
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
4.01
 test_installed
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
3.02
 test_schema
100.00% covered (success)
100.00%
55 / 55
100.00% covered (success)
100.00%
1 / 1
14
 test_rename_index
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 test_schema_generator
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
1
 test_table_indexes
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
6
 test_add_autoincrement_column
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2/**
3*
4* This file is part of the phpBB Forum Software package.
5*
6* @copyright (c) phpBB Limited <https://www.phpbb.com>
7* @license GNU General Public License, version 2 (GPL-2.0)
8*
9* For full copyright and license information, please see
10* the docs/CREDITS.txt file.
11*
12*/
13
14require_once __DIR__ . '/migration/dummy.php';
15require_once __DIR__ . '/migration/unfulfillable.php';
16require_once __DIR__ . '/migration/if.php';
17require_once __DIR__ . '/migration/recall.php';
18require_once __DIR__ . '/migration/if_params.php';
19require_once __DIR__ . '/migration/recall_params.php';
20require_once __DIR__ . '/migration/revert.php';
21require_once __DIR__ . '/migration/revert_with_dependency.php';
22require_once __DIR__ . '/migration/revert_table.php';
23require_once __DIR__ . '/migration/revert_table_with_dependency.php';
24require_once __DIR__ . '/migration/fail.php';
25require_once __DIR__ . '/migration/installed.php';
26require_once __DIR__ . '/migration/schema.php';
27require_once __DIR__ . '/migration/schema_index.php';
28require_once __DIR__ . '/migration/schema_add_autoincrement.php';
29
30class phpbb_dbal_migrator_test extends phpbb_database_test_case
31{
32    /** @var \phpbb\db\driver\driver_interface */
33    protected $db;
34
35    /** @var \Doctrine\DBAL\Connection */
36    protected $doctrine_db;
37
38    /** @var \phpbb\db\tools\tools_interface */
39    protected $db_tools;
40
41    /** @var \phpbb\db\migrator */
42    protected $migrator;
43
44    /** @var \phpbb\config\config */
45    protected $config;
46
47    /** @var \phpbb\extension\manager */
48    protected $extension_manager;
49
50    /** @var string */
51    protected $table_prefix;
52
53    public function getDataSet()
54    {
55        return $this->createXMLDataSet(__DIR__.'/fixtures/migrator.xml');
56    }
57
58    protected function setUp(): void
59    {
60        global $table_prefix;
61
62        parent::setUp();
63
64        $this->table_prefix = $table_prefix;
65        $this->db = $this->new_dbal();
66        $this->doctrine_db = $this->new_doctrine_dbal();
67        $factory = new \phpbb\db\tools\factory();
68        $this->db_tools = $factory->get($this->doctrine_db);
69        $this->db_tools->set_table_prefix($this->table_prefix);
70
71        $this->config = new \phpbb\config\db($this->db, new phpbb_mock_cache, 'phpbb_config');
72
73        $finder_factory = $this->createMock('\phpbb\finder\factory');
74
75        $tools = array(
76            new \phpbb\db\migration\tool\config($this->config),
77        );
78
79        $container = new phpbb_mock_container_builder();
80
81        $this->migrator = new \phpbb\db\migrator(
82            $container,
83            $this->config,
84            $this->db,
85            $this->db_tools,
86            'phpbb_migrations',
87            __DIR__ . '/../../phpBB/',
88            'php',
89            'phpbb_',
90            self::get_core_tables(),
91            $tools,
92            new \phpbb\db\migration\helper()
93        );
94        $container->set('migrator', $this->migrator);
95        $container->set('event_dispatcher', new phpbb_mock_event_dispatcher());
96
97        $this->extension_manager = new \phpbb\extension\manager(
98            $container,
99            $this->db,
100            $this->config,
101            $finder_factory,
102            'phpbb_ext',
103            __DIR__ . '/../../phpBB/',
104            null
105        );
106    }
107
108    public function test_update()
109    {
110        $this->migrator->set_migrations(array('phpbb_dbal_migration_dummy'));
111
112        // schema
113        $start_time = time();
114        $this->migrator->update();
115        $this->assertFalse($this->migrator->finished());
116
117        $this->assertSqlResultEquals(
118            array(array('success' => '1')),
119            "SELECT 1 as success
120                FROM phpbb_migrations
121                WHERE migration_name = 'phpbb_dbal_migration_dummy'
122                    AND migration_start_time >= " . ($start_time - 1) . "
123                    AND migration_start_time <= " . (time() + 1),
124            'Start time set correctly'
125        );
126
127        // data
128        $start_time = time();
129        $this->migrator->update();
130        $this->assertTrue($this->migrator->finished());
131
132        $this->assertSqlResultEquals(
133            array(array('extra_column' => '1')),
134            "SELECT extra_column FROM phpbb_config WHERE config_name = 'foo'",
135            'Dummy migration created extra_column with value 1 in all rows.'
136        );
137
138        $this->assertSqlResultEquals(
139            array(array('success' => '1')),
140            "SELECT 1 as success
141                FROM phpbb_migrations
142                WHERE migration_name = 'phpbb_dbal_migration_dummy'
143                    AND migration_start_time <= migration_end_time
144                    AND migration_end_time >= " . ($start_time - 1) . "
145                    AND migration_end_time <= " . (time() + 1),
146            'End time set correctly'
147        );
148
149        // cleanup
150        $this->db_tools->sql_column_remove('phpbb_config', 'extra_column');
151    }
152
153    public function test_unfulfillable()
154    {
155        $this->migrator->set_migrations(array('phpbb_dbal_migration_unfulfillable', 'phpbb_dbal_migration_dummy'));
156
157        while (!$this->migrator->finished())
158        {
159            $this->migrator->update();
160        }
161
162        $this->assertTrue($this->migrator->finished());
163
164        $this->assertSqlResultEquals(
165            array(array('extra_column' => '1')),
166            "SELECT extra_column FROM phpbb_config WHERE config_name = 'foo'",
167            'Dummy migration was run, even though an unfulfillable migration was found.'
168        );
169
170        $this->db_tools->sql_column_remove('phpbb_config', 'extra_column');
171    }
172
173    public function test_if()
174    {
175        $this->migrator->set_migrations(array('phpbb_dbal_migration_if'));
176
177        // Don't like this, but I'm not sure there is any other way to do this
178        global $migrator_test_if_true_failed, $migrator_test_if_false_failed;
179        $migrator_test_if_true_failed = true;
180        $migrator_test_if_false_failed = false;
181
182        while (!$this->migrator->finished())
183        {
184            $this->migrator->update();
185        }
186
187        $this->assertFalse($migrator_test_if_true_failed, 'True test failed');
188        $this->assertFalse($migrator_test_if_false_failed, 'False test failed');
189
190        while ($this->migrator->migration_state('phpbb_dbal_migration_if') !== false)
191        {
192            $this->migrator->revert('phpbb_dbal_migration_if');
193        }
194
195        $this->assertFalse($migrator_test_if_true_failed, 'True test after revert failed');
196        $this->assertFalse($migrator_test_if_false_failed, 'False test after revert failed');
197    }
198
199    public function test_recall()
200    {
201        $this->migrator->set_migrations(array('phpbb_dbal_migration_recall'));
202
203        global $migrator_test_call_input;
204
205        // Run the schema first
206        $this->migrator->update();
207
208        $i = 0;
209        while (!$this->migrator->finished())
210        {
211            $this->migrator->update();
212
213            $this->assertSame($i, $migrator_test_call_input);
214
215            $i++;
216        }
217
218        $this->assertSame(10, $migrator_test_call_input);
219    }
220
221    public function test_if_params()
222    {
223        $this->migrator->set_migrations(array('phpbb_dbal_migration_if_params'));
224
225        // Don't like this, but I'm not sure there is any other way to do this
226        global $migrator_test_if_true_failed, $migrator_test_if_false_failed;
227        $migrator_test_if_true_failed = true;
228        $migrator_test_if_false_failed = false;
229
230        while (!$this->migrator->finished())
231        {
232            $this->migrator->update();
233        }
234
235        $this->assertFalse($migrator_test_if_true_failed, 'True test failed');
236        $this->assertFalse($migrator_test_if_false_failed, 'False test failed');
237
238        while ($this->migrator->migration_state('phpbb_dbal_migration_if_params') !== false)
239        {
240            $this->migrator->revert('phpbb_dbal_migration_if_params');
241        }
242
243        $this->assertFalse($migrator_test_if_true_failed, 'True test after revert failed');
244        $this->assertFalse($migrator_test_if_false_failed, 'False test after revert failed');
245    }
246
247    public function test_recall_params()
248    {
249        $this->migrator->set_migrations(array('phpbb_dbal_migration_recall_params'));
250
251        global $migrator_test_call_input;
252
253        // Run the schema first
254        $this->migrator->update();
255
256        $i = 0;
257        while (!$this->migrator->finished())
258        {
259            $this->migrator->update();
260
261            $this->assertSame($i, $migrator_test_call_input);
262
263            $i++;
264        }
265
266        $this->assertSame(5, $migrator_test_call_input);
267    }
268
269    public function test_revert()
270    {
271        global $migrator_test_revert_counter;
272
273        // Make sure there are no other migrations in the db, this could cause issues
274        $this->db->sql_query("DELETE FROM phpbb_migrations");
275        $this->migrator->load_migration_state();
276
277        $migrator_test_revert_counter = 0;
278
279        $this->migrator->set_migrations(array('phpbb_dbal_migration_revert', 'phpbb_dbal_migration_revert_with_dependency'));
280
281        $this->assertFalse($this->migrator->migration_state('phpbb_dbal_migration_revert'));
282        $this->assertFalse($this->migrator->migration_state('phpbb_dbal_migration_revert_with_dependency'));
283
284        // Install the migration first
285        while (!$this->migrator->finished())
286        {
287            $this->migrator->update();
288        }
289
290        $this->assertTrue($this->migrator->migration_state('phpbb_dbal_migration_revert') !== false);
291        $this->assertTrue($this->migrator->migration_state('phpbb_dbal_migration_revert_with_dependency') !== false);
292
293        $this->assertSqlResultEquals(
294            array(array('bar_column' => '1')),
295            "SELECT bar_column FROM phpbb_config WHERE config_name = 'foo'",
296            'Installing revert migration failed to create bar_column.'
297        );
298
299        $this->assertTrue(isset($this->config['foobartest']));
300
301        while ($this->migrator->migration_state('phpbb_dbal_migration_revert') !== false)
302        {
303            $this->migrator->revert('phpbb_dbal_migration_revert');
304        }
305
306        $this->assertFalse($this->migrator->migration_state('phpbb_dbal_migration_revert'));
307        $this->assertFalse($this->migrator->migration_state('phpbb_dbal_migration_revert_with_dependency'));
308
309        $this->assertFalse(isset($this->config['foobartest']));
310
311        $sql = 'SELECT * FROM phpbb_config';
312        $result = $this->db->sql_query_limit($sql, 1);
313        $row = $this->db->sql_fetchrow($result);
314        $this->db->sql_freeresult($result);
315
316        if (isset($row['bar_column']))
317        {
318            $this->fail('Revert did not remove test_column.');
319        }
320
321        $this->assertEquals(1, $migrator_test_revert_counter, 'Revert did call custom function again');
322    }
323
324    public function test_revert_table()
325    {
326        // Make sure there are no other migrations in the db, this could cause issues
327        $this->db->sql_query("DELETE FROM phpbb_migrations");
328        $this->migrator->load_migration_state();
329
330        $this->migrator->set_migrations(array('phpbb_dbal_migration_revert_table', 'phpbb_dbal_migration_revert_table_with_dependency'));
331
332        $this->assertFalse($this->migrator->migration_state('phpbb_dbal_migration_revert_table'));
333        $this->assertFalse($this->migrator->migration_state('phpbb_dbal_migration_revert_table_with_dependency'));
334
335        // Install the migration first
336        while (!$this->migrator->finished())
337        {
338            $this->migrator->update();
339        }
340
341        $this->assertTrue($this->migrator->migration_state('phpbb_dbal_migration_revert_table') !== false);
342        $this->assertTrue($this->migrator->migration_state('phpbb_dbal_migration_revert_table_with_dependency') !== false);
343
344        $this->assertTrue($this->db_tools->sql_column_exists('phpbb_foobar', 'baz_column'));
345        $this->assertFalse($this->db_tools->sql_column_exists('phpbb_foobar', 'bar_column'));
346
347        // Revert migrations
348        while ($this->migrator->migration_state('phpbb_dbal_migration_revert_table') !== false)
349        {
350            $this->migrator->revert('phpbb_dbal_migration_revert_table');
351        }
352
353        $this->assertFalse($this->migrator->migration_state('phpbb_dbal_migration_revert_table'));
354        $this->assertFalse($this->migrator->migration_state('phpbb_dbal_migration_revert_table_with_dependency'));
355
356        $this->assertFalse($this->db_tools->sql_table_exists('phpbb_foobar'));
357    }
358
359    public function test_fail()
360    {
361        $this->migrator->set_migrations(array('phpbb_dbal_migration_fail'));
362
363        $this->assertFalse(isset($this->config['foobar3']));
364
365        try
366        {
367            while (!$this->migrator->finished())
368            {
369                $this->migrator->update();
370            }
371        }
372        catch (\phpbb\db\migration\exception $e) {}
373
374        // Failure should have caused an automatic roll-back, so this should not exist.
375        $this->assertFalse(isset($this->config['foobar3']));
376
377        $sql = 'SELECT * FROM phpbb_config';
378        $result = $this->db->sql_query_limit($sql, 1);
379        $row = $this->db->sql_fetchrow($result);
380        $this->db->sql_freeresult($result);
381
382        if (isset($row['test_column']))
383        {
384            $this->fail('Revert did not remove test_column.');
385        }
386    }
387
388    public function test_installed()
389    {
390        $this->migrator->set_migrations(array('phpbb_dbal_migration_installed'));
391
392        global $migrator_test_installed_failed;
393        $migrator_test_installed_failed = false;
394
395        while (!$this->migrator->finished())
396        {
397            $this->migrator->update();
398        }
399
400        $this->assertTrue($this->migrator->migration_state('phpbb_dbal_migration_installed') !== false);
401
402        if ($migrator_test_installed_failed)
403        {
404            $this->fail('Installed test failed');
405        }
406    }
407
408    public function test_schema()
409    {
410        $this->migrator->set_migrations(array('phpbb_dbal_migration_schema'));
411
412        while (!$this->migrator->finished())
413        {
414            $this->migrator->update();
415        }
416
417        $this->assertTrue($this->db_tools->sql_column_exists('phpbb_config', 'test_column1'));
418        $this->assertTrue($this->db_tools->sql_table_exists('phpbb_foobar'));
419
420        $short_table_name = \phpbb\db\doctrine\table_helper::generate_shortname('foobar');
421        $index_data_row = $this->db_tools->sql_get_table_index_data('phpbb_foobar');
422        $this->assertEquals(4, count($index_data_row));
423        $this->assertTrue(isset($index_data_row[$short_table_name . '_i_simple']));
424        $this->assertTrue(isset($index_data_row[$short_table_name . '_i_uniq']));
425        $this->assertTrue(isset($index_data_row[$short_table_name . '_i_auth']));
426
427        $is_mysql = $this->db->get_sql_layer() === 'mysqli'; // Key 'lengths' option only applies to MySQL indexes
428
429        // MSSQL primary index key has 'clustered' flag, 'nonclustered' otherwise
430        // See https://learn.microsoft.com/en-us/sql/relational-databases/indexes/clustered-and-nonclustered-indexes-described?view=sql-server-ver17#indexes-and-constraints 
431        $is_mssql = in_array($this->db->get_sql_layer(), ['mssqlnative', 'mssql_odbc']);
432
433        foreach ($index_data_row as $index_name => $index_data)
434        {
435            switch ($index_name)
436            {
437                case $short_table_name . '_i_simple':
438                    $this->assertEquals(['user_id', 'endpoint'], $index_data['columns']);
439                    $this->assertEquals($is_mssql ? ['nonclustered'] : [], $index_data['flags']);
440                    $this->assertFalse($index_data['is_primary']);
441                    $this->assertFalse($index_data['is_unique']);
442                    $this->assertTrue($index_data['is_simple']);
443                    $this->assertEquals(2, count($index_data['options']['lengths']));
444                    $this->assertEmpty($index_data['options']['lengths'][0]);
445                    $this->assertEquals($is_mysql ? 191 : null, $index_data['options']['lengths'][1]);
446                break;
447                case $short_table_name . '_i_uniq':
448                    $this->assertEquals(['expiration_time', 'p256dh'], $index_data['columns']);
449                    $this->assertEquals($is_mssql ? ['nonclustered'] : [], $index_data['flags']);
450                    $this->assertFalse($index_data['is_primary']);
451                    $this->assertTrue($index_data['is_unique']);
452                    $this->assertFalse($index_data['is_simple']);
453                    $this->assertEquals(2, count($index_data['options']['lengths']));
454                    $this->assertEmpty($index_data['options']['lengths'][0]);
455                    $this->assertEquals($is_mysql ? 100 : null, $index_data['options']['lengths'][1]);
456                break;
457                case $short_table_name . '_i_auth':
458                    $this->assertEquals(['auth'], $index_data['columns']);
459                    $this->assertEquals($is_mssql ? ['nonclustered'] : [], $index_data['flags']);
460                    $this->assertFalse($index_data['is_primary']);
461                    $this->assertFalse($index_data['is_unique']);
462                    $this->assertTrue($index_data['is_simple']);
463                    $this->assertEquals(1, count($index_data['options']['lengths']));
464                    $this->assertEmpty($index_data['options']['lengths'][0]);
465                break;
466                default: // Primary key
467                    $this->assertEquals(['module_id'], $index_data['columns']);
468                    $this->assertEquals($is_mssql ? ['clustered'] : [], $index_data['flags']);
469                    $this->assertTrue($index_data['is_primary']);
470                    $this->assertTrue($index_data['is_unique']);
471                    $this->assertFalse($index_data['is_simple']);
472                    $this->assertEquals(1, count($index_data['options']['lengths']));
473                    $this->assertEmpty($index_data['options']['lengths'][0]);
474                break;
475            }
476        }
477
478        while ($this->migrator->migration_state('phpbb_dbal_migration_schema'))
479        {
480            $this->migrator->revert('phpbb_dbal_migration_schema');
481        }
482
483        $this->assertFalse($this->db_tools->sql_column_exists('phpbb_config', 'test_column1'));
484        $this->assertFalse($this->db_tools->sql_table_exists('phpbb_foobar'));
485    }
486
487    public function test_rename_index()
488    {
489        $this->migrator->set_migrations(array('phpbb_dbal_migration_schema_index'));
490
491        while (!$this->migrator->finished())
492        {
493            $this->migrator->update();
494        }
495
496        $this->assertTrue($this->db_tools->sql_unique_index_exists('phpbb_foobar1', 'fbr1_user_id'));
497        $this->assertTrue($this->db_tools->sql_index_exists('phpbb_foobar1', 'fbr1_username'));
498        $this->assertTrue($this->db_tools->sql_unique_index_exists('phpbb_foobar2', 'fbr2_ban_userid'));
499        $this->assertTrue($this->db_tools->sql_index_exists('phpbb_foobar2', 'fbr2_ban_data'));
500
501        while ($this->migrator->migration_state('phpbb_dbal_migration_schema_index'))
502        {
503            $this->migrator->revert('phpbb_dbal_migration_schema_index');
504        }
505
506        $this->assertFalse($this->db_tools->sql_table_exists('phpbb_foobar1'));
507        $this->assertFalse($this->db_tools->sql_table_exists('phpbb_foobar2'));
508    }
509
510    public function test_schema_generator(): array
511    {
512        global $phpbb_root_path, $phpEx;
513
514        $finder_factory = new \phpbb\finder\factory(null, false, $phpbb_root_path, $phpEx);
515        $finder = $finder_factory->get();
516        $migrator_classes = $finder->core_path('phpbb/db/migration/data/')->get_classes();
517
518        $schema_generator = new \phpbb\db\migration\schema_generator(
519            $migrator_classes,
520            $this->config,
521            $this->db,
522            $this->db_tools,
523            $phpbb_root_path,
524            $phpEx,
525            'phpbb_',
526            self::get_core_tables()
527        );
528        $db_table_schema = $schema_generator->get_schema();
529
530        $this->assertNotEmpty($db_table_schema);
531
532        return $db_table_schema;
533    }
534
535    /**
536     * @depends test_schema_generator
537     */
538    public function test_table_indexes(array $db_table_schema)
539    {
540        $table_keys = [];
541        foreach ($db_table_schema as $table_name => $table_data)
542        {
543            if (isset($table_data['KEYS']))
544            {
545                foreach ($table_data['KEYS'] as $key_name => $key_data)
546                {
547                    $table_keys[$table_name][] = $key_name;
548                }
549            }
550        }
551
552        $this->assertNotEmpty($table_keys);
553
554        $table_names = array_merge(array_keys($db_table_schema), ['phpbb_custom_table']);
555        $short_table_names = \phpbb\db\doctrine\table_helper::map_short_table_names($table_names, 'phpbb_');
556        $this->assertEquals('phpbb_custom_table', array_search(\phpbb\db\doctrine\table_helper::generate_shortname('custom_table'), $short_table_names));
557        $this->assertEquals($short_table_names['phpbb_custom_table'], \phpbb\db\doctrine\table_helper::generate_shortname('custom_table'));
558
559        foreach ($table_keys as $table_name => $key_names)
560        {
561            $index_prefix = $short_table_names[$table_name] . '_';
562            foreach ($key_names as $key_name)
563            {
564                $this->assertEquals(0, strpos($key_name, $index_prefix), "$key_name does not contain $index_prefix");
565            }
566        }
567    }
568
569    public function test_add_autoincrement_column()
570    {
571        $this->migrator->set_migrations(['schema_add_autoincrement']);
572
573        while (!$this->migrator->finished())
574        {
575            $this->migrator->update();
576        }
577
578        $this->assertTrue($this->db_tools->sql_table_exists('phpbb_noid'));
579        $this->assertTrue($this->db_tools->sql_column_exists('phpbb_noid', 'id'));
580
581        while ($this->migrator->migration_state('schema_add_autoincrement'))
582        {
583            $this->migrator->revert('schema_add_autoincrement');
584        }
585
586        $this->assertFalse($this->db_tools->sql_table_exists('phpbb_noid'));
587    }
588}