251 lines
7.5 KiB
JavaScript
251 lines
7.5 KiB
JavaScript
|
|
import assert from "node:assert/strict";
|
||
|
|
import test from "node:test";
|
||
|
|
import {
|
||
|
|
buildKocDirectMessage,
|
||
|
|
oneClickPushTask,
|
||
|
|
OneClickPushError,
|
||
|
|
} from "../lib/task-one-click-push-service.ts";
|
||
|
|
import {
|
||
|
|
clearWecomAccessTokenCacheForTests,
|
||
|
|
resolveWecomConfig,
|
||
|
|
} from "../lib/wecom-client.ts";
|
||
|
|
|
||
|
|
const baseBindings = {
|
||
|
|
WECOM_CORP_ID: "corp-test",
|
||
|
|
WECOM_AGENT_ID: "1000001",
|
||
|
|
WECOM_SECRET: "secret-test",
|
||
|
|
};
|
||
|
|
|
||
|
|
const taskRow = {
|
||
|
|
id: "task-1",
|
||
|
|
name: "测试任务",
|
||
|
|
brand: "品牌A",
|
||
|
|
quantity: 3,
|
||
|
|
due_at: "2026-09-05",
|
||
|
|
task_type: "content_publish",
|
||
|
|
platform: "小红书",
|
||
|
|
content_format: "image_text",
|
||
|
|
share_token: "tok-123",
|
||
|
|
project_id: "",
|
||
|
|
};
|
||
|
|
|
||
|
|
const boundPartners = [
|
||
|
|
{ id: "p-1", name: "合作方甲", wecom_external_user_id: "wmOK1" },
|
||
|
|
{ id: "p-2", name: "合作方乙", wecom_external_user_id: "wmOK2" },
|
||
|
|
{ id: "p-3", name: "合作方丙", wecom_external_user_id: "wmFAIL" },
|
||
|
|
];
|
||
|
|
|
||
|
|
function fakePushDb({
|
||
|
|
task = taskRow,
|
||
|
|
partners = boundPartners,
|
||
|
|
todayLog = null,
|
||
|
|
} = {}) {
|
||
|
|
const executed = [];
|
||
|
|
const makeStatement = (sql, params = []) => ({
|
||
|
|
sql,
|
||
|
|
params,
|
||
|
|
bind(...args) {
|
||
|
|
return makeStatement(sql, args);
|
||
|
|
},
|
||
|
|
async run() {
|
||
|
|
executed.push({ sql, params });
|
||
|
|
return { meta: { changes: 1 } };
|
||
|
|
},
|
||
|
|
async first() {
|
||
|
|
executed.push({ sql, params });
|
||
|
|
if (/FROM tasks WHERE id = \?/.test(sql)) return task;
|
||
|
|
if (/FROM wecom_push_logs/.test(sql) && /DATE\(created_at\) = CURDATE\(\)/.test(sql)) {
|
||
|
|
return todayLog;
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
},
|
||
|
|
async all() {
|
||
|
|
executed.push({ sql, params });
|
||
|
|
if (/FROM partners/.test(sql) && /wecom_external_user_id/.test(sql)) {
|
||
|
|
return { results: partners };
|
||
|
|
}
|
||
|
|
return { results: [] };
|
||
|
|
},
|
||
|
|
});
|
||
|
|
return {
|
||
|
|
executed,
|
||
|
|
prepare(sql) {
|
||
|
|
return makeStatement(sql);
|
||
|
|
},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function fakeWecomFetch(calls, { invalidUsers = [] } = {}) {
|
||
|
|
return 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/message/send")) {
|
||
|
|
const body = JSON.parse(String(init.body));
|
||
|
|
if (invalidUsers.includes(body.touser)) {
|
||
|
|
return Response.json({
|
||
|
|
errcode: 0,
|
||
|
|
errmsg: "ok",
|
||
|
|
invaliduser: body.touser,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
return Response.json({ errcode: 0, errmsg: "ok" });
|
||
|
|
}
|
||
|
|
return new Response("not found", { status: 404 });
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
test.beforeEach(() => {
|
||
|
|
clearWecomAccessTokenCacheForTests();
|
||
|
|
});
|
||
|
|
|
||
|
|
test("buildKocDirectMessage targets KOC with claim url and OAuth hint", () => {
|
||
|
|
const message = buildKocDirectMessage(taskRow, "https://portal.example.com/");
|
||
|
|
assert.match(message, /【新任务】测试任务/);
|
||
|
|
assert.match(message, /先到先得/);
|
||
|
|
assert.match(message, /免输身份/);
|
||
|
|
assert.match(message, /https:\/\/portal\.example\.com\/\?task=tok-123/);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("oneClickPushTask sends only to selected partners and logs the push", async () => {
|
||
|
|
process.env.KOC_PORTAL_URL = "https://portal.example.com";
|
||
|
|
try {
|
||
|
|
const db = fakePushDb();
|
||
|
|
const calls = [];
|
||
|
|
const outcome = await oneClickPushTask(
|
||
|
|
db,
|
||
|
|
{
|
||
|
|
taskId: "task-1",
|
||
|
|
sendMainGroup: false,
|
||
|
|
sendKoc: true,
|
||
|
|
createGroupTemplate: false,
|
||
|
|
selectedKocPartnerIds: ["p-1", "p-3"],
|
||
|
|
},
|
||
|
|
resolveWecomConfig(baseBindings),
|
||
|
|
fakeWecomFetch(calls, { invalidUsers: ["wmFAIL"] }),
|
||
|
|
);
|
||
|
|
const appCalls = calls.filter((call) =>
|
||
|
|
call.url.pathname.endsWith("/cgi-bin/message/send"),
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
appCalls.map((call) => JSON.parse(String(call.init.body)).touser),
|
||
|
|
["wmOK1", "wmFAIL"],
|
||
|
|
);
|
||
|
|
for (const call of appCalls) {
|
||
|
|
assert.match(JSON.parse(String(call.init.body)).text.content, /【新任务】测试任务/);
|
||
|
|
}
|
||
|
|
assert.equal(outcome.kocSent, 1);
|
||
|
|
assert.equal(outcome.kocFailed, 1);
|
||
|
|
assert.equal(outcome.kocResults[1].partnerName, "合作方丙");
|
||
|
|
assert.match(outcome.kocResults[1].error, /invaliduser/);
|
||
|
|
assert.equal(outcome.linkUrl, "https://portal.example.com/?task=tok-123");
|
||
|
|
|
||
|
|
const insert = db.executed.find((item) => /INSERT INTO wecom_push_logs/.test(item.sql));
|
||
|
|
assert.ok(insert, "push log should be written");
|
||
|
|
assert.equal(insert.params[1], "task-1");
|
||
|
|
assert.equal(insert.params[3], "task_open");
|
||
|
|
assert.equal(insert.params[6], 1);
|
||
|
|
const kocUserids = JSON.parse(insert.params[5]);
|
||
|
|
assert.equal(kocUserids.length, 2);
|
||
|
|
assert.equal(kocUserids[0].externalUserId, "wmOK1");
|
||
|
|
} finally {
|
||
|
|
delete process.env.KOC_PORTAL_URL;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test("oneClickPushTask blocks duplicate same-day push unless forced", async () => {
|
||
|
|
process.env.KOC_PORTAL_URL = "https://portal.example.com";
|
||
|
|
try {
|
||
|
|
const db = fakePushDb({
|
||
|
|
todayLog: { id: "wpl-old", task_id: "task-1", push_kind: "task_open" },
|
||
|
|
});
|
||
|
|
const calls = [];
|
||
|
|
await assert.rejects(
|
||
|
|
() =>
|
||
|
|
oneClickPushTask(
|
||
|
|
db,
|
||
|
|
{ taskId: "task-1", sendMainGroup: false, sendKoc: true },
|
||
|
|
resolveWecomConfig(baseBindings),
|
||
|
|
fakeWecomFetch(calls),
|
||
|
|
),
|
||
|
|
(error) =>
|
||
|
|
error instanceof OneClickPushError && /仍要推送/.test(error.message) && error.status === 409,
|
||
|
|
);
|
||
|
|
assert.equal(calls.length, 0, "no wecom api calls on duplicate block");
|
||
|
|
|
||
|
|
const outcome = await oneClickPushTask(
|
||
|
|
db,
|
||
|
|
{
|
||
|
|
taskId: "task-1",
|
||
|
|
sendMainGroup: false,
|
||
|
|
sendKoc: true,
|
||
|
|
selectedKocPartnerIds: ["p-2"],
|
||
|
|
force: true,
|
||
|
|
},
|
||
|
|
resolveWecomConfig(baseBindings),
|
||
|
|
fakeWecomFetch(calls),
|
||
|
|
);
|
||
|
|
assert.equal(outcome.kocSent, 1);
|
||
|
|
} finally {
|
||
|
|
delete process.env.KOC_PORTAL_URL;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test("oneClickPushTask without project skips project targets and still reaches KOC", async () => {
|
||
|
|
process.env.KOC_PORTAL_URL = "https://portal.example.com";
|
||
|
|
try {
|
||
|
|
const db = fakePushDb({ task: { ...taskRow, project_id: "" } });
|
||
|
|
const calls = [];
|
||
|
|
const outcome = await oneClickPushTask(
|
||
|
|
db,
|
||
|
|
{ taskId: "task-1", sendMainGroup: true, sendKoc: true },
|
||
|
|
resolveWecomConfig(baseBindings),
|
||
|
|
fakeWecomFetch(calls),
|
||
|
|
);
|
||
|
|
assert.ok(
|
||
|
|
outcome.skippedReasons.some((reason) => /未挂项目/.test(reason)),
|
||
|
|
"should explain project skip",
|
||
|
|
);
|
||
|
|
assert.equal(outcome.projectOutcome, null);
|
||
|
|
assert.equal(outcome.kocSent, 3);
|
||
|
|
assert.equal(
|
||
|
|
calls.filter((call) => call.url.pathname.includes("/cgi-bin/externalcontact/")).length,
|
||
|
|
0,
|
||
|
|
"no group template calls expected",
|
||
|
|
);
|
||
|
|
} finally {
|
||
|
|
delete process.env.KOC_PORTAL_URL;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test("oneClickPushTask requires share token and at least one target", async () => {
|
||
|
|
await assert.rejects(
|
||
|
|
() =>
|
||
|
|
oneClickPushTask(
|
||
|
|
fakePushDb({ task: { ...taskRow, share_token: "" } }),
|
||
|
|
{ taskId: "task-1", sendMainGroup: false, sendKoc: true },
|
||
|
|
resolveWecomConfig(baseBindings),
|
||
|
|
fakeWecomFetch([]),
|
||
|
|
),
|
||
|
|
(error) => error instanceof OneClickPushError && /领取链接/.test(error.message),
|
||
|
|
);
|
||
|
|
|
||
|
|
await assert.rejects(
|
||
|
|
() =>
|
||
|
|
oneClickPushTask(
|
||
|
|
fakePushDb(),
|
||
|
|
{ taskId: "task-1", sendMainGroup: false, sendKoc: false, createGroupTemplate: false },
|
||
|
|
resolveWecomConfig(baseBindings),
|
||
|
|
fakeWecomFetch([]),
|
||
|
|
),
|
||
|
|
(error) => error instanceof OneClickPushError && /至少选择一个推送目标/.test(error.message),
|
||
|
|
);
|
||
|
|
});
|