forked from EduCraft/curriculum-project-hub
feat(filelib): 操作日志模块——防篡改哈希链、组合查询与 CSV 导出
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -163,16 +163,44 @@ describe("treeService · D17 breadcrumb", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("treeService · 审计落库(C3)", () => {
|
||||
it("创建/改名/移动/删除均写 AuditEntry", async () => {
|
||||
describe("treeService · 审计落库(ADR-0039)", () => {
|
||||
it("创建/改名/移动/删除均写 FileLibAuditLog", async () => {
|
||||
const root = await createNode(deps(), ADMIN, { parentId: null, kind: "FOLDER", name: "物理" });
|
||||
await renameNode(deps(), ADMIN, root.id, "物理学");
|
||||
await softDeleteNode(deps(), ADMIN, root.id);
|
||||
const actions = (await prisma.auditEntry.findMany({
|
||||
const rows = await prisma.fileLibAuditLog.findMany({
|
||||
where: { organizationId: DEFAULT_ORG_ID },
|
||||
select: { action: true },
|
||||
orderBy: { createdAt: "asc" },
|
||||
})).map((e) => e.action);
|
||||
expect(actions).toEqual(["folder.create", "folder.rename", "folder.delete"]);
|
||||
orderBy: { seq: "asc" },
|
||||
});
|
||||
expect(rows.map((e) => e.action)).toEqual(["folder.create", "folder.rename", "folder.delete"]);
|
||||
|
||||
// 改名要留下前后值 —— 这正是旧 metadata blob 做不到的事。
|
||||
const rename = rows[1]!;
|
||||
expect(rename.beforeValue).toMatchObject({ name: "物理" });
|
||||
expect(rename.afterValue).toMatchObject({ name: "物理学" });
|
||||
expect(rename.result).toBe("SUCCESS");
|
||||
expect(rename.actorUserId).toBe(ADMIN.userId);
|
||||
// 姓名快照与对象名称直接落列,查询无需回查 User/Node。
|
||||
expect(rename.actorName).not.toBe("");
|
||||
expect(rename.objectName).toBe("物理学");
|
||||
|
||||
// 哈希链:seq 连续,prevHash 串上一条。
|
||||
expect(rows.map((e) => e.seq)).toEqual([1n, 2n, 3n]);
|
||||
expect(rows[0]!.prevHash).toBeNull();
|
||||
expect(rows[1]!.prevHash).toBe(rows[0]!.entryHash);
|
||||
expect(rows[2]!.prevHash).toBe(rows[1]!.entryHash);
|
||||
});
|
||||
|
||||
it("日志只追加:UPDATE 与 DELETE 被数据库触发器拒绝", async () => {
|
||||
const node = await createNode(deps(), ADMIN, { parentId: null, kind: "FOLDER", name: "不可篡改" });
|
||||
const row = await prisma.fileLibAuditLog.findFirstOrThrow({
|
||||
where: { organizationId: DEFAULT_ORG_ID, objectId: node.id },
|
||||
});
|
||||
await expect(
|
||||
prisma.$executeRawUnsafe(`UPDATE "FileLibAuditLog" SET "action" = 'forged' WHERE "id" = $1`, row.id),
|
||||
).rejects.toThrow(/immutable/);
|
||||
await expect(
|
||||
prisma.$executeRawUnsafe(`DELETE FROM "FileLibAuditLog" WHERE "id" = $1`, row.id),
|
||||
).rejects.toThrow(/append-only/);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user