文件
hyb-oa/server/tests/unit/services/integrations/dispatcher.test.js
T

196 行
6.1 KiB
JavaScript

const dispatcher = require("../../../../src/services/integrations/dispatcher");
const { clearWecomAppAccessTokenCache } = require("../../../../src/services/integrations/wecomAccessToken");
describe("Integration Dispatcher", () => {
afterEach(() => {
clearWecomAppAccessTokenCache();
vi.restoreAllMocks();
});
it("dispatches erp.reimbursement.paid", async () => {
const result = await dispatcher.dispatchOutboxEvent({
config: { erpSyncEnabled: true, erpSyncProvider: "mock" },
eventType: "erp.reimbursement.paid",
payload: { reimbursementId: "rb-1", formNumber: "BX-001" },
});
expect(result.channel).toBe("erp");
expect(result.code).toBe("mock_synced");
});
it("dispatches notification.im with mock channel", async () => {
const result = await dispatcher.dispatchOutboxEvent({
prisma: {
notificationChannel: {
findUnique: vi.fn().mockResolvedValue({
id: "channel-1",
channel_type: "wecom",
name: "test",
enabled: true,
config_json: { mode: "mock", routing: {} },
template_json: null,
}),
},
},
config: { imNotifierEnabled: true },
eventType: "notification.im",
payload: { channelId: "channel-1", channelType: "wecom", title: "title", body: "body" },
});
expect(result.channel).toBe("im");
expect(result.code).toBe("mock_sent");
});
it("marks encrypted secret decrypt failure as non-retryable IM dispatch error", async () => {
const encryptedSecret = "v1:bad:payload:value";
await expect(
dispatcher.dispatchOutboxEvent({
prisma: {
notificationChannel: {
findUnique: vi.fn().mockResolvedValue({
id: "channel-2",
channel_type: "wecom",
name: "test",
enabled: true,
config_json: {
mode: "wecom_webhook",
webhookUrl: "https://example.com/wecom",
secretEncrypted: encryptedSecret,
timeoutMs: 500,
routing: {},
},
template_json: null,
}),
},
},
config: {
imNotifierEnabled: true,
fieldEncryptionKey: Buffer.alloc(32, 7).toString("base64"),
},
eventType: "notification.im",
payload: { channelId: "channel-2", channelType: "wecom", title: "title", body: "body" },
}),
).rejects.toMatchObject({
name: "IntegrationDispatchError",
code: "wecom_secret_decrypt_failed",
retryable: false,
});
});
it("dispatches notification.im app message as no-bound when recipient has no wecom binding", async () => {
const prisma = {
notificationChannel: {
findUnique: vi.fn().mockResolvedValue({
id: "channel-app",
channel_type: "wecom",
name: "app-message",
enabled: true,
config_json: { mode: "wecom_app_message", routing: {} },
template_json: null,
}),
},
wecomIdentityBinding: {
findMany: vi.fn().mockResolvedValue([]),
},
};
const result = await dispatcher.dispatchOutboxEvent({
prisma,
config: { imNotifierEnabled: true },
eventType: "notification.im",
payload: {
channelId: "channel-app",
channelType: "wecom",
title: "title",
body: "body",
recipientEmployeeIds: ["employee-1"],
},
});
expect(result.channel).toBe("im");
expect(result.code).toBe("wecom_app_no_bound_recipients");
expect(prisma.wecomIdentityBinding.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: { employee_id: { in: ["employee-1"] } },
}),
);
});
it("dispatches notification.im app message to bound wecom member", async () => {
const fetchMock = vi
.fn()
.mockResolvedValueOnce({
ok: true,
status: 200,
json: vi.fn().mockResolvedValue({ errcode: 0, access_token: "token-1", expires_in: 7200 }),
})
.mockResolvedValueOnce({
ok: true,
status: 200,
json: vi.fn().mockResolvedValue({ errcode: 0, errmsg: "ok" }),
});
vi.stubGlobal("fetch", fetchMock);
const result = await dispatcher.dispatchOutboxEvent({
prisma: {
notificationChannel: {
findUnique: vi.fn().mockResolvedValue({
id: "channel-app",
channel_type: "wecom",
name: "app-message",
enabled: true,
config_json: { mode: "wecom_app_message", timeoutMs: 500, routing: {} },
template_json: null,
}),
},
wecomIdentityBinding: {
findMany: vi.fn().mockResolvedValue([{ external_user_id: "oa_smoke_02" }]),
},
},
config: {
imNotifierEnabled: true,
wecomCorpId: "corp-1",
wecomAgentId: "1000002",
wecomAgentSecret: "secret-1",
},
eventType: "notification.im",
payload: {
channelId: "channel-app",
channelType: "wecom",
title: "待办通知",
body: "请处理审批",
detailUrl: "https://oa.example.com/api/auth/deeplink?token=abc",
recipientEmployeeIds: ["employee-1"],
},
});
expect(result.channel).toBe("im");
expect(result.code).toBe("wecom_app_sent");
expect(fetchMock).toHaveBeenCalledTimes(2);
const sendBody = JSON.parse(fetchMock.mock.calls[1][1].body);
expect(sendBody).toEqual(
expect.objectContaining({
touser: "oa_smoke_02",
msgtype: "textcard",
agentid: 1000002,
textcard: expect.objectContaining({
title: "待办通知",
url: "https://oa.example.com/api/auth/deeplink?token=abc",
}),
}),
);
expect(String(fetchMock.mock.calls[1][0])).toContain("/cgi-bin/message/send");
expect(JSON.stringify(result.response)).not.toContain("token-1");
});
it("throws error for unsupported event", async () => {
await expect(
dispatcher.dispatchOutboxEvent({
config: {},
eventType: "unknown.event",
}),
).rejects.toThrow("unsupported_outbox_event:unknown.event");
});
});