R50 通知推送:dispatcher 投递服务、业务事件接入与员工推送开关
这个提交包含在:
@@ -32,8 +32,45 @@ function buildChannelRecord(overrides = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function createPrismaState({ channels = [] } = {}) {
|
||||
function buildNotificationRecord(overrides = {}) {
|
||||
const now = new Date('2026-03-13T09:00:00.000Z');
|
||||
return {
|
||||
id: '22222222-2222-4222-8222-222222222222',
|
||||
recipient_employee_id: 'employee-2',
|
||||
type: 'workflow.task_assigned',
|
||||
title: '【待办】采购申请待审批',
|
||||
body: '请及时处理审批任务。',
|
||||
target_type: 'workflow_task',
|
||||
target_id: '33333333-3333-4333-8333-333333333333',
|
||||
read_at: null,
|
||||
created_at: now,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function buildDeliveryRecord(overrides = {}) {
|
||||
const notification = overrides.notification ?? buildNotificationRecord();
|
||||
const channel = overrides.channel ?? buildChannelRecord();
|
||||
const now = new Date('2026-03-13T10:00:00.000Z');
|
||||
return {
|
||||
id: '44444444-4444-4444-8444-444444444444',
|
||||
notification_id: notification.id,
|
||||
channel_id: channel.id,
|
||||
status: 'success',
|
||||
attempt_no: 1,
|
||||
result_code: 'mock_sent',
|
||||
error_message: null,
|
||||
response_body: { mode: 'mock' },
|
||||
attempted_at: now,
|
||||
notification,
|
||||
channel,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function createPrismaState({ channels = [], deliveries = [] } = {}) {
|
||||
const stateChannels = channels.map((channel) => ({ ...channel }));
|
||||
const stateDeliveries = deliveries.map((delivery) => ({ ...delivery }));
|
||||
const auditEntries = [];
|
||||
|
||||
const notificationChannel = {
|
||||
@@ -117,6 +154,26 @@ function createPrismaState({ channels = [] } = {}) {
|
||||
return {
|
||||
prisma: {
|
||||
notificationChannel,
|
||||
notificationDelivery: {
|
||||
findMany: vi.fn().mockImplementation(async ({ where, take }) => {
|
||||
let items = [...stateDeliveries];
|
||||
if (where?.notification_id) {
|
||||
items = items.filter((item) => item.notification_id === where.notification_id);
|
||||
}
|
||||
if (where?.channel_id) {
|
||||
items = items.filter((item) => item.channel_id === where.channel_id);
|
||||
}
|
||||
|
||||
items.sort((left, right) => {
|
||||
const timeDiff = right.attempted_at.getTime() - left.attempted_at.getTime();
|
||||
if (timeDiff !== 0) {
|
||||
return timeDiff;
|
||||
}
|
||||
return Number(right.attempt_no ?? 0) - Number(left.attempt_no ?? 0);
|
||||
});
|
||||
return items.slice(0, take ?? items.length);
|
||||
}),
|
||||
},
|
||||
auditLog: {
|
||||
create: vi.fn().mockImplementation(async ({ data }) => {
|
||||
auditEntries.push(data);
|
||||
@@ -443,4 +500,79 @@ describe('notifications channel routes', () => {
|
||||
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it('lists grouped delivery summaries with latest status and attempt count', async () => {
|
||||
const notification = buildNotificationRecord({
|
||||
id: '55555555-5555-4555-8555-555555555555',
|
||||
title: '【待办】采购申请 CG-001 待审批',
|
||||
});
|
||||
const channel = buildChannelRecord();
|
||||
const { prisma } = createPrismaState({
|
||||
channels: [channel],
|
||||
deliveries: [
|
||||
buildDeliveryRecord({
|
||||
id: 'delivery-2',
|
||||
notification,
|
||||
channel,
|
||||
notification_id: notification.id,
|
||||
channel_id: channel.id,
|
||||
status: 'failed',
|
||||
attempt_no: 2,
|
||||
result_code: 'wecom_timeout',
|
||||
error_message: 'timeout',
|
||||
attempted_at: new Date('2026-03-13T10:02:00.000Z'),
|
||||
}),
|
||||
buildDeliveryRecord({
|
||||
id: 'delivery-1',
|
||||
notification,
|
||||
channel,
|
||||
notification_id: notification.id,
|
||||
channel_id: channel.id,
|
||||
status: 'failed',
|
||||
attempt_no: 1,
|
||||
result_code: 'wecom_http_429',
|
||||
error_message: 'rate limit',
|
||||
attempted_at: new Date('2026-03-13T10:01:00.000Z'),
|
||||
}),
|
||||
],
|
||||
});
|
||||
const app = await buildApp({ prisma });
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/api/notifications/deliveries?status=failed',
|
||||
headers: { authorization: 'Bearer test-token' },
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json().data.items).toEqual([
|
||||
expect.objectContaining({
|
||||
notificationId: notification.id,
|
||||
channelId: channel.id,
|
||||
channelName: channel.name,
|
||||
notificationTitle: notification.title,
|
||||
status: 'failed',
|
||||
attemptCount: 2,
|
||||
resultCode: 'wecom_timeout',
|
||||
}),
|
||||
]);
|
||||
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it('rejects delivery list without notification.channel.manage permission', async () => {
|
||||
const { prisma } = createPrismaState();
|
||||
const app = await buildApp({ prisma, permissionCodes: [] });
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/api/notifications/deliveries',
|
||||
headers: { authorization: 'Bearer test-token' },
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(403);
|
||||
expect(response.json().error.code).toBe('PERM_001');
|
||||
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
在新工单中引用
屏蔽一个用户