文件
hyb-oa/server/tests/unit/integrations.dispatcher.test.js
T
wangsli 1f8d5b658a 完善借款、编号、付款与预算并发及验收(09-06-release-gate-integrity)
汇总已落盘的借款本金与还款、兼容历史编号的周期化编号、付款责任与回单核验、报销进度和排序、PWA/采购事务、错误原因与日期校验及ERP退役。
统一共享预算池锁序和锁后判断,恢复差旅原预算选择,补原因码固定夹具及反向完整性检查。
纳入配套迁移、测试、受管QA、分层验收合同、规约及任务记录;原始证据留在ignored artifacts。

关联任务:09-17-company-loan-principal-repayment、09-17-erp-exclusive-retirement、09-18-reimbursement-periodic-numbering、09-08-reimbursement-payment-closeout、09-18-local-concurrency-product-fixes、09-19-budget-pool-concurrency、09-05-error-explainability-payment-readiness。
预算补审由Claude定向验证2文件928项通过,原4446/28及r6结果仍仅对应各自历史输入;本轮未重跑测试。
完整新候选门禁、R1真实入口接入与12项公网义务仍开放。生产编号未启用、差旅仍冻结;不推送、不部署,不宣称发布资格。
2026-09-19 13:11:04 +08:00

296 行
7.9 KiB
JavaScript

const {
IntegrationDispatchError,
dispatchOutboxEvent,
} = require('../../src/services/integrations/dispatcher');
describe('dispatchOutboxEvent', () => {
const originalFetch = global.fetch;
afterEach(() => {
global.fetch = originalFetch;
vi.restoreAllMocks();
});
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' }),
headers: { get: () => 'application/json' },
});
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' }),
headers: { get: () => 'application/json' },
});
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('maps encrypted secret decrypt failure as non-retryable IM dispatch error', async () => {
await expect(
dispatchOutboxEvent({
prisma: {
notificationChannel: {
findUnique: vi.fn().mockResolvedValue({
id: 'channel-6',
channel_type: 'wecom',
name: 'wecom-secret-bad',
enabled: true,
config_json: {
mode: 'wecom_webhook',
webhookUrl: 'https://example.com/wecom',
secretEncrypted: 'v1:bad:payload:value',
timeoutMs: 500,
routing: {},
},
template_json: null,
}),
},
},
config: {
imNotifierEnabled: true,
fieldEncryptionKey: Buffer.alloc(32, 7).toString('base64'),
},
eventType: 'notification.im',
payload: {
channelId: 'channel-6',
channelType: 'wecom',
title: 'test',
body: 'body',
},
}),
).rejects.toMatchObject({
name: 'IntegrationDispatchError',
code: 'wecom_secret_decrypt_failed',
retryable: false,
});
});
it('throws error for unsupported event', async () => {
await expect(
dispatchOutboxEvent({
config: {},
eventType: 'unknown.event',
}),
).rejects.toThrow('unsupported_outbox_event:unknown.event');
});
});