61 lines
3.6 KiB
TypeScript
61 lines
3.6 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import Database from "better-sqlite3";
|
|
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import { loadConfig, prepareDataDirectories } from "../server/config.js";
|
|
import { openDatabase } from "../server/db/index.js";
|
|
|
|
describe("数据库迁移", () => {
|
|
it("从 0000 旧库升级时保留记录并幂等应用新字段", () => {
|
|
const dataDir = mkdtempSync(path.join(tmpdir(), "tallynote-migration-"));
|
|
const previousDataDir = process.env.TALLYNOTE_DATA_DIR;
|
|
process.env.TALLYNOTE_DATA_DIR = dataDir;
|
|
process.env.TALLYNOTE_PUBLIC_ORIGIN = "http://127.0.0.1:3997";
|
|
process.env.TALLYNOTE_COOKIE_SECURE = "false";
|
|
let migrated: ReturnType<typeof openDatabase> | undefined;
|
|
try {
|
|
const config = loadConfig();
|
|
prepareDataDirectories(config);
|
|
const legacy = new Database(config.dbPath);
|
|
legacy.exec(readFileSync(path.join(config.migrationsDir, "0000_initial.sql"), "utf8"));
|
|
legacy.exec("CREATE TABLE schema_migrations (name TEXT PRIMARY KEY, applied_at INTEGER NOT NULL) STRICT");
|
|
legacy.prepare("INSERT INTO schema_migrations(name, applied_at) VALUES ('0000_initial.sql', ?)").run(Date.now());
|
|
legacy.prepare(`
|
|
INSERT INTO admins(id, username, username_norm, display_name, password_hash, status,
|
|
must_change_password, auth_version, version, created_at)
|
|
VALUES ('legacy-admin', 'legacy', 'legacy', '旧管理员', 'hash', 'active', 0, 1, 1, ?)
|
|
`).run(Date.now());
|
|
legacy.prepare(`
|
|
INSERT INTO expenses(id, paid_at, amount_cents, note, status, version, created_at, created_by,
|
|
updated_at, updated_by)
|
|
VALUES ('00000000-0000-4000-8000-000000000099', ?, 100, '旧账目', 'unreimbursed', 1, ?, 'legacy-admin', ?, 'legacy-admin')
|
|
`).run(Date.now(), Date.now(), Date.now());
|
|
legacy.close();
|
|
|
|
migrated = openDatabase(config);
|
|
const columns = migrated.sqlite.prepare("PRAGMA table_info(expenses)").all() as Array<{ name: string }>;
|
|
expect(columns.some((column) => column.name === "invoice_missing_reason")).toBe(true);
|
|
expect(migrated.sqlite.prepare("SELECT name FROM schema_migrations ORDER BY name").all()).toEqual([
|
|
{ name: "0000_initial.sql" },
|
|
{ name: "0001_invoice_missing_reason.sql" },
|
|
{ name: "0002_update_jobs.sql" },
|
|
{ name: "0003_update_job_ownership.sql" },
|
|
{ name: "0004_update_download_apply.sql" },
|
|
{ name: "0005_update_progress.sql" },
|
|
]);
|
|
const updateColumns = migrated.sqlite.prepare("PRAGMA table_info(update_jobs)").all() as Array<{ name: string }>;
|
|
expect(updateColumns.map((column) => column.name)).toEqual(expect.arrayContaining(["admin_id", "session_hash", "request_id", "requested_at", "started_at", "operation", "downloaded_bytes", "download_started_at", "download_speed_bps"]));
|
|
expect(migrated.sqlite.prepare("SELECT note, invoice_missing_reason AS reason FROM expenses WHERE id='00000000-0000-4000-8000-000000000099'").get()).toEqual({ note: "旧账目", reason: null });
|
|
migrated.sqlite.close();
|
|
migrated = openDatabase(config);
|
|
expect(migrated.sqlite.prepare("SELECT COUNT(*) AS count FROM schema_migrations WHERE name='0001_invoice_missing_reason.sql'").get()).toEqual({ count: 1 });
|
|
} finally {
|
|
migrated?.sqlite.close();
|
|
if (previousDataDir === undefined) delete process.env.TALLYNOTE_DATA_DIR;
|
|
else process.env.TALLYNOTE_DATA_DIR = previousDataDir;
|
|
rmSync(dataDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|