文件
hyb-oa/server/tests/unit/wecomOAuthOnboarding.service.test.js
T

406 行
14 KiB
JavaScript

const {
WecomOAuthOnboardingError,
fetchWecomOAuthOnboardingProfile,
provisionWecomOAuthEmployee,
} = require("../../src/services/integrations/wecomOAuthOnboarding");
function jsonResponse(payload, status = 200) {
return {
ok: status >= 200 && status < 300,
status,
json: async () => payload,
};
}
function buildRequest() {
return {
id: "request-onboard-1",
ip: "127.0.0.1",
headers: { "user-agent": "vitest" },
};
}
describe("wecom OAuth onboarding service", () => {
it("combines member-authorized private fields with active contact and department data", async () => {
const calls = [];
const fetchImpl = vi.fn(async (url, init = {}) => {
const parsed = new URL(String(url));
calls.push({ path: parsed.pathname, body: init.body ? JSON.parse(init.body) : null });
if (parsed.pathname === "/cgi-bin/auth/getuserdetail") {
return jsonResponse({
errcode: 0,
userid: "member-1",
mobile: "19900000001",
email: "[email protected]",
});
}
if (parsed.pathname === "/cgi-bin/user/get") {
return jsonResponse({
errcode: 0,
userid: "member-1",
name: "测试成员",
status: 1,
department: [2],
position: "工程师",
});
}
if (parsed.pathname === "/cgi-bin/department/list") {
return jsonResponse({ errcode: 0, department: [{ id: 2, name: "研发部" }] });
}
return jsonResponse({ errcode: 40001 }, 400);
});
const profile = await fetchWecomOAuthOnboardingProfile({
accessToken: "agent-token",
externalUserId: "member-1",
userTicket: "private-ticket",
fetchImpl,
});
expect(profile).toEqual({
externalUserId: "member-1",
name: "测试成员",
phone: "19900000001",
email: "[email protected]",
departmentExternalId: "2",
departmentName: "研发部",
title: "工程师",
});
expect(calls.find((call) => call.path === "/cgi-bin/auth/getuserdetail")?.body).toEqual({
user_ticket: "private-ticket",
});
});
it("falls back to a valid business email and rejects malformed authorized fields", async () => {
const buildFetch = (privateDetail) => vi.fn(async (url) => {
const path = new URL(String(url)).pathname;
if (path === "/cgi-bin/auth/getuserdetail") return jsonResponse(privateDetail);
if (path === "/cgi-bin/user/get") {
return jsonResponse({
errcode: 0,
userid: "member-fields",
name: "测试成员",
status: 1,
department: [1],
});
}
return jsonResponse({ errcode: 0, department: [{ id: 1, name: "总部" }] });
});
const profile = await fetchWecomOAuthOnboardingProfile({
accessToken: "agent-token",
externalUserId: "member-fields",
userTicket: "ticket",
fetchImpl: buildFetch({
errcode: 0,
userid: "member-fields",
mobile: "19900000006",
email: " ",
biz_mail: "[email protected]",
}),
});
expect(profile.email).toBe("[email protected]");
const profileWithoutPhone = await fetchWecomOAuthOnboardingProfile({
accessToken: "agent-token",
externalUserId: "member-fields",
userTicket: "ticket",
fetchImpl: buildFetch({
errcode: 0,
userid: "member-fields",
mobile: "",
biz_mail: "[email protected]",
}),
});
expect(profileWithoutPhone.phone).toBe("");
expect(profileWithoutPhone.email).toBe("[email protected]");
await expect(fetchWecomOAuthOnboardingProfile({
accessToken: "agent-token",
externalUserId: "member-fields",
userTicket: "ticket",
fetchImpl: buildFetch({
errcode: 0,
userid: "member-fields",
mobile: "1",
email: "not-an-email",
}),
})).rejects.toMatchObject({ code: "missing_required_field" });
});
it("does not expose request credentials when the provider request fails", async () => {
const privateTicket = "private-ticket-must-not-leak";
let caught;
try {
await fetchWecomOAuthOnboardingProfile({
accessToken: "agent-token-must-not-leak",
externalUserId: "member-network",
userTicket: privateTicket,
fetchImpl: vi.fn().mockRejectedValue(new Error(`network failed ${privateTicket}`)),
});
} catch (error) {
caught = error;
}
expect(caught).toMatchObject({ code: "wecom_api_failed" });
expect(JSON.stringify(caught)).not.toContain(privateTicket);
expect(JSON.stringify(caught)).not.toContain("agent-token-must-not-leak");
});
it("rejects inactive members and identity mismatches", async () => {
const mismatchedFetch = vi.fn(async (url) => {
const path = new URL(String(url)).pathname;
if (path === "/cgi-bin/auth/getuserdetail") {
return jsonResponse({ errcode: 0, userid: "another-member", mobile: "19900000002", email: "[email protected]" });
}
if (path === "/cgi-bin/user/get") {
return jsonResponse({ errcode: 0, userid: "member-2", name: "成员", status: 1, department: [1] });
}
return jsonResponse({ errcode: 0, department: [{ id: 1, name: "总部" }] });
});
await expect(fetchWecomOAuthOnboardingProfile({
accessToken: "agent-token",
externalUserId: "member-2",
userTicket: "ticket",
fetchImpl: mismatchedFetch,
})).rejects.toMatchObject({ code: "identity_mismatch" });
const inactiveFetch = vi.fn(async (url) => {
const path = new URL(String(url)).pathname;
if (path === "/cgi-bin/auth/getuserdetail") {
return jsonResponse({ errcode: 0, userid: "member-2", mobile: "19900000002", email: "[email protected]" });
}
if (path === "/cgi-bin/user/get") {
return jsonResponse({ errcode: 0, userid: "member-2", name: "成员", status: 2, department: [1] });
}
return jsonResponse({ errcode: 0, department: [{ id: 1, name: "总部" }] });
});
await expect(fetchWecomOAuthOnboardingProfile({
accessToken: "agent-token",
externalUserId: "member-2",
userTicket: "ticket",
fetchImpl: inactiveFetch,
})).rejects.toMatchObject({ code: "member_inactive" });
});
it("self-provisions only the employee role and stores a dehydrated audit record", async () => {
const tx = {
wecomIdentityBinding: {
findUnique: vi.fn().mockResolvedValue(null),
upsert: vi.fn().mockResolvedValue({ id: "binding-1" }),
},
employee: {
findMany: vi.fn().mockResolvedValue([]),
create: vi.fn().mockResolvedValue({ id: "employee-1", token_version: 0 }),
update: vi.fn(),
},
integrationBinding: {
findFirst: vi.fn().mockResolvedValue(null),
create: vi.fn().mockResolvedValue({ id: "department-binding-1" }),
},
department: {
findMany: vi.fn().mockResolvedValue([{ id: "department-1" }]),
findUnique: vi.fn(),
create: vi.fn(),
},
employeeRole: {
create: vi.fn().mockResolvedValue({ employee_id: "employee-1", role_code: "employee" }),
},
auditLog: {
create: vi.fn().mockResolvedValue({ id: "audit-1" }),
},
};
const prisma = { $transaction: vi.fn(async (callback) => callback(tx)) };
const result = await provisionWecomOAuthEmployee({
prisma,
request: buildRequest(),
profile: {
externalUserId: "member-1",
name: "测试成员",
phone: "19900000001",
email: "[email protected]",
departmentExternalId: "2",
departmentName: "研发部",
title: "工程师",
},
});
expect(result).toEqual({ id: "employee-1", token_version: 0, created: true });
expect(tx.employeeRole.create).toHaveBeenCalledWith({
data: expect.objectContaining({ role_code: "employee" }),
});
expect(tx.auditLog.create).toHaveBeenCalledWith({
data: expect.objectContaining({
actor_id: "employee-1",
action: "AUTH_WECOM_SELF_PROVISIONED",
extra: {
created: true,
source: "wecom_oauth",
fieldPresence: { phone: true, email: true },
},
}),
});
const persisted = JSON.stringify({
role: tx.employeeRole.create.mock.calls,
audit: tx.auditLog.create.mock.calls,
});
expect(persisted).not.toContain("private-ticket");
expect(persisted).not.toContain("19900000001");
expect(persisted).not.toContain("[email protected]");
});
it("creates a member with a null phone when the authorized profile has no phone", async () => {
const tx = {
wecomIdentityBinding: {
findUnique: vi.fn().mockResolvedValue(null),
upsert: vi.fn().mockResolvedValue({ id: "binding-1" }),
},
employee: {
findMany: vi.fn().mockResolvedValue([]),
create: vi.fn().mockResolvedValue({ id: "employee-1", token_version: 0 }),
},
integrationBinding: {
findFirst: vi.fn().mockResolvedValue({ local_id: "department-1" }),
},
department: {
findUnique: vi.fn().mockResolvedValue({ id: "department-1", archived_at: null }),
},
employeeRole: { create: vi.fn().mockResolvedValue({}) },
auditLog: { create: vi.fn().mockResolvedValue({}) },
};
const prisma = { $transaction: vi.fn(async (callback) => callback(tx)) };
await provisionWecomOAuthEmployee({
prisma,
request: buildRequest(),
profile: {
externalUserId: "member-no-phone",
name: "缺手机号成员",
phone: "",
email: "[email protected]",
departmentExternalId: "1",
departmentName: "总部",
title: "员工",
},
});
expect(tx.employee.create).toHaveBeenCalledWith({
data: expect.objectContaining({ phone: null }),
select: expect.any(Object),
});
expect(tx.auditLog.create).toHaveBeenCalledWith({
data: expect.objectContaining({
extra: expect.objectContaining({ fieldPresence: { phone: false, email: true } }),
}),
});
});
it("rejects contact data that resolves to multiple OA employees", async () => {
const tx = {
wecomIdentityBinding: { findUnique: vi.fn().mockResolvedValue(null) },
employee: {
findMany: vi.fn().mockResolvedValue([
{ id: "employee-1", status: "active" },
{ id: "employee-2", status: "active" },
]),
},
};
const prisma = { $transaction: vi.fn(async (callback) => callback(tx)) };
await expect(provisionWecomOAuthEmployee({
prisma,
request: buildRequest(),
profile: {
externalUserId: "member-conflict",
name: "冲突成员",
phone: "19900000003",
email: "[email protected]",
departmentExternalId: "1",
departmentName: "总部",
title: "员工",
},
})).rejects.toBeInstanceOf(WecomOAuthOnboardingError);
});
it("does not rebind an OA employee that already belongs to another WeCom identity", async () => {
const tx = {
wecomIdentityBinding: { findUnique: vi.fn().mockResolvedValue(null) },
employee: {
findMany: vi.fn().mockResolvedValue([{
id: "employee-1",
status: "active",
wecom_identity_binding: { external_user_id: "another-member" },
}]),
},
};
const prisma = { $transaction: vi.fn(async (callback) => callback(tx)) };
await expect(provisionWecomOAuthEmployee({
prisma,
request: buildRequest(),
profile: {
externalUserId: "member-new",
name: "测试成员",
phone: "19900000004",
email: "[email protected]",
departmentExternalId: "1",
departmentName: "总部",
title: "员工",
},
})).rejects.toMatchObject({ code: "employee_conflict" });
});
it("completes an existing bound employee profile without changing roles", async () => {
const existing = {
id: "employee-1",
status: "active",
wecom_identity_binding: { external_user_id: "member-1" },
};
const tx = {
wecomIdentityBinding: {
findUnique: vi.fn().mockResolvedValue({ employee: existing }),
upsert: vi.fn().mockResolvedValue({ id: "binding-1" }),
},
employee: {
findMany: vi.fn().mockResolvedValue([existing]),
update: vi.fn().mockResolvedValue({ id: "employee-1", token_version: 3 }),
create: vi.fn(),
},
integrationBinding: {
findFirst: vi.fn().mockResolvedValue({ local_id: "department-1" }),
},
department: {
findUnique: vi.fn().mockResolvedValue({ id: "department-1", archived_at: null }),
},
employeeRole: { create: vi.fn() },
auditLog: { create: vi.fn().mockResolvedValue({ id: "audit-1" }) },
};
const prisma = { $transaction: vi.fn(async (callback) => callback(tx)) };
const result = await provisionWecomOAuthEmployee({
prisma,
request: buildRequest(),
profile: {
externalUserId: "member-1",
name: "测试成员",
phone: "19900000001",
email: "[email protected]",
departmentExternalId: "2",
departmentName: "研发部",
title: "工程师",
},
});
expect(result).toEqual({ id: "employee-1", token_version: 3, created: false });
expect(tx.employee.update).toHaveBeenCalled();
expect(tx.employee.create).not.toHaveBeenCalled();
expect(tx.employeeRole.create).not.toHaveBeenCalled();
expect(tx.auditLog.create).toHaveBeenCalledWith({
data: expect.objectContaining({ action: "AUTH_WECOM_PROFILE_COMPLETED" }),
});
});
});