mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 10:06:15 -08:00
132 lines
4.9 KiB
TypeScript
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;
|
|
});
|