274 行
7.3 KiB
JavaScript
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");
|
|
});
|
|
});
|