Files
XC_VM/tests/Unit/MigrateModuleTablesTest.php
T
Divarion_D efe3676286 fix(migration): keep core tables of migrations out of the module files
- Tables a core migration creates (category_templates, ...) and
  MigrationRunner's own log were taken for module tables: only
  database.sql was read. coreSchemaTables() reads the up-migrations and
  counts the log too.
- A module table that is the parent of a foreign key (telegram_bots)
  could not be dropped on its own: the drops run with foreign-key checks
  off, and a saved table loses its foreign keys, which name the backup's
  other tables.
- No options file (migrate run by hand) and a lines table without pid
  no longer warn.
2026-10-08 20:34:18 +03:00

119 lines
5.6 KiB
PHP

<?php
use PHPUnit\Framework\TestCase;
use XcVm\Cli\Commands\MigrateCommand;
use XcVm\Core\Events\Migration\LegacyTableMigrationEvent;
/**
* Backup tables the core does not own (MigrateCommand::moduleTables(), no list
* of modules): the migration saves each to a file of its own, from which its
* module copies it, at the migration or when it is installed later.
* The file is SQL: the table's own definition (as `legacy_<table>`) and its
* rows as INSERTs, loaded into that staging table for a later module.
*/
final class MigrateModuleTablesTest extends TestCase {
private string $rDir;
protected function setUp(): void {
$this->rDir = sys_get_temp_dir() . '/xcvm-module-tables-' . bin2hex(random_bytes(4)) . '/';
LegacyTableMigrationEvent::useDir($this->rDir);
}
protected function tearDown(): void {
LegacyTableMigrationEvent::useDir(null);
foreach (glob($this->rDir . '*') ?: [] as $rFile) {
unlink($rFile);
}
@rmdir($this->rDir);
}
public function testTablesTheCoreDoesNotOwnAreSavedForModulesWhateverTheirNames(): void {
$rBackup = ['streams', 'lines', 'lines_activity', 'user_activity', 'reg_users', 'category_templates', 'migrations', 'watch_folders', 'watch_logs', 'acme_addon_items'];
$rSaved = MigrateCommand::moduleTables($rBackup, MigrateCommand::coreSchemaTables());
$this->assertSame(['watch_folders', 'watch_logs', 'acme_addon_items'], $rSaved, 'migrated (streams, reg_users), core schema (lines_activity, category_templates, migrations) and junk (user_activity) stay out');
}
public function testTheCoreSchemaIsReadFromDatabaseSql(): void {
$rTables = MigrateCommand::coreSchemaTables();
$this->assertContains('streams', $rTables);
$this->assertContains('lines_activity', $rTables);
$this->assertNotContains('watch_folders', $rTables, 'a module table is not the core\'s');
$this->assertContains('category_templates', $rTables, 'created by a core migration, not by database.sql');
$this->assertContains('migrations', $rTables, "MigrationRunner's own log");
}
public function testATableIsSavedAsSqlAndLoadedBackForAModuleInstalledLater(): void {
$rDb = new TestDb();
$rDb->exec('CREATE TABLE `watch_folders` (`id` int PRIMARY KEY, `directory` varchar(255), `bouquets` varchar(255) NULL);');
$rDb->exec("INSERT INTO `watch_folders` VALUES (1, 'it''s a \"dir\"\nwith ? and \\\\ in it', NULL);");
$rValues = [];
foreach (range(2, 2500) as $i) {
$rValues[] = '(' . $i . ", '/media/Фильмы " . $i . "', '[\"" . $i . "\"]')";
}
$rDb->exec('INSERT INTO `watch_folders` VALUES ' . implode(',', $rValues) . ';');
$rNow = LegacyTableMigrationEvent::dump($rDb, 'watch_folders', 'xc');
$this->assertCount(2500, iterator_to_array($rNow->rows(), false), 'at the migration: read from the backup table, in chunks');
$this->assertFileExists(LegacyTableMigrationEvent::sqlFile('watch_folders'));
$rDb->exec('DROP TABLE `watch_folders`;'); // the migration drops it from the backup
$rLater = LegacyTableMigrationEvent::fromBackup('watch_folders', $rDb);
$this->assertSame('xc', $rLater->format);
$rRows = iterator_to_array($rLater->rows(), false);
$this->assertCount(2500, $rRows);
$this->assertEquals(['id' => 1, 'directory' => "it's a \"dir\"\nwith ? and \\ in it", 'bouquets' => null], $rRows[0]);
$this->assertEquals(['id' => 2500, 'directory' => '/media/Фильмы 2500', 'bouquets' => '["2500"]'], $rRows[2499]);
$rLater->discard();
$this->assertFileDoesNotExist(LegacyTableMigrationEvent::sqlFile('watch_folders'));
$rDb->query("SHOW TABLES LIKE 'legacy_watch_folders';");
$this->assertSame(0, $rDb->num_rows(), 'the staging table goes too');
$this->assertNull(LegacyTableMigrationEvent::fromBackup('watch_folders', $rDb));
}
public function testASavedTableLosesItsForeignKeysAndLoadsWithoutItsParent(): void {
$rDb = new TestDb();
$rDb->exec('CREATE TABLE `acme_bots` (`id` int PRIMARY KEY) ENGINE=InnoDB;');
$rDb->exec('CREATE TABLE `acme_chats` (`id` int PRIMARY KEY, `bot_id` int, CONSTRAINT `fk_acme_bot` FOREIGN KEY (`bot_id`) REFERENCES `acme_bots` (`id`) ON DELETE CASCADE) ENGINE=InnoDB;');
$rDb->query('INSERT INTO `acme_bots` VALUES (7);');
$rDb->query('INSERT INTO `acme_chats` VALUES (1, 7);');
LegacyTableMigrationEvent::dump($rDb, 'acme_chats', 'xui');
$this->assertStringNotContainsString('FOREIGN KEY', (string) file_get_contents(LegacyTableMigrationEvent::sqlFile('acme_chats')));
$rDb->exec('SET FOREIGN_KEY_CHECKS = 0;');
$rDb->exec('DROP TABLE `acme_bots`;');
$rDb->exec('DROP TABLE `acme_chats`;');
$rDb->exec('SET FOREIGN_KEY_CHECKS = 1;');
$rLater = LegacyTableMigrationEvent::fromBackup('acme_chats', $rDb);
$this->assertEquals([['id' => 1, 'bot_id' => 7]], iterator_to_array($rLater->rows(), false));
$rLater->discard();
}
public function testAnEmptyOrAbsentTableSavesNothing(): void {
$rDb = new TestDb();
$this->assertNull(LegacyTableMigrationEvent::dump($rDb, 'watch_folders', 'xui'));
$rDb->exec('CREATE TABLE `watch_folders` (`id` int PRIMARY KEY);');
$this->assertNull(LegacyTableMigrationEvent::dump($rDb, 'watch_folders', 'xui'));
$this->assertNull(LegacyTableMigrationEvent::fromBackup('watch_folders', $rDb));
}
public function testATableThatCannotBeSavedSaysSo(): void {
$rDb = new TestDb();
$rDb->exec('CREATE TABLE `watch_folders` (`id` int PRIMARY KEY);');
$rDb->query('INSERT INTO `watch_folders` VALUES (1);');
touch(rtrim($this->rDir, '/')); // a file where the directory should be
LegacyTableMigrationEvent::useDir($this->rDir . 'sub/');
$this->expectException(RuntimeException::class);
try {
LegacyTableMigrationEvent::dump($rDb, 'watch_folders', 'xui');
} finally {
unlink(rtrim($this->rDir, '/'));
}
}
}