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 = ''; 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 = "1756700000"; 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", async () => { const inserts = []; const fakeDb = { prepare(sql) { return { bind(...params) { return { async run() { inserts.push({ sql, params }); return { success: true }; }, }; }, }; }, }; const ack = await handleWecomCallbackEvent(fakeDb, { 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"); assert.equal(inserts.length, 1); assert.match(inserts[0].sql, /INSERT INTO wecom_push_logs/); assert.equal(inserts[0].params[0].startsWith("wpl-"), true); assert.equal(inserts[0].params[1], "t123"); const view = await handleWecomCallbackEvent(fakeDb, { 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 = await handleWecomCallbackEvent(fakeDb, { MsgType: "event", Event: "change_contact", }); assert.equal(other.recognized, false); });