- 任务/项目负责人 union scope:任务中心分组视图、工作台项目进度卡、收录验证列 - 项目负责人可管理自己项目:编辑/删除项目、编辑任务、分配负责人、推送、项目内建任务;堵死转移归属/越权新建/任务迁移 - 只读依赖放开给已登录:inspect_feishu、wecom_list_internal_contacts - 企微 OAuth 自助绑定:/api/wecom-bind(state 签名校验+登录态一致性+归属冲突检查),未绑定 banner 增加入口 - 工作台项目进度卡与未绑定 banner 排版修复 - 修 user-auth 判空分支被 Turbopack dev 折叠删除导致匿名请求 500
93 lines
3.2 KiB
JavaScript
93 lines
3.2 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import { readFileSync } from "node:fs";
|
|
import {
|
|
signWecomBindState,
|
|
verifyWecomBindState,
|
|
signWecomIdentityTicket,
|
|
WECOM_BIND_STATE_TTL_MS,
|
|
} from "../lib/wecom-oauth-service.ts";
|
|
|
|
const SECRET = "internal-token-test";
|
|
|
|
const bindRoute = readFileSync(
|
|
new URL("../app/api/wecom-bind/route.ts", import.meta.url),
|
|
"utf8",
|
|
);
|
|
const adminApp = readFileSync(
|
|
new URL("../app/admin-app.tsx", import.meta.url),
|
|
"utf8",
|
|
);
|
|
|
|
test("bind state sign → verify roundtrip", async () => {
|
|
const state = await signWecomBindState("user-abc", { secret: SECRET });
|
|
const verified = await verifyWecomBindState(state, { secret: SECRET });
|
|
assert.deepEqual(verified, { userId: "user-abc" });
|
|
});
|
|
|
|
test("bind state rejects expiry, tampering, wrong secret", async () => {
|
|
const state = await signWecomBindState("user-abc", {
|
|
secret: SECRET,
|
|
nowMs: Date.now() - WECOM_BIND_STATE_TTL_MS - 120_000,
|
|
});
|
|
assert.equal(await verifyWecomBindState(state, { secret: SECRET }), null);
|
|
|
|
const fresh = await signWecomBindState("user-abc", { secret: SECRET });
|
|
const [payload, signature] = fresh.split(".");
|
|
assert.ok(payload.length > 8);
|
|
const flip = payload[5] === "a" ? "b" : "a";
|
|
const tampered = `${payload.slice(0, 5)}${flip}${payload.slice(6)}.${signature}`;
|
|
assert.notEqual(tampered, fresh);
|
|
assert.equal(await verifyWecomBindState(tampered, { secret: SECRET }), null);
|
|
assert.equal(
|
|
await verifyWecomBindState(fresh, { secret: "other-secret" }),
|
|
null,
|
|
);
|
|
assert.equal(await verifyWecomBindState("garbage", { secret: SECRET }), null);
|
|
assert.equal(await verifyWecomBindState("", { secret: SECRET }), null);
|
|
});
|
|
|
|
test("identity ticket cannot be replayed as bind state", async () => {
|
|
const { ticket } = await signWecomIdentityTicket("wmExt123", "张三", {
|
|
secret: SECRET,
|
|
});
|
|
assert.equal(await verifyWecomBindState(ticket, { secret: SECRET }), null);
|
|
});
|
|
|
|
test("bind state requires a user id", async () => {
|
|
await assert.rejects(
|
|
() => signWecomBindState("", { secret: SECRET }),
|
|
/后台账号 ID/,
|
|
);
|
|
});
|
|
|
|
test("bind route: start redirects logged-in users to wecom oauth", () => {
|
|
assert.ok(bindRoute.includes("buildWecomOAuthUrl"));
|
|
assert.ok(bindRoute.includes("signWecomBindState"));
|
|
assert.ok(bindRoute.includes("redirectUri"));
|
|
assert.ok(/APP_ORIGIN/.test(bindRoute));
|
|
assert.ok(/principal\.kind !== "user"/.test(bindRoute));
|
|
});
|
|
|
|
test("bind route: callback verifies state, session and conflicts", () => {
|
|
assert.ok(bindRoute.includes("verifyWecomBindState"));
|
|
assert.ok(/principal\.user\.id !== verified\.userId/.test(bindRoute));
|
|
assert.ok(bindRoute.includes("exchangeWecomOAuthCode"));
|
|
assert.ok(bindRoute.includes("!identity.userId"));
|
|
assert.ok(
|
|
/SELECT id, username FROM users WHERE wecom_userid = \? AND id <> \?/.test(
|
|
bindRoute,
|
|
),
|
|
);
|
|
assert.ok(
|
|
/UPDATE users SET wecom_userid = \?, updated_at = CURRENT_TIMESTAMP WHERE id = \?/.test(
|
|
bindRoute,
|
|
),
|
|
);
|
|
});
|
|
|
|
test("admin banner exposes self-service bind entry", () => {
|
|
assert.ok(adminApp.includes('href="/api/wecom-bind"'));
|
|
assert.ok(adminApp.includes("owner-bind-button"));
|
|
});
|