2026-08-31 17:11:20 +08:00
|
|
|
import assert from "node:assert/strict";
|
|
|
|
|
import test from "node:test";
|
|
|
|
|
import {
|
|
|
|
|
getWecomOAuthSecret,
|
|
|
|
|
resolveWecomClaimant,
|
|
|
|
|
signWecomIdentityTicket,
|
|
|
|
|
verifyWecomIdentityTicket,
|
|
|
|
|
wecomClaimantPartnerId,
|
|
|
|
|
WECOM_IDENTITY_TTL_MS,
|
|
|
|
|
} from "../lib/wecom-oauth-service.ts";
|
|
|
|
|
|
|
|
|
|
const SECRET = "internal-token-test";
|
|
|
|
|
|
|
|
|
|
function fakePartnerDb(partners = []) {
|
|
|
|
|
return {
|
|
|
|
|
prepare(sql) {
|
|
|
|
|
return {
|
|
|
|
|
bind(...params) {
|
|
|
|
|
return {
|
|
|
|
|
async first() {
|
|
|
|
|
if (/WHERE wecom_external_user_id = \?/.test(sql)) {
|
|
|
|
|
const row = partners.find(
|
|
|
|
|
(item) => item.wecom_external_user_id === params[0],
|
|
|
|
|
);
|
|
|
|
|
return row ? { id: row.id } : null;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test("identity ticket sign → verify roundtrip", async () => {
|
|
|
|
|
const { ticket } = await signWecomIdentityTicket("wmExt123", "张三", {
|
|
|
|
|
secret: SECRET,
|
|
|
|
|
});
|
|
|
|
|
const identity = await verifyWecomIdentityTicket(ticket, { secret: SECRET });
|
|
|
|
|
assert.deepEqual(identity, { externalUserId: "wmExt123", displayName: "张三" });
|
|
|
|
|
});
|
|
|
|
|
|
2026-09-03 11:30:26 +08:00
|
|
|
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,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2026-08-31 17:11:20 +08:00
|
|
|
test("identity ticket falls back to default display name when empty", async () => {
|
|
|
|
|
const { ticket } = await signWecomIdentityTicket("wmExt123", " ", {
|
|
|
|
|
secret: SECRET,
|
|
|
|
|
});
|
|
|
|
|
const identity = await verifyWecomIdentityTicket(ticket, { secret: SECRET });
|
|
|
|
|
assert.equal(identity.displayName, "企微客户");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("identity ticket default TTL is 30 minutes and expiresIn matches", async () => {
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
const { ticket, expiresIn } = await signWecomIdentityTicket("wmExt123", "张三", {
|
|
|
|
|
secret: SECRET,
|
|
|
|
|
nowMs: now,
|
|
|
|
|
});
|
|
|
|
|
assert.equal(expiresIn, 30 * 60);
|
|
|
|
|
const identity = await verifyWecomIdentityTicket(ticket, {
|
|
|
|
|
secret: SECRET,
|
|
|
|
|
nowMs: now + WECOM_IDENTITY_TTL_MS - 1,
|
|
|
|
|
});
|
|
|
|
|
assert.ok(identity, "still valid just before expiry");
|
|
|
|
|
const expired = await verifyWecomIdentityTicket(ticket, {
|
|
|
|
|
secret: SECRET,
|
|
|
|
|
nowMs: now + WECOM_IDENTITY_TTL_MS + 61_000,
|
|
|
|
|
});
|
|
|
|
|
assert.equal(expired, null, "expired beyond grace window");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("identity ticket passes within 60s grace window", async () => {
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
const { ticket } = await signWecomIdentityTicket("wmExt123", "张三", {
|
|
|
|
|
secret: SECRET,
|
|
|
|
|
nowMs: now,
|
|
|
|
|
});
|
|
|
|
|
const identity = await verifyWecomIdentityTicket(ticket, {
|
|
|
|
|
secret: SECRET,
|
|
|
|
|
nowMs: now + WECOM_IDENTITY_TTL_MS + 30_000,
|
|
|
|
|
});
|
|
|
|
|
assert.deepEqual(identity?.externalUserId, "wmExt123");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("identity ticket verification fails on wrong secret", async () => {
|
|
|
|
|
const { ticket } = await signWecomIdentityTicket("wmExt123", "张三", {
|
|
|
|
|
secret: SECRET,
|
|
|
|
|
});
|
|
|
|
|
assert.equal(
|
|
|
|
|
await verifyWecomIdentityTicket(ticket, { secret: "other-secret" }),
|
|
|
|
|
null,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("identity ticket verification fails on tampered payload", async () => {
|
|
|
|
|
const { ticket } = await signWecomIdentityTicket("wmExt123", "张三", {
|
|
|
|
|
secret: SECRET,
|
|
|
|
|
});
|
|
|
|
|
const [payload, signature] = ticket.split(".");
|
|
|
|
|
const decoded = JSON.parse(
|
|
|
|
|
Buffer.from(payload.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString(
|
|
|
|
|
"utf8",
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
decoded.ext = "wmVictim";
|
|
|
|
|
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 verification fails on malformed input", async () => {
|
|
|
|
|
for (const bad of ["", "no-dot", ".abc", "abc.", "a b.c d", "xxx.yyy"]) {
|
|
|
|
|
assert.equal(
|
|
|
|
|
await verifyWecomIdentityTicket(bad, { secret: SECRET }),
|
|
|
|
|
null,
|
|
|
|
|
`rejects ${JSON.stringify(bad)}`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("getWecomOAuthSecret throws when ADMIN_INTERNAL_TOKEN missing", () => {
|
|
|
|
|
const previous = process.env.ADMIN_INTERNAL_TOKEN;
|
|
|
|
|
delete process.env.ADMIN_INTERNAL_TOKEN;
|
|
|
|
|
try {
|
|
|
|
|
assert.throws(() => getWecomOAuthSecret(), /ADMIN_INTERNAL_TOKEN/);
|
|
|
|
|
} finally {
|
|
|
|
|
if (previous !== undefined) process.env.ADMIN_INTERNAL_TOKEN = previous;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("wecomClaimantPartnerId derives stable partner-ext id", () => {
|
|
|
|
|
const first = wecomClaimantPartnerId("wmExt123");
|
|
|
|
|
assert.match(first, /^partner-ext-[a-z0-9]+$/);
|
|
|
|
|
assert.equal(first, wecomClaimantPartnerId("wmExt123"));
|
|
|
|
|
assert.notEqual(first, wecomClaimantPartnerId("wmExt456"));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("resolveWecomClaimant reuses partner bound by operator", async () => {
|
|
|
|
|
const db = fakePartnerDb([
|
|
|
|
|
{ id: "partner-9", wecom_external_user_id: "wmExt123" },
|
|
|
|
|
]);
|
|
|
|
|
const resolved = await resolveWecomClaimant(db, "wmExt123");
|
|
|
|
|
assert.deepEqual(resolved, { partnerId: "partner-9", matchedExisting: true });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("resolveWecomClaimant derives new id when unbound", async () => {
|
|
|
|
|
const db = fakePartnerDb();
|
|
|
|
|
const resolved = await resolveWecomClaimant(db, "wmExt123");
|
|
|
|
|
assert.equal(resolved.matchedExisting, false);
|
|
|
|
|
assert.equal(resolved.partnerId, wecomClaimantPartnerId("wmExt123"));
|
|
|
|
|
});
|