feat: 项目负责人+内部任务群+portal 内部身份领取+群主链接归属
- 迁移 0012:projects.owner_userid / tasks.task_chat_id / claims.owner_partner_id - lib/task-group-service.ts:懒建 appchat 任务群(首个内部群主领取时创建,成员 diff 补员),播报/卡片文案 - wecom-client 新增 appchat create/update/get/send 封装 - OAuth ticket 增可选 uid(v:1 兼容),内部成员领取按 wecom_userid 解析群主身份 - 外部 OAuth 领取人 upsert 打 partner_type='group_owner' - portal 链接带 ?owner= 穿透 OAuth 往返,claims 记录归属群主 - 后台任务卡「群主链接」面板:复制链接/推送内部群主/任务群状态 - 催办与超期升级同步播报进任务群
This commit is contained in:
@@ -41,6 +41,48 @@ test("identity ticket sign → verify roundtrip", async () => {
|
||||
assert.deepEqual(identity, { externalUserId: "wmExt123", displayName: "张三" });
|
||||
});
|
||||
|
||||
test("internal identity ticket carries userId and no external id", async () => {
|
||||
const { ticket } = await signWecomIdentityTicket("", "", {
|
||||
secret: SECRET,
|
||||
userId: "zhangsan",
|
||||
});
|
||||
const identity = await verifyWecomIdentityTicket(ticket, { secret: SECRET });
|
||||
assert.equal(identity.externalUserId, "");
|
||||
assert.equal(identity.userId, "zhangsan");
|
||||
});
|
||||
|
||||
test("signWecomIdentityTicket rejects when neither external nor internal id present", async () => {
|
||||
await assert.rejects(
|
||||
() => signWecomIdentityTicket("", "", { secret: SECRET }),
|
||||
/缺少企微身份/,
|
||||
);
|
||||
});
|
||||
|
||||
test("internal identity ticket forgery is rejected", async () => {
|
||||
const { ticket } = await signWecomIdentityTicket("", "", {
|
||||
secret: SECRET,
|
||||
userId: "zhangsan",
|
||||
});
|
||||
const [payload, signature] = ticket.split(".");
|
||||
const decoded = JSON.parse(
|
||||
Buffer.from(payload.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString(
|
||||
"utf8",
|
||||
),
|
||||
);
|
||||
decoded.uid = "wangwu";
|
||||
const forgedPayload = Buffer.from(JSON.stringify(decoded))
|
||||
.toString("base64")
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=+$/, "");
|
||||
assert.equal(
|
||||
await verifyWecomIdentityTicket(`${forgedPayload}.${signature}`, {
|
||||
secret: SECRET,
|
||||
}),
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
test("identity ticket falls back to default display name when empty", async () => {
|
||||
const { ticket } = await signWecomIdentityTicket("wmExt123", " ", {
|
||||
secret: SECRET,
|
||||
|
||||
Reference in New Issue
Block a user