test/scripts/database-snapshot-safety.test.ts
Rama main · réplica de lectura del repositorio original.
import type { Config } from "@libsql/client";
import { expect } from "@std/expect";
import { fromFileUrl, join } from "@std/path";
import { describe, it as test } from "@std/testing/bdd";
import { stub } from "@std/testing/mock";
import {
createDatabaseSnapshot,
type SnapshotClient,
type SnapshotClientFactory,
type SnapshotQueryResult,
type SnapshotRequest,
} from "#scripts/database-snapshot-lib.ts";
import { directoryNames, pathExists, withTempDir } from "#test-utils/files.ts";
const request = (outputPath: string): SnapshotRequest => ({
dbToken: "token",
dbUrl: "libsql://database.example.com",
outputPath,
});
const unusedExecute = (): Promise<never> =>
Promise.reject(new Error("execute should not be called"));
const unusedSync = (): Promise<never> =>
Promise.reject(new Error("sync should not be called"));
const replicaClient = (
sync: () => Promise<unknown>,
close: () => void = () => {},
): SnapshotClient => ({ close, execute: unusedExecute, sync });
const localClient = (
execute: SnapshotClient["execute"],
close: () => void = () => {},
): SnapshotClient => ({ close, execute, sync: unusedSync });
const rows = (values: Record<string, unknown>[]) =>
Promise.resolve({ rows: values });
const checkpointOk = { busy: 0, checkpointed: 0, log: 0 };
const integrityOk = { integrity_check: "ok" };
const fakeDatabaseFactory = (
onSync: (path: string) => void | Promise<void>,
localRows: Record<string, unknown>[][],
onLocalClose: (path: string) => void = () => {},
): SnapshotClientFactory => {
let localCall = 0;
return (config: Config): SnapshotClient => {
const path = fromFileUrl(config.url);
if (config.syncUrl) {
return replicaClient(async () => {
await onSync(path);
return {};
});
}
return localClient(
() => rows(localRows[localCall++] ?? []),
() => onLocalClose(path),
);
};
};
const expectExistingFilePreserved = async (
outputPath: string,
existingPath: string,
content: string,
factory: SnapshotClientFactory,
): Promise<void> => {
await expect(
createDatabaseSnapshot(request(outputPath), factory),
).rejects.toThrow(`Output already exists: ${existingPath}`);
expect(await Deno.readTextFile(existingPath)).toBe(content);
};
const assertAbortedSnapshot = async (
started: Promise<void>,
snapshot: Promise<unknown>,
controller: AbortController,
interruption: unknown,
isClosed: () => boolean,
dir: string,
): Promise<void> => {
await started;
controller.abort(interruption);
await expect(snapshot).rejects.toBe(interruption);
expect(isClosed()).toBe(true);
expect(await directoryNames(dir)).toEqual([]);
};
describe("database snapshot safety", () => {
test("stops a source sync when its signal is aborted", () =>
withTempDir(async (dir) => {
const controller = new AbortController();
const interruption = new Error("snapshot interrupted");
const syncStarted = Promise.withResolvers<void>();
const syncNeverFinishes = new Promise<unknown>(() => {});
let closed = false;
const snapshot = createDatabaseSnapshot(
request(join(dir, "snapshot.sqlite")),
() =>
replicaClient(
() => {
syncStarted.resolve();
return syncNeverFinishes;
},
() => {
closed = true;
},
),
undefined,
controller.signal,
);
await assertAbortedSnapshot(
syncStarted.promise,
snapshot,
controller,
interruption,
() => closed,
dir,
);
}));
test("stops verification when its signal is aborted", () =>
withTempDir(async (dir) => {
const controller = new AbortController();
const interruption = new Error("verification interrupted");
const checkpointStarted = Promise.withResolvers<void>();
const checkpointNeverFinishes = new Promise<SnapshotQueryResult>(
() => {},
);
let closed = false;
const factory: SnapshotClientFactory = (config) => {
const path = fromFileUrl(config.url);
if (config.syncUrl) {
return replicaClient(async () => {
await Deno.writeTextFile(path, "snapshot");
return {};
});
}
return localClient(
() => {
checkpointStarted.resolve();
return checkpointNeverFinishes;
},
() => {
closed = true;
},
);
};
const snapshot = createDatabaseSnapshot(
request(join(dir, "snapshot.sqlite")),
factory,
undefined,
controller.signal,
);
await assertAbortedSnapshot(
checkpointStarted.promise,
snapshot,
controller,
interruption,
() => closed,
dir,
);
}));
test("refuses an existing output before syncing", () =>
withTempDir(async (dir) => {
const outputPath = join(dir, "snapshot.sqlite");
await Deno.writeTextFile(outputPath, "keep me");
let clientCreated = false;
await expectExistingFilePreserved(
outputPath,
outputPath,
"keep me",
() => {
clientCreated = true;
return replicaClient(unusedSync);
},
);
expect(clientCreated).toBe(false);
}));
test("refuses existing SQLite sidecars before syncing", () =>
withTempDir(async (dir) => {
for (const suffix of ["-wal", "-shm"]) {
const outputPath = join(dir, `snapshot${suffix}.sqlite`);
const sidecarPath = `${outputPath}${suffix}`;
await Deno.writeTextFile(sidecarPath, "keep me");
let clientCreated = false;
await expectExistingFilePreserved(
outputPath,
sidecarPath,
"keep me",
() => {
clientCreated = true;
return replicaClient(unusedSync);
},
);
expect(clientCreated).toBe(false);
}
}));
test("rejects a missing output directory before syncing", () =>
withTempDir(async (dir) => {
const parentPath = join(dir, "missing");
await expect(
createDatabaseSnapshot(
request(join(parentPath, "snapshot.sqlite")),
() => replicaClient(unusedSync),
),
).rejects.toThrow(`Output directory does not exist: ${parentPath}`);
}));
test("rejects an output parent that is not a directory", () =>
withTempDir(async (dir) => {
const parentPath = join(dir, "file");
await Deno.writeTextFile(parentPath, "not a directory");
await expect(
createDatabaseSnapshot(
request(join(parentPath, "snapshot.sqlite")),
() => replicaClient(unusedSync),
),
).rejects.toThrow(`Output directory is not a directory: ${parentPath}`);
}));
test("surfaces output directory read errors", () =>
withTempDir(async (dir) => {
const readError = new Deno.errors.PermissionDenied("blocked");
using _stat = stub(Deno, "stat", () => Promise.reject(readError));
await expect(
createDatabaseSnapshot(request(join(dir, "snapshot.sqlite")), () =>
replicaClient(unusedSync),
),
).rejects.toBe(readError);
}));
test("does not overwrite an output created while syncing", () =>
withTempDir(async (dir) => {
const outputPath = join(dir, "snapshot.sqlite");
const factory = fakeDatabaseFactory(
async (path) => {
await Deno.writeTextFile(path, "new snapshot");
await Deno.writeTextFile(outputPath, "racing output");
},
[[checkpointOk], [integrityOk]],
);
await expectExistingFilePreserved(
outputPath,
outputPath,
"racing output",
factory,
);
expect(await directoryNames(dir)).toEqual(["snapshot.sqlite"]);
}));
test("does not publish beside a sidecar created while syncing", () =>
withTempDir(async (dir) => {
const outputPath = join(dir, "snapshot.sqlite");
const sidecarPath = `${outputPath}-wal`;
const factory = fakeDatabaseFactory(
async (path) => {
await Deno.writeTextFile(path, "new snapshot");
await Deno.writeTextFile(sidecarPath, "racing WAL");
},
[[checkpointOk], [integrityOk]],
);
await expectExistingFilePreserved(
outputPath,
sidecarPath,
"racing WAL",
factory,
);
expect(await pathExists(outputPath)).toBe(false);
}));
test("does not overwrite an output created during atomic publication", () =>
withTempDir(async (dir) => {
const outputPath = join(dir, "snapshot.sqlite");
const factory = fakeDatabaseFactory(
(path) => Deno.writeTextFile(path, "new snapshot"),
[[checkpointOk], [integrityOk]],
);
const link = Deno.link;
using _link = stub(Deno, "link", async (oldPath, newPath) => {
await Deno.writeTextFile(newPath, "racing output");
await link(oldPath, newPath);
});
await expectExistingFilePreserved(
outputPath,
outputPath,
"racing output",
factory,
);
expect(await directoryNames(dir)).toEqual(["snapshot.sqlite"]);
}));
test("closes the replica and cleans temporary files when sync fails", () =>
withTempDir(async (dir) => {
const outputPath = join(dir, "snapshot.sqlite");
const syncError = new Error("sync failed");
let closed = false;
const factory: SnapshotClientFactory = (config) =>
replicaClient(
async () => {
await Deno.writeTextFile(fromFileUrl(config.url), "partial");
throw syncError;
},
() => {
closed = true;
},
);
await expect(
createDatabaseSnapshot(request(outputPath), factory),
).rejects.toBe(syncError);
expect(closed).toBe(true);
expect(await directoryNames(dir)).toEqual([]);
}));
test("rejects a busy WAL checkpoint and cleans temporary files", () =>
withTempDir(async (dir) => {
const outputPath = join(dir, "snapshot.sqlite");
const factory = fakeDatabaseFactory(
(path) => Deno.writeTextFile(path, "partial"),
[[{ busy: 1, checkpointed: 2, log: 3 }]],
);
await expect(
createDatabaseSnapshot(request(outputPath), factory),
).rejects.toThrow("Database snapshot checkpoint did not empty the WAL");
expect(await directoryNames(dir)).toEqual([]);
}));
test("rejects an invalid integrity result and cleans temporary files", () =>
withTempDir(async (dir) => {
const outputPath = join(dir, "snapshot.sqlite");
const factory = fakeDatabaseFactory(
(path) => Deno.writeTextFile(path, "partial"),
[[checkpointOk], [{ integrity_check: "page 4 is corrupt" }]],
);
await expect(
createDatabaseSnapshot(request(outputPath), factory),
).rejects.toThrow("Database snapshot integrity check failed");
expect(await directoryNames(dir)).toEqual([]);
}));
test("rejects WAL frames left after checkpoint and cleans temporary files", () =>
withTempDir(async (dir) => {
const outputPath = join(dir, "snapshot.sqlite");
const factory = fakeDatabaseFactory(
(path) => Deno.writeTextFile(path, "partial"),
[[checkpointOk], [integrityOk]],
(path) => Deno.writeTextFileSync(`${path}-wal`, "remaining frame"),
);
await expect(
createDatabaseSnapshot(request(outputPath), factory),
).rejects.toThrow("Database snapshot WAL is not empty");
expect(await pathExists(outputPath)).toBe(false);
expect(await directoryNames(dir)).toEqual([]);
}));
test("surfaces atomic publication errors and cleans temporary files", () =>
withTempDir(async (dir) => {
const outputPath = join(dir, "snapshot.sqlite");
const linkError = new Deno.errors.PermissionDenied("links blocked");
const factory = fakeDatabaseFactory(
(path) => Deno.writeTextFile(path, "complete"),
[[checkpointOk], [integrityOk]],
);
using _link = stub(Deno, "link", () => Promise.reject(linkError));
await expect(
createDatabaseSnapshot(request(outputPath), factory),
).rejects.toBe(linkError);
expect(await pathExists(outputPath)).toBe(false);
expect(await directoryNames(dir)).toEqual([]);
}));
test("publishes the snapshot with owner-only permissions", () =>
withTempDir(async (dir) => {
const outputPath = join(dir, "snapshot.sqlite");
const factory = fakeDatabaseFactory(
(path) => Deno.writeTextFile(path, "complete"),
[[checkpointOk], [integrityOk]],
);
await createDatabaseSnapshot(request(outputPath), factory);
const mode = (await Deno.stat(outputPath)).mode;
if (mode === null) throw new Error("File mode is unavailable");
expect(mode & 0o777).toBe(0o600);
}));
test("reports each snapshot stage in order", () =>
withTempDir(async (dir) => {
const progress: string[] = [];
const factory = fakeDatabaseFactory(
(path) => Deno.writeTextFile(path, "complete"),
[[checkpointOk], [integrityOk]],
);
await createDatabaseSnapshot(
request(join(dir, "snapshot.sqlite")),
factory,
(message: string) => progress.push(message),
);
expect(progress).toEqual([
"[1/4] Checking destination",
"[2/4] Syncing remote database",
"[3/4] Checkpointing and checking integrity",
"[4/4] Publishing standalone SQLite file",
]);
}));
});