Files
boss/tests/admin-backups-route.test.ts
2026-05-17 02:20:08 +08:00

181 lines
6.3 KiB
TypeScript

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 { NextRequest } from "next/server";
let runtimeRoot = "";
let data: typeof import("../src/lib/boss-data.ts");
let authCookie = "";
let getBackups: (typeof import("../src/app/api/v1/admin/backups/route.ts"))["GET"];
let postBackups: (typeof import("../src/app/api/v1/admin/backups/route.ts"))["POST"];
let baseState: Awaited<ReturnType<typeof import("../src/lib/boss-data.ts")["readState"]>>;
async function setup() {
if (runtimeRoot) return;
runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-admin-backups-"));
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
process.env.BOSS_STATE_BACKUP_DIR = path.join(runtimeRoot, "state-backups");
process.env.BOSS_STATE_AUTO_BACKUP_ENABLED = "1";
process.env.BOSS_STATE_AUTO_BACKUP_INTERVAL_MS = "0";
const [dataModule, authModule, routeModule] = await Promise.all([
import("../src/lib/boss-data.ts"),
import("../src/lib/boss-auth.ts"),
import("../src/app/api/v1/admin/backups/route.ts"),
]);
data = dataModule;
authCookie = authModule.AUTH_SESSION_COOKIE;
getBackups = routeModule.GET;
postBackups = routeModule.POST;
baseState = structuredClone(await data.readState());
}
test.after(async () => {
if (runtimeRoot) {
await rm(runtimeRoot, { recursive: true, force: true });
}
});
test.beforeEach(async () => {
await setup();
const state = structuredClone(baseState);
state.authAccounts = [
{
id: "account-owner",
account: "owner@boss.com",
passwordHash: "secret",
displayName: "平台管理员",
role: "highest_admin",
createdAt: "2026-05-16T10:00:00+08:00",
updatedAt: "2026-05-16T10:00:00+08:00",
},
{
id: "account-member",
account: "member@boss.com",
passwordHash: "secret",
displayName: "普通成员",
role: "member",
createdAt: "2026-05-16T10:00:00+08:00",
updatedAt: "2026-05-16T10:00:00+08:00",
},
];
state.projects = [
{
id: "project-before",
name: "备份前项目",
pinned: false,
deviceIds: [],
preview: "before",
updatedAt: "2026-05-16T10:00:00+08:00",
lastMessageAt: "2026-05-16T10:00:00+08:00",
isGroup: false,
threadMeta: {
projectId: "project-before",
threadId: "thread-before",
threadDisplayName: "备份前线程",
folderName: "before",
activityIconCount: 0,
updatedAt: "2026-05-16T10:00:00+08:00",
},
groupMembers: [],
createdByAgent: false,
collaborationMode: "development",
approvalState: "not_required",
unreadCount: 0,
riskLevel: "low",
messages: [],
goals: [],
versions: [],
},
];
state.authSessions = [];
await data.writeState(state);
});
async function requestFor(account: string, role: "member" | "admin" | "highest_admin", init: RequestInit = {}) {
const session = await data.createAuthSession({
account,
role,
displayName: account,
loginMethod: "password",
});
return new NextRequest("http://127.0.0.1:3000/api/v1/admin/backups", {
...init,
headers: {
"content-type": "application/json",
...(init.headers ?? {}),
cookie: `${authCookie}=${session.sessionToken}`,
},
});
}
test("admin backups require highest admin", async () => {
await setup();
const unauthenticated = await getBackups(new NextRequest("http://127.0.0.1:3000/api/v1/admin/backups"));
assert.equal(unauthenticated.status, 401);
const forbidden = await getBackups(await requestFor("member@boss.com", "member"));
assert.equal(forbidden.status, 403);
});
test("highest admin can create, list, and restore a state snapshot", async () => {
const createResponse = await postBackups(
await requestFor("owner@boss.com", "highest_admin", {
method: "POST",
body: JSON.stringify({ action: "create_snapshot", reason: "before risky operation" }),
}),
);
assert.equal(createResponse.status, 200);
const createPayload = await createResponse.json();
assert.equal(createPayload.ok, true);
assert.equal(createPayload.snapshot.reason, "before risky operation");
assert.equal(createPayload.snapshot.actorAccount, "owner@boss.com");
assert.match(createPayload.snapshot.snapshotId, /^state-snapshot-/);
const mutated = await data.readState();
mutated.projects[0]!.name = "误操作后的项目";
await data.writeState(mutated);
const listResponse = await getBackups(await requestFor("owner@boss.com", "highest_admin"));
assert.equal(listResponse.status, 200);
const listPayload = await listResponse.json();
assert.equal(listPayload.ok, true);
assert.equal(listPayload.status.restorePointCount >= 1, true);
assert.equal(listPayload.snapshots.some((snapshot: { snapshotId: string }) => snapshot.snapshotId === createPayload.snapshot.snapshotId), true);
const restoreResponse = await postBackups(
await requestFor("owner@boss.com", "highest_admin", {
method: "POST",
body: JSON.stringify({ action: "restore_snapshot", snapshotId: createPayload.snapshot.snapshotId }),
}),
);
assert.equal(restoreResponse.status, 200);
const restorePayload = await restoreResponse.json();
assert.equal(restorePayload.ok, true);
assert.equal(restorePayload.restored.snapshotId, createPayload.snapshot.snapshotId);
assert.match(restorePayload.preRestoreSnapshot.snapshotId, /^state-snapshot-/);
const restored = await data.readState();
assert.equal(restored.projects.find((project) => project.id === "project-before")?.name, "备份前项目");
});
test("state writes create automatic restore points", async () => {
const state = await data.readState();
state.projects[0]!.name = "自动备份触发项目";
await data.writeState(state);
const listResponse = await getBackups(await requestFor("owner@boss.com", "highest_admin"));
assert.equal(listResponse.status, 200);
const listPayload = await listResponse.json();
assert.equal(listPayload.ok, true);
assert.equal(
listPayload.snapshots.some((snapshot: { reason?: string; actorAccount?: string }) =>
snapshot.reason === "auto:writeState" && snapshot.actorAccount === "system",
),
true,
);
});