81 lines
3.3 KiB
TypeScript
81 lines
3.3 KiB
TypeScript
import assert from 'node:assert/strict'
|
||
import test from 'node:test'
|
||
import { loadProjectConfigs } from '../config'
|
||
import { createCanonicalSignature, safeEqual } from '../signature'
|
||
|
||
test('每个项目必须使用独立且足够长的密钥', () => {
|
||
const projects = loadProjectConfigs(JSON.stringify([
|
||
{ code: 'fixed_asset', name: '固定资产', secret: '123456789012345678901234', allowedHosts: ['asset.example.com'] },
|
||
]))
|
||
assert.equal(projects.get('fixed_asset')?.name, '固定资产')
|
||
assert.throws(() => loadProjectConfigs(JSON.stringify([
|
||
{ code: 'bad', name: '错误项目', secret: 'short', allowedHosts: ['example.com'] },
|
||
])))
|
||
assert.throws(() => loadProjectConfigs(JSON.stringify([
|
||
{ code: 'bad', name: '错误项目', secret: '123456789012345678901234', allowedHosts: ['example.com:444'] },
|
||
])))
|
||
})
|
||
|
||
test('项目可登记唯一花名并解析到钉钉 userId', () => {
|
||
const project = loadProjectConfigs(JSON.stringify([{
|
||
code: 'fixed_asset',
|
||
name: '固定资产',
|
||
secret: '123456789012345678901234',
|
||
allowedHosts: ['asset.example.com'],
|
||
recipients: [{ alias: '海棠', userId: 'ding-user-haitang' }],
|
||
}])).get('fixed_asset')
|
||
assert.equal(project?.recipientAliases.get('海棠'), 'ding-user-haitang')
|
||
assert.throws(() => loadProjectConfigs(JSON.stringify([{
|
||
code: 'fixed_asset',
|
||
name: '固定资产',
|
||
secret: '123456789012345678901234',
|
||
allowedHosts: ['asset.example.com'],
|
||
recipients: [
|
||
{ alias: 'Alice', userId: 'ding-user-1' },
|
||
{ alias: 'ALICE', userId: 'ding-user-2' },
|
||
],
|
||
}])), /收件人花名重复/)
|
||
})
|
||
|
||
test('免登配置必须使用项目域名、标准 HTTPS 回调和内部路径白名单', () => {
|
||
const project = loadProjectConfigs(JSON.stringify([{
|
||
code: 'fixed_asset',
|
||
name: '固定资产',
|
||
secret: '123456789012345678901234',
|
||
allowedHosts: ['asset.example.com'],
|
||
login: {
|
||
enabled: true,
|
||
callbackUrl: 'https://asset.example.com/api/dingtalk/broker-login',
|
||
allowedRedirectPathPrefixes: ['/workflows/disposals/', '/dashboard'],
|
||
},
|
||
}])).get('fixed_asset')
|
||
assert.deepEqual(project?.login?.allowedRedirectPathPrefixes, ['/workflows/disposals', '/dashboard'])
|
||
|
||
for (const callbackUrl of [
|
||
'http://asset.example.com/api/dingtalk/broker-login',
|
||
'https://evil.example.com/api/dingtalk/broker-login',
|
||
'https://asset.example.com:444/api/dingtalk/broker-login',
|
||
'https://user:pass@asset.example.com/api/dingtalk/broker-login',
|
||
]) {
|
||
assert.throws(() => loadProjectConfigs(JSON.stringify([{
|
||
code: 'fixed_asset', name: '固定资产', secret: '123456789012345678901234', allowedHosts: ['asset.example.com'],
|
||
login: { enabled: true, callbackUrl, allowedRedirectPathPrefixes: ['/workflows/disposals'] },
|
||
}])))
|
||
}
|
||
})
|
||
|
||
test('签名包含项目密钥、时间、nonce、方法、路径和正文', () => {
|
||
const input = {
|
||
secret: '123456789012345678901234',
|
||
timestamp: '1784102400',
|
||
nonce: 'nonce_1234567890abcdef',
|
||
method: 'POST',
|
||
path: '/v1/notifications',
|
||
body: JSON.stringify({ title: '测试' }),
|
||
}
|
||
const signature = createCanonicalSignature(input)
|
||
assert.equal(signature.length, 64)
|
||
assert.equal(safeEqual(signature, createCanonicalSignature(input)), true)
|
||
assert.equal(safeEqual(signature, createCanonicalSignature({ ...input, path: '/v1/other' })), false)
|
||
})
|