163 lines
5.5 KiB
TypeScript
163 lines
5.5 KiB
TypeScript
/** @jest-environment node */
|
||
|
||
import {
|
||
processPerformanceSurveyNotification,
|
||
recoverDuePerformanceSurveyNotifications,
|
||
retryPerformanceSurveyNotification,
|
||
} from '@/lib/performance-survey-notifications'
|
||
import { prisma } from '@/lib/prisma'
|
||
import { sendDingtalkWorkNotice } from '@/lib/dingtalk-client'
|
||
|
||
jest.mock('@/lib/prisma', () => ({
|
||
prisma: {
|
||
performanceSurveyNotification: {
|
||
findUnique: jest.fn(),
|
||
update: jest.fn(),
|
||
updateMany: jest.fn(),
|
||
findMany: jest.fn(),
|
||
},
|
||
performanceSurveyEmployeeProfile: {
|
||
findUnique: jest.fn(),
|
||
},
|
||
user: {
|
||
findUnique: jest.fn(),
|
||
findFirst: jest.fn(),
|
||
},
|
||
},
|
||
}))
|
||
|
||
jest.mock('@/lib/dingtalk-client', () => ({
|
||
sendDingtalkWorkNotice: jest.fn(),
|
||
}))
|
||
|
||
jest.mock('@/lib/public-app-url', () => ({
|
||
getPublicAppOrigin: jest.fn(() => 'https://sc.jiansun.vip'),
|
||
}))
|
||
|
||
function notification(overrides: Record<string, unknown> = {}) {
|
||
return {
|
||
id: 'notification-1',
|
||
dingtalkUserId: null,
|
||
recipientUserId: null,
|
||
recipientEmployeeId: null,
|
||
title: '你有新的考核调研待填写',
|
||
content: '请填写考核调研',
|
||
actionUrl: 'https://sc.jiansun.vip/performance-survey/survey/survey-1',
|
||
status: 'pending',
|
||
attemptCount: 0,
|
||
maxAttempts: 5,
|
||
...overrides,
|
||
}
|
||
}
|
||
|
||
describe('performance survey notifications', () => {
|
||
beforeEach(() => {
|
||
jest.clearAllMocks()
|
||
jest.useFakeTimers()
|
||
;(prisma.performanceSurveyNotification.update as jest.Mock).mockResolvedValue({})
|
||
;(prisma.performanceSurveyNotification.updateMany as jest.Mock).mockResolvedValue({ count: 0 })
|
||
;(prisma.performanceSurveyNotification.findMany as jest.Mock).mockResolvedValue([])
|
||
;(prisma.performanceSurveyEmployeeProfile.findUnique as jest.Mock).mockResolvedValue(null)
|
||
;(prisma.user.findUnique as jest.Mock).mockResolvedValue(null)
|
||
;(prisma.user.findFirst as jest.Mock).mockResolvedValue(null)
|
||
;(sendDingtalkWorkNotice as jest.Mock).mockResolvedValue({
|
||
taskId: 'task-1',
|
||
raw: { errcode: 0, task_id: 'task-1' },
|
||
})
|
||
})
|
||
|
||
afterEach(() => {
|
||
jest.useRealTimers()
|
||
})
|
||
|
||
test('发送前会从素材库用户绑定补齐钉钉 userId', async () => {
|
||
;(prisma.performanceSurveyNotification.findUnique as jest.Mock).mockResolvedValue(
|
||
notification({ recipientUserId: 'user-1' }),
|
||
)
|
||
;(prisma.user.findUnique as jest.Mock).mockResolvedValue({ dingtalkUserId: 'ding-user-1' })
|
||
|
||
const result = await processPerformanceSurveyNotification('notification-1')
|
||
|
||
expect(result).toEqual({ success: true, status: 'succeeded' })
|
||
expect(sendDingtalkWorkNotice).toHaveBeenCalledWith({
|
||
userIds: ['ding-user-1'],
|
||
title: '你有新的考核调研待填写',
|
||
text: '请填写考核调研',
|
||
actionUrl: 'https://sc.jiansun.vip/performance-survey/survey/survey-1',
|
||
})
|
||
expect(prisma.performanceSurveyNotification.update).toHaveBeenCalledWith({
|
||
where: { id: 'notification-1' },
|
||
data: expect.objectContaining({ dingtalkUserId: 'ding-user-1' }),
|
||
})
|
||
})
|
||
|
||
test('人员档案缺 userId 时会通过邮箱匹配素材库用户', async () => {
|
||
;(prisma.performanceSurveyNotification.findUnique as jest.Mock).mockResolvedValue(
|
||
notification({ recipientEmployeeId: 'employee-1' }),
|
||
)
|
||
;(prisma.performanceSurveyEmployeeProfile.findUnique as jest.Mock).mockResolvedValue({
|
||
dingtalkUserId: null,
|
||
dingEmail: 'user@example.com',
|
||
})
|
||
;(prisma.user.findFirst as jest.Mock).mockResolvedValue({ dingtalkUserId: 'ding-user-2' })
|
||
|
||
await processPerformanceSurveyNotification('notification-1')
|
||
|
||
expect(sendDingtalkWorkNotice).toHaveBeenCalledWith(expect.objectContaining({
|
||
userIds: ['ding-user-2'],
|
||
}))
|
||
})
|
||
|
||
test('缺少钉钉 userId 时记录为 skipped,不调用钉钉接口', async () => {
|
||
;(prisma.performanceSurveyNotification.findUnique as jest.Mock).mockResolvedValue(notification())
|
||
|
||
const result = await processPerformanceSurveyNotification('notification-1')
|
||
|
||
expect(result).toEqual({ success: false, status: 'skipped' })
|
||
expect(sendDingtalkWorkNotice).not.toHaveBeenCalled()
|
||
expect(prisma.performanceSurveyNotification.update).toHaveBeenCalledWith({
|
||
where: { id: 'notification-1' },
|
||
data: expect.objectContaining({
|
||
status: 'skipped',
|
||
lastError: '收件人缺少钉钉 userId',
|
||
}),
|
||
})
|
||
})
|
||
|
||
test('手工补发会重置重试次数和历史发送结果', async () => {
|
||
await retryPerformanceSurveyNotification('notification-1')
|
||
|
||
expect(prisma.performanceSurveyNotification.update).toHaveBeenCalledWith({
|
||
where: { id: 'notification-1' },
|
||
data: expect.objectContaining({
|
||
status: 'pending',
|
||
attemptCount: 0,
|
||
maxAttempts: 5,
|
||
lastStatusCode: null,
|
||
lastResponseBody: null,
|
||
lastError: null,
|
||
providerMessageId: null,
|
||
sentAt: null,
|
||
}),
|
||
})
|
||
})
|
||
|
||
test('维护任务会把长时间 processing 的通知重新入队', async () => {
|
||
;(prisma.performanceSurveyNotification.findMany as jest.Mock).mockResolvedValue([{ id: 'notification-1' }])
|
||
|
||
const recovered = await recoverDuePerformanceSurveyNotifications()
|
||
|
||
expect(recovered).toBe(1)
|
||
expect(prisma.performanceSurveyNotification.updateMany).toHaveBeenCalledWith({
|
||
where: {
|
||
status: 'processing',
|
||
updatedAt: { lte: expect.any(Date) },
|
||
},
|
||
data: expect.objectContaining({
|
||
status: 'retrying',
|
||
lastError: '上次投递处理中断,已重新入队',
|
||
}),
|
||
})
|
||
})
|
||
})
|