141 lines
4.5 KiB
TypeScript
141 lines
4.5 KiB
TypeScript
import test from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
|
|
import {
|
|
DingTalkApiError,
|
|
getDingTalkConfigStatus,
|
|
getDingTalkRobotSendResult,
|
|
sendDingTalkRobotNotification
|
|
} from '../dingtalkClient'
|
|
|
|
const envKeys = [
|
|
'DINGTALK_APP_KEY',
|
|
'DINGTALK_APP_SECRET',
|
|
'DINGTALK_AGENT_ID',
|
|
'DINGTALK_ROBOT_CODE',
|
|
'DINGTALK_CORP_ID',
|
|
'DINGTALK_PUBLIC_BASE_URL',
|
|
'DINGTALK_SEND_ENABLED',
|
|
'DINGTALK_NOTIFICATION_CHANNEL'
|
|
] as const
|
|
|
|
const originalEnv = Object.fromEntries(envKeys.map((key) => [key, process.env[key]]))
|
|
const originalFetch = global.fetch
|
|
|
|
const configureDingTalk = () => {
|
|
process.env.DINGTALK_APP_KEY = 'ding-app-key'
|
|
process.env.DINGTALK_APP_SECRET = 'ding-app-secret'
|
|
process.env.DINGTALK_AGENT_ID = '10001'
|
|
process.env.DINGTALK_ROBOT_CODE = 'ding-robot-code'
|
|
process.env.DINGTALK_CORP_ID = 'ding-corp'
|
|
process.env.DINGTALK_PUBLIC_BASE_URL = 'https://asset.example.com'
|
|
process.env.DINGTALK_SEND_ENABLED = 'true'
|
|
process.env.DINGTALK_NOTIFICATION_CHANNEL = 'robot_oto'
|
|
}
|
|
|
|
test.after(() => {
|
|
global.fetch = originalFetch
|
|
for (const key of envKeys) {
|
|
if (originalEnv[key] === undefined) delete process.env[key]
|
|
else process.env[key] = originalEnv[key]
|
|
}
|
|
})
|
|
|
|
test('机器人单聊配置状态要求 RobotCode 且不返回任何密钥值', () => {
|
|
configureDingTalk()
|
|
const status = getDingTalkConfigStatus()
|
|
assert.deepEqual(status, {
|
|
configured: true,
|
|
channel: 'robot_oto',
|
|
send_enabled: true,
|
|
missing: []
|
|
})
|
|
assert.equal(JSON.stringify(status).includes('ding-app-secret'), false)
|
|
assert.equal(JSON.stringify(status).includes('ding-robot-code'), false)
|
|
})
|
|
|
|
test('机器人单聊使用 batchSend 并返回 processQueryKey', async () => {
|
|
configureDingTalk()
|
|
const calls: Array<{ url: string; init?: RequestInit }> = []
|
|
global.fetch = (async (url: string | URL | Request, init?: RequestInit) => {
|
|
calls.push({ url: String(url), init })
|
|
if (String(url).includes('/gettoken?')) {
|
|
return new Response(JSON.stringify({ errcode: 0, access_token: 'access-token', expires_in: 7200 }), {
|
|
status: 200,
|
|
headers: { 'Content-Type': 'application/json' }
|
|
})
|
|
}
|
|
return new Response(JSON.stringify({ processQueryKey: 'query-key-1' }), {
|
|
status: 200,
|
|
headers: { 'Content-Type': 'application/json' }
|
|
})
|
|
}) as typeof fetch
|
|
|
|
const taskId = await sendDingTalkRobotNotification({
|
|
userId: 'ding-user-1',
|
|
title: '报废审批提醒',
|
|
content: '有一条报废申请等待审批',
|
|
actionUrl: '/workflows/disposals/53'
|
|
})
|
|
|
|
assert.equal(taskId, 'query-key-1')
|
|
const sendCall = calls.find((call) => call.url.includes('/v1.0/robot/oToMessages/batchSend'))
|
|
assert.ok(sendCall)
|
|
assert.equal((sendCall.init?.headers as Record<string, string>)['x-acs-dingtalk-access-token'], 'access-token')
|
|
const body = JSON.parse(String(sendCall.init?.body))
|
|
assert.equal(body.robotCode, 'ding-robot-code')
|
|
assert.deepEqual(body.userIds, ['ding-user-1'])
|
|
assert.equal(body.msgKey, 'sampleActionCard')
|
|
const msgParam = JSON.parse(body.msgParam)
|
|
assert.equal(msgParam.singleTitle, '查看详情')
|
|
assert.equal(
|
|
msgParam.singleURL,
|
|
'https://asset.example.com/dingtalk-entry?redirect=%2Fworkflows%2Fdisposals%2F53'
|
|
)
|
|
})
|
|
|
|
test('机器人单聊通过 processQueryKey 查询已读状态', async () => {
|
|
configureDingTalk()
|
|
global.fetch = (async (url: string | URL | Request) => {
|
|
assert.ok(String(url).includes('/v1.0/robot/oToMessages/readStatus'))
|
|
return new Response(JSON.stringify({
|
|
sendStatus: 'success',
|
|
messageReadInfoList: [
|
|
{ userId: 'ding-read', readStatus: 'read' },
|
|
{ userId: 'ding-unread', readStatus: 'unread' }
|
|
]
|
|
}), {
|
|
status: 200,
|
|
headers: { 'Content-Type': 'application/json' }
|
|
})
|
|
}) as typeof fetch
|
|
|
|
const result = await getDingTalkRobotSendResult('query-key-1')
|
|
assert.deepEqual(result, {
|
|
read_user_id_list: ['ding-read'],
|
|
unread_user_id_list: ['ding-unread'],
|
|
delivery_failed: false
|
|
})
|
|
})
|
|
|
|
test('无效收件人会作为确定性错误停止重试', async () => {
|
|
configureDingTalk()
|
|
global.fetch = (async () => new Response(JSON.stringify({
|
|
invalidStaffIdList: ['ding-invalid']
|
|
}), {
|
|
status: 200,
|
|
headers: { 'Content-Type': 'application/json' }
|
|
})) as typeof fetch
|
|
|
|
await assert.rejects(
|
|
sendDingTalkRobotNotification({
|
|
userId: 'ding-invalid',
|
|
title: '通知',
|
|
content: '内容',
|
|
actionUrl: '/dashboard'
|
|
}),
|
|
(error: unknown) => error instanceof DingTalkApiError &&
|
|
error.code === 'INVALID_USER' && error.deterministic
|
|
)
|
|
})
|