import assert from "node:assert/strict"; import test from "node:test"; import { clearWecomAccessTokenCacheForTests, resolveWecomConfig, sendWecomAppMessage, sendWecomRobotMessage, WecomClientError, } from "../lib/wecom-client.ts"; const baseBindings = { WECOM_CORP_ID: "corp-test", WECOM_AGENT_ID: "1000001", WECOM_SECRET: "secret-test", WECOM_ROBOT_WEBHOOK: "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=robot-test", WECOM_NOTIFY_DUE_DAYS: "3", }; function fakeWecom(options = {}) { const calls = []; const fetchImpl = async (input, init = {}) => { const url = new URL(String(input)); calls.push({ url, init }); if (url.pathname.endsWith("/cgi-bin/gettoken")) { return Response.json({ errcode: 0, errmsg: "ok", access_token: "token-test", expires_in: 7200, }); } if (url.pathname.endsWith("/cgi-bin/webhook/send")) { return Response.json({ errcode: options.robotErrcode ?? 0, errmsg: options.robotErrmsg ?? "ok", }); } if (url.pathname.endsWith("/cgi-bin/message/send")) { const body = JSON.parse(String(init.body ?? "{}")); const invalid = options.invalidUser ? body.touser.split("|").filter((u) => u === options.invalidUser) : []; return Response.json({ errcode: 0, errmsg: "ok", invaliduser: invalid.join("|"), }); } return new Response("not found", { status: 404 }); }; return { calls, fetchImpl }; } test.beforeEach(() => { clearWecomAccessTokenCacheForTests(); }); test("resolveWecomConfig applies dueDays and trims values", () => { const config = resolveWecomConfig({ WECOM_CORP_ID: " corp ", WECOM_AGENT_ID: " 10 ", WECOM_SECRET: " s ", WECOM_ROBOT_WEBHOOK: " https://hook ", WECOM_NOTIFY_DUE_DAYS: "5", }); assert.equal(config.corpId, "corp"); assert.equal(config.agentId, "10"); assert.equal(config.secret, "s"); assert.equal(config.robotWebhook, "https://hook"); assert.equal(config.dueDays, 5); }); test("resolveWecomConfig falls back to default dueDays for bad input", () => { assert.equal(resolveWecomConfig({ WECOM_NOTIFY_DUE_DAYS: "0" }).dueDays, 3); assert.equal(resolveWecomConfig({ WECOM_NOTIFY_DUE_DAYS: "abc" }).dueDays, 3); assert.equal( resolveWecomConfig({ WECOM_NOTIFY_DUE_DAYS: "100" }).dueDays, 30, ); }); test("sendWecomRobotMessage posts text payload and returns void", async () => { const { calls, fetchImpl } = fakeWecom(); await sendWecomRobotMessage( "hello", resolveWecomConfig(baseBindings), fetchImpl, ); const hookCall = calls.find((c) => c.url.pathname.endsWith("/cgi-bin/webhook/send"), ); assert.ok(hookCall, "robot webhook was called"); const body = JSON.parse(String(hookCall.init.body)); assert.equal(body.msgtype, "text"); assert.equal(body.text.content, "hello"); }); test("sendWecomRobotMessage skips silently when webhook is empty", async () => { const { calls, fetchImpl } = fakeWecom(); const config = resolveWecomConfig({ ...baseBindings, WECOM_ROBOT_WEBHOOK: "" }); await sendWecomRobotMessage("hello", config, fetchImpl); assert.equal( calls.filter((c) => c.url.pathname.endsWith("/cgi-bin/webhook/send"), ).length, 0, ); }); test("sendWecomRobotMessage throws WecomClientError on provider error", async () => { const { fetchImpl } = fakeWecom({ robotErrcode: 93000, robotErrmsg: "invalid webhook url", }); await assert.rejects( () => sendWecomRobotMessage( "hello", resolveWecomConfig(baseBindings), fetchImpl, ), (error) => { assert.ok(error instanceof WecomClientError); assert.equal(error.status, 502); return true; }, ); }); test("sendWecomAppMessage fetches access_token then sends to message/send", async () => { const { calls, fetchImpl } = fakeWecom(); const result = await sendWecomAppMessage( ["user-a", "user-b"], "催办内容", resolveWecomConfig(baseBindings), fetchImpl, ); assert.equal(result.sent, 2); assert.equal(result.failed, 0); assert.equal(result.skipped, false); const tokenCall = calls.find((c) => c.url.pathname.endsWith("/cgi-bin/gettoken"), ); assert.ok(tokenCall, "gettoken was called"); const sendCall = calls.find((c) => c.url.pathname.endsWith("/cgi-bin/message/send"), ); assert.ok(sendCall, "message/send was called"); assert.equal(sendCall.url.searchParams.get("access_token"), "token-test"); const body = JSON.parse(String(sendCall.init.body)); assert.equal(body.touser, "user-a|user-b"); assert.equal(body.agentid, 1000001); assert.equal(body.text.content, "催办内容"); }); test("sendWecomAppMessage skips when no external user ids", async () => { const { calls, fetchImpl } = fakeWecom(); const result = await sendWecomAppMessage( [], "催办", resolveWecomConfig(baseBindings), fetchImpl, ); assert.equal(result.skipped, true); assert.equal( calls.filter((c) => c.url.pathname.endsWith("/cgi-bin/message/send"), ).length, 0, ); }); test("sendWecomAppMessage skips when corp credentials are missing", async () => { const { calls, fetchImpl } = fakeWecom(); const result = await sendWecomAppMessage( ["user-a"], "催办", resolveWecomConfig({ ...baseBindings, WECOM_CORP_ID: "", WECOM_AGENT_ID: "", WECOM_SECRET: "", }), fetchImpl, ); assert.equal(result.skipped, true); assert.equal( calls.filter((c) => c.url.pathname.endsWith("/cgi-bin/gettoken"), ).length, 0, ); }); test("sendWecomAppMessage reports failed when provider marks invaliduser", async () => { const { fetchImpl } = fakeWecom({ invalidUser: "user-a" }); const result = await sendWecomAppMessage( ["user-a", "user-b"], "催办", resolveWecomConfig(baseBindings), fetchImpl, ); assert.equal(result.sent, 1); assert.equal(result.failed, 1); }); test("access_token is cached across calls", async () => { const { calls, fetchImpl } = fakeWecom(); const config = resolveWecomConfig(baseBindings); await sendWecomAppMessage(["u1"], "a", config, fetchImpl); await sendWecomAppMessage(["u2"], "b", config, fetchImpl); const tokenCalls = calls.filter((c) => c.url.pathname.endsWith("/cgi-bin/gettoken"), ); assert.equal(tokenCalls.length, 1, "access_token cached for second call"); });