364 行
10 KiB
JavaScript
364 行
10 KiB
JavaScript
const { dispatchNotificationBatch } = require('../../../src/services/notificationDispatcher');
|
|
|
|
function buildChannel(overrides = {}) {
|
|
return {
|
|
id: 'channel-1',
|
|
channel_type: 'wecom',
|
|
name: '审批通知群',
|
|
enabled: true,
|
|
config_json: {
|
|
mode: 'mock',
|
|
webhookUrl: null,
|
|
timeoutMs: 5000,
|
|
routing: {
|
|
roleCodes: [],
|
|
domainScopes: [],
|
|
allowedTypes: [],
|
|
},
|
|
},
|
|
template_json: {
|
|
title: '{{title}}',
|
|
body: '{{body}}',
|
|
footer: '来自海音达工作台',
|
|
deepLinkLabel: '查看详情',
|
|
},
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function createPrismaState({ employees = [], channels = [] } = {}) {
|
|
const deliveryState = [];
|
|
|
|
return {
|
|
prisma: {
|
|
employee: {
|
|
findMany: vi.fn().mockImplementation(async ({ where }) => {
|
|
const ids = new Set(where?.id?.in ?? []);
|
|
return employees
|
|
.filter((item) => ids.has(item.id))
|
|
.filter((item) => item.status === 'active')
|
|
.filter((item) => item.notification_push_enabled !== false)
|
|
.map((item) => ({
|
|
id: item.id,
|
|
roles: (item.roles ?? []).map((roleCode) => ({ role_code: roleCode })),
|
|
}));
|
|
}),
|
|
},
|
|
notificationChannel: {
|
|
findMany: vi.fn().mockResolvedValue(channels),
|
|
},
|
|
notificationDelivery: {
|
|
create: vi.fn().mockImplementation(async ({ data }) => {
|
|
const row = {
|
|
id: `delivery-${deliveryState.length + 1}`,
|
|
...data,
|
|
};
|
|
deliveryState.push(row);
|
|
return row;
|
|
}),
|
|
update: vi.fn().mockImplementation(async ({ where, data }) => {
|
|
const current = deliveryState.find((item) => item.id === where.id);
|
|
Object.assign(current, data);
|
|
return current;
|
|
}),
|
|
},
|
|
},
|
|
deliveryState,
|
|
};
|
|
}
|
|
|
|
describe('notificationDispatcher', () => {
|
|
afterEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('dispatches a mock channel and persists a success attempt', async () => {
|
|
const { prisma, deliveryState } = createPrismaState({
|
|
employees: [
|
|
{ id: 'manager-1', status: 'active', notification_push_enabled: true, roles: ['manager'] },
|
|
],
|
|
channels: [buildChannel()],
|
|
});
|
|
const sendMessage = vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
retryable: false,
|
|
code: 'mock_sent',
|
|
message: null,
|
|
response: { mode: 'mock' },
|
|
});
|
|
|
|
const result = await dispatchNotificationBatch({
|
|
prisma,
|
|
config: {
|
|
imNotifierEnabled: true,
|
|
appOrigin: 'https://oa.example.com',
|
|
jwtSecret: 'test-secret',
|
|
},
|
|
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
requestId: 'req-1',
|
|
notifications: [
|
|
{
|
|
id: 'notification-1',
|
|
recipient_employee_id: 'manager-1',
|
|
target_type: 'workflow_task',
|
|
},
|
|
],
|
|
type: 'workflow.task_assigned',
|
|
title: '测试待办',
|
|
body: '请处理审批',
|
|
targetType: 'workflow_task',
|
|
targetId: 'task-1',
|
|
targetPath: '/workflow/tasks/task-1',
|
|
sendMessage,
|
|
retryDelayMs: 0,
|
|
});
|
|
|
|
expect(sendMessage).toHaveBeenCalledTimes(1);
|
|
expect(deliveryState).toHaveLength(1);
|
|
expect(deliveryState[0]).toEqual(
|
|
expect.objectContaining({
|
|
notification_id: 'notification-1',
|
|
channel_id: 'channel-1',
|
|
status: 'success',
|
|
result_code: 'mock_sent',
|
|
}),
|
|
);
|
|
expect(result).toEqual(
|
|
expect.objectContaining({
|
|
matchedChannelCount: 1,
|
|
eligibleRecipientCount: 1,
|
|
deliveredChannels: [
|
|
expect.objectContaining({
|
|
channelId: 'channel-1',
|
|
status: 'success',
|
|
}),
|
|
],
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('retries retryable failures until a later success', async () => {
|
|
const { prisma, deliveryState } = createPrismaState({
|
|
employees: [
|
|
{ id: 'manager-1', status: 'active', notification_push_enabled: true, roles: ['manager'] },
|
|
],
|
|
channels: [buildChannel()],
|
|
});
|
|
const sendMessage = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
ok: false,
|
|
retryable: true,
|
|
code: 'wecom_timeout',
|
|
message: 'timeout',
|
|
response: null,
|
|
})
|
|
.mockResolvedValueOnce({
|
|
ok: false,
|
|
retryable: true,
|
|
code: 'wecom_http_429',
|
|
message: 'rate limited',
|
|
response: null,
|
|
})
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
retryable: false,
|
|
code: 'wecom_sent',
|
|
message: null,
|
|
response: { errcode: 0 },
|
|
});
|
|
|
|
const result = await dispatchNotificationBatch({
|
|
prisma,
|
|
config: {
|
|
imNotifierEnabled: true,
|
|
appOrigin: 'https://oa.example.com',
|
|
jwtSecret: 'test-secret',
|
|
},
|
|
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
requestId: 'req-2',
|
|
notifications: [
|
|
{
|
|
id: 'notification-1',
|
|
recipient_employee_id: 'manager-1',
|
|
target_type: 'workflow_task',
|
|
},
|
|
],
|
|
type: 'workflow.task_assigned',
|
|
title: '测试待办',
|
|
body: '请处理审批',
|
|
targetType: 'workflow_task',
|
|
targetId: 'task-1',
|
|
sendMessage,
|
|
retryDelayMs: 0,
|
|
});
|
|
|
|
expect(sendMessage).toHaveBeenCalledTimes(3);
|
|
expect(deliveryState).toHaveLength(3);
|
|
expect(deliveryState.map((item) => item.status)).toEqual(['failed', 'failed', 'success']);
|
|
expect(result.deliveredChannels[0]).toEqual(
|
|
expect.objectContaining({
|
|
status: 'success',
|
|
attemptNo: 3,
|
|
resultCode: 'wecom_sent',
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('does not retry when provider marks the failure as non-retryable', async () => {
|
|
const { prisma, deliveryState } = createPrismaState({
|
|
employees: [
|
|
{ id: 'manager-1', status: 'active', notification_push_enabled: true, roles: ['manager'] },
|
|
],
|
|
channels: [buildChannel()],
|
|
});
|
|
const sendMessage = vi.fn().mockResolvedValue({
|
|
ok: false,
|
|
retryable: false,
|
|
code: 'wecom_api_error',
|
|
message: 'invalid webhook',
|
|
response: { errcode: 93000 },
|
|
});
|
|
|
|
const result = await dispatchNotificationBatch({
|
|
prisma,
|
|
config: {
|
|
imNotifierEnabled: true,
|
|
appOrigin: 'https://oa.example.com',
|
|
jwtSecret: 'test-secret',
|
|
},
|
|
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
requestId: 'req-3',
|
|
notifications: [
|
|
{
|
|
id: 'notification-1',
|
|
recipient_employee_id: 'manager-1',
|
|
target_type: 'workflow_task',
|
|
},
|
|
],
|
|
type: 'workflow.task_assigned',
|
|
title: '测试待办',
|
|
body: '请处理审批',
|
|
targetType: 'workflow_task',
|
|
targetId: 'task-1',
|
|
sendMessage,
|
|
retryDelayMs: 0,
|
|
});
|
|
|
|
expect(sendMessage).toHaveBeenCalledTimes(1);
|
|
expect(deliveryState).toHaveLength(1);
|
|
expect(result.deliveredChannels[0]).toEqual(
|
|
expect.objectContaining({
|
|
status: 'failed',
|
|
attemptNo: 1,
|
|
resultCode: 'wecom_api_error',
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('skips dispatch when all recipients opted out of external push', async () => {
|
|
const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
|
|
const { prisma, deliveryState } = createPrismaState({
|
|
employees: [
|
|
{ id: 'manager-1', status: 'active', notification_push_enabled: false, roles: ['manager'] },
|
|
],
|
|
channels: [buildChannel()],
|
|
});
|
|
const sendMessage = vi.fn();
|
|
|
|
const result = await dispatchNotificationBatch({
|
|
prisma,
|
|
config: {
|
|
imNotifierEnabled: true,
|
|
appOrigin: 'https://oa.example.com',
|
|
jwtSecret: 'test-secret',
|
|
},
|
|
logger,
|
|
requestId: 'req-4',
|
|
notifications: [
|
|
{
|
|
id: 'notification-1',
|
|
recipient_employee_id: 'manager-1',
|
|
target_type: 'workflow_task',
|
|
},
|
|
],
|
|
type: 'workflow.task_assigned',
|
|
title: '测试待办',
|
|
body: '请处理审批',
|
|
targetType: 'workflow_task',
|
|
targetId: 'task-1',
|
|
sendMessage,
|
|
retryDelayMs: 0,
|
|
});
|
|
|
|
expect(sendMessage).not.toHaveBeenCalled();
|
|
expect(deliveryState).toHaveLength(0);
|
|
expect(result).toEqual(
|
|
expect.objectContaining({
|
|
skippedReason: 'no_eligible_recipients',
|
|
}),
|
|
);
|
|
expect(logger.info).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
notificationType: 'workflow.task_assigned',
|
|
skippedReason: 'no_eligible_recipients',
|
|
}),
|
|
'Notification dispatch skipped',
|
|
);
|
|
});
|
|
|
|
it('skips when no channel matches recipient roles and notification scope', async () => {
|
|
const { prisma, deliveryState } = createPrismaState({
|
|
employees: [
|
|
{ id: 'employee-1', status: 'active', notification_push_enabled: true, roles: ['employee'] },
|
|
],
|
|
channels: [
|
|
buildChannel({
|
|
config_json: {
|
|
mode: 'mock',
|
|
webhookUrl: null,
|
|
timeoutMs: 5000,
|
|
routing: {
|
|
roleCodes: ['manager'],
|
|
domainScopes: ['workflow'],
|
|
allowedTypes: ['workflow.task_assigned'],
|
|
},
|
|
},
|
|
}),
|
|
],
|
|
});
|
|
|
|
const result = await dispatchNotificationBatch({
|
|
prisma,
|
|
config: {
|
|
imNotifierEnabled: true,
|
|
appOrigin: 'https://oa.example.com',
|
|
jwtSecret: 'test-secret',
|
|
},
|
|
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
requestId: 'req-5',
|
|
notifications: [
|
|
{
|
|
id: 'notification-1',
|
|
recipient_employee_id: 'employee-1',
|
|
target_type: 'workflow_task',
|
|
},
|
|
],
|
|
type: 'workflow.task_assigned',
|
|
title: '测试待办',
|
|
body: '请处理审批',
|
|
targetType: 'workflow_task',
|
|
targetId: 'task-1',
|
|
retryDelayMs: 0,
|
|
});
|
|
|
|
expect(deliveryState).toHaveLength(0);
|
|
expect(result).toEqual(
|
|
expect.objectContaining({
|
|
skippedReason: 'no_matching_channel',
|
|
eligibleRecipientCount: 1,
|
|
}),
|
|
);
|
|
});
|
|
});
|