fix: harden backup verification before restore
This commit is contained in:
@@ -2,7 +2,7 @@ import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { mkdtemp, readFile, rm, unlink, writeFile } from "node:fs/promises";
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
let runtimeRoot = "";
|
||||
@@ -125,6 +125,7 @@ function assertBackupProjection(snapshot: {
|
||||
assert.equal(snapshot.verification?.ok, true);
|
||||
assert.equal(typeof snapshot.businessSummary?.projectCount, "number");
|
||||
assert.equal(typeof snapshot.businessSummary?.accountCount, "number");
|
||||
assert.equal("absolutePath" in snapshot, false);
|
||||
}
|
||||
|
||||
test("admin backups require highest admin", async () => {
|
||||
@@ -208,6 +209,64 @@ test("highest admin can verify a backup snapshot checksum", async () => {
|
||||
assert.equal(verifyPayload.verification.sha256, createPayload.snapshot.sha256);
|
||||
});
|
||||
|
||||
test("backup actions reject snapshots with missing or mismatched metadata", async () => {
|
||||
const createResponse = await postBackups(
|
||||
await requestFor("owner@boss.com", "highest_admin", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ action: "create_snapshot", reason: "tamper test" }),
|
||||
}),
|
||||
);
|
||||
assert.equal(createResponse.status, 200);
|
||||
const createPayload = await createResponse.json();
|
||||
const snapshotId = createPayload.snapshot.snapshotId as string;
|
||||
const snapshotPath = path.join(process.env.BOSS_STATE_BACKUP_DIR!, `${snapshotId}.json`);
|
||||
const metaPath = path.join(process.env.BOSS_STATE_BACKUP_DIR!, `${snapshotId}.meta.json`);
|
||||
|
||||
await unlink(metaPath);
|
||||
const missingMetaVerify = await postBackups(
|
||||
await requestFor("owner@boss.com", "highest_admin", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ action: "verify_snapshot", snapshotId }),
|
||||
}),
|
||||
);
|
||||
assert.equal(missingMetaVerify.status, 200);
|
||||
const missingMetaPayload = await missingMetaVerify.json();
|
||||
assert.equal(missingMetaPayload.verification.ok, false);
|
||||
assert.equal(missingMetaPayload.verification.message, "snapshot metadata is missing");
|
||||
|
||||
const missingMetaPreview = await postBackups(
|
||||
await requestFor("owner@boss.com", "highest_admin", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ action: "preview_restore", snapshotId }),
|
||||
}),
|
||||
);
|
||||
assert.equal(missingMetaPreview.status, 400);
|
||||
assert.equal((await missingMetaPreview.json()).message, "BACKUP_SNAPSHOT_VERIFICATION_FAILED");
|
||||
|
||||
const repaired = await postBackups(
|
||||
await requestFor("owner@boss.com", "highest_admin", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ action: "create_snapshot", reason: "tamper mismatch" }),
|
||||
}),
|
||||
);
|
||||
const repairedPayload = await repaired.json();
|
||||
const mismatchId = repairedPayload.snapshot.snapshotId as string;
|
||||
const mismatchPath = path.join(process.env.BOSS_STATE_BACKUP_DIR!, `${mismatchId}.json`);
|
||||
await writeFile(mismatchPath, (await readFile(mismatchPath, "utf8")).replace("备份前项目", "被篡改项目"), "utf8");
|
||||
|
||||
const mismatchRestore = await postBackups(
|
||||
await requestFor("owner@boss.com", "highest_admin", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ action: "restore_snapshot", snapshotId: mismatchId }),
|
||||
}),
|
||||
);
|
||||
assert.equal(mismatchRestore.status, 400);
|
||||
assert.equal((await mismatchRestore.json()).message, "BACKUP_SNAPSHOT_VERIFICATION_FAILED");
|
||||
|
||||
// Keep the local variable used so the test clearly documents both paths under the same backup dir.
|
||||
assert.match(snapshotPath, /state-snapshot-/);
|
||||
});
|
||||
|
||||
test("restore preview and dry run report impact without writing state", async () => {
|
||||
const createResponse = await postBackups(
|
||||
await requestFor("owner@boss.com", "highest_admin", {
|
||||
|
||||
Reference in New Issue
Block a user