文件
hyb-oa/server/tests/unit/wecomLoginAutoBinding.service.test.js
T
wangsli 9b12451b85
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s
实现企微首次免密自动绑定(07-28-all-account-login-readiness)
2026-07-28 11:18:50 +08:00

274 行
7.3 KiB
JavaScript

const {
WecomLoginAutoBindingError,
autoBindExistingWecomEmployee,
fetchWecomLoginIdentity,
} = require("../../src/services/integrations/wecomLoginAutoBinding");
const EMPLOYEE_ID = "11111111-1111-4111-8111-111111111111";
function buildRequest() {
return {
id: "request-1",
ip: "127.0.0.1",
headers: { "user-agent": "test-agent" },
};
}
function buildPrisma({
employees,
useridBinding = null,
employeeBinding = null,
} = {}) {
const tx = {
employee: {
findMany: vi.fn().mockResolvedValue(
employees ?? [
{
id: EMPLOYEE_ID,
employee_no: "E0001",
email: "[email protected]",
status: "active",
archived_at: null,
token_version: 3,
},
],
),
},
wecomIdentityBinding: {
findUnique: vi
.fn()
.mockResolvedValueOnce(useridBinding)
.mockResolvedValueOnce(employeeBinding),
create: vi.fn().mockResolvedValue({ id: "binding-1" }),
},
auditLog: { create: vi.fn().mockResolvedValue({ id: "audit-1" }) },
};
return {
tx,
prisma: { $transaction: vi.fn(async (callback) => callback(tx)) },
};
}
describe("WeCom passwordless login auto binding", () => {
it("reads and normalizes the enterprise email for the OAuth userid", async () => {
const fetchImpl = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
errcode: 0,
userid: "wecom-user-1",
status: 1,
biz_mail: "[email protected]",
}),
});
await expect(
fetchWecomLoginIdentity({
accessToken: "access-token",
externalUserId: "wecom-user-1",
fetchImpl,
}),
).resolves.toEqual({
externalUserId: "wecom-user-1",
email: "[email protected]",
});
expect(String(fetchImpl.mock.calls[0][0])).toContain("/cgi-bin/user/get");
});
it.each([
[
"inactive member",
{
errcode: 0,
userid: "wecom-user-1",
status: 2,
biz_mail: "[email protected]",
},
"wecom_member_inactive",
],
[
"identity mismatch",
{
errcode: 0,
userid: "another-user",
status: 1,
biz_mail: "[email protected]",
},
"wecom_identity_mismatch",
],
])("fails closed for %s", async (_label, payload, code) => {
const fetchImpl = vi
.fn()
.mockResolvedValue({ ok: true, json: async () => payload });
await expect(
fetchWecomLoginIdentity({
accessToken: "access-token",
externalUserId: "wecom-user-1",
fetchImpl,
}),
).rejects.toMatchObject({ code });
});
it("keeps userid matching available when the directory profile has no enterprise email", async () => {
const fetchImpl = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ errcode: 0, userid: "wecom-user-1", status: 1 }),
});
await expect(
fetchWecomLoginIdentity({
accessToken: "access-token",
externalUserId: "wecom-user-1",
fetchImpl,
}),
).resolves.toEqual({ externalUserId: "wecom-user-1", email: null });
});
it("binds a unique active OA employee by enterprise email and writes a redacted audit", async () => {
const { prisma, tx } = buildPrisma();
const result = await autoBindExistingWecomEmployee({
prisma,
request: buildRequest(),
identity: {
externalUserId: "wecom-user-1",
email: "[email protected]",
},
externalOpenUserId: "open-user-1",
});
expect(result).toEqual({
id: EMPLOYEE_ID,
status: "active",
token_version: 3,
autoBound: true,
});
expect(tx.employee.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: {
OR: [
{ employee_no: "wecom-user-1" },
{
email: {
equals: "[email protected]",
mode: "insensitive",
},
},
],
},
}),
);
expect(tx.wecomIdentityBinding.create).toHaveBeenCalledWith({
data: expect.objectContaining({
employee_id: EMPLOYEE_ID,
external_user_id: "wecom-user-1",
external_open_user_id: "open-user-1",
}),
});
expect(tx.auditLog.create).toHaveBeenCalledWith({
data: expect.objectContaining({
action: "AUTH_WECOM_AUTO_BOUND",
extra: { matchBy: "email", emailMatched: true },
}),
});
expect(JSON.stringify(tx.auditLog.create.mock.calls)).not.toContain(
"[email protected]",
);
expect(JSON.stringify(tx.auditLog.create.mock.calls)).not.toContain(
"wecom-user-1",
);
});
it("fails closed when userid and email point to different OA employees", async () => {
const { prisma, tx } = buildPrisma({
employees: [
{
id: EMPLOYEE_ID,
employee_no: "wecom-user-1",
email: "[email protected]",
status: "active",
archived_at: null,
token_version: 0,
},
{
id: "22222222-2222-4222-8222-222222222222",
employee_no: "E0002",
email: "[email protected]",
status: "active",
archived_at: null,
token_version: 0,
},
],
});
await expect(
autoBindExistingWecomEmployee({
prisma,
request: buildRequest(),
identity: {
externalUserId: "wecom-user-1",
email: "[email protected]",
},
}),
).rejects.toMatchObject({ code: "oa_employee_conflict" });
expect(tx.wecomIdentityBinding.create).not.toHaveBeenCalled();
expect(tx.auditLog.create).not.toHaveBeenCalled();
});
it("binds by exact userid when the OA employee number already carries the userid", async () => {
const { prisma, tx } = buildPrisma({
employees: [
{
id: EMPLOYEE_ID,
employee_no: "wecom-user-1",
email: "[email protected]",
status: "active",
archived_at: null,
token_version: 1,
},
],
});
await expect(
autoBindExistingWecomEmployee({
prisma,
request: buildRequest(),
identity: { externalUserId: "wecom-user-1", email: null },
}),
).resolves.toEqual(
expect.objectContaining({ id: EMPLOYEE_ID, autoBound: true }),
);
expect(tx.auditLog.create).toHaveBeenCalledWith({
data: expect.objectContaining({
extra: { matchBy: "userid", emailMatched: false },
}),
});
});
it("does not overwrite an OA employee binding for another userid", async () => {
const { prisma, tx } = buildPrisma({
employeeBinding: { external_user_id: "another-wecom-user" },
});
await expect(
autoBindExistingWecomEmployee({
prisma,
request: buildRequest(),
identity: {
externalUserId: "wecom-user-1",
email: "[email protected]",
},
}),
).rejects.toMatchObject({ code: "oa_employee_binding_conflict" });
expect(tx.wecomIdentityBinding.create).not.toHaveBeenCalled();
});
it("exposes only stable error codes", () => {
const error = new WecomLoginAutoBindingError(
"oa_employee_not_found",
"internal detail",
);
expect(error.code).toBe("oa_employee_not_found");
});
});