Files
koc-loop/tests/project-service.test.mjs
ABAPPLO a6f5a5fd4a feat: 新增项目中心——按项目组织任务/任务群/总群并一键推送
- 新表 projects / project_group_chats,partners 加 partner_type/wecom_userid,tasks 加 project_id(迁移 0010)
- 项目 CRUD、总群成员配置、任务群人工映射(群主→合作方)、任务挂项目
- 推送:总群成员逐人应用消息 + 任务群按群主企业群发,结果分项回显
2026-08-31 16:35:17 +08:00

379 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import test from "node:test";
import {
deleteProject,
getProjectDetail,
listProjects,
parseMemberIds,
ProjectError,
saveGroupOwnerPartner,
saveProject,
setProjectGroupChats,
} from "../lib/project-service.ts";
function fakeProjectDb({
projects = [],
links = [],
tasks = [],
groupChats = [],
partners = [],
} = {}) {
const executed = [];
const applyWrite = (sql, params) => {
const normalized = sql.trim();
if (/^INSERT INTO projects/.test(normalized)) {
projects.push({
id: params[0],
name: params[1],
brand: params[2],
main_group_member_ids: params[3],
status: "active",
note: params[4],
created_at: params[5],
});
return 1;
}
if (/^UPDATE projects/.test(normalized)) {
const row = projects.find((item) => item.id === params[4]);
if (!row) return 0;
Object.assign(row, {
name: params[0],
brand: params[1],
main_group_member_ids: params[2],
note: params[3],
});
return 1;
}
if (/^DELETE FROM projects/.test(normalized)) {
const index = projects.findIndex((item) => item.id === params[0]);
if (index < 0) return 0;
projects.splice(index, 1);
return 1;
}
if (/^INSERT INTO project_group_chats/.test(normalized)) {
links.push({ project_id: params[1], chat_id: params[2], partner_id: params[3] });
return 1;
}
if (/^DELETE FROM project_group_chats/.test(normalized)) {
for (let index = links.length - 1; index >= 0; index -= 1) {
if (links[index].project_id === params[0]) links.splice(index, 1);
}
return 1;
}
if (/^UPDATE tasks SET project_id = ''/.test(normalized)) {
for (const task of tasks) {
if (task.project_id === params[0]) task.project_id = "";
}
return 1;
}
if (/^INSERT INTO partners/.test(normalized)) {
partners.push({
id: params[0],
name: params[1],
wecom_userid: params[2],
partner_type: "group_owner",
});
return 1;
}
if (/^UPDATE partners/.test(normalized)) {
const row = partners.find((item) => item.id === params[2]);
if (!row) return 0;
Object.assign(row, { name: params[0], wecom_userid: params[1] });
return 1;
}
return 1;
};
const makeStatement = (sql, params = []) => ({
sql,
params,
bind(...args) {
return makeStatement(sql, args);
},
async run() {
executed.push({ sql, params });
return { meta: { changes: applyWrite(sql, params) } };
},
async first() {
executed.push({ sql, params });
if (/FROM projects WHERE id = \?/.test(sql)) {
return projects.find((item) => item.id === params[0]) ?? null;
}
if (/FROM partners WHERE id = \?/.test(sql)) {
return partners.find((item) => item.id === params[0]) ?? null;
}
return null;
},
async all() {
executed.push({ sql, params });
if (/FROM projects p/.test(sql)) {
return {
results: projects.map((item) => ({
...item,
task_count: tasks.filter((task) => task.project_id === item.id).length,
group_chat_count: links.filter((link) => link.project_id === item.id).length,
})),
};
}
if (/FROM tasks WHERE project_id/.test(sql)) {
return {
results: tasks
.filter((task) => task.project_id === params[0])
.map((task) => ({ ...task })),
};
}
if (/FROM project_group_chats g/.test(sql)) {
return {
results: links
.filter((link) => link.project_id === params[0])
.map((link) => {
const chat = groupChats.find((item) => item.chat_id === link.chat_id);
return {
chat_id: link.chat_id,
partner_id: link.partner_id,
chat_name: chat ? chat.name : null,
owner_user_id: chat ? chat.owner_user_id : null,
member_count: chat ? chat.member_count : null,
chat_status: chat ? chat.status : null,
partner_name:
partners.find((item) => item.id === link.partner_id)?.name ?? null,
};
}),
};
}
if (/FROM partners/.test(sql)) {
return { results: partners.map((item) => ({ ...item })) };
}
return { results: [] };
},
});
return {
projects,
links,
tasks,
groupChats,
partners,
executed,
prepare(sql) {
return makeStatement(sql);
},
async batch(statements) {
const output = [];
for (const statement of statements) output.push(await statement.run());
return output;
},
};
}
test("parseMemberIds accepts lines, arrays and mixed separators, dedupes", () => {
assert.deepEqual(parseMemberIds("zhangsan\nlisi zhangsan"), ["zhangsan", "lisi"]);
assert.deepEqual(parseMemberIds(["a", " b ", "a"]), ["a", "b"]);
assert.deepEqual(parseMemberIds("a,b;c,d"), ["a", "b", "c", "d"]);
assert.deepEqual(parseMemberIds(" \n "), []);
});
test("saveProject inserts with proj- prefixed id and JSON members", async () => {
const db = fakeProjectDb();
const project = await saveProject(db, {
name: "暑期焕新",
brand: "青柠实验室",
mainGroupMemberIds: "zhangsan\nlisi\nzhangsan",
note: "备注",
});
assert.match(project.id, /^proj-/);
assert.equal(project.name, "暑期焕新");
assert.deepEqual(project.mainGroupMemberIds, ["zhangsan", "lisi"]);
assert.equal(db.projects.length, 1);
assert.equal(db.projects[0].status, "active");
});
test("saveProject updates existing row and rejects blank name", async () => {
const db = fakeProjectDb({
projects: [
{
id: "proj-1",
name: "旧名",
brand: "",
main_group_member_ids: "[]",
status: "active",
note: "",
created_at: "2026-08-01",
},
],
});
await saveProject(db, {
id: "proj-1",
name: "新名",
brand: "品牌",
mainGroupMemberIds: ["wangwu"],
});
assert.equal(db.projects[0].name, "新名");
assert.equal(db.projects[0].brand, "品牌");
assert.deepEqual(JSON.parse(db.projects[0].main_group_member_ids), ["wangwu"]);
assert.equal(db.projects.length, 1, "update must not insert");
await assert.rejects(
() => saveProject(db, { id: "proj-1", name: " " }),
(error) => error instanceof ProjectError && error.status === 400,
);
});
test("saveProject rejects unknown id on update", async () => {
const db = fakeProjectDb();
await assert.rejects(
() => saveProject(db, { id: "proj-404", name: "x" }),
(error) => error instanceof ProjectError && error.status === 404,
);
});
test("listProjects aggregates task and group chat counts", async () => {
const db = fakeProjectDb({
projects: [
{
id: "proj-1",
name: "A",
brand: "",
main_group_member_ids: "[\"u1\",\"u2\"]",
status: "active",
note: "",
created_at: "2026-08-01",
},
],
tasks: [
{ id: "t1", project_id: "proj-1" },
{ id: "t2", project_id: "proj-1" },
{ id: "t3", project_id: "" },
],
links: [{ project_id: "proj-1", chat_id: "wr-1", partner_id: "" }],
});
const projects = await listProjects(db);
assert.equal(projects.length, 1);
assert.equal(projects[0].taskCount, 2);
assert.equal(projects[0].groupChatCount, 1);
assert.deepEqual(projects[0].mainGroupMemberIds, ["u1", "u2"]);
});
test("getProjectDetail marks missing chats and dangling partners", async () => {
const db = fakeProjectDb({
projects: [
{
id: "proj-1",
name: "A",
brand: "",
main_group_member_ids: "[]",
status: "active",
note: "",
created_at: "2026-08-01",
},
],
links: [
{ project_id: "proj-1", chat_id: "wr-1", partner_id: "p-1" },
{ project_id: "proj-1", chat_id: "wr-gone", partner_id: "" },
],
groupChats: [
{ chat_id: "wr-1", name: "一群", owner_user_id: "zhangsan", member_count: 12, status: 0 },
],
partners: [],
});
const detail = await getProjectDetail(db, "proj-1");
assert.equal(detail.groupChats.length, 2);
const alive = detail.groupChats.find((chat) => chat.chatId === "wr-1");
assert.equal(alive.missing, false);
assert.equal(alive.partnerName, "", "dangling partner shows blank name");
const gone = detail.groupChats.find((chat) => chat.chatId === "wr-gone");
assert.equal(gone.missing, true);
assert.equal(gone.name, "");
});
test("getProjectDetail throws 404 for unknown project", async () => {
const db = fakeProjectDb();
await assert.rejects(
() => getProjectDetail(db, "proj-404"),
(error) => error instanceof ProjectError && error.status === 404,
);
});
test("setProjectGroupChats replaces links and dedupes chat ids", async () => {
const db = fakeProjectDb({
projects: [
{
id: "proj-1",
name: "A",
brand: "",
main_group_member_ids: "[]",
status: "active",
note: "",
created_at: "2026-08-01",
},
],
links: [{ project_id: "proj-1", chat_id: "wr-old", partner_id: "" }],
groupChats: [
{ chat_id: "wr-1", name: "一群", owner_user_id: "zhangsan", member_count: 12, status: 0 },
],
partners: [{ id: "p-1", name: "张三", wecom_userid: "zhangsan" }],
});
const detail = await setProjectGroupChats(db, "proj-1", [
{ chatId: "wr-1", partnerId: "p-1" },
{ chatId: "wr-1", partnerId: "p-1" },
{ chatId: " ", partnerId: "" },
]);
assert.equal(detail.groupChats.length, 1);
assert.equal(detail.groupChats[0].chatId, "wr-1");
assert.equal(detail.groupChats[0].partnerName, "张三");
assert.equal(db.links.length, 1);
assert.equal(db.links[0].chat_id, "wr-1");
});
test("deleteProject removes links and unlinks tasks", async () => {
const db = fakeProjectDb({
projects: [
{
id: "proj-1",
name: "A",
brand: "",
main_group_member_ids: "[]",
status: "active",
note: "",
created_at: "2026-08-01",
},
],
links: [{ project_id: "proj-1", chat_id: "wr-1", partner_id: "" }],
tasks: [
{ id: "t1", project_id: "proj-1" },
{ id: "t2", project_id: "proj-2" },
],
});
await deleteProject(db, "proj-1");
assert.equal(db.projects.length, 0);
assert.equal(db.links.length, 0);
assert.equal(db.tasks[0].project_id, "");
assert.equal(db.tasks[1].project_id, "proj-2");
await assert.rejects(
() => deleteProject(db, "proj-404"),
(error) => error instanceof ProjectError && error.status === 404,
);
});
test("saveGroupOwnerPartner inserts and updates group_owner partners", async () => {
const db = fakeProjectDb();
const created = await saveGroupOwnerPartner(db, {
name: "张三",
wecomUserid: "zhangsan",
});
assert.match(created.id, /^partner-go-/);
assert.equal(db.partners[0].partner_type, "group_owner");
const updated = await saveGroupOwnerPartner(db, {
id: created.id,
name: "张三丰",
wecomUserid: "zhangsanfeng",
});
assert.equal(updated.name, "张三丰");
assert.equal(db.partners.length, 1);
await assert.rejects(
() => saveGroupOwnerPartner(db, { name: "李四", wecomUserid: "" }),
(error) => error instanceof ProjectError,
);
});