Files
TallyNote/tests/migration.test.ts
T
Qiufeng a268eb5fe9
TallyNote release / linux-x64 (push) Failing after 2m51s
feat: add staged release updates
2026-09-01 14:46:32 +08:00

60 lines
3.5 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" },
]);
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"]));
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 });
}
});
});