forked from EduCraft/curriculum-project-hub
f8c3e16a52
A-4 FeishuEventReceipt: - 新增 FeishuEventReceipt 表(eventId @unique),lark ws 至少一次投递的幂等兜底 - MessageReceiveEvent 加 header.event_id 字段 - trigger 入口查重:已存在的 event_id → 跳过(不建第二次 run) - resetDb 加入 FeishuEventReceipt A-8 审计写入: - AuditEntry 加 projectId(权限拒绝/slash 命令等无 run 的审计点可定位) - 新建 src/audit.ts:writeAudit(prisma, entry) best-effort 写入(吞错,不阻断 trigger) - trigger 五个审计点:trigger.denied / trigger.role_denied / run.created / run.lock_race / run.finished / run.failed - 新增 audit unit 测试(3) + trigger 集成测试(去重 + 审计,2) 59 tests 全过。
74 lines
2.3 KiB
TypeScript
74 lines
2.3 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import type { PrismaClient } from "@prisma/client";
|
|
import { writeAudit } from "../../src/audit.js";
|
|
|
|
/// Minimal mock: only the surface `writeAudit` touches. Cast through unknown
|
|
/// so we don't have to implement the rest of the PrismaClient surface — this
|
|
/// is a unit test, not a DB test.
|
|
interface AuditEntryMock {
|
|
create: ReturnType<typeof vi.fn>;
|
|
}
|
|
function mockPrisma(): PrismaClient & { auditEntry: AuditEntryMock } {
|
|
return { auditEntry: { create: vi.fn() } } as unknown as PrismaClient & {
|
|
auditEntry: AuditEntryMock;
|
|
};
|
|
}
|
|
|
|
describe("writeAudit (A-8 audit write path)", () => {
|
|
it("creates a row with all fields set", async () => {
|
|
const prisma = mockPrisma();
|
|
prisma.auditEntry.create.mockResolvedValue({ id: "aud-1" });
|
|
|
|
await writeAudit(prisma, {
|
|
runId: "run-1",
|
|
projectId: "proj-1",
|
|
actorUserId: "user-1",
|
|
action: "run.triggered",
|
|
metadata: { source: "feishu" },
|
|
});
|
|
|
|
expect(prisma.auditEntry.create).toHaveBeenCalledTimes(1);
|
|
expect(prisma.auditEntry.create).toHaveBeenCalledWith({
|
|
data: {
|
|
runId: "run-1",
|
|
projectId: "proj-1",
|
|
actorUserId: "user-1",
|
|
action: "run.triggered",
|
|
metadata: { source: "feishu" },
|
|
},
|
|
});
|
|
});
|
|
|
|
it("creates a row with only required fields (action + metadata)", async () => {
|
|
const prisma = mockPrisma();
|
|
prisma.auditEntry.create.mockResolvedValue({ id: "aud-2" });
|
|
|
|
await writeAudit(prisma, {
|
|
action: "permission.denied",
|
|
metadata: { reason: "no-edit-role" },
|
|
});
|
|
|
|
expect(prisma.auditEntry.create).toHaveBeenCalledTimes(1);
|
|
expect(prisma.auditEntry.create).toHaveBeenCalledWith({
|
|
data: {
|
|
action: "permission.denied",
|
|
metadata: { reason: "no-edit-role" },
|
|
},
|
|
});
|
|
});
|
|
|
|
it("swallows a prisma error (best-effort, must not throw)", async () => {
|
|
const prisma = mockPrisma();
|
|
prisma.auditEntry.create.mockRejectedValue(new Error("db down"));
|
|
|
|
// Must not throw — audit is observability, not control.
|
|
await expect(
|
|
writeAudit(prisma, {
|
|
action: "run.triggered",
|
|
metadata: {},
|
|
}),
|
|
).resolves.toBeUndefined();
|
|
expect(prisma.auditEntry.create).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|