test(import): add legacy import compatibility coverage

This commit is contained in:
Zimeng Xiong
2026-02-06 14:54:02 -08:00
parent 94694deb91
commit 01fda32bcd
5 changed files with 520 additions and 35 deletions
@@ -0,0 +1,290 @@
import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest";
import request from "supertest";
import fs from "fs";
import path from "path";
import os from "os";
import { getTestPrisma, setupTestDb, cleanupTestDb } from "./testUtils";
type LegacyDbOptions = {
tableStyle: "prisma" | "plural-lower";
includeCollections: boolean;
includeMigrationsTable: boolean;
includeTrashDrawing: boolean;
};
const createTempDir = () => fs.mkdtempSync(path.join(os.tmpdir(), "excalidash-legacy-"));
const openWritableDb = (filePath: string): any => {
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { DatabaseSync } = require("node:sqlite") as any;
return new DatabaseSync(filePath, { enableForeignKeyConstraints: false });
} catch (_err) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Database = require("better-sqlite3") as any;
return new Database(filePath);
}
};
const createLegacySqliteDb = (opts: LegacyDbOptions): string => {
const dir = createTempDir();
const filePath = path.join(dir, "legacy-export.db");
const db = openWritableDb(filePath);
const tableDrawing = opts.tableStyle === "plural-lower" ? "drawings" : "Drawing";
const tableCollection = opts.tableStyle === "plural-lower" ? "collections" : "Collection";
try {
if (opts.includeCollections) {
db.exec(`
CREATE TABLE "${tableCollection}" (
id TEXT PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
createdAt TEXT,
updatedAt TEXT
);
`);
db.prepare(`INSERT INTO "${tableCollection}" (id, name, createdAt, updatedAt) VALUES (?, ?, ?, ?)`).run(
"legacy-collection-1",
"Legacy Collection",
new Date("2024-01-01T00:00:00.000Z").toISOString(),
new Date("2024-01-02T00:00:00.000Z").toISOString(),
);
}
db.exec(`
CREATE TABLE "${tableDrawing}" (
id TEXT PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
elements TEXT NOT NULL,
appState TEXT NOT NULL,
files TEXT,
preview TEXT,
version INTEGER,
collectionId TEXT,
collectionName TEXT,
createdAt TEXT,
updatedAt TEXT
);
`);
const now = new Date("2024-01-03T00:00:00.000Z").toISOString();
const insertDrawing = db.prepare(
`INSERT INTO "${tableDrawing}"
(id, name, elements, appState, files, preview, version, collectionId, collectionName, createdAt, updatedAt)
VALUES
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
);
insertDrawing.run(
"legacy-drawing-1",
"Legacy Drawing 1",
JSON.stringify([]),
JSON.stringify({}),
JSON.stringify({}),
null,
1,
opts.includeCollections ? "legacy-collection-1" : null,
opts.includeCollections ? "Legacy Collection" : null,
now,
now,
);
insertDrawing.run(
"legacy-drawing-2",
"Legacy Drawing 2 (unorganized)",
JSON.stringify([]),
JSON.stringify({}),
JSON.stringify({}),
null,
2,
null,
null,
now,
now,
);
if (opts.includeTrashDrawing) {
insertDrawing.run(
"legacy-drawing-trash",
"Legacy Trash Drawing",
JSON.stringify([]),
JSON.stringify({}),
JSON.stringify({}),
null,
1,
"trash",
"Trash",
now,
now,
);
}
if (opts.includeMigrationsTable) {
db.exec(`
CREATE TABLE "_prisma_migrations" (
id TEXT PRIMARY KEY NOT NULL,
checksum TEXT NOT NULL,
finished_at TEXT,
migration_name TEXT NOT NULL,
logs TEXT,
rolled_back_at TEXT,
started_at TEXT NOT NULL,
applied_steps_count INTEGER NOT NULL DEFAULT 0
);
`);
db.prepare(
`INSERT INTO "_prisma_migrations"
(id, checksum, finished_at, migration_name, logs, rolled_back_at, started_at, applied_steps_count)
VALUES
(?, ?, ?, ?, ?, ?, ?, ?)`
).run(
"m1",
"checksum",
new Date("2024-01-04T00:00:00.000Z").toISOString(),
"20240104000000_initial",
null,
null,
new Date("2024-01-04T00:00:00.000Z").toISOString(),
1,
);
}
} finally {
db.close();
}
return filePath;
};
describe("Import compatibility (legacy exports)", () => {
const uploadsDir = path.resolve(__dirname, "../../uploads");
const userAgent = "vitest-import-compat";
let prisma: ReturnType<typeof getTestPrisma>;
let app: any;
let csrfHeaderName: string;
let csrfToken: string;
beforeAll(async () => {
setupTestDb();
prisma = getTestPrisma();
fs.mkdirSync(uploadsDir, { recursive: true });
// Import the server AFTER DATABASE_URL is set by setupTestDb/getTestPrisma.
({ app } = await import("../index"));
const csrfRes = await request(app).get("/csrf-token").set("User-Agent", userAgent);
csrfHeaderName = csrfRes.body.header;
csrfToken = csrfRes.body.token;
expect(typeof csrfHeaderName).toBe("string");
expect(typeof csrfToken).toBe("string");
});
beforeEach(async () => {
await cleanupTestDb(prisma);
});
afterAll(async () => {
await prisma.$disconnect();
});
it("verifies a v0.1.xv0.3.2-style SQLite export (Drawing/Collection tables) and returns migration info when present", async () => {
const legacyDb = createLegacySqliteDb({
tableStyle: "prisma",
includeCollections: true,
includeMigrationsTable: true,
includeTrashDrawing: false,
});
const res = await request(app)
.post("/import/sqlite/legacy/verify")
.set("User-Agent", userAgent)
.set(csrfHeaderName, csrfToken)
.attach("db", legacyDb);
expect(res.status).toBe(200);
expect(res.body.valid).toBe(true);
expect(res.body.drawings).toBe(2);
expect(res.body.collections).toBe(1);
expect(res.body.latestMigration).toBe("20240104000000_initial");
expect(typeof res.body.currentLatestMigration === "string").toBe(true);
});
it("merge-imports a legacy SQLite export into the current account without replacing the database", async () => {
const legacyDb = createLegacySqliteDb({
tableStyle: "prisma",
includeCollections: true,
includeMigrationsTable: false,
includeTrashDrawing: true,
});
const res = await request(app)
.post("/import/sqlite/legacy")
.set("User-Agent", userAgent)
.set(csrfHeaderName, csrfToken)
.attach("db", legacyDb);
expect(res.status).toBe(200);
expect(res.body.success).toBe(true);
expect(res.body.collections?.created).toBeGreaterThanOrEqual(1);
expect(res.body.drawings?.created).toBeGreaterThanOrEqual(3);
const importedDrawings = await prisma.drawing.findMany({
orderBy: { name: "asc" },
select: { id: true, name: true, collectionId: true, userId: true },
});
// In single-user mode, imports land on the bootstrap acting user.
expect(importedDrawings.every((d) => d.userId === "bootstrap-admin")).toBe(true);
expect(importedDrawings.map((d) => d.id)).toEqual(
expect.arrayContaining(["legacy-drawing-1", "legacy-drawing-2", "legacy-drawing-trash"])
);
const trash = await prisma.collection.findUnique({ where: { id: "trash" } });
expect(trash).toBeTruthy();
});
it("supports older exports with plural/lowercase table names (drawings/collections)", async () => {
const legacyDb = createLegacySqliteDb({
tableStyle: "plural-lower",
includeCollections: true,
includeMigrationsTable: false,
includeTrashDrawing: false,
});
const verify = await request(app)
.post("/import/sqlite/legacy/verify")
.set("User-Agent", userAgent)
.set(csrfHeaderName, csrfToken)
.attach("db", legacyDb);
expect(verify.status).toBe(200);
expect(verify.body.drawings).toBe(2);
expect(verify.body.collections).toBe(1);
const res = await request(app)
.post("/import/sqlite/legacy")
.set("User-Agent", userAgent)
.set(csrfHeaderName, csrfToken)
.attach("db", legacyDb);
expect(res.status).toBe(200);
expect(res.body.success).toBe(true);
});
it("fails verification if the legacy DB is missing a Drawing table", async () => {
const dir = createTempDir();
const filePath = path.join(dir, "invalid.db");
const db = openWritableDb(filePath);
db.exec(`CREATE TABLE "NotDrawing" (id TEXT PRIMARY KEY NOT NULL);`);
db.close();
const res = await request(app)
.post("/import/sqlite/legacy/verify")
.set("User-Agent", userAgent)
.set(csrfHeaderName, csrfToken)
.attach("db", filePath);
expect(res.status).toBe(400);
expect(res.body.error).toBe("Invalid legacy DB");
});
});