WCBK1 实现企微审批回调只读 POC

这个提交包含在:
2026-05-19 15:00:22 +08:00
父节点 a32ec1e658
当前提交 8122898348
共修改 30 个文件,包含 1650 行新增和 53 行删除
@@ -0,0 +1,120 @@
const Fastify = require("fastify");
const wecomApprovalCallbackRoutes = require("../../../src/routes/wecomApprovalCallback");
const {
buildEncryptedApprovalEventSample,
buildUrlVerificationSample,
} = require("../../../src/services/integrations/wecomCallbackCrypto");
const TOKEN = "callback-token";
const AES_KEY = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG";
const CORP_ID = "ww-callback-corp";
async function buildApp({ config = {}, evidenceStore } = {}) {
const app = Fastify();
app.decorate("config", {
wecomCorpId: CORP_ID,
wecomCallbackToken: TOKEN,
wecomCallbackEncodingAesKey: AES_KEY,
...config,
});
await app.register(wecomApprovalCallbackRoutes, { prefix: "/api", evidenceStore });
return app;
}
function urlQuery(sample) {
return `msg_signature=${encodeURIComponent(sample.signature)}&timestamp=${encodeURIComponent(sample.timestamp)}&nonce=${encodeURIComponent(sample.nonce)}`;
}
describe("wecomApprovalCallback routes", () => {
it("verifies callback URL and returns decrypted echostr", async () => {
const app = await buildApp();
const sample = buildUrlVerificationSample({
token: TOKEN,
encodingAesKey: AES_KEY,
receiveId: CORP_ID,
timestamp: "1770000000",
nonce: "nonce",
plainText: "wcbk1-url-verify-ok",
});
const response = await app.inject({
method: "GET",
url: `/api/wecom/approval/callback?${urlQuery(sample)}&echostr=${encodeURIComponent(sample.encrypted)}`,
});
expect(response.statusCode).toBe(200);
expect(response.body).toBe("wcbk1-url-verify-ok");
await app.close();
});
it("rejects invalid URL verification signatures", async () => {
const app = await buildApp();
const sample = buildUrlVerificationSample({
token: TOKEN,
encodingAesKey: AES_KEY,
receiveId: CORP_ID,
timestamp: "1770000000",
nonce: "nonce",
plainText: "wcbk1-url-verify-ok",
});
const response = await app.inject({
method: "GET",
url: `/api/wecom/approval/callback?msg_signature=bad&timestamp=${sample.timestamp}&nonce=${sample.nonce}&echostr=${encodeURIComponent(sample.encrypted)}`,
});
expect(response.statusCode).toBe(403);
expect(response.body).toBe("invalid signature");
await app.close();
});
it("accepts approval callback POST and records only normalized evidence", async () => {
const evidenceStore = {
record: vi.fn().mockResolvedValue({ recorded: true, duplicate: false }),
};
const app = await buildApp({ evidenceStore });
const sample = buildEncryptedApprovalEventSample({
token: TOKEN,
encodingAesKey: AES_KEY,
receiveId: CORP_ID,
timestamp: "1770000000",
nonce: "nonce",
spNo: "SPFULLIDENTIFIERSHOULDNOTLEAK",
templateId: "TPLFULLIDENTIFIERSHOULDNOTLEAK",
});
const response = await app.inject({
method: "POST",
url: `/api/wecom/approval/callback?${urlQuery(sample)}`,
headers: { "content-type": "text/xml" },
payload: sample.encryptedXml,
});
expect(response.statusCode).toBe(200);
expect(response.body).toBe("success");
expect(evidenceStore.record).toHaveBeenCalledTimes(1);
const summary = evidenceStore.record.mock.calls[0][0];
expect(summary).toMatchObject({
eventType: "open_approval_change",
callbackKind: "approval_status_change",
approvalType: "leave",
statusLabel: "已通过",
});
expect(JSON.stringify(summary)).not.toContain("SPFULLIDENTIFIERSHOULDNOTLEAK");
expect(JSON.stringify(summary)).not.toContain("TPLFULLIDENTIFIERSHOULDNOTLEAK");
await app.close();
});
it("reports missing callback config without treating it as pass", async () => {
const app = await buildApp({ config: { wecomCallbackToken: "" } });
const response = await app.inject({
method: "GET",
url: "/api/wecom/approval/callback?msg_signature=x&timestamp=1&nonce=2&echostr=3",
});
expect(response.statusCode).toBe(503);
expect(response.body).toBe("callback config missing");
await app.close();
});
});