2026-08-28 22:24:32 +08:00

81 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
})