文件
hyb-oa/server/tests/unit/services/wecomCallbackCrypto.test.js
T

150 行
4.7 KiB
JavaScript

const fs = require("node:fs/promises");
const path = require("node:path");
const {
WecomCallbackError,
buildEncryptedApprovalEventSample,
buildUrlVerificationSample,
createWecomSignature,
decryptWecomMessage,
extractEncryptedFromXml,
maskWecomIdentifier,
normalizeApprovalCallbackEvent,
readCallbackEvidence,
recordCallbackEvidence,
verifyWecomSignature,
} = require("../../../src/services/integrations/wecomCallbackCrypto");
const TOKEN = "callback-token";
const AES_KEY = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG";
const CORP_ID = "ww-callback-corp";
const SENSITIVE_SP_NO = "SPFULLIDENTIFIERSHOULDNOTLEAK0001";
const SENSITIVE_TEMPLATE_ID = "TPLFULLIDENTIFIERSHOULDNOTLEAK0001";
describe("wecomCallbackCrypto", () => {
it("creates and verifies WeCom callback signatures", () => {
const signature = createWecomSignature({
token: TOKEN,
timestamp: "1770000000",
nonce: "nonce",
encrypted: "encrypted-payload",
});
expect(signature).toMatch(/^[0-9a-f]{40}$/);
expect(verifyWecomSignature({
token: TOKEN,
timestamp: "1770000000",
nonce: "nonce",
encrypted: "encrypted-payload",
signature,
})).toBe(true);
expect(verifyWecomSignature({
token: TOKEN,
timestamp: "1770000000",
nonce: "nonce",
encrypted: "other",
signature,
})).toBe(false);
});
it("decrypts URL verification echostr samples", () => {
const sample = buildUrlVerificationSample({
token: TOKEN,
encodingAesKey: AES_KEY,
receiveId: CORP_ID,
timestamp: "1770000000",
nonce: "nonce",
plainText: "wcbk1-url-verify-ok",
});
const decrypted = decryptWecomMessage({
encrypted: sample.encrypted,
encodingAesKey: AES_KEY,
receiveId: CORP_ID,
});
expect(sample.signature).toMatch(/^[0-9a-f]{40}$/);
expect(decrypted.plainXml).toBe("wcbk1-url-verify-ok");
expect(decrypted.receiveId).toBe(CORP_ID);
});
it("normalizes approval callback events without leaking raw identifiers", () => {
const sample = buildEncryptedApprovalEventSample({
token: TOKEN,
encodingAesKey: AES_KEY,
receiveId: CORP_ID,
timestamp: "1770000000",
nonce: "nonce",
spNo: SENSITIVE_SP_NO,
templateId: SENSITIVE_TEMPLATE_ID,
});
const encrypted = extractEncryptedFromXml(sample.encryptedXml);
const decrypted = decryptWecomMessage({
encrypted,
encodingAesKey: AES_KEY,
receiveId: CORP_ID,
});
const summary = normalizeApprovalCallbackEvent(decrypted.plainXml);
expect(summary).toMatchObject({
eventType: "open_approval_change",
msgType: "event",
callbackKind: "approval_status_change",
approvalType: "leave",
spStatus: "2",
statusLabel: "已通过",
spNoPrefix: maskWecomIdentifier(SENSITIVE_SP_NO),
templateIdPrefix: maskWecomIdentifier(SENSITIVE_TEMPLATE_ID),
});
expect(JSON.stringify(summary)).not.toContain(SENSITIVE_SP_NO);
expect(JSON.stringify(summary)).not.toContain(SENSITIVE_TEMPLATE_ID);
expect(summary.idempotencyKey).toHaveLength(32);
});
it("rejects mismatched receiveId", () => {
const sample = buildUrlVerificationSample({
token: TOKEN,
encodingAesKey: AES_KEY,
receiveId: CORP_ID,
timestamp: "1770000000",
nonce: "nonce",
plainText: "ok",
});
expect(() => decryptWecomMessage({
encrypted: sample.encrypted,
encodingAesKey: AES_KEY,
receiveId: "other-corp",
})).toThrow(WecomCallbackError);
});
it("records callback evidence idempotently and only stores redacted fields", async () => {
const evidenceFile = path.join("/tmp", `wcbk1-evidence-${Date.now()}-${Math.random()}.jsonl`);
const summary = {
eventType: "open_approval_change",
msgType: "event",
callbackKind: "approval_status_change",
approvalType: "leave",
spStatus: "2",
statusLabel: "已通过",
spNoPrefix: maskWecomIdentifier(SENSITIVE_SP_NO),
templateIdPrefix: maskWecomIdentifier(SENSITIVE_TEMPLATE_ID),
createTime: "1770000000",
applyTime: "1770000000",
agentIdPrefix: "1000***",
idempotencyKey: "idem-key",
};
const first = await recordCallbackEvidence(summary, { evidenceFile });
const second = await recordCallbackEvidence(summary, { evidenceFile });
const events = await readCallbackEvidence({ evidenceFile });
const raw = await fs.readFile(evidenceFile, "utf8");
expect(first.recorded).toBe(true);
expect(second.duplicate).toBe(true);
expect(events).toHaveLength(1);
expect(raw).not.toContain(SENSITIVE_SP_NO);
expect(raw).not.toContain(SENSITIVE_TEMPLATE_ID);
});
});