- 任务/项目负责人 union scope:任务中心分组视图、工作台项目进度卡、收录验证列 - 项目负责人可管理自己项目:编辑/删除项目、编辑任务、分配负责人、推送、项目内建任务;堵死转移归属/越权新建/任务迁移 - 只读依赖放开给已登录:inspect_feishu、wecom_list_internal_contacts - 企微 OAuth 自助绑定:/api/wecom-bind(state 签名校验+登录态一致性+归属冲突检查),未绑定 banner 增加入口 - 工作台项目进度卡与未绑定 banner 排版修复 - 修 user-auth 判空分支被 Turbopack dev 折叠删除导致匿名请求 500
698 lines
21 KiB
JavaScript
698 lines
21 KiB
JavaScript
import assert from "node:assert/strict";
|
||
import test from "node:test";
|
||
import {
|
||
projectMainChatId,
|
||
deleteProject,
|
||
ensureProjectMainChat,
|
||
effectiveProjectMembers,
|
||
findOrCreateGroupOwnerPartner,
|
||
getProjectDetail,
|
||
listProjects,
|
||
parseMemberIds,
|
||
ProjectError,
|
||
saveGroupOwnerPartner,
|
||
saveProject,
|
||
} from "../lib/project-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",
|
||
};
|
||
|
||
test.beforeEach(() => {
|
||
clearWecomAccessTokenCacheForTests();
|
||
});
|
||
|
||
function fakeWecom(options = {}) {
|
||
const calls = [];
|
||
const chats = new Map(options.chats ?? []);
|
||
const failCreate = options.failCreate ?? false;
|
||
const failGet = options.failGet ?? false;
|
||
const createExisted = options.createExisted ?? false;
|
||
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/appchat/create")) {
|
||
if (createExisted) {
|
||
return Response.json({ errcode: 86215, errmsg: "chatid existed" });
|
||
}
|
||
if (failCreate) {
|
||
return Response.json({ errcode: 60111, errmsg: "user not in scope" });
|
||
}
|
||
const body = JSON.parse(String(init.body ?? "{}"));
|
||
chats.set(body.chatid, { name: body.name, userlist: body.userlist });
|
||
return Response.json({ errcode: 0, errmsg: "ok", chatid: body.chatid });
|
||
}
|
||
if (url.pathname.endsWith("/cgi-bin/appchat/update")) {
|
||
const body = JSON.parse(String(init.body ?? "{}"));
|
||
const chat = chats.get(body.chatid);
|
||
if (!chat) {
|
||
return Response.json({ errcode: 60111, errmsg: "chat not found" });
|
||
}
|
||
for (const userid of body.add_user_list ?? []) {
|
||
if (!chat.userlist.includes(userid)) chat.userlist.push(userid);
|
||
}
|
||
for (const userid of body.del_user_list ?? []) {
|
||
chat.userlist = chat.userlist.filter((id) => id !== userid);
|
||
}
|
||
return Response.json({ errcode: 0, errmsg: "ok" });
|
||
}
|
||
if (url.pathname.endsWith("/cgi-bin/appchat/get")) {
|
||
if (failGet) {
|
||
return Response.json({ errcode: 86001, errmsg: "chatid invalid" });
|
||
}
|
||
const body = JSON.parse(String(init.body ?? "{}"));
|
||
const chat = chats.get(body.chatid);
|
||
if (!chat) {
|
||
return Response.json({ errcode: 60111, errmsg: "chat not found" });
|
||
}
|
||
return Response.json({
|
||
errcode: 0,
|
||
errmsg: "ok",
|
||
chat_info: {
|
||
chatid: body.chatid,
|
||
name: chat.name,
|
||
userlist: chat.userlist,
|
||
},
|
||
});
|
||
}
|
||
return new Response("not found", { status: 404 });
|
||
};
|
||
return { calls, fetchImpl, chats };
|
||
}
|
||
|
||
function fakeProjectDb({ projects = [], tasks = [], 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],
|
||
owner_userid: params[4],
|
||
main_chat_id: "",
|
||
status: "active",
|
||
note: params[5],
|
||
created_at: params[6],
|
||
});
|
||
return 1;
|
||
}
|
||
if (/^UPDATE projects SET main_chat_id/.test(normalized)) {
|
||
const row = projects.find((item) => item.id === params[1]);
|
||
if (row) row.main_chat_id = params[0];
|
||
return row ? 1 : 0;
|
||
}
|
||
if (/^UPDATE projects/.test(normalized)) {
|
||
const row = projects.find((item) => item.id === params[5]);
|
||
if (!row) return 0;
|
||
Object.assign(row, {
|
||
name: params[0],
|
||
brand: params[1],
|
||
main_group_member_ids: params[2],
|
||
owner_userid: params[3],
|
||
note: params[4],
|
||
});
|
||
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 (/^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;
|
||
}
|
||
if (/partner_type = 'group_owner' AND wecom_userid = \?/.test(sql)) {
|
||
return (
|
||
partners.find(
|
||
(item) =>
|
||
item.partner_type === "group_owner" &&
|
||
item.wecom_userid === params[0],
|
||
) ?? null
|
||
);
|
||
}
|
||
return null;
|
||
},
|
||
async all() {
|
||
executed.push({ sql, params });
|
||
if (/FROM projects p/.test(sql)) {
|
||
const ownerFiltered = /WHERE p\.owner_userid = \?/.test(sql);
|
||
const rows = ownerFiltered
|
||
? projects.filter((item) => item.owner_userid === params[0])
|
||
: projects;
|
||
return {
|
||
results: rows.map((item) => ({
|
||
...item,
|
||
task_count: tasks.filter((task) => task.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 partners/.test(sql)) {
|
||
return { results: partners.map((item) => ({ ...item })) };
|
||
}
|
||
return { results: [] };
|
||
},
|
||
});
|
||
return {
|
||
projects,
|
||
tasks,
|
||
partners,
|
||
executed,
|
||
prepare(sql) {
|
||
return makeStatement(sql);
|
||
},
|
||
async batch(statements) {
|
||
const output = [];
|
||
for (const statement of statements) output.push(await statement.run());
|
||
return output;
|
||
},
|
||
};
|
||
}
|
||
|
||
const projectRow = (overrides = {}) => ({
|
||
id: "proj-1",
|
||
name: "A",
|
||
brand: "",
|
||
main_group_member_ids: "[]",
|
||
owner_userid: "",
|
||
main_chat_id: "",
|
||
status: "active",
|
||
note: "",
|
||
created_at: "2026-08-01",
|
||
...overrides,
|
||
});
|
||
|
||
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("effectiveProjectMembers parses stored JSON and merges owner", () => {
|
||
assert.deepEqual(
|
||
effectiveProjectMembers('["u1","u2"]', "owner-a"),
|
||
["u1", "u2", "owner-a"],
|
||
);
|
||
assert.deepEqual(effectiveProjectMembers("u1\nu1,u2", ""), ["u1", "u2"]);
|
||
assert.deepEqual(effectiveProjectMembers('["u1"]', "u1"), ["u1"]);
|
||
});
|
||
|
||
test("saveProject inserts with proj- prefixed id and JSON members incl owner", async () => {
|
||
const db = fakeProjectDb();
|
||
const { project, mainChat } = await saveProject(db, {
|
||
name: "暑期焕新",
|
||
brand: "青柠实验室",
|
||
mainGroupMemberIds: "zhangsan\nlisi\nzhangsan",
|
||
ownerUserid: "owner-zhang",
|
||
note: "备注",
|
||
});
|
||
assert.match(project.id, /^proj-/);
|
||
assert.equal(project.name, "暑期焕新");
|
||
assert.equal(project.ownerUserid, "owner-zhang");
|
||
assert.deepEqual(project.mainGroupMemberIds, ["zhangsan", "lisi", "owner-zhang"]);
|
||
assert.equal(db.projects.length, 1);
|
||
assert.equal(db.projects[0].status, "active");
|
||
assert.deepEqual(JSON.parse(db.projects[0].main_group_member_ids), [
|
||
"zhangsan",
|
||
"lisi",
|
||
"owner-zhang",
|
||
]);
|
||
assert.equal(mainChat.reason, "no_credentials", "no config → lazy chat");
|
||
});
|
||
|
||
test("saveProject updates existing row and rejects blank name", async () => {
|
||
const db = fakeProjectDb({
|
||
projects: [projectRow({ id: "proj-1", name: "旧名" })],
|
||
});
|
||
await saveProject(db, {
|
||
id: "proj-1",
|
||
name: "新名",
|
||
brand: "品牌",
|
||
mainGroupMemberIds: ["wangwu"],
|
||
ownerUserid: "owner-li",
|
||
});
|
||
assert.equal(db.projects[0].name, "新名");
|
||
assert.equal(db.projects[0].brand, "品牌");
|
||
assert.equal(db.projects[0].owner_userid, "owner-li");
|
||
assert.deepEqual(JSON.parse(db.projects[0].main_group_member_ids), [
|
||
"wangwu",
|
||
"owner-li",
|
||
]);
|
||
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("saveProject with config creates main chat once members reach 3", async () => {
|
||
const db = fakeProjectDb();
|
||
const wecom = fakeWecom();
|
||
const { project, mainChat } = await saveProject(
|
||
db,
|
||
{
|
||
name: "满三人群",
|
||
mainGroupMemberIds: "u1\nu2",
|
||
ownerUserid: "owner-a",
|
||
},
|
||
resolveWecomConfig(baseBindings),
|
||
wecom.fetchImpl,
|
||
);
|
||
assert.equal(mainChat.chatId, projectMainChatId(project.id));
|
||
assert.equal(mainChat.reason, undefined);
|
||
assert.equal(db.projects[0].main_chat_id, projectMainChatId(project.id));
|
||
const createBody = JSON.parse(
|
||
String(
|
||
wecom.calls.find((c) => c.url.pathname.endsWith("/cgi-bin/appchat/create"))
|
||
.init.body,
|
||
),
|
||
);
|
||
assert.equal(createBody.chatid, projectMainChatId(project.id));
|
||
assert.deepEqual(createBody.userlist.sort(), ["owner-a", "u1", "u2"]);
|
||
});
|
||
|
||
test("listProjects aggregates task counts and exposes mainChatId", async () => {
|
||
const db = fakeProjectDb({
|
||
projects: [
|
||
projectRow({
|
||
id: "proj-1",
|
||
main_group_member_ids: '["u1","u2"]',
|
||
main_chat_id: "pgcproj1",
|
||
}),
|
||
],
|
||
tasks: [
|
||
{ id: "t1", project_id: "proj-1" },
|
||
{ id: "t2", project_id: "proj-1" },
|
||
{ id: "t3", project_id: "" },
|
||
],
|
||
});
|
||
const projects = await listProjects(db);
|
||
assert.equal(projects.length, 1);
|
||
assert.equal(projects[0].taskCount, 2);
|
||
assert.equal(projects[0].mainChatId, "pgcproj1");
|
||
assert.deepEqual(projects[0].mainGroupMemberIds, ["u1", "u2"]);
|
||
});
|
||
|
||
test("listProjects filters by project owner when ownerUserid given", async () => {
|
||
const db = fakeProjectDb({
|
||
projects: [
|
||
projectRow({ id: "proj-1", owner_userid: "owner-a" }),
|
||
projectRow({ id: "proj-2", owner_userid: "owner-b" }),
|
||
projectRow({ id: "proj-3", owner_userid: "" }),
|
||
],
|
||
});
|
||
const mine = await listProjects(db, "owner-a");
|
||
assert.deepEqual(
|
||
mine.map((item) => item.id),
|
||
["proj-1"],
|
||
);
|
||
const unbound = await listProjects(db, "");
|
||
assert.deepEqual(
|
||
unbound,
|
||
[],
|
||
"已登录未绑定企微的空范围不应漏出无主项目",
|
||
);
|
||
const all = await listProjects(db);
|
||
assert.equal(all.length, 3);
|
||
});
|
||
|
||
test("getProjectDetail returns tasks with owner and mainChat members", async () => {
|
||
const db = fakeProjectDb({
|
||
projects: [
|
||
projectRow({
|
||
id: "proj-1",
|
||
main_group_member_ids: '["u1"]',
|
||
owner_userid: "owner-a",
|
||
main_chat_id: "pgcproj1",
|
||
}),
|
||
],
|
||
tasks: [
|
||
{
|
||
id: "t1",
|
||
name: "任务一",
|
||
project_id: "proj-1",
|
||
owner_userid: "owner-a",
|
||
task_chat_id: "tgc_t1",
|
||
},
|
||
],
|
||
});
|
||
const detail = await getProjectDetail(db, "proj-1");
|
||
assert.equal(detail.project.taskCount, 1);
|
||
assert.equal(detail.project.mainChatId, "pgcproj1");
|
||
assert.equal(detail.tasks[0].owner_userid, "owner-a");
|
||
assert.equal(detail.tasks[0].task_chat_id, "tgc_t1");
|
||
assert.equal(detail.mainChat.chatId, "pgcproj1");
|
||
assert.deepEqual(detail.mainChat.members, ["u1", "owner-a"]);
|
||
});
|
||
|
||
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("deleteProject unlinks tasks", async () => {
|
||
const db = fakeProjectDb({
|
||
projects: [projectRow({ id: "proj-1" })],
|
||
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.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("ensureProjectMainChat returns no_project for unknown ids", async () => {
|
||
const wecom = fakeWecom();
|
||
const db = fakeProjectDb();
|
||
const result = await ensureProjectMainChat(
|
||
db,
|
||
"proj-404",
|
||
resolveWecomConfig(baseBindings),
|
||
wecom.fetchImpl,
|
||
);
|
||
assert.equal(result.reason, "no_project");
|
||
assert.equal(result.chatId, null);
|
||
});
|
||
|
||
test("ensureProjectMainChat skips without app credentials", async () => {
|
||
const wecom = fakeWecom();
|
||
const db = fakeProjectDb({
|
||
projects: [projectRow({ main_group_member_ids: '["u1","u2","u3"]' })],
|
||
});
|
||
const result = await ensureProjectMainChat(
|
||
db,
|
||
"proj-1",
|
||
resolveWecomConfig({ ...baseBindings, WECOM_SECRET: "" }),
|
||
wecom.fetchImpl,
|
||
);
|
||
assert.equal(result.reason, "no_credentials");
|
||
assert.equal(result.chatId, null);
|
||
assert.equal(wecom.calls.length, 0);
|
||
});
|
||
|
||
test("ensureProjectMainChat skips creation when fewer than 3 members", async () => {
|
||
const wecom = fakeWecom();
|
||
const db = fakeProjectDb({
|
||
projects: [projectRow({ main_group_member_ids: '["u1","u2"]' })],
|
||
});
|
||
const result = await ensureProjectMainChat(
|
||
db,
|
||
"proj-1",
|
||
resolveWecomConfig(baseBindings),
|
||
wecom.fetchImpl,
|
||
);
|
||
assert.equal(result.reason, "not_enough_members");
|
||
assert.equal(result.chatId, null);
|
||
assert.equal(db.projects[0].main_chat_id, "");
|
||
assert.equal(
|
||
wecom.calls.filter((c) => c.url.pathname.endsWith("/cgi-bin/appchat/create"))
|
||
.length,
|
||
0,
|
||
);
|
||
});
|
||
|
||
test("ensureProjectMainChat probes before creating and persists chat id", async () => {
|
||
const wecom = fakeWecom();
|
||
const db = fakeProjectDb({
|
||
projects: [
|
||
projectRow({
|
||
name: "项目一",
|
||
main_group_member_ids: '["u1","u2"]',
|
||
owner_userid: "owner-a",
|
||
}),
|
||
],
|
||
});
|
||
const result = await ensureProjectMainChat(
|
||
db,
|
||
"proj-1",
|
||
resolveWecomConfig(baseBindings),
|
||
wecom.fetchImpl,
|
||
);
|
||
assert.equal(result.chatId, "pgcproj1");
|
||
assert.deepEqual(result.members.sort(), ["owner-a", "u1", "u2"]);
|
||
assert.equal(db.projects[0].main_chat_id, "pgcproj1");
|
||
const paths = wecom.calls.map((c) => c.url.pathname);
|
||
assert.ok(paths.some((p) => p.endsWith("/cgi-bin/appchat/get")), "probe first");
|
||
assert.ok(paths.some((p) => p.endsWith("/cgi-bin/appchat/create")));
|
||
assert.ok(
|
||
paths.indexOf(paths.find((p) => p.endsWith("/cgi-bin/appchat/get"))) <
|
||
paths.indexOf(paths.find((p) => p.endsWith("/cgi-bin/appchat/create"))),
|
||
"probe precedes create",
|
||
);
|
||
});
|
||
|
||
test("ensureProjectMainChat is idempotent when chat already exists remotely", async () => {
|
||
const wecom = fakeWecom({
|
||
chats: [["pgcproj1", { name: "【总群】项目一", userlist: ["u1", "u2", "owner-a"] }]],
|
||
});
|
||
const db = fakeProjectDb({
|
||
projects: [
|
||
projectRow({
|
||
name: "项目一",
|
||
main_group_member_ids: '["u1","u2"]',
|
||
owner_userid: "owner-a",
|
||
}),
|
||
],
|
||
});
|
||
const result = await ensureProjectMainChat(
|
||
db,
|
||
"proj-1",
|
||
resolveWecomConfig(baseBindings),
|
||
wecom.fetchImpl,
|
||
);
|
||
assert.equal(result.chatId, "pgcproj1");
|
||
assert.equal(db.projects[0].main_chat_id, "pgcproj1", "id persisted on recovery");
|
||
assert.equal(
|
||
wecom.calls.filter((c) => c.url.pathname.endsWith("/cgi-bin/appchat/create"))
|
||
.length,
|
||
0,
|
||
"no duplicate create",
|
||
);
|
||
});
|
||
|
||
test("ensureProjectMainChat diffs members add and del on existing chat", async () => {
|
||
const wecom = fakeWecom({
|
||
chats: [["pgcproj1", { name: "【总群】项目一", userlist: ["u1", "gone-user"] }]],
|
||
});
|
||
const db = fakeProjectDb({
|
||
projects: [
|
||
projectRow({
|
||
main_group_member_ids: '["u1","u2"]',
|
||
owner_userid: "owner-a",
|
||
main_chat_id: "pgcproj1",
|
||
}),
|
||
],
|
||
});
|
||
const result = await ensureProjectMainChat(
|
||
db,
|
||
"proj-1",
|
||
resolveWecomConfig(baseBindings),
|
||
wecom.fetchImpl,
|
||
);
|
||
assert.equal(result.chatId, "pgcproj1");
|
||
const updateBody = JSON.parse(
|
||
String(
|
||
wecom.calls.find((c) => c.url.pathname.endsWith("/cgi-bin/appchat/update"))
|
||
.init.body,
|
||
),
|
||
);
|
||
assert.deepEqual(updateBody.add_user_list.sort(), ["owner-a", "u2"]);
|
||
assert.deepEqual(updateBody.del_user_list, ["gone-user"]);
|
||
});
|
||
|
||
test("ensureProjectMainChat reports warning and keeps main_chat_id empty on create failure", async () => {
|
||
const wecom = fakeWecom({ failCreate: true });
|
||
const db = fakeProjectDb({
|
||
projects: [projectRow({ main_group_member_ids: '["u1","u2","u3"]' })],
|
||
});
|
||
const result = await ensureProjectMainChat(
|
||
db,
|
||
"proj-1",
|
||
resolveWecomConfig(baseBindings),
|
||
wecom.fetchImpl,
|
||
);
|
||
assert.equal(result.chatId, null);
|
||
assert.match(result.warning, /创建失败/);
|
||
assert.equal(db.projects[0].main_chat_id, "", "chat id not persisted on failure");
|
||
});
|
||
|
||
test("ensureProjectMainChat adopts chat when create reports chatid existed", async () => {
|
||
// 可见范围非根部门时 appchat/get 永远 86001,但群实际已建过(create 返回 86215)
|
||
const wecom = fakeWecom({ failGet: true, createExisted: true });
|
||
const db = fakeProjectDb({
|
||
projects: [
|
||
projectRow({
|
||
name: "项目一",
|
||
main_group_member_ids: '["u1","u2"]',
|
||
owner_userid: "owner-a",
|
||
}),
|
||
],
|
||
});
|
||
const result = await ensureProjectMainChat(
|
||
db,
|
||
"proj-1",
|
||
resolveWecomConfig(baseBindings),
|
||
wecom.fetchImpl,
|
||
);
|
||
assert.equal(result.chatId, "pgcproj1");
|
||
assert.equal(result.warning, undefined);
|
||
assert.equal(db.projects[0].main_chat_id, "pgcproj1", "existing chat id adopted");
|
||
});
|
||
|
||
test("ensureProjectMainChat falls back to add-only sync when get is unavailable", async () => {
|
||
const wecom = fakeWecom({
|
||
failGet: true,
|
||
chats: [["pgcproj1", { name: "【总群】项目一", userlist: ["u1", "gone-user"] }]],
|
||
});
|
||
const db = fakeProjectDb({
|
||
projects: [
|
||
projectRow({
|
||
main_group_member_ids: '["u1","u2"]',
|
||
owner_userid: "owner-a",
|
||
main_chat_id: "pgcproj1",
|
||
}),
|
||
],
|
||
});
|
||
const result = await ensureProjectMainChat(
|
||
db,
|
||
"proj-1",
|
||
resolveWecomConfig(baseBindings),
|
||
wecom.fetchImpl,
|
||
);
|
||
assert.equal(result.chatId, "pgcproj1");
|
||
assert.equal(result.warning, undefined);
|
||
const updateBody = JSON.parse(
|
||
String(
|
||
wecom.calls.find((c) => c.url.pathname.endsWith("/cgi-bin/appchat/update"))
|
||
.init.body,
|
||
),
|
||
);
|
||
assert.deepEqual(updateBody.add_user_list.sort(), ["owner-a", "u1", "u2"]);
|
||
assert.equal(updateBody.del_user_list, undefined, "no removals without member list");
|
||
});
|
||
|
||
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,
|
||
);
|
||
});
|
||
|
||
test("findOrCreateGroupOwnerPartner returns existing row without duplicating", async () => {
|
||
const db = fakeProjectDb({
|
||
partners: [
|
||
{ id: "partner-go-old", name: "负责人", wecom_userid: "owner-a", partner_type: "group_owner" },
|
||
],
|
||
});
|
||
const found = await findOrCreateGroupOwnerPartner(db, "owner-a");
|
||
assert.equal(found.id, "partner-go-old");
|
||
assert.equal(db.partners.length, 1);
|
||
});
|
||
|
||
test("findOrCreateGroupOwnerPartner creates partner with userid fallback name", async () => {
|
||
const db = fakeProjectDb();
|
||
const created = await findOrCreateGroupOwnerPartner(db, "owner-b");
|
||
assert.match(created.id, /^partner-go-/);
|
||
assert.equal(created.name, "owner-b");
|
||
assert.equal(created.wecom_userid, "owner-b");
|
||
assert.equal(db.partners.length, 1);
|
||
});
|