import Database from "better-sqlite3"; import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { DatabaseService } from "./database.service.js"; const previousDatabasePath = process.env.DATABASE_PATH; const previousStorageDir = process.env.STORAGE_DIR; const tempDirs: string[] = []; afterEach(() => { process.env.DATABASE_PATH = previousDatabasePath; process.env.STORAGE_DIR = previousStorageDir; for (const dir of tempDirs.splice(0)) { rmSync(dir, { recursive: true, force: true }); } }); describe("database migrations", () => { it("adds metadata columns to an existing comic-capable books table before creating dependent indexes", () => { const dir = mkdtempSync(join(tmpdir(), "readabook-migration-")); tempDirs.push(dir); const databasePath = join(dir, "readabook.sqlite"); const storageDir = join(dir, "storage"); const legacy = new Database(databasePath); legacy.exec(` CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT NOT NULL UNIQUE, name TEXT, password_hash TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'user' CHECK (role IN ('admin','user')), created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE libraries ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, path TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE books ( id INTEGER PRIMARY KEY AUTOINCREMENT, library_id INTEGER NOT NULL REFERENCES libraries(id) ON DELETE CASCADE, title TEXT NOT NULL, author TEXT, description TEXT, isbn TEXT, language TEXT, publisher TEXT, published_date TEXT, format TEXT NOT NULL CHECK (format IN ('epub','pdf','cbz','cbr')), file_path TEXT NOT NULL UNIQUE, cover_path TEXT, file_size INTEGER NOT NULL, file_mtime TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE progress ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, book_id INTEGER NOT NULL REFERENCES books(id) ON DELETE CASCADE, locator TEXT NOT NULL, percent INTEGER NOT NULL CHECK (percent >= 0 AND percent <= 100), created_at TEXT NOT NULL, updated_at TEXT NOT NULL, UNIQUE(user_id, book_id) ); CREATE TABLE jobs ( id INTEGER PRIMARY KEY AUTOINCREMENT, type TEXT NOT NULL, status TEXT NOT NULL CHECK (status IN ('queued','running','succeeded','failed')), detail TEXT, error TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); `); legacy.close(); process.env.DATABASE_PATH = databasePath; process.env.STORAGE_DIR = storageDir; const database = new DatabaseService(); const bookColumns = database.sqlite.prepare("PRAGMA table_info(books)").all() as Array<{ name: string }>; const bookIndexes = database.sqlite.prepare("PRAGMA index_list(books)").all() as Array<{ name: string }>; const metadataSources = database.sqlite.prepare("SELECT provider FROM metadata_source_config ORDER BY priority").all() as Array<{ provider: string }>; const automationSettings = database.sqlite.prepare("SELECT id, isbn_priority_enabled FROM automation_settings").get() as | { id: number; isbn_priority_enabled: number } | undefined; expect(bookColumns.map((column) => column.name)).toContain("isbn13"); expect(bookColumns.map((column) => column.name)).toContain("identifiers_json"); expect(bookIndexes.map((index) => index.name)).toContain("books_isbn13_idx"); expect(metadataSources.map((source) => source.provider)).toEqual(["local", "openlibrary", "googlebooks", "bnf"]); expect(automationSettings).toMatchObject({ id: 1, isbn_priority_enabled: 1 }); database.onModuleDestroy(); }); });