125 lines
6.1 KiB
TypeScript
125 lines
6.1 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
||
import { readFileSync } from "node:fs";
|
||
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();
|
||
|
||
expect(source).toContain("const canAccessAuditLogs = computed");
|
||
expect(source).toContain("isSystemAdmin");
|
||
expect(source).toContain("getProjectRole");
|
||
expect(source).toContain("study.currentStudyRole");
|
||
expect(source).toContain('projectRole.value === "PM"');
|
||
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 (!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");
|
||
|
||
const combined = `${auditSource}\n${overviewSource}`;
|
||
expect(combined).not.toContain("可行性评估");
|
||
expect(combined).not.toContain("伦理审批");
|
||
expect(combined).not.toContain("立项可行性");
|
||
expect(combined).not.toContain("立项伦理");
|
||
expect(auditSource).toContain('startup_feasibility: "立项记录"');
|
||
expect(auditSource).toContain('startup_ethics: "伦理记录"');
|
||
expect(overviewSource).toContain('label: "伦理记录"');
|
||
});
|
||
});
|