const test = require('node:test'); const assert = require('node:assert/strict'); require('../src/common'); const { getPendingMigrations } = require('../src/db/migrations'); function createPool(responses) { const queries = []; return { queries, async query(sql, params) { queries.push([sql, params]); const text = String(sql); for (const response of responses) { if (response.match(text, params)) { return response.result; } } return [[]]; } }; } test('pending migrations are empty when the schema already matches the app version', async () => { const pool = createPool([ { match(sql) { return sql.includes('FROM information_schema.COLUMNS') && sql.includes('TABLE_NAME = ?') && sql.includes('COLUMN_NAME = ?'); }, result: [[{ column_count: 1 }]] }, { match(_sql, params) { return Array.isArray(params) && params[0] === 'i_schedule_groups'; }, result: [[{ table_count: 0 }]] }, { match(_sql, params) { return Array.isArray(params) && params[0] === 'i_schedule_entries'; }, result: [[{ table_count: 0 }]] } ]); const pendingMigrations = await getPendingMigrations(pool, { currentVersion: require('../package.json').version }); assert.equal(pendingMigrations.length, 0); }); test('query index migration upgrades a 2.17.4 installation', async () => { const queries = []; const pool = { queries, async query(sql, params) { queries.push([sql, params]); if (String(sql).includes('FROM information_schema.STATISTICS')) { return [[{ index_count: params[1] === 'idx_background_tasks_status' ? 1 : 0 }]]; } return [[]]; } }; const pending = await getPendingMigrations(pool, { currentVersion: '2.17.4' }); const indexMigration = pending.find(migration => migration.version === '2.17.5'); assert.ok(indexMigration); await indexMigration.run(pool); const alterations = queries.map(([sql]) => String(sql)).filter(sql => sql.startsWith('ALTER TABLE')); assert.equal(alterations.length, 9); assert.ok(alterations.some(sql => sql.includes('ADD INDEX idx_playlist_slides_playlist_position'))); assert.ok(alterations.some(sql => sql.includes('ADD INDEX idx_template_regions_template_z'))); assert.ok(alterations.some(sql => sql.includes('ADD INDEX idx_slides_template'))); assert.ok(alterations.some(sql => sql.includes('ADD INDEX idx_templates_canvas_size'))); assert.ok(alterations.some(sql => sql.includes('ADD INDEX idx_sessions_expires_at'))); assert.ok(alterations.some(sql => sql.includes('ADD INDEX idx_background_tasks_status_created_at'))); assert.ok(alterations.some(sql => sql.includes('ADD INDEX idx_rss_feeds_modified_at'))); assert.ok(alterations.some(sql => sql.includes('ADD INDEX idx_api_source_groups_modified_at'))); assert.ok(alterations.some(sql => sql.includes('DROP INDEX idx_background_tasks_status'))); }); test('playlist shuffle migration upgrades an existing 2.15.0 installation', async () => { const pool = createPool([]); const pending = await getPendingMigrations(pool, { currentVersion: '2.15.0' }); const shuffleMigration = pending.find(migration => migration.version === '2.15.1'); assert.ok(shuffleMigration); await shuffleMigration.run(pool); assert.ok(pool.queries.some(([sql]) => sql.includes('ADD COLUMN group_shuffle'))); }); test('playlist repeat migration upgrades an existing 2.15.1 installation', async () => { const pool = createPool([]); const pending = await getPendingMigrations(pool, { currentVersion: '2.15.1' }); const repeatMigration = pending.find(migration => migration.version === '2.15.2'); assert.ok(repeatMigration); await repeatMigration.run(pool); assert.ok(pool.queries.some(([sql]) => sql.includes('ADD COLUMN group_mode'))); assert.ok(pool.queries.some(([sql]) => sql.includes('ADD COLUMN group_repeat_count'))); }); test('canvas size enabled migration upgrades an existing 2.15.2 installation', async () => { const pool = createPool([]); const pending = await getPendingMigrations(pool, { currentVersion: '2.15.2' }); const canvasMigration = pending.find(migration => migration.version === '2.15.3'); assert.ok(canvasMigration); await canvasMigration.run(pool); assert.ok(pool.queries.some(([sql]) => sql.includes('ALTER TABLE c_canvas_sizes ADD COLUMN enabled'))); }); test('playlist group schema migration is pending for version 2.14.3', async () => { const pool = createPool([ { match(sql) { return sql.includes('FROM information_schema.COLUMNS') && sql.includes('TABLE_NAME = ?') && sql.includes('COLUMN_NAME = ?'); }, result: [[{ column_count: 1 }]] }, { match(_sql, params) { return Array.isArray(params) && params[0] === 'i_schedule_groups'; }, result: [[{ table_count: 0 }]] }, { match(_sql, params) { return Array.isArray(params) && params[0] === 'i_schedule_entries'; }, result: [[{ table_count: 0 }]] } ]); const pendingMigrations = await getPendingMigrations(pool, { currentVersion: '2.14.3' }); assert.ok(pendingMigrations.some(function (migration) { return migration.version === '2.15.0'; })); }); test('pending migrations are reported when an older schema still needs scripts', async () => { const pool = createPool([ { match(sql) { return sql.includes('FROM information_schema.COLUMNS') && sql.includes('TABLE_NAME = ?') && sql.includes('COLUMN_NAME = ?'); }, result: [[{ column_count: 0 }]] }, { match(_sql, params) { return Array.isArray(params) && params[0] === 'i_schedule_groups'; }, result: [[{ table_count: 1 }]] }, { match(_sql, params) { return Array.isArray(params) && params[0] === 'i_schedule_entries'; }, result: [[{ table_count: 1 }]] } ]); const pendingMigrations = await getPendingMigrations(pool, { currentVersion: '2.6.17' }); assert.ok(pendingMigrations.length > 0); assert.equal(pendingMigrations.filter(function (migration) { return migration.version.indexOf('2.8.') === 0; }).length, 4); assert.equal(pendingMigrations.find(function (migration) { return migration.version.indexOf('2.8.') === 0; }).version, '2.8.0'); }); test('a partial timetable schema does not fast-forward migration detection', async () => { const pool = createPool([ { match(sql) { return sql.includes('FROM information_schema.COLUMNS') && sql.includes('TABLE_NAME = ?') && sql.includes('COLUMN_NAME = ?'); }, result: [[{ column_count: 0 }]] }, { match(_sql, params) { return Array.isArray(params) && params[0] === 'i_timetable_groups'; }, result: [[{ table_count: 1 }]] }, { match(_sql, params) { return Array.isArray(params) && params[0] === 'i_timetable_entries'; }, result: [[{ table_count: 0 }]] }, { match(_sql, params) { return Array.isArray(params) && params[0] === 'i_schedule_groups'; }, result: [[{ table_count: 0 }]] }, { match(_sql, params) { return Array.isArray(params) && params[0] === 'i_schedule_entries'; }, result: [[{ table_count: 0 }]] } ]); const pendingMigrations = await getPendingMigrations(pool, { currentVersion: '0.0.0' }); assert.ok(pendingMigrations.some(function (migration) { return migration.version === '2.6.16'; })); assert.ok(pendingMigrations.some(function (migration) { return migration.version === '2.6.17'; })); assert.ok(pendingMigrations.some(function (migration) { return migration.version === '2.6.18'; })); }); test('template region defaults migration upgrades a 2.17.5 installation', async () => { const pool = createPool([]); const pending = await getPendingMigrations(pool, { currentVersion: '2.17.5' }); const regionContentMigration = pending.find(migration => migration.version === '2.18.0'); assert.ok(regionContentMigration); await regionContentMigration.run(pool); assert.ok(pool.queries.some(([sql]) => String(sql).includes('ALTER TABLE c_template_regions ADD COLUMN default_content_json JSON NULL'))); assert.ok(pool.queries.some(([sql]) => String(sql).includes('ALTER TABLE c_template_regions ADD COLUMN read_only TINYINT(1) NOT NULL DEFAULT 0'))); });