Files
koc-loop/tests/task-customer-chat-service.test.mjs

211 lines
7.9 KiB
JavaScript
Raw Normal View History

import assert from "node:assert/strict";
import test from "node:test";
import {
bindTaskCustomerChat,
unbindTaskCustomerChat,
} from "../lib/task-customer-chat-service.ts";
import { resolveWecomConfig } from "../lib/wecom-client.ts";
const baseBindings = {
WECOM_CORP_ID: "corp-test",
WECOM_AGENT_ID: "1000001",
WECOM_SECRET: "secret-test",
};
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/externalcontact/groupchat/add_join_way")) {
if (options.addErrcode) {
return Response.json({ errcode: options.addErrcode, errmsg: "add failed" });
}
return Response.json({ errcode: 0, errmsg: "ok", config_id: options.configId ?? "cfg-new" });
}
if (url.pathname.endsWith("/cgi-bin/externalcontact/groupchat/update_join_way")) {
if (options.updateErrcode) {
return Response.json({ errcode: options.updateErrcode, errmsg: "update failed" });
}
return Response.json({ errcode: 0, errmsg: "ok" });
}
if (url.pathname.endsWith("/cgi-bin/externalcontact/groupchat/get_join_way")) {
if (options.getErrcode) {
return Response.json({ errcode: options.getErrcode, errmsg: "get failed" });
}
return Response.json({
errcode: 0,
errmsg: "ok",
join_way: { qr_code: options.qrCode ?? "https://p.qpic.cn/qr-live.png" },
});
}
if (url.pathname.endsWith("/cgi-bin/externalcontact/groupchat/del_join_way")) {
if (options.delErrcode) {
return Response.json({ errcode: options.delErrcode, errmsg: "del failed" });
}
return Response.json({ errcode: 0, errmsg: "ok" });
}
return new Response("not found", { status: 404 });
};
return { calls, fetchImpl };
}
function captureDb(taskRow) {
const captured = [];
const makeStatement = (sql, values) => ({
async first() {
captured.push({ sql, values });
if (/FROM tasks WHERE id = \?/.test(sql)) return taskRow ?? null;
return null;
},
async run() {
captured.push({ sql, values });
return { changes: 1 };
},
});
const db = {
prepare(sql) {
const unbound = makeStatement(sql, []);
return {
bind(...values) {
return makeStatement(sql, values);
},
first: unbound.first,
run: unbound.run,
};
},
};
return { db, captured };
}
function callsTo(calls, suffix) {
return calls.filter((c) => c.url.pathname.endsWith(suffix));
}
test("bindTaskCustomerChat creates join way when task has no config", async () => {
const { db, captured } = captureDb({
id: "task-1",
name: "美团探店任务",
join_config_id: null,
});
const { calls, fetchImpl } = fakeWecom();
const result = await bindTaskCustomerChat(
db,
{ taskId: "task-1", chatId: "wr-normal-1", chatName: "KOC 一群" },
resolveWecomConfig(baseBindings),
fetchImpl,
);
assert.equal(callsTo(calls, "/cgi-bin/externalcontact/groupchat/add_join_way").length, 1);
assert.equal(callsTo(calls, "/cgi-bin/externalcontact/groupchat/update_join_way").length, 0);
const addBody = JSON.parse(String(callsTo(calls, "/add_join_way")[0].init.body));
assert.deepEqual(addBody.chat_id_list, ["wr-normal-1"]);
assert.equal(addBody.room_base_name, "美团探店任务");
assert.equal(addBody.state, "task-task-1");
assert.equal(result.joinConfigId, "cfg-new");
assert.equal(result.joinQrCode, "https://p.qpic.cn/qr-live.png");
assert.equal(result.qrError, undefined);
const update = captured.find((c) => /UPDATE tasks/.test(c.sql));
assert.ok(update, "tasks row updated");
assert.match(update.sql, /customer_chat_id = \?/);
assert.deepEqual(update.values, ["wr-normal-1", "KOC 一群", "cfg-new", "https://p.qpic.cn/qr-live.png", "task-1"]);
});
test("bindTaskCustomerChat reuses update_join_way on rebind and falls back to add", async () => {
const { db } = captureDb({ id: "task-1", name: "任务", join_config_id: "cfg-old" });
const { calls, fetchImpl } = fakeWecom();
const result = await bindTaskCustomerChat(
db,
{ taskId: "task-1", chatId: "wr-normal-2", chatName: "KOC 二群" },
resolveWecomConfig(baseBindings),
fetchImpl,
);
assert.equal(callsTo(calls, "/cgi-bin/externalcontact/groupchat/update_join_way").length, 1);
assert.equal(callsTo(calls, "/cgi-bin/externalcontact/groupchat/add_join_way").length, 0);
assert.equal(result.joinConfigId, "cfg-old", "config id preserved so qr stays valid");
const { db: db2 } = captureDb({ id: "task-1", name: "任务", join_config_id: "cfg-old" });
const { calls: calls2, fetchImpl: fetch2 } = fakeWecom({
updateErrcode: 40068,
configId: "cfg-rebuilt",
});
const result2 = await bindTaskCustomerChat(
db2,
{ taskId: "task-1", chatId: "wr-normal-2", chatName: "KOC 二群" },
resolveWecomConfig(baseBindings),
fetch2,
);
assert.equal(callsTo(calls2, "/cgi-bin/externalcontact/groupchat/update_join_way").length, 1);
assert.equal(callsTo(calls2, "/cgi-bin/externalcontact/groupchat/add_join_way").length, 1);
assert.equal(result2.joinConfigId, "cfg-rebuilt");
});
test("bindTaskCustomerChat tolerates qr fetch failure and records qrError", async () => {
const { db } = captureDb({ id: "task-1", name: "任务", join_config_id: null });
const { fetchImpl } = fakeWecom({ getErrcode: 40068 });
const result = await bindTaskCustomerChat(
db,
{ taskId: "task-1", chatId: "wr-normal-1", chatName: "KOC 一群" },
resolveWecomConfig(baseBindings),
fetchImpl,
);
assert.equal(result.joinQrCode, "");
assert.match(result.qrError, /活码二维码获取失败/);
});
test("bindTaskCustomerChat validates missing task / chat", async () => {
const { db: missingTask } = captureDb(null);
await assert.rejects(
() =>
bindTaskCustomerChat(
missingTask,
{ taskId: "task-x", chatId: "wr-1", chatName: "群" },
resolveWecomConfig(baseBindings),
fakeWecom().fetchImpl,
),
/任务不存在/,
);
const { db } = captureDb({ id: "task-1", name: "任务", join_config_id: null });
await assert.rejects(
() =>
bindTaskCustomerChat(
db,
{ taskId: "task-1", chatId: "", chatName: "" },
resolveWecomConfig(baseBindings),
fakeWecom().fetchImpl,
),
/请选择要绑定的客户群/,
);
});
test("unbindTaskCustomerChat deletes join way (tolerating failure) and clears columns", async () => {
const { db, captured } = captureDb({ id: "task-1", join_config_id: "cfg-old" });
const { calls, fetchImpl } = fakeWecom();
await unbindTaskCustomerChat(db, "task-1", resolveWecomConfig(baseBindings), fetchImpl);
assert.equal(callsTo(calls, "/cgi-bin/externalcontact/groupchat/del_join_way").length, 1);
const update = captured.find((c) => /UPDATE tasks/.test(c.sql));
assert.ok(update);
assert.match(update.sql, /customer_chat_id = ''/);
const { db: db2, captured: captured2 } = captureDb({ id: "task-1", join_config_id: "cfg-old" });
const { fetchImpl: fetch2 } = fakeWecom({ delErrcode: 40068 });
await unbindTaskCustomerChat(db2, "task-1", resolveWecomConfig(baseBindings), fetch2);
assert.ok(
captured2.some((c) => /UPDATE tasks/.test(c.sql)),
"local columns still cleared when del_join_way fails",
);
const { db: db3, captured: captured3 } = captureDb({ id: "task-1", join_config_id: "" });
const { calls: calls3, fetchImpl: fetch3 } = fakeWecom();
await unbindTaskCustomerChat(db3, "task-1", resolveWecomConfig(baseBindings), fetch3);
assert.equal(calls3.length, 0, "no wecom calls when nothing bound");
assert.ok(captured3.some((c) => /UPDATE tasks/.test(c.sql)));
});