import Database from 'better-sqlite3'; import { execFileSync } from 'node:child_process'; import { mkdtempSync, readdirSync, readFileSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { afterEach, describe, expect, it, vi } from 'vitest'; const temporaryDirectories: string[] = []; afterEach(async () => { const { closeDatabase } = await import('./database.js'); closeDatabase(); delete process.env.SENDEWERK_DB_PATH; delete process.env.SENDEWERK_UPLOAD_DIR; for (const directory of temporaryDirectories.splice(0)) { rmSync(directory, { recursive: true, force: true }); } vi.resetModules(); }); describe('database setup', () => { it('runs migrations and the seed repeatedly in an isolated database', () => { const directory = mkdtempSync(path.join(tmpdir(), 'sendewerk-test-')); temporaryDirectories.push(directory); const databasePath = path.join(directory, 'test.sqlite'); const environment = { ...process.env, SENDEWERK_DB_PATH: databasePath, SENDEWERK_UPLOAD_DIR: path.join(directory, 'uploads') }; for (let run = 0; run < 2; run += 1) { execFileSync(process.execPath, ['scripts/migrate.mjs'], { cwd: process.cwd(), env: environment }); execFileSync(process.execPath, ['scripts/seed.mjs'], { cwd: process.cwd(), env: environment }); } const database = new Database(databasePath, { readonly: true }); const counts = database .prepare( `SELECT (SELECT count(*) FROM schema_migrations) AS migrations, (SELECT count(*) FROM pieces) AS pieces, (SELECT count(*) FROM shows) AS shows, (SELECT count(*) FROM piece_usages) AS usages, (SELECT count(*) FROM show_templates) AS templates, (SELECT count(*) FROM show_sections WHERE is_reserve_bank = 1) AS reserve_banks` ) .get() as { migrations: number; pieces: number; shows: number; usages: number; templates: number; reserve_banks: number; }; database.close(); expect(counts).toEqual({ migrations: 20, pieces: 6, shows: 2, usages: 5, templates: 2, reserve_banks: 2 }); expect(databasePath).not.toBe(path.resolve('.data/sendewerk.sqlite')); }); it('does not count reserve pieces or explicit sections twice', async () => { const directory = mkdtempSync(path.join(tmpdir(), 'sendewerk-duration-test-')); temporaryDirectories.push(directory); process.env.SENDEWERK_DB_PATH = path.join(directory, 'test.sqlite'); process.env.SENDEWERK_UPLOAD_DIR = path.join(directory, 'uploads'); execFileSync(process.execPath, ['scripts/migrate.mjs'], { cwd: process.cwd(), env: process.env }); execFileSync(process.execPath, ['scripts/seed.mjs'], { cwd: process.cwd(), env: process.env }); const { getShow } = await import('./previews.js'); const show = getShow(); expect(show.sections.map((section) => section.durationSeconds)).toEqual([300, 1200, 3600]); expect(show.totalDurationSeconds).toBe(5100); expect(show.sections[1].usages.find((usage) => usage.isReserve)?.durationSeconds).toBe(540); }); it('upgrades existing completed shows into stable finalizations exactly once', () => { const directory = mkdtempSync(path.join(tmpdir(), 'sendewerk-upgrade-test-')); temporaryDirectories.push(directory); const databasePath = path.join(directory, 'test.sqlite'); const environment = { ...process.env, SENDEWERK_DB_PATH: databasePath, SENDEWERK_UPLOAD_DIR: path.join(directory, 'uploads') }; const database = new Database(databasePath); database.pragma('foreign_keys = ON'); database.exec( 'CREATE TABLE schema_migrations (name TEXT PRIMARY KEY, applied_at_utc TEXT NOT NULL)' ); const migrationDirectory = path.resolve('migrations'); const oldMigrations = readdirSync(migrationDirectory) .filter( (name) => /^\d+.+\.sql$/.test(name) && !name.startsWith('0015_') && !name.startsWith('0016_') ) .sort(); for (const name of oldMigrations) { database.exec(readFileSync(path.join(migrationDirectory, name), 'utf8')); database .prepare('INSERT INTO schema_migrations (name, applied_at_utc) VALUES (?, ?)') .run(name, new Date().toISOString()); } database.close(); execFileSync(process.execPath, ['scripts/seed.mjs'], { cwd: process.cwd(), env: environment }); const legacy = new Database(databasePath); legacy.pragma('foreign_keys = ON'); const usage = legacy .prepare( `SELECT u.id FROM piece_usages u JOIN show_sections s ON s.id = u.show_section_id WHERE s.show_id = 'show-october' AND s.is_reserve_bank = 0 LIMIT 1` ) .get() as { id: string }; legacy.prepare("UPDATE piece_usages SET status = 'done' WHERE id = ?").run(usage.id); legacy.prepare("UPDATE shows SET status = 'completed' WHERE id = 'show-october'").run(); const completionMigration = '0015_completion_archive_and_shares.sql'; legacy.exec(readFileSync(path.join(migrationDirectory, completionMigration), 'utf8')); legacy .prepare('INSERT INTO schema_migrations (name, applied_at_utc) VALUES (?, ?)') .run(completionMigration, new Date().toISOString()); const now = new Date().toISOString(); legacy .prepare( `INSERT INTO users (id, email, email_normalized, display_name, password_hash, role, password_changed_at_utc, created_at_utc, updated_at_utc) VALUES ('upgrade-barkeeper', 'upgrade@example.test', 'upgrade@example.test', 'Upgrade', 'unused', 'barkeeper', ?, ?, ?)` ) .run(now, now, now); legacy .prepare( `INSERT INTO show_shares (id, show_id, token_hash, scope, password_hash, expires_at_utc, revoked_at_utc, created_by_user_id, created_at_utc) VALUES ('upgrade-share', 'show-october', 'upgrade-token-hash', 'rundown', NULL, NULL, NULL, 'upgrade-barkeeper', ?)` ) .run(now); legacy .prepare( `INSERT INTO show_share_sessions (token_hash, share_id, created_at_utc, expires_at_utc) VALUES ('upgrade-session-hash', 'upgrade-share', ?, ?)` ) .run(now, new Date(Date.now() + 60_000).toISOString()); legacy.close(); for (let run = 0; run < 2; run += 1) { execFileSync(process.execPath, ['scripts/migrate.mjs'], { cwd: process.cwd(), env: environment }); } const upgraded = new Database(databasePath, { readonly: true }); expect( upgraded .prepare( `SELECT (SELECT count(*) FROM show_finalizations WHERE show_id = 'show-october') AS finalizations, (SELECT count(*) FROM piece_usage_history WHERE show_id = 'show-october') AS history, (SELECT count(*) FROM show_shares WHERE id = 'upgrade-share') AS shares, (SELECT count(*) FROM show_share_sessions WHERE token_hash = 'upgrade-session-hash') AS sessions` ) .get() ).toEqual({ finalizations: 1, history: 1, shares: 1, sessions: 1 }); expect(upgraded.pragma('foreign_key_check')).toEqual([]); upgraded.close(); }); });