完善审计日志展示与导出权限

This commit is contained in:
Cheng Zhou
2026-06-10 10:20:04 +08:00
parent 1fef12cd86
commit ea3f19e241
33 changed files with 1862 additions and 240 deletions
+88 -1
View File
@@ -5,6 +5,29 @@ import { resolve } from "node:path";
const readAuditLogsView = () => readFileSync(resolve(__dirname, "./AuditLogs.vue"), "utf8");
describe("audit logs access", () => {
it("loads audit logs from the selected project context", () => {
const source = readAuditLogsView();
expect(source).toContain("const selectedStudy = ref<Study | null>(null)");
expect(source).toContain("const selectedStudyId = computed({");
expect(source).toContain("get: () => selectedStudy.value?.id || \"\"");
expect(source).toContain("set: (studyId: string) => {");
expect(source).toContain("await fetchAuditLogs(selectedStudyId.value, params)");
expect(source).toContain("await fetchApiEndpointPermissions(selectedStudyId.value)");
expect(source).toContain("await createAuditEvent(selectedStudyId.value,");
expect(source).not.toContain("await fetchAuditLogs(study.currentStudy.id, params)");
});
it("limits non-admin project choices to PM-owned projects", () => {
const source = readAuditLogsView();
expect(source).toContain('const availableItems = isAdmin.value ? items : items.filter((item) => item.role_in_study === "PM");');
expect(source).toContain("studies.value = availableItems;");
expect(source).toContain("availableItems.find((item) => item.id === queryProjectId)");
expect(source).toContain("availableItems.find((item) => item.id === study.currentStudy?.id)");
expect(source).toContain("availableItems[0]");
});
it("allows only admins or authorized PMs to use audit export", () => {
const source = readAuditLogsView();
@@ -16,11 +39,75 @@ describe("audit logs access", () => {
expect(source).toContain("isApiPermissionAllowed");
expect(source).toContain('permissionMatrix.value?.[projectRole.value]?.["audit_logs:read"]');
expect(source).toContain('permissionMatrix.value?.[projectRole.value]?.["audit_logs:export"]');
expect(source).toContain('if (scope === "project" && !canProjectExport.value) return;');
expect(source).toContain("if (!canProjectExport.value) return;");
expect(source).toContain('action: "AUDIT_EXPORT_PROJECT"');
expect(source).not.toContain('command="system"');
expect(source).not.toMatch(/auth\.user\?\.role/);
expect(source).not.toContain('!== "ADMIN"');
});
it("exports all matching audit log pages instead of a fixed first page", () => {
const source = readAuditLogsView();
expect(source).toContain("const fetchAuditLogPages = async");
expect(source).toContain("while (true)");
expect(source).toContain("if (items.length < batchLimit) break;");
expect(source).not.toContain("limit: 2000");
});
it("keeps the audit table compact by removing the duplicate content column", () => {
const source = readAuditLogsView();
expect(source).not.toContain('prop="actionText"');
expect(source).not.toContain("TEXT.modules.adminAuditLogs.columns.action");
expect(source).toContain("TEXT.modules.adminAuditLogs.columns.target");
expect(source).toContain("TEXT.modules.adminAuditLogs.columns.diff");
});
it("shows actor names without avatar icons in the audit table", () => {
const source = readAuditLogsView();
expect(source).toContain('<span class="actor-name">{{ scope.row.actorName }}</span>');
expect(source).not.toContain("actor-avatar");
expect(source).not.toContain("getInitials(scope.row.actorName)");
});
it("lets target and detail columns absorb remaining table width", () => {
const source = readAuditLogsView();
expect(source).toContain('<el-table-column :label="TEXT.modules.adminAuditLogs.columns.target" min-width="240">');
expect(source).toContain('<el-table-column :label="TEXT.modules.adminAuditLogs.columns.diff" min-width="360">');
expect(source).not.toContain(':label="TEXT.modules.adminAuditLogs.columns.target" width="240"');
expect(source).not.toContain(':label="TEXT.modules.adminAuditLogs.columns.diff" width="360"');
});
it("parses diff lines by the last field separator before the arrow", () => {
const source = readAuditLogsView();
expect(source).toMatch(/line\.match\(\/\^\(\.\+\)\[:\]\\s\*\(\.\*\?\)\\s\*->\\s\*\(\.\+\)\$\/\)/);
expect(source).not.toMatch(/line\.match\(\/\^\(\.\+\?\)\(\.\+\?\)\\s\*->\\s\*\(\.\+\)\$\/\)/);
});
it("renders detail object and action as two cards in one row", () => {
const source = readAuditLogsView();
expect(source).toContain('<span class="meta-label">对象</span>');
expect(source).toContain('<span class="meta-label">动作</span>');
expect(source).not.toContain("detail-meta-full");
expect(source).not.toContain("meta-card-full");
});
it("groups setup diff detail rows by business item path", () => {
const source = readAuditLogsView();
expect(source).toContain("const buildDetailDiffGroups = (lines: any[]): DetailDiffGroup[] => {");
expect(source).toContain('const groupedIndex = new Map<string, DetailDiffGroup>();');
expect(source).toContain('title: parts.slice(0, -1).join(" / "),');
expect(source).toContain("field: parts[parts.length - 1],");
expect(source).toContain('class="diff-group-title"');
expect(source).toContain("{{ selectedDiffGroups.length }} 项");
});
it("does not expose legacy startup ethics business labels", () => {
const auditSource = readFileSync(resolve(__dirname, "../../audit/index.ts"), "utf8");
const overviewSource = readFileSync(resolve(__dirname, "../ia/project-overview/overview.adapter.ts"), "utf8");