166 行
5.7 KiB
JavaScript
166 行
5.7 KiB
JavaScript
const {
|
|
executeArchiveCheck,
|
|
parseArgs,
|
|
sanitizeArchiveRow,
|
|
} = require("../../../src/scripts/check-wecom-approval-archive");
|
|
const { maskApprovalIdentifier } = require("../../../src/services/integrations/wecomApproval");
|
|
|
|
const SENSITIVE_SP_NO = "SPARCHIVEREADFULLIDENTIFIERSHOULDNOTLEAK";
|
|
const SENSITIVE_TEMPLATE_ID = "TPLARCHIVEREADFULLIDENTIFIERSHOULDNOTLEAK";
|
|
const SENSITIVE_USER_ID = "archive_read_userid_full_should_not_leak";
|
|
const SENSITIVE_REASON = "原因全文不得进入离线报告";
|
|
|
|
function archiveRow(overrides = {}) {
|
|
return {
|
|
id: "archive-1",
|
|
sp_no: SENSITIVE_SP_NO,
|
|
template_id: SENSITIVE_TEMPLATE_ID,
|
|
template_type: "leave",
|
|
normalized_status: "approved",
|
|
submitted_at: new Date("2026-05-19T01:00:00.000Z"),
|
|
completed_at: new Date("2026-05-19T02:00:00.000Z"),
|
|
status_observed_at: new Date("2026-05-19T02:00:00.000Z"),
|
|
last_source: "compensation_pull",
|
|
readability_status: "PASS_READABLE",
|
|
single_tap_approve_risk: true,
|
|
template_adjustment_required: false,
|
|
participants: [{
|
|
role: "applicant",
|
|
node_index: 0,
|
|
external_user_id: SENSITIVE_USER_ID,
|
|
mapping_status: "mapped_or_external",
|
|
is_current: false,
|
|
}],
|
|
actions: [{
|
|
action_type: "approve",
|
|
operator_external_user_id: "approver_full_userid_should_not_leak",
|
|
action_time: new Date("2026-05-19T02:00:00.000Z"),
|
|
node_index: 1,
|
|
source: "compensation_pull",
|
|
comment_summary: SENSITIVE_REASON,
|
|
}],
|
|
field_summaries: [{
|
|
field_name: "请假原因",
|
|
field_type: "Textarea",
|
|
sensitive_level: "restricted",
|
|
display_allowed: false,
|
|
normalized_value: SENSITIVE_REASON,
|
|
}],
|
|
reconcile_findings: [],
|
|
_count: {
|
|
participants: 1,
|
|
actions: 1,
|
|
field_summaries: 1,
|
|
attachment_summaries: 0,
|
|
callback_events: 1,
|
|
compensation_items: 1,
|
|
reconcile_findings: 0,
|
|
},
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("check-wecom-approval-archive", () => {
|
|
it("parses offline archive filters", () => {
|
|
const options = parseArgs([
|
|
"--offline",
|
|
"--format=json",
|
|
"--template=leave",
|
|
"--status=approved",
|
|
"--limit=50",
|
|
]);
|
|
|
|
expect(options).toMatchObject({
|
|
offline: true,
|
|
format: "json",
|
|
templates: ["leave"],
|
|
status: "approved",
|
|
limit: 50,
|
|
});
|
|
expect(parseArgs(["--template=outing", "--template=overtime"]).templates).toEqual(["outing", "overtime"]);
|
|
expect(() => parseArgs(["--template=unknown"])).toThrow(/Unsupported --template/);
|
|
expect(() => parseArgs(["--limit=101"])).toThrow(/--limit/);
|
|
});
|
|
|
|
it("sanitizes archive rows without leaking full identifiers or restricted values", () => {
|
|
const sanitized = sanitizeArchiveRow(archiveRow());
|
|
const serialized = JSON.stringify(sanitized);
|
|
|
|
expect(sanitized.spNoPrefix).toBe(maskApprovalIdentifier(SENSITIVE_SP_NO));
|
|
expect(sanitized.templateIdPrefix).toBe(maskApprovalIdentifier(SENSITIVE_TEMPLATE_ID));
|
|
expect(sanitized.participants[0].userIdPrefix).toBe(maskApprovalIdentifier(SENSITIVE_USER_ID));
|
|
expect(sanitized.fieldSummaries[0]).toMatchObject({
|
|
fieldName: null,
|
|
fieldType: "Textarea",
|
|
sensitiveLevel: "restricted",
|
|
displayAllowed: false,
|
|
});
|
|
expect(serialized).not.toContain(SENSITIVE_SP_NO);
|
|
expect(serialized).not.toContain(SENSITIVE_TEMPLATE_ID);
|
|
expect(serialized).not.toContain(SENSITIVE_USER_ID);
|
|
expect(serialized).not.toContain(SENSITIVE_REASON);
|
|
});
|
|
|
|
it("passes when OA DB archived history can be read offline", async () => {
|
|
const report = await executeArchiveCheck(parseArgs(["--offline", "--format=json"]), {
|
|
generatedAt: "2026-05-20T00:00:00.000Z",
|
|
prisma: {},
|
|
readArchiveRowsImpl: vi.fn(async () => [archiveRow()]),
|
|
});
|
|
|
|
expect(report.status).toBe("PASS_ARCHIVE_HISTORY_READABLE");
|
|
expect(report.scope.wecomApiCalls).toBe(false);
|
|
expect(report.checks.archiveRead.status).toBe("OK");
|
|
expect(report.checks.archiveRead.count).toBe(1);
|
|
});
|
|
|
|
it("returns PASS_WITH_FINDINGS when open reconcile findings exist", async () => {
|
|
const report = await executeArchiveCheck(parseArgs(["--offline"]), {
|
|
generatedAt: "2026-05-20T00:00:00.000Z",
|
|
prisma: {},
|
|
readArchiveRowsImpl: vi.fn(async () => [archiveRow({
|
|
reconcile_findings: [{
|
|
finding_type: "callback_missing_for_pulled_detail",
|
|
status: "open",
|
|
severity: "info",
|
|
source: "compensation_compare",
|
|
detected_at: new Date("2026-05-19T03:00:00.000Z"),
|
|
}],
|
|
})]),
|
|
});
|
|
|
|
expect(report.status).toBe("PASS_WITH_FINDINGS");
|
|
expect(report.checks.archives[0].openFindingCounts).toMatchObject({
|
|
callback_missing_for_pulled_detail: 1,
|
|
});
|
|
});
|
|
|
|
it("returns PARTIAL_ARCHIVE_NEEDS_DATA when no archive rows exist", async () => {
|
|
const report = await executeArchiveCheck(parseArgs(["--offline"]), {
|
|
generatedAt: "2026-05-20T00:00:00.000Z",
|
|
prisma: {},
|
|
readArchiveRowsImpl: vi.fn(async () => []),
|
|
});
|
|
|
|
expect(report.status).toBe("PARTIAL_ARCHIVE_NEEDS_DATA");
|
|
expect(report.configGaps.map((item) => item.id)).toContain("archive_history_data");
|
|
});
|
|
|
|
it("classifies DB read errors as external blockers", async () => {
|
|
const report = await executeArchiveCheck(parseArgs(["--offline"]), {
|
|
generatedAt: "2026-05-20T00:00:00.000Z",
|
|
prisma: {},
|
|
readArchiveRowsImpl: vi.fn(async () => {
|
|
throw new Error("database down with secret-like-value-should-be-redacted");
|
|
}),
|
|
});
|
|
|
|
expect(report.status).toBe("SKIP");
|
|
expect(report.conclusion).toBe("BLOCKED_BY_EXTERNAL_CONFIG");
|
|
expect(report.mainBlockers[0]).toMatchObject({
|
|
id: "archive_db_read",
|
|
reason: "oa_archive_db_read_failed",
|
|
});
|
|
});
|
|
});
|