150 行
4.7 KiB
JavaScript
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);
|
|
});
|
|
});
|