mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
82 lines
3.3 KiB
TypeScript
82 lines
3.3 KiB
TypeScript
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
|||
|
|
import { tmpdir } from "node:os";
|
||
|
|
import { join } from "node:path";
|
||
|
|
import { describe, expect, it } from "vitest";
|
||
|
|
import {
|
||
|
|
analyzeSqlite,
|
||
|
|
countLibraryBlobs,
|
||
|
|
evaluateBootState,
|
||
|
|
resolveSource,
|
||
|
|
} from "../../../apps/api/src/db/sqlite-import.js";
|
||
|
|
import { buildLegacySqlite, seedRealistic1xData } from "../../helpers/legacy-sqlite-fixture.js";
|
||
|
|
|
||
|
|
describe("resolveSource", () => {
|
||
|
|
it("returns null for the 'off' sentinel", () => {
|
||
|
|
expect(resolveSource({ SQLITE_MIGRATE_PATH: "off", DATA_DIR: "/data" })).toBeNull();
|
||
|
|
expect(resolveSource({ SQLITE_MIGRATE_PATH: "OFF", DATA_DIR: "/data" })).toBeNull();
|
||
|
|
});
|
||
|
|
it("prefers an explicit path", () => {
|
||
|
|
expect(resolveSource({ SQLITE_MIGRATE_PATH: "/x/foo.db", DATA_DIR: "/data" })).toBe(
|
||
|
|
"/x/foo.db",
|
||
|
|
);
|
||
|
|
});
|
||
|
|
it("probes DATA_DIR/snapotter.db when unset", () => {
|
||
|
|
const dir = mkdtempSync(join(tmpdir(), "src-"));
|
||
|
|
writeFileSync(join(dir, "snapotter.db"), "x");
|
||
|
|
expect(resolveSource({ SQLITE_MIGRATE_PATH: "", DATA_DIR: dir })).toBe(
|
||
|
|
join(dir, "snapotter.db"),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
it("returns null when nothing is present", () => {
|
||
|
|
const dir = mkdtempSync(join(tmpdir(), "src-"));
|
||
|
|
expect(resolveSource({ SQLITE_MIGRATE_PATH: "", DATA_DIR: dir })).toBeNull();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("evaluateBootState", () => {
|
||
|
|
it("import when users empty, source present, no marker", () => {
|
||
|
|
expect(evaluateBootState({ usersCount: 0, source: "/a.db", marker: null })).toBe("import");
|
||
|
|
});
|
||
|
|
it("leftover when a completed marker exists", () => {
|
||
|
|
expect(
|
||
|
|
evaluateBootState({ usersCount: 5, source: "/a.db", marker: { status: "completed" } }),
|
||
|
|
).toBe("leftover");
|
||
|
|
});
|
||
|
|
it("locked when source present, users non-empty, no marker", () => {
|
||
|
|
expect(evaluateBootState({ usersCount: 1, source: "/a.db", marker: null })).toBe("locked");
|
||
|
|
});
|
||
|
|
it("none when no source", () => {
|
||
|
|
expect(evaluateBootState({ usersCount: 0, source: null, marker: null })).toBe("none");
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("countLibraryBlobs", () => {
|
||
|
|
it("counts present and missing library blobs", async () => {
|
||
|
|
const dir = mkdtempSync(join(tmpdir(), "blob-"));
|
||
|
|
const dbPath = join(dir, "legacy.db");
|
||
|
|
buildLegacySqlite(dbPath);
|
||
|
|
await seedRealistic1xData(dbPath); // seeds user_files uf-1 -> stored_name abc123.png
|
||
|
|
const filesDir = join(dir, "files");
|
||
|
|
mkdirSync(filesDir, { recursive: true });
|
||
|
|
writeFileSync(join(filesDir, "abc123.png"), "img");
|
||
|
|
expect(await countLibraryBlobs(dbPath, filesDir)).toEqual({ present: 1, missing: 0 });
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("analyzeSqlite", () => {
|
||
|
|
it("reports counts, blobs, and out-of-enum statuses without a live Postgres", async () => {
|
||
|
|
const dir = mkdtempSync(join(tmpdir(), "analyze-"));
|
||
|
|
const dbPath = join(dir, "legacy.db");
|
||
|
|
buildLegacySqlite(dbPath);
|
||
|
|
await seedRealistic1xData(dbPath); // 1 user, 2 jobs (one "error"), 1 user_file
|
||
|
|
const filesDir = join(dir, "files");
|
||
|
|
mkdirSync(filesDir, { recursive: true });
|
||
|
|
writeFileSync(join(filesDir, "abc123.png"), "img"); // the uf-1 blob
|
||
|
|
const report = await analyzeSqlite(dbPath, filesDir);
|
||
|
|
expect(report.tables.users).toBe(1);
|
||
|
|
expect(report.tables.jobs).toBe(2);
|
||
|
|
expect(report.blobs).toEqual({ present: 1, missing: 0 });
|
||
|
|
expect(report.badStatuses).toContain("error"); // will be mapped to "failed" on import
|
||
|
|
});
|
||
|
|
});
|