211 lines
7.9 KiB
JavaScript
211 lines
7.9 KiB
JavaScript
|
|
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)));
|
||
|
|
});
|