fix: harden backup verification before restore

This commit is contained in:
AI Bot
2026-06-06 19:21:02 +08:00
parent 58cc4a1a5a
commit cfd41b4fbf
2 changed files with 90 additions and 12 deletions

View File

@@ -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", {