feat: 企微回调服务基建(加解密+事件分发,默认关闭)
This commit is contained in:
114
tests/wecom-callback-crypto.test.mjs
Normal file
114
tests/wecom-callback-crypto.test.mjs
Normal file
@@ -0,0 +1,114 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import test from "node:test";
|
||||
import {
|
||||
decryptWecomCallback,
|
||||
encryptWecomCallback,
|
||||
resolveWecomCallbackAesKey,
|
||||
verifyWecomCallbackSignature,
|
||||
wecomSignaturesMatch,
|
||||
WecomCallbackCryptoError,
|
||||
} from "../lib/wecom-callback-crypto.ts";
|
||||
import {
|
||||
extractWecomEncryptField,
|
||||
handleWecomCallbackEvent,
|
||||
parseWecomCallbackXml,
|
||||
} from "../lib/wecom-callback-events.ts";
|
||||
|
||||
const AES_KEY = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQ";
|
||||
const CORP_ID = "wwCallbackCorp01";
|
||||
const TOKEN = "callback-token-test";
|
||||
|
||||
test("aes key resolution accepts 43-char key and rejects invalid input", () => {
|
||||
const key = resolveWecomCallbackAesKey(AES_KEY);
|
||||
assert.equal(key.length, 32);
|
||||
assert.throws(
|
||||
() => resolveWecomCallbackAesKey("short"),
|
||||
WecomCallbackCryptoError,
|
||||
);
|
||||
assert.throws(
|
||||
() => resolveWecomCallbackAesKey("abcdefghij;klmnopqrstuvwxyzABCDEFGHIJKLMNOP"),
|
||||
WecomCallbackCryptoError,
|
||||
);
|
||||
});
|
||||
|
||||
test("encrypt → decrypt roundtrip preserves chinese message and receiveId", () => {
|
||||
const plain =
|
||||
'<xml><ToUserName><![CDATA[wwCallbackCorp01]]></ToUserName><Event><![CDATA[模板卡片]]></Event></xml>';
|
||||
const encrypted = encryptWecomCallback(plain, CORP_ID, AES_KEY);
|
||||
const decrypted = decryptWecomCallback(encrypted, AES_KEY);
|
||||
assert.equal(decrypted.message, plain);
|
||||
assert.equal(decrypted.receiveId, CORP_ID);
|
||||
});
|
||||
|
||||
test("decrypt rejects tampered ciphertext", () => {
|
||||
const encrypted = encryptWecomCallback("hello", CORP_ID, AES_KEY);
|
||||
const corrupted = `${encrypted.slice(0, -4)}AAAA`;
|
||||
assert.throws(
|
||||
() => decryptWecomCallback(corrupted, AES_KEY),
|
||||
WecomCallbackCryptoError,
|
||||
);
|
||||
});
|
||||
|
||||
test("decrypt rejects non-block-aligned ciphertext", () => {
|
||||
assert.throws(
|
||||
() => decryptWecomCallback("AAAAAAAA", AES_KEY),
|
||||
WecomCallbackCryptoError,
|
||||
);
|
||||
});
|
||||
|
||||
test("signature = sha1 of sorted join, order independent", () => {
|
||||
const encrypted = encryptWecomCallback("hello", CORP_ID, AES_KEY);
|
||||
const expected = createHash("sha1")
|
||||
.update([TOKEN, "1756700000", "nonce-abc", encrypted].sort().join(""))
|
||||
.digest("hex");
|
||||
const actual = verifyWecomCallbackSignature(
|
||||
TOKEN,
|
||||
"1756700000",
|
||||
"nonce-abc",
|
||||
encrypted,
|
||||
);
|
||||
assert.equal(actual, expected);
|
||||
assert.ok(wecomSignaturesMatch(actual, expected));
|
||||
assert.ok(!wecomSignaturesMatch(actual, `${expected}x`));
|
||||
assert.ok(!wecomSignaturesMatch(actual, ""));
|
||||
});
|
||||
|
||||
test("parse callback xml handles CDATA and plain fields", () => {
|
||||
const body =
|
||||
"<xml><ToUserName><![CDATA[wwCorp]]></ToUserName><Encrypt><![CDATA[RW5jcnlwdGVkQmFzZTY0PT0=]]></Encrypt><CreateTime>1756700000</CreateTime></xml>";
|
||||
const fields = parseWecomCallbackXml(body);
|
||||
assert.equal(fields.ToUserName, "wwCorp");
|
||||
assert.equal(fields.Encrypt, "RW5jcnlwdGVkQmFzZTY0PT0=");
|
||||
assert.equal(fields.CreateTime, "1756700000");
|
||||
assert.equal(extractWecomEncryptField(body), "RW5jcnlwdGVkQmFzZTY0PT0=");
|
||||
});
|
||||
|
||||
test("event dispatcher classifies template_card mark_handled and ignores others", () => {
|
||||
const ack = handleWecomCallbackEvent({
|
||||
MsgType: "event",
|
||||
Event: "template_card_event",
|
||||
TaskId: "task:t123:due",
|
||||
BtnKey: "mark_handled",
|
||||
FromUserName: "opsUser01",
|
||||
});
|
||||
assert.equal(ack.recognized, true);
|
||||
assert.equal(ack.action, "card_ack");
|
||||
assert.equal(ack.taskId, "task:t123:due");
|
||||
assert.equal(ack.operator, "opsUser01");
|
||||
|
||||
const view = handleWecomCallbackEvent({
|
||||
MsgType: "event",
|
||||
Event: "template_card_event",
|
||||
TaskId: "task:t123:due",
|
||||
BtnKey: "view_task",
|
||||
});
|
||||
assert.equal(view.recognized, true);
|
||||
assert.equal(view.action, "ignored");
|
||||
|
||||
const other = handleWecomCallbackEvent({
|
||||
MsgType: "event",
|
||||
Event: "change_contact",
|
||||
});
|
||||
assert.equal(other.recognized, false);
|
||||
});
|
||||
Reference in New Issue
Block a user