feat: add TallyNote local reimbursement ledger
TallyNote release / linux-x64 (push) Failing after 2m41s
TallyNote release / linux-x64 (push) Failing after 2m41s
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
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" },
|
||||
]);
|
||||
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"]));
|
||||
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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user