Files
iptvnator/libs/shared/database/src/lib/testing/connection-upgrade.ts
T

132 lines
4.9 KiB
TypeScript

import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import Database from 'better-sqlite3';
import { closeDatabase, getDatabasePath, initDatabase } from '../connection';
const tables = [
'playlists',
'categories',
'content',
'favorites',
'recently_viewed',
'playback_positions',
'downloads',
] as const;
function seed(sqlite: Database.Database) {
sqlite.exec(`
INSERT INTO playlists (id, name, type, serverUrl, username, password)
VALUES ('saved', 'Saved source', 'xtream', 'https://source.invalid',
'synthetic-user', 'synthetic-password');
INSERT INTO categories (id, playlist_id, name, type, xtream_id, hidden)
VALUES (1, 'saved', 'Movies', 'movies', 42, 1);
INSERT INTO content (id, category_id, title, xtream_id, type, added)
VALUES (1, 1, 'Saved movie', 7, 'movie', '1700000000');
INSERT INTO favorites (content_id, playlist_id) VALUES (1, 'saved');
INSERT INTO recently_viewed (content_id, playlist_id) VALUES (1, 'saved');
INSERT INTO playback_positions
(playlist_id, content_xtream_id, content_type, position_seconds, duration_seconds)
VALUES ('saved', 7, 'vod', 123, 1800);
INSERT INTO downloads (playlist_id, xtream_id, content_type, title, url, status)
VALUES ('saved', 7, 'vod', 'Saved movie', 'https://source.invalid/movie', 'completed');
`);
const columns = sqlite.pragma('table_info(content)') as { name: string }[];
if (columns.some(({ name }) => name === 'epg_channel_id')) {
sqlite.exec("UPDATE content SET epg_channel_id = 'retained-epg-id'");
}
}
function snapshot(sqlite: Database.Database) {
return tables.map((table) => {
const columns = sqlite.pragma(`table_info(${table})`) as {
name: string;
}[];
const query = `SELECT ${columns.map(({ name }) => `"${name}"`).join(', ')} FROM ${table} ORDER BY id`;
return { query, rows: sqlite.prepare(query).all() };
});
}
function epgIndex(sqlite: Database.Database) {
return sqlite
.prepare(
"SELECT sql, rootpage FROM sqlite_master WHERE name = 'idx_content_epg_channel'"
)
.get();
}
async function main() {
const fixture = process.argv[2];
assert(
process.env['IPTVNATOR_E2E_DATA_DIR'],
'Never open the user database'
);
const warnings: unknown[][] = [];
console.warn = (...args) => warnings.push(args);
console.log = () => undefined;
const databasePath = getDatabasePath();
if (fixture === 'fresh') {
await initDatabase();
closeDatabase();
}
const legacy = new Database(databasePath);
legacy.pragma('foreign_keys = ON');
if (fixture !== 'fresh') legacy.exec(readFileSync(fixture, 'utf8'));
seed(legacy);
const before = snapshot(legacy);
const originalIndex = epgIndex(legacy);
legacy.close();
let firstSchema: unknown;
for (let startup = 0; startup < 2; startup++) {
try {
await initDatabase();
const sqlite = new Database(databasePath, { readonly: true });
try {
for (const { query, rows } of before) {
assert.deepEqual(sqlite.prepare(query).all(), rows, query);
}
assert.deepEqual(sqlite.pragma('foreign_key_check'), []);
assert.deepEqual(sqlite.pragma('integrity_check'), [
{ integrity_check: 'ok' },
]);
const index = epgIndex(sqlite);
assert(index, 'EPG index must exist after initialization');
assert.deepEqual(
(
sqlite.pragma(
'index_info(idx_content_epg_channel)'
) as { name: string }[]
).map(({ name }) => name),
['epg_channel_id']
);
if (originalIndex) assert.deepEqual(index, originalIndex);
// closeDatabase() may add SQLite's own planner statistics via
// PRAGMA optimize; compare only application schema objects.
const schema = sqlite
.prepare(
"SELECT type, name, sql FROM sqlite_master WHERE name NOT GLOB 'sqlite_*' ORDER BY type, name"
)
.all();
if (startup === 0) firstSchema = schema;
else assert.deepEqual(schema, firstSchema);
} finally {
sqlite.close();
}
} finally {
closeDatabase();
}
}
assert.deepEqual(
warnings,
[],
'Initialization must not silently skip failed migrations'
);
process.stdout.write('upgrade verified');
}
main().catch((error: unknown) => {
console.error(error);
process.exitCode = 1;
});