328 lines
12 KiB
TypeScript
328 lines
12 KiB
TypeScript
/** @jest-environment node */
|
|
|
|
describe('dingtalk client', () => {
|
|
const originalEnv = process.env
|
|
|
|
beforeEach(() => {
|
|
jest.resetModules()
|
|
process.env = { ...originalEnv }
|
|
delete process.env.DINGTALK_CLIENT_ID
|
|
delete process.env.DINGTALK_CLIENT_SECRET
|
|
delete process.env.DINGTALK_WORK_NOTICE_CLIENT_ID
|
|
delete process.env.DINGTALK_WORK_NOTICE_CLIENT_SECRET
|
|
delete process.env.DINGTALK_WORK_NOTICE_AGENT_ID
|
|
delete process.env.DINGTALK_ROBOT_CODE
|
|
delete process.env.DINGTALK_PERFORMANCE_SURVEY_NOTIFICATION_CHANNEL
|
|
delete process.env.DINGTALK_ROBOT_OTO_TEST_USER_IDS
|
|
delete process.env.YYD_NOTIFICATION_CENTER_TEST_USER_IDS
|
|
delete process.env.DINGTALK_WORK_NOTICE_CORP_ID
|
|
delete process.env.DINGTALK_CORP_ID
|
|
delete process.env.DINGTALK_CORPID
|
|
delete process.env.NEXT_PUBLIC_DINGTALK_CORP_ID
|
|
global.fetch = jest.fn()
|
|
})
|
|
|
|
afterEach(() => {
|
|
process.env = originalEnv
|
|
jest.restoreAllMocks()
|
|
})
|
|
|
|
test('工作通知优先使用独立应用凭证获取 token', async () => {
|
|
process.env.DINGTALK_CLIENT_ID = 'login-client-id'
|
|
process.env.DINGTALK_CLIENT_SECRET = 'login-client-secret'
|
|
process.env.DINGTALK_WORK_NOTICE_CLIENT_ID = 'notice-client-id'
|
|
process.env.DINGTALK_WORK_NOTICE_CLIENT_SECRET = 'notice-client-secret'
|
|
process.env.DINGTALK_WORK_NOTICE_AGENT_ID = '4279191869'
|
|
|
|
;(global.fetch as jest.Mock)
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ errcode: 0, access_token: 'notice-token', expires_in: 7200 }),
|
|
})
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ errcode: 0, task_id: 123, request_id: 'request-1' }),
|
|
})
|
|
|
|
const { sendDingtalkWorkNotice } = await import('@/lib/dingtalk-client')
|
|
|
|
await sendDingtalkWorkNotice({
|
|
userIds: ['ding-user-1'],
|
|
title: '测试通知',
|
|
text: '这是一条测试通知',
|
|
})
|
|
|
|
expect(global.fetch).toHaveBeenNthCalledWith(
|
|
1,
|
|
expect.stringContaining('appkey=notice-client-id'),
|
|
)
|
|
expect(global.fetch).toHaveBeenNthCalledWith(
|
|
1,
|
|
expect.not.stringContaining('appkey=login-client-id'),
|
|
)
|
|
const [, sendOptions] = (global.fetch as jest.Mock).mock.calls[1]
|
|
expect(JSON.parse(sendOptions.body)).toEqual(expect.objectContaining({
|
|
agent_id: 4279191869,
|
|
userid_list: 'ding-user-1',
|
|
}))
|
|
})
|
|
|
|
test('工作通知未配置独立凭证时仍可复用登录应用凭证', async () => {
|
|
process.env.DINGTALK_CLIENT_ID = 'login-client-id'
|
|
process.env.DINGTALK_CLIENT_SECRET = 'login-client-secret'
|
|
process.env.DINGTALK_WORK_NOTICE_AGENT_ID = '123'
|
|
|
|
const { getDingtalkWorkNoticeConfigStatus } = await import('@/lib/dingtalk-client')
|
|
|
|
expect(getDingtalkWorkNoticeConfigStatus()).toEqual(expect.objectContaining({
|
|
ready: true,
|
|
appSource: 'login',
|
|
}))
|
|
})
|
|
|
|
test('工作通知免登入口优先使用工作通知应用 clientId', async () => {
|
|
process.env.DINGTALK_CLIENT_ID = 'login-client-id'
|
|
process.env.DINGTALK_CLIENT_SECRET = 'login-client-secret'
|
|
process.env.DINGTALK_WORK_NOTICE_CLIENT_ID = 'notice-client-id'
|
|
process.env.DINGTALK_WORK_NOTICE_CLIENT_SECRET = 'notice-client-secret'
|
|
|
|
const { getDingtalkWorkNoticeClientId } = await import('@/lib/dingtalk-client')
|
|
|
|
expect(getDingtalkWorkNoticeClientId()).toBe('notice-client-id')
|
|
})
|
|
|
|
test('带跳转地址的工作通知优先使用钉钉应用内打开链接', async () => {
|
|
process.env.DINGTALK_CLIENT_ID = 'login-client-id'
|
|
process.env.DINGTALK_CLIENT_SECRET = 'login-client-secret'
|
|
process.env.DINGTALK_WORK_NOTICE_AGENT_ID = '4279191869'
|
|
process.env.DINGTALK_CORP_ID = 'ding-test-corp'
|
|
|
|
;(global.fetch as jest.Mock)
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ errcode: 0, access_token: 'notice-token', expires_in: 7200 }),
|
|
})
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ errcode: 0, task_id: 123, request_id: 'request-1' }),
|
|
})
|
|
|
|
const { sendDingtalkWorkNotice } = await import('@/lib/dingtalk-client')
|
|
|
|
await sendDingtalkWorkNotice({
|
|
userIds: ['ding-user-1'],
|
|
title: '测试通知',
|
|
text: '这是一条测试通知',
|
|
actionUrl: 'https://sc.jiansun.vip/dingtalk-entry?redirect=%2Fperformance-survey',
|
|
actionTitle: '立即填写',
|
|
})
|
|
|
|
const [, sendOptions] = (global.fetch as jest.Mock).mock.calls[1]
|
|
const body = JSON.parse(sendOptions.body)
|
|
expect(body.msg.msgtype).toBe('action_card')
|
|
expect(body.msg.action_card.title).toBe('测试通知')
|
|
expect(body.msg.action_card.markdown).toBe('这是一条测试通知')
|
|
expect(body.msg.action_card.single_title).toBe('立即填写')
|
|
expect(body.msg.action_card.single_url).toContain('dingtalk://dingtalkclient/action/openapp')
|
|
expect(body.msg.action_card.single_url).toContain('corpid=ding-test-corp')
|
|
expect(body.msg.action_card.single_url).toContain('app_id=0_4279191869')
|
|
expect(body.msg.action_card.single_url).toContain(encodeURIComponent('https://sc.jiansun.vip/dingtalk-entry?redirect=%2Fperformance-survey'))
|
|
})
|
|
|
|
test('工作通知发送结果会区分已读和未读人员', async () => {
|
|
process.env.DINGTALK_CLIENT_ID = 'login-client-id'
|
|
process.env.DINGTALK_CLIENT_SECRET = 'login-client-secret'
|
|
process.env.DINGTALK_WORK_NOTICE_AGENT_ID = '4279191869'
|
|
|
|
;(global.fetch as jest.Mock)
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ errcode: 0, access_token: 'notice-token', expires_in: 7200 }),
|
|
})
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({
|
|
errcode: 0,
|
|
send_result: {
|
|
read_user_id_list: ['ding-read'],
|
|
unread_user_id_list: ['ding-unread'],
|
|
failed_user_id_list: ['ding-failed'],
|
|
},
|
|
}),
|
|
})
|
|
|
|
const { getDingtalkWorkNoticeSendResult } = await import('@/lib/dingtalk-client')
|
|
const result = await getDingtalkWorkNoticeSendResult('task-1')
|
|
|
|
expect(result.readUserIds).toEqual(['ding-read'])
|
|
expect(result.unreadUserIds).toEqual(['ding-unread'])
|
|
expect(result.failedUserIds).toEqual(['ding-failed'])
|
|
const [, options] = (global.fetch as jest.Mock).mock.calls[1]
|
|
expect(JSON.parse(options.body)).toEqual({
|
|
agent_id: 4279191869,
|
|
task_id: 'task-1',
|
|
})
|
|
})
|
|
|
|
test('机器人单聊使用 RobotCode 和 userIds 发送 ActionCard', async () => {
|
|
process.env.DINGTALK_CLIENT_ID = 'login-client-id'
|
|
process.env.DINGTALK_CLIENT_SECRET = 'login-client-secret'
|
|
process.env.DINGTALK_ROBOT_CODE = 'ding-robot-code'
|
|
|
|
;(global.fetch as jest.Mock)
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ errcode: 0, access_token: 'robot-token', expires_in: 7200 }),
|
|
})
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({
|
|
processQueryKey: 'query-1',
|
|
invalidStaffIdList: [],
|
|
flowControlledStaffIdList: [],
|
|
}),
|
|
})
|
|
|
|
const { sendDingtalkRobotOtoMessage } = await import('@/lib/dingtalk-client')
|
|
const result = await sendDingtalkRobotOtoMessage({
|
|
userIds: ['ding-user-1'],
|
|
title: '测试通知',
|
|
text: '这是一条测试通知',
|
|
actionUrl: 'https://sc.jiansun.vip/performance-survey',
|
|
actionTitle: '立即填写',
|
|
})
|
|
|
|
expect(result.processQueryKey).toBe('query-1')
|
|
expect(global.fetch).toHaveBeenNthCalledWith(
|
|
2,
|
|
'https://api.dingtalk.com/v1.0/robot/oToMessages/batchSend',
|
|
expect.objectContaining({
|
|
method: 'POST',
|
|
headers: expect.objectContaining({
|
|
'x-acs-dingtalk-access-token': 'robot-token',
|
|
}),
|
|
}),
|
|
)
|
|
const [, options] = (global.fetch as jest.Mock).mock.calls[1]
|
|
const body = JSON.parse(options.body)
|
|
expect(body).toEqual(expect.objectContaining({
|
|
robotCode: 'ding-robot-code',
|
|
userIds: ['ding-user-1'],
|
|
msgKey: 'sampleActionCard',
|
|
}))
|
|
expect(JSON.parse(body.msgParam)).toEqual({
|
|
title: '测试通知',
|
|
text: '这是一条测试通知',
|
|
singleTitle: '立即填写',
|
|
singleURL: 'https://sc.jiansun.vip/performance-survey',
|
|
})
|
|
})
|
|
|
|
test('机器人单聊已读查询会区分已读和未读人员', async () => {
|
|
process.env.DINGTALK_CLIENT_ID = 'login-client-id'
|
|
process.env.DINGTALK_CLIENT_SECRET = 'login-client-secret'
|
|
process.env.DINGTALK_ROBOT_CODE = 'ding-robot-code'
|
|
|
|
;(global.fetch as jest.Mock)
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ errcode: 0, access_token: 'robot-token', expires_in: 7200 }),
|
|
})
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({
|
|
sendStatus: 'success',
|
|
messageReadInfoList: [
|
|
{ userId: 'ding-read', readStatus: 'read' },
|
|
{ userId: 'ding-unread', readStatus: 'unread' },
|
|
],
|
|
}),
|
|
})
|
|
|
|
const { getDingtalkRobotOtoReadResult } = await import('@/lib/dingtalk-client')
|
|
const result = await getDingtalkRobotOtoReadResult('query-1')
|
|
|
|
expect(result.readUserIds).toEqual(['ding-read'])
|
|
expect(result.unreadUserIds).toEqual(['ding-unread'])
|
|
const [url, options] = (global.fetch as jest.Mock).mock.calls[1]
|
|
expect(url).toContain('/v1.0/robot/oToMessages/readStatus?')
|
|
expect(url).toContain('robotCode=ding-robot-code')
|
|
expect(url).toContain('processQueryKey=query-1')
|
|
expect(options).toEqual(expect.objectContaining({ method: 'GET' }))
|
|
})
|
|
|
|
test('机器人单聊唯一收件人被过滤时不会误报成功', async () => {
|
|
process.env.DINGTALK_CLIENT_ID = 'login-client-id'
|
|
process.env.DINGTALK_CLIENT_SECRET = 'login-client-secret'
|
|
process.env.DINGTALK_ROBOT_CODE = 'ding-robot-code'
|
|
|
|
;(global.fetch as jest.Mock)
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ errcode: 0, access_token: 'robot-token', expires_in: 7200 }),
|
|
})
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({
|
|
processQueryKey: 'query-1',
|
|
invalidStaffIdList: [],
|
|
filteredStaffIdList: ['ding-user-1'],
|
|
flowControlledStaffIdList: [],
|
|
}),
|
|
})
|
|
|
|
const { sendDingtalkRobotOtoMessage } = await import('@/lib/dingtalk-client')
|
|
|
|
await expect(sendDingtalkRobotOtoMessage({
|
|
userIds: ['ding-user-1'],
|
|
title: '测试通知',
|
|
text: '这是一条测试通知',
|
|
})).rejects.toThrow('收件人被钉钉过滤')
|
|
})
|
|
|
|
test('机器人单聊不会把两套应用的半套凭证拼成已就绪', async () => {
|
|
process.env.DINGTALK_WORK_NOTICE_CLIENT_ID = 'notice-client-id'
|
|
process.env.DINGTALK_CLIENT_SECRET = 'login-client-secret'
|
|
process.env.DINGTALK_ROBOT_CODE = 'ding-robot-code'
|
|
|
|
const { getDingtalkRobotOtoConfigStatus } = await import('@/lib/dingtalk-client')
|
|
|
|
expect(getDingtalkRobotOtoConfigStatus()).toEqual(expect.objectContaining({
|
|
ready: false,
|
|
appConfigured: false,
|
|
appSource: 'workNotice',
|
|
}))
|
|
})
|
|
|
|
test('机器人单聊灰度白名单只允许指定人员切换通道', async () => {
|
|
process.env.DINGTALK_PERFORMANCE_SURVEY_NOTIFICATION_CHANNEL = 'robot_oto'
|
|
process.env.DINGTALK_ROBOT_OTO_TEST_USER_IDS = 'ding-user-1, ding-user-2,ding-user-1'
|
|
|
|
const {
|
|
getDingtalkPerformanceSurveyNotificationChannelForUser,
|
|
getDingtalkRobotOtoTestUserIds,
|
|
} = await import('@/lib/dingtalk-client')
|
|
|
|
expect(getDingtalkRobotOtoTestUserIds()).toEqual(['ding-user-1', 'ding-user-2'])
|
|
expect(getDingtalkPerformanceSurveyNotificationChannelForUser('ding-user-1')).toBe('robot_oto')
|
|
expect(getDingtalkPerformanceSurveyNotificationChannelForUser('ding-user-3')).toBe('work_notice')
|
|
})
|
|
|
|
test('机器人单聊灰度白名单留空时按全量通道处理', async () => {
|
|
process.env.DINGTALK_PERFORMANCE_SURVEY_NOTIFICATION_CHANNEL = 'robot_oto'
|
|
|
|
const { getDingtalkPerformanceSurveyNotificationChannelForUser } = await import('@/lib/dingtalk-client')
|
|
|
|
expect(getDingtalkPerformanceSurveyNotificationChannelForUser('ding-user-1')).toBe('robot_oto')
|
|
})
|
|
|
|
test('统一通知中心白名单留空时对所有钉钉账户生效', async () => {
|
|
process.env.DINGTALK_PERFORMANCE_SURVEY_NOTIFICATION_CHANNEL = 'notification_center'
|
|
|
|
const { getDingtalkPerformanceSurveyNotificationChannelForUser } = await import('@/lib/dingtalk-client')
|
|
|
|
expect(getDingtalkPerformanceSurveyNotificationChannelForUser('ding-user-1')).toBe('notification_center')
|
|
expect(getDingtalkPerformanceSurveyNotificationChannelForUser('ding-user-2')).toBe('notification_center')
|
|
})
|
|
})
|