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

291 行
7.6 KiB
JavaScript

const {
IntegrationDispatchError,
dispatchOutboxEvent,
} = require("../../src/services/integrations/dispatcher");
describe("dispatchOutboxEvent", () => {
const originalFetch = global.fetch;
afterEach(() => {
global.fetch = originalFetch;
vi.restoreAllMocks();
});
it("handles mock ERP success", async () => {
const result = await 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");
expect(result.response.externalId).toContain("BX-001");
});
it("loads IM channel and dispatches mock wecom message", async () => {
const prisma = {
notificationChannel: {
findUnique: vi.fn().mockResolvedValue({
id: "channel-1",
channel_type: "wecom",
name: "采购经理群",
enabled: true,
config_json: {
mode: "mock",
routing: {},
},
template_json: {
title: "{{title}}",
body: "{{body}}",
footer: "来自海音达工作台",
deepLinkLabel: "查看详情",
},
}),
},
};
const result = await dispatchOutboxEvent({
prisma,
config: {
imNotifierEnabled: true,
},
eventType: "notification.im",
payload: {
channelId: "channel-1",
channelType: "wecom",
notificationType: "workflow.task_assigned",
title: "【待办】采购申请待审批",
body: "采购申请已流转到你,请及时处理。",
},
});
expect(prisma.notificationChannel.findUnique).toHaveBeenCalledWith({
where: { id: "channel-1" },
select: {
id: true,
channel_type: true,
name: true,
enabled: true,
config_json: true,
template_json: true,
},
});
expect(result.channel).toBe("im");
expect(result.code).toBe("mock_sent");
expect(result.previewMessage).toContain("采购申请待审批");
});
it("safely skips missing IM channel", async () => {
const result = await dispatchOutboxEvent({
prisma: {
notificationChannel: {
findUnique: vi.fn().mockResolvedValue(null),
},
},
config: {
imNotifierEnabled: true,
},
eventType: "notification.im",
payload: {
channelId: "missing-channel",
channelType: "wecom",
},
});
expect(result.channel).toBe("im");
expect(result.code).toBe("channel_not_found");
expect(result.skipped).toBe(true);
});
it("safely skips disabled IM channel", async () => {
const result = await dispatchOutboxEvent({
prisma: {
notificationChannel: {
findUnique: vi.fn().mockResolvedValue({
id: "channel-2",
channel_type: "wecom",
name: "disabled",
enabled: false,
config_json: { mode: "mock", routing: {} },
template_json: null,
}),
},
},
config: {
imNotifierEnabled: true,
},
eventType: "notification.im",
payload: {
channelId: "channel-2",
channelType: "wecom",
},
});
expect(result.code).toBe("channel_disabled");
expect(result.skipped).toBe(true);
});
it("maps wecom 4xx as non-retryable IM dispatch error", async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 400,
json: vi.fn().mockResolvedValue({ errcode: 40013, errmsg: "bad request" }),
});
await expect(
dispatchOutboxEvent({
prisma: {
notificationChannel: {
findUnique: vi.fn().mockResolvedValue({
id: "channel-3",
channel_type: "wecom",
name: "wecom-4xx",
enabled: true,
config_json: {
mode: "wecom_webhook",
webhookUrl: "https://example.com/wecom",
timeoutMs: 500,
routing: {},
},
template_json: null,
}),
},
},
config: {
imNotifierEnabled: true,
},
eventType: "notification.im",
payload: {
channelId: "channel-3",
channelType: "wecom",
title: "test",
body: "body",
},
}),
).rejects.toMatchObject({
name: "IntegrationDispatchError",
code: "wecom_http_4xx",
retryable: false,
});
});
it("maps wecom 5xx as retryable IM dispatch error", async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 503,
json: vi.fn().mockResolvedValue({ errcode: 500, errmsg: "server error" }),
});
await expect(
dispatchOutboxEvent({
prisma: {
notificationChannel: {
findUnique: vi.fn().mockResolvedValue({
id: "channel-4",
channel_type: "wecom",
name: "wecom-5xx",
enabled: true,
config_json: {
mode: "wecom_webhook",
webhookUrl: "https://example.com/wecom",
timeoutMs: 500,
routing: {},
},
template_json: null,
}),
},
},
config: {
imNotifierEnabled: true,
},
eventType: "notification.im",
payload: {
channelId: "channel-4",
channelType: "wecom",
title: "test",
body: "body",
},
}),
).rejects.toMatchObject({
name: "IntegrationDispatchError",
code: "wecom_http_5xx",
retryable: true,
});
});
it("maps wecom timeout as retryable IM dispatch error", async () => {
global.fetch = vi.fn().mockRejectedValue({ name: "AbortError" });
await expect(
dispatchOutboxEvent({
prisma: {
notificationChannel: {
findUnique: vi.fn().mockResolvedValue({
id: "channel-5",
channel_type: "wecom",
name: "wecom-timeout",
enabled: true,
config_json: {
mode: "wecom_webhook",
webhookUrl: "https://example.com/wecom",
timeoutMs: 500,
routing: {},
},
template_json: null,
}),
},
},
config: {
imNotifierEnabled: true,
},
eventType: "notification.im",
payload: {
channelId: "channel-5",
channelType: "wecom",
title: "test",
body: "body",
},
}),
).rejects.toMatchObject({
name: "IntegrationDispatchError",
code: "wecom_timeout",
retryable: true,
});
});
it("throws non-retryable error for unsupported event", async () => {
await expect(
dispatchOutboxEvent({
config: { erpSyncEnabled: true, erpSyncProvider: "mock" },
eventType: "unsupported.event",
payload: {},
}),
).rejects.toMatchObject({
name: "IntegrationDispatchError",
code: "unsupported_outbox_event",
retryable: false,
});
});
it("preserves error class semantics", () => {
const error = new IntegrationDispatchError({
code: "unit_test",
message: "unit test",
retryable: true,
statusCode: 503,
});
expect(error).toBeInstanceOf(Error);
expect(error.code).toBe("unit_test");
expect(error.retryable).toBe(true);
expect(error.statusCode).toBe(503);
});
});