Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
52.03% covered (warning)
52.03%
77 / 148
55.56% covered (warning)
55.56%
10 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
phpbb_database_test_case
52.03% covered (warning)
52.03%
77 / 148
55.56% covered (warning)
55.56%
10 / 18
363.13
0.00% covered (danger)
0.00%
0 / 1
 setup_extensions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setUpBeforeClass
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
20
 tearDownAfterClass
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 tearDown
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
 setUp
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
2
 database_synchronisation
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 createXMLDataSet
8.33% covered (danger)
8.33%
3 / 36
0.00% covered (danger)
0.00%
0 / 1
122.92
 get_test_case_helpers
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 get_database_config
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 getConnection
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 new_dbal
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 new_doctrine_dbal
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 assertSqlResultEquals
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 setExpectedTriggerError
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 create_connection_manager
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 array_diff_assoc_recursive
69.23% covered (warning)
69.23%
9 / 13
0.00% covered (danger)
0.00%
0 / 1
9.86
 assert_array_content_equals
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
 get_core_tables
57.14% covered (warning)
57.14%
4 / 7
0.00% covered (danger)
0.00%
0 / 1
3.71
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
14use PHPUnit\DbUnit\TestCase;
15use Symfony\Component\Yaml\Yaml;
16
17abstract class phpbb_database_test_case extends TestCase
18{
19    private static $already_connected;
20
21    private $db_connections;
22
23    protected $test_case_helpers;
24
25    protected $fixture_xml_data;
26
27    protected static $schema_file;
28
29    protected static $phpbb_schema_copy;
30
31    protected static $install_schema_file;
32
33    /**
34     * @var \Doctrine\DBAL\Connection[]
35     */
36    private $db_connections_doctrine;
37
38    /**
39    * @return array List of extensions that should be set up
40    */
41    protected static function setup_extensions()
42    {
43        return array();
44    }
45
46    public static function setUpBeforeClass(): void
47    {
48        global $phpbb_root_path, $phpEx;
49
50        $setup_extensions = static::setup_extensions();
51
52        $finder = new \phpbb\finder\finder(null, false, $phpbb_root_path, $phpEx);
53        $finder->core_path('phpbb/db/migration/data/');
54        if (!empty($setup_extensions))
55        {
56            $finder->set_extensions($setup_extensions)
57                ->extension_directory('/migrations');
58        }
59        $classes = $finder->get_classes();
60
61        $schema_sha1 = sha1(serialize($classes));
62        self::$schema_file = __DIR__ . '/../tmp/' . $schema_sha1 . '.json';
63        self::$install_schema_file = __DIR__ . '/../../phpBB/install/schemas/schema.json';
64
65        if (!file_exists(self::$schema_file))
66        {
67            global $table_prefix;
68
69            $db = new \phpbb\db\driver\sqlite3();
70            // Some local setups may not configure a DB driver. If the configured
71            // 'dbms' is null, skip this test instead of failing with a TypeError in
72            // the connection factory which expects a non-null driver name.
73            $mock_config = new phpbb_mock_config_php_file();
74            if ($mock_config->get('dbms') === null)
75            {
76                self::markTestSkipped('Skipping schema generator tests: dbms is not configured (null) in local environment.');
77            }
78            $doctrine = \phpbb\db\doctrine\connection_factory::get_connection($mock_config);
79            $factory = new \phpbb\db\tools\factory();
80            $db_tools = $factory->get($doctrine, true);
81            $db_tools->set_table_prefix($table_prefix);
82
83            $schema_generator = new \phpbb\db\migration\schema_generator($classes, new \phpbb\config\config(array()), $db, $db_tools, $phpbb_root_path, $phpEx, $table_prefix, self::get_core_tables());
84            file_put_contents(self::$schema_file, json_encode($schema_generator->get_schema()));
85        }
86
87        copy(self::$schema_file, self::$install_schema_file);
88
89        parent::setUpBeforeClass();
90    }
91
92    public static function tearDownAfterClass(): void
93    {
94        if (file_exists(self::$install_schema_file))
95        {
96            unlink(self::$install_schema_file);
97        }
98
99        parent::tearDownAfterClass();
100    }
101
102    protected function tearDown(): void
103    {
104        parent::tearDown();
105
106        // Close all database connections from this test
107        if (!empty($this->db_connections))
108        {
109            foreach ($this->db_connections as $db)
110            {
111                $db->sql_close();
112            }
113        }
114
115        if (!empty($this->db_connections_doctrine))
116        {
117            foreach ($this->db_connections_doctrine as $db)
118            {
119                $db->close();
120            }
121        }
122    }
123
124    protected function setUp(): void
125    {
126        parent::setUp();
127
128        $this->setBackupStaticPropertiesExcludeList([
129            'SebastianBergmann\CodeCoverage\CodeCoverage' => ['instance'],
130            'SebastianBergmann\CodeCoverage\Filter' => ['instance'],
131            'SebastianBergmann\CodeCoverage\Util' => ['ignoredLines', 'templateMethods'],
132            'SebastianBergmann\Timer\Timer' => ['startTimes'],
133            'PHP_Token_Stream' => ['customTokens'],
134            'PHP_Token_Stream_CachingFactory' => ['cache'],
135
136            'phpbb_database_test_case' => ['already_connected'],
137        ]);
138
139        $this->db_connections = [];
140        $this->db_connections_doctrine = [];
141
142        // Resynchronise tables if a fixture was loaded
143        if (isset($this->fixture_xml_data))
144        {
145            $config = $this->get_database_config();
146            $manager = $this->create_connection_manager($config);
147            $manager->connect();
148            $manager->post_setup_synchronisation($this->fixture_xml_data);
149        }
150    }
151
152    /**
153    * Performs synchronisations for a given table/column set on the database
154    *
155    * @param    array    $table_column_map        Information about the tables/columns to synchronise
156    *
157    * @return null
158    */
159    protected function database_synchronisation($table_column_map)
160    {
161        $config = $this->get_database_config();
162        $manager = $this->create_connection_manager($config);
163        $manager->connect();
164        $manager->database_synchronisation($table_column_map);
165    }
166
167    /**
168     * Create xml data set for insertion into database
169     *
170     * @param string $path Path to fixture XML
171     * @return PHPUnit\DbUnit\DataSet\DefaultDataSet|PHPUnit\DbUnit\DataSet\XmlDataSet
172     */
173    public function createXMLDataSet($path)
174    {
175        $this->fixture_xml_data = parent::createXMLDataSet($path);
176
177        // Extend XML data set on MSSQL
178        if (strpos($this->get_database_config()['dbms'], 'mssql') !== false)
179        {
180            $newXmlData = new PHPUnit\DbUnit\DataSet\DefaultDataSet([]);
181            $db = $this->new_dbal();
182            foreach ($this->fixture_xml_data as $key => $value)
183            {
184                /** @var PHPUnit\DbUnit\DataSet\DefaultTableMetaData $tableMetaData */
185                $tableMetaData = $value->getTableMetaData();
186                $columns = $tableMetaData->getColumns();
187                $primaryKeys = $tableMetaData->getPrimaryKeys();
188
189                $sql = "SELECT COLUMN_NAME AS identity_column
190                    FROM INFORMATION_SCHEMA.COLUMNS
191                    WHERE COLUMNPROPERTY(object_id(TABLE_SCHEMA + '.' + TABLE_NAME), COLUMN_NAME, 'IsIdentity') = 1
192                        AND TABLE_NAME = '$key'
193                    ORDER BY TABLE_NAME";
194                $result = $db->sql_query($sql);
195                $identity_columns = $db->sql_fetchrowset($result);
196                $has_default_identity = false;
197                $add_primary_keys = false;
198
199                // Iterate over identity columns to check for missing primary
200                // keys in data set and special identity column 'mssqlindex'
201                // that might have been added when no default identity column
202                // exists in the current table.
203                foreach ($identity_columns as $column)
204                {
205                    if (in_array($column['identity_column'], $columns) && !in_array($column['identity_column'], $primaryKeys))
206                    {
207                        $primaryKeys[] = $column['identity_column'];
208                        $add_primary_keys = true;
209                    }
210
211                    if ($column['identity_column'] === 'mssqlindex')
212                    {
213                        $has_default_identity = true;
214                        break;
215                    }
216                }
217
218                if ($has_default_identity || $add_primary_keys)
219                {
220                    // Add default identity column to columns list
221                    if ($has_default_identity)
222                    {
223                        $columns[] = 'mssqlindex';
224                    }
225
226                    $newMetaData = new PHPUnit\DbUnit\DataSet\DefaultTableMetaData($key, $columns, $primaryKeys);
227                    $newTable = new PHPUnit\DbUnit\DataSet\DefaultTable($newMetaData);
228                    for ($i = 0; $i < $value->getRowCount(); $i++)
229                    {
230                        $dataRow = $value->getRow($i);
231                        if ($has_default_identity)
232                        {
233                            $dataRow['mssqlindex'] = $i + 1;
234                        }
235                        $newTable->addRow($dataRow);
236                    }
237                    $newXmlData->addTable($newTable);
238                }
239                else
240                {
241                    $newXmlData->addTable($value);
242                }
243            }
244
245            $this->fixture_xml_data = $newXmlData;
246        }
247        return $this->fixture_xml_data;
248    }
249
250    public function get_test_case_helpers()
251    {
252        if (!$this->test_case_helpers)
253        {
254            $this->test_case_helpers = new phpbb_test_case_helpers($this);
255        }
256
257        return $this->test_case_helpers;
258    }
259
260    public function get_database_config()
261    {
262        $config = phpbb_test_case_helpers::get_test_config();
263
264        if (!isset($config['dbms']))
265        {
266            $this->markTestSkipped('Missing test_config.php: See first error.');
267        }
268
269        return $config;
270    }
271
272    public function getConnection()
273    {
274        $config = $this->get_database_config();
275
276        $manager = $this->create_connection_manager($config);
277
278        if (!self::$already_connected)
279        {
280            $manager->recreate_db();
281        }
282
283        $manager->connect();
284
285        if (!self::$already_connected)
286        {
287            $manager->load_schema($this->new_dbal(), $this->new_doctrine_dbal());
288            self::$already_connected = true;
289        }
290
291        return $this->createDefaultDBConnection($manager->get_pdo(), 'testdb');
292    }
293
294    public function new_dbal() : \phpbb\db\driver\driver_interface
295    {
296        $config = $this->get_database_config();
297
298        /** @var \phpbb\db\driver\driver_interface $db */
299        $db = new $config['dbms']();
300        $db->sql_connect($config['dbhost'], $config['dbuser'], $config['dbpasswd'], $config['dbname'], $config['dbport']);
301
302        $this->db_connections[] = $db;
303
304        return $db;
305    }
306
307    public function new_doctrine_dbal(): \Doctrine\DBAL\Connection
308    {
309        $config = $this->get_database_config();
310
311        $db = \phpbb\db\doctrine\connection_factory::get_connection_from_params($config['dbms'], $config['dbhost'], $config['dbuser'], $config['dbpasswd'], $config['dbname'], $config['dbport']);
312        $this->db_connections_doctrine[] = $db;
313
314        return $db;
315    }
316
317    public function assertSqlResultEquals($expected, $sql, $message = '')
318    {
319        $db = $this->new_dbal();
320
321        $result = $db->sql_query($sql);
322        $rows = $db->sql_fetchrowset($result);
323        $db->sql_freeresult($result);
324
325        $this->assertEquals($expected, $rows, $message);
326    }
327
328    public function setExpectedTriggerError($errno, $message = '')
329    {
330        $this->get_test_case_helpers()->setExpectedTriggerError($errno, $message);
331    }
332
333    protected function create_connection_manager($config)
334    {
335        return new phpbb_database_test_connection_manager($config);
336    }
337
338    /** array_diff() does not corretly compare multidimensionsl arrays
339     *  This solution used for that https://www.codeproject.com/Questions/780780/PHP-Finding-differences-in-two-multidimensional-ar
340     */
341    function array_diff_assoc_recursive($array1, $array2)
342    {
343        $difference = array();
344        foreach ($array1 as $key => $value)
345        {
346            if (is_array($value))
347            {
348                if (!isset($array2[$key]))
349                {
350                    $difference[$key] = $value;
351                }
352                else if (!is_array($array2[$key]))
353                {
354                    $difference[$key] = $value;
355                }
356                else
357                {
358                    $new_diff = $this->array_diff_assoc_recursive($value, $array2[$key]);
359                    if (!empty($new_diff))
360                    {
361                        $difference[$key] = $new_diff;
362                    }
363                }
364            }
365            else if (!isset($array2[$key]) || $array2[$key] != $value)
366            {
367                $difference[$key] = $value;
368            }
369        }
370        return $difference;
371    }
372
373    public function assert_array_content_equals($one, $two)
374    {
375        // one-way comparison is not enough!
376        if (count($this->array_diff_assoc_recursive($one, $two)) || count($this->array_diff_assoc_recursive($two, $one)))
377        {
378            // get a nice error message
379            $this->assertEquals($one, $two);
380        }
381        else
382        {
383            // increase assertion count
384            $this->assertTrue(true);
385        }
386    }
387
388    public static function get_core_tables() : array
389    {
390        global $phpbb_root_path, $table_prefix;
391
392        static $core_tables = [];
393
394        if (empty($core_tables))
395        {
396            $tables_yml_data = Yaml::parseFile($phpbb_root_path . '/config/default/container/tables.yml', Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE);
397
398            foreach ($tables_yml_data['parameters'] as $parameter => $table)
399            {
400                $core_tables[str_replace('tables.', '', $parameter)] = str_replace('%core.table_prefix%', $table_prefix, $table);
401            }
402        }
403
404        return $core_tables;
405    }
406}