feat: align with upstream 1.5.9.9

This commit is contained in:
2026-09-12 18:21:11 +08:00
commit 34f0679117
805 changed files with 376275 additions and 0 deletions
+71
View File
@@ -0,0 +1,71 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import {
canBindOAuthIdentity,
isAllowedDuringPasswordChange,
isPublicApiPath,
shouldBlockDuringPasswordChange,
shouldBypassPublicApiAuthentication
} from '../../server/utils/auth-route-policy.ts'
test('匿名用户可以访问公共接口', () => {
assert.equal(isPublicApiPath('/api/songs/public', 'GET'), true)
assert.equal(shouldBypassPublicApiAuthentication('/api/songs/public', 'GET', false), true)
})
test('OAuth 注册选项接口对匿名请求放行(binding-token 由接口自身校验)', () => {
assert.equal(isPublicApiPath('/api/auth/oauth-register-options', 'GET'), true)
assert.equal(isPublicApiPath('/api/auth/oauth-register-options', 'POST'), false)
})
test('携带登录态访问公共接口时必须经过认证检查', () => {
assert.equal(shouldBypassPublicApiAuthentication('/api/songs/public', 'GET', true), false)
assert.equal(shouldBypassPublicApiAuthentication('/api/site-config', 'GET', true), false)
assert.equal(shouldBypassPublicApiAuthentication('/api/proxy/image', 'GET', true), false)
})
test('受保护接口不能绕过认证', () => {
assert.equal(shouldBypassPublicApiAuthentication('/api/admin/users', 'GET', false), false)
assert.equal(shouldBypassPublicApiAuthentication('/api/admin/users', 'GET', true), false)
})
test('强制改密期间只放行必要的认证接口', () => {
assert.equal(isAllowedDuringPasswordChange('/api/auth/change-password', 'POST'), true)
assert.equal(isAllowedDuringPasswordChange('/api/auth/set-initial-password', 'POST'), true)
assert.equal(isAllowedDuringPasswordChange('/api/auth/logout', 'POST'), true)
assert.equal(isAllowedDuringPasswordChange('/api/site-config', 'GET'), true)
assert.equal(isAllowedDuringPasswordChange('/api/songs/public', 'GET'), false)
assert.equal(isAllowedDuringPasswordChange('/api/admin/system-settings', 'GET'), false)
})
test('公共 API 路径匹配不会接受相似但未注册的路径', () => {
assert.equal(isPublicApiPath('/api/auth/loginX', 'POST'), false)
assert.equal(isPublicApiPath('/api/site-config.evil', 'GET'), false)
assert.equal(isPublicApiPath('/api/site-config/extra', 'GET'), false)
assert.equal(isPublicApiPath('/api/auth/webauthn/login/options', 'POST'), true)
assert.equal(isAllowedDuringPasswordChange('/api/auth/change-passwordX', 'POST'), false)
})
test('公共 API 只放行已注册的 HTTP 方法', () => {
assert.equal(isPublicApiPath('/api/auth/login', 'POST'), true)
assert.equal(isPublicApiPath('/api/auth/login', 'GET'), false)
assert.equal(isPublicApiPath('/api/site-config', 'GET'), true)
assert.equal(isPublicApiPath('/api/site-config', 'POST'), false)
assert.equal(isPublicApiPath('/api/music/state', 'POST'), true)
assert.equal(isPublicApiPath('/api/music/state', 'GET'), false)
})
test('强制改密门控放行公共 API,但阻止业务写入和 OAuth provider 路由', () => {
assert.equal(shouldBlockDuringPasswordChange('/api/site-config', 'GET', true), false)
assert.equal(shouldBlockDuringPasswordChange('/api/sys/time', 'GET', true), false)
assert.equal(shouldBlockDuringPasswordChange('/api/music/state', 'POST', true), false)
assert.equal(shouldBlockDuringPasswordChange('/api/auth/github', 'GET', true), true)
assert.equal(shouldBlockDuringPasswordChange('/api/auth/github/callback', 'GET', true), true)
assert.equal(shouldBlockDuringPasswordChange('/api/admin/users', 'GET', true), true)
assert.equal(shouldBlockDuringPasswordChange('/api/admin/users', 'GET', false), false)
})
test('强制改密状态禁止持久化 OAuth 身份绑定', () => {
assert.equal(canBindOAuthIdentity(true), false)
assert.equal(canBindOAuthIdentity(false), true)
})
@@ -0,0 +1,108 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { resolveAvatarSource } from '../../server/utils/user-avatar.ts'
test('清除聚合登录绑定:准确识别聚合登录身份并排除其他提供商', () => {
const identities = [
{ id: 1, userId: 101, provider: 'aggregate:qq', providerUserId: 'qq_123' },
{ id: 2, userId: 101, provider: 'aggregate:wx', providerUserId: 'wx_456' },
{ id: 3, userId: 101, provider: 'github', providerUserId: 'gh_789' },
{ id: 4, userId: 102, provider: 'aggregate', providerUserId: 'agg_999' },
{ id: 5, userId: 102, provider: 'casdoor', providerUserId: 'cas_888' },
{ id: 6, userId: 103, provider: 'google', providerUserId: 'goog_777' }
]
const isAggregate = (provider: string) => provider === 'aggregate' || provider.startsWith('aggregate:')
const targetIdentities = identities.filter((i) => isAggregate(i.provider))
const remainingIdentities = identities.filter((i) => !isAggregate(i.provider))
assert.equal(targetIdentities.length, 3)
assert.deepEqual(
targetIdentities.map((i) => i.id),
[1, 2, 4]
)
const affectedUserIds = Array.from(new Set(targetIdentities.map((i) => i.userId)))
assert.deepEqual(affectedUserIds, [101, 102])
assert.equal(remainingIdentities.length, 3)
assert.deepEqual(
remainingIdentities.map((i) => i.id),
[3, 5, 6]
)
})
test('清除聚合登录绑定:当用户头像来源为聚合登录且拥有其他第三方身份时,正确回退头像', () => {
const user = {
id: 101,
avatarProvider: 'aggregate:qq',
avatarProviderUserId: 'qq_123'
}
const allUserIdentities = [
{
id: 1,
userId: 101,
provider: 'aggregate:qq',
providerUserId: 'qq_123',
avatar: 'https://example.com/qq.png',
createdAt: new Date('2024-01-01')
},
{
id: 2,
userId: 101,
provider: 'github',
providerUserId: 'gh_789',
avatar: 'https://example.com/gh.png',
createdAt: new Date('2024-01-02')
}
]
const targetKeys = new Set(['aggregate:qq:qq_123'])
const remainingIdentities = allUserIdentities.filter(
(i) => !targetKeys.has(`${i.provider}:${i.providerUserId}`)
)
const isAvatarSourceDeleted =
user.avatarProvider &&
(user.avatarProvider === 'aggregate' ||
user.avatarProvider.startsWith('aggregate:') ||
targetKeys.has(`${user.avatarProvider}:${user.avatarProviderUserId}`))
assert.equal(isAvatarSourceDeleted, true)
const fallback = resolveAvatarSource(user, remainingIdentities)
assert.deepEqual(fallback, {
provider: 'github',
providerUserId: 'gh_789',
url: 'https://example.com/gh.png'
})
})
test('清除聚合登录绑定:当用户头像来源为聚合登录且无其他第三方身份时,头像源置为 null', () => {
const user = {
id: 102,
avatarProvider: 'aggregate',
avatarProviderUserId: 'agg_999'
}
const allUserIdentities = [
{
id: 4,
userId: 102,
provider: 'aggregate',
providerUserId: 'agg_999',
avatar: 'https://example.com/agg.png',
createdAt: new Date('2024-01-01')
}
]
const targetKeys = new Set(['aggregate:agg_999'])
const remainingIdentities = allUserIdentities.filter(
(i) => !targetKeys.has(`${i.provider}:${i.providerUserId}`)
)
const fallback = resolveAvatarSource(user, remainingIdentities)
assert.equal(fallback, null)
})
+63
View File
@@ -0,0 +1,63 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { isTrustedOrigin, normalizeOrigin } from '../../server/utils/request-utils.ts'
test('允许 HTTPS 来源访问按 HTTP 配置的同主机内部 API', () => {
const source = normalizeOrigin('https://voicehub.hyzx1915.top', 'http:')
const trusted = normalizeOrigin('http://voicehub.hyzx1915.top', 'http:')
assert.equal(isTrustedOrigin(source, trusted), true)
})
test('同主机但协议降级仍按跨域拦截', () => {
const source = normalizeOrigin('http://voicehub.hyzx1915.top', 'https:')
const trusted = normalizeOrigin('https://voicehub.hyzx1915.top', 'https:')
assert.equal(isTrustedOrigin(source, trusted), false)
})
test('主机名或端口不同不能视为可信来源', () => {
assert.equal(
isTrustedOrigin(
normalizeOrigin('https://voicehub.hyzx1915.top', 'http:'),
normalizeOrigin('http://evil.example.com', 'http:')
),
false
)
assert.equal(
isTrustedOrigin(
normalizeOrigin('https://voicehub.hyzx1915.top:8443', 'http:'),
normalizeOrigin('http://voicehub.hyzx1915.top:9443', 'http:')
),
false
)
})
test('协议升级时保留显式端口匹配', () => {
assert.equal(
isTrustedOrigin(
normalizeOrigin('https://voicehub.hyzx1915.top:8443', 'http:'),
normalizeOrigin('http://voicehub.hyzx1915.top:8443', 'http:')
),
true
)
})
test('相同来源仍然可信', () => {
assert.equal(
isTrustedOrigin(
normalizeOrigin('https://voicehub.hyzx1915.top', 'https:'),
normalizeOrigin('https://voicehub.hyzx1915.top', 'https:')
),
true
)
})
test('EdgeOne 实际转发 Host 可作为同源校验依据', () => {
const source = normalizeOrigin('https://preview.edgeone.app', 'https:')
const configured = normalizeOrigin('https://voicehub.example.com', 'https:')
const forwardedHost = normalizeOrigin('https://preview.edgeone.app', 'https:')
assert.equal(isTrustedOrigin(source, configured), false)
assert.equal(isTrustedOrigin(source, forwardedHost), true)
})
+32
View File
@@ -0,0 +1,32 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { parseClassInput } from '../../app/utils/grade-class-input.ts'
test('数字区间生成对应数量的班名', () => {
assert.deepEqual(parseClassInput('1-3'), ['1班', '2班', '3班'])
assert.deepEqual(parseClassInput('1-1'), ['1班'])
})
test('逗号分隔与区间可混合使用并去重', () => {
const result = parseClassInput('一班,3-4,一班')
assert.deepEqual(result, ['一班', '3班', '4班'])
})
test('支持中文/英文逗号、换行、分号分隔', () => {
assert.deepEqual(parseClassInput('一班,二班\n三班;4-5'), ['一班', '二班', '三班', '4班', '5班'])
})
test('非法区间按字面保留', () => {
assert.deepEqual(parseClassInput('5-3'), ['5-3'])
assert.deepEqual(parseClassInput('一-三'), ['一-三'])
})
test('超长区间按字面保留(上限 100 防呆)', () => {
const result = parseClassInput('1-101')
assert.deepEqual(result, ['1-101'])
})
test('空输入与非字符串返回空数组', () => {
assert.deepEqual(parseClassInput(''), [])
assert.deepEqual(parseClassInput(undefined), [])
})
+45
View File
@@ -0,0 +1,45 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import {
resolveGradeClassOptions,
smartSort
} from '../../server/utils/grade-class-core.ts'
test('配置非空时优先使用配置项', () => {
const config = [{ grade: '高一', class: '一班' }]
const fallback = [{ grade: '高二', class: '二班' }]
assert.deepEqual(resolveGradeClassOptions(config, fallback), config)
})
test('配置为空时回退到用户提取并按学段排序', () => {
const config = []
const fallback = [
{ grade: '高二', class: '二班' },
{ grade: '高一', class: '一班' }
]
const result = resolveGradeClassOptions(config, fallback)
assert.deepEqual(result.map((item) => item.grade), ['高一', '高二'])
})
test('过滤年级或班级为空的数据项', () => {
const items = [
{ grade: '高一', class: '一班' },
{ grade: '', class: '二班' },
{ grade: '高一', class: '' }
]
const result = resolveGradeClassOptions(items, [])
assert.equal(result.length, 1)
assert.deepEqual(result[0], { grade: '高一', class: '一班' })
})
test('smartSort 按学段权重排序,未知学段按字典序', () => {
assert.equal(smartSort('初一', '高一') < 0, true)
assert.equal(smartSort('高一', '高二') < 0, true)
assert.equal(smartSort('教师', '高一') > 0, true)
const values = ['十年级', '二年级']
values.sort(smartSort)
assert.deepEqual(values, ['二年级', '十年级'])
})
@@ -0,0 +1,93 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import {
canSendSystemNotification,
createNotificationSenderSnapshot,
createNotificationReadUpdate,
createNotificationUserDeleteUpdate,
resolveImportantFlag,
resolveNotificationSource,
shouldCheckImportantNotification,
shouldDeliverSystemNotification,
shouldRetainNotificationHistory,
serializeNotificationSender
} from '../../server/utils/important-notification-policy.ts'
test('未登录、无用户或强制改密时不检查重要通知', () => {
assert.equal(shouldCheckImportantNotification(false, null), false)
assert.equal(shouldCheckImportantNotification(true, null), false)
assert.equal(shouldCheckImportantNotification(true, 1, true), false)
assert.equal(shouldCheckImportantNotification(true, 1), true)
})
test('关闭通知使用服务端已读更新并刷新更新时间', () => {
const updatedAt = new Date('2026-07-29T12:00:00.000Z')
assert.deepEqual(createNotificationReadUpdate(updatedAt), { read: true, updatedAt })
})
test('用户删除通知时保留记录并同步标记为已读', () => {
const updatedAt = new Date('2026-07-29T12:30:00.000Z')
assert.deepEqual(createNotificationUserDeleteUpdate(updatedAt), {
userDeleted: true,
read: true,
updatedAt
})
})
test('重要通知绕过普通通知开关,普通通知保持原策略', () => {
assert.equal(shouldDeliverSystemNotification(false, false), false)
assert.equal(shouldDeliverSystemNotification(false, true), true)
assert.equal(shouldDeliverSystemNotification(true, false), true)
})
test('只有后台手工发送的通知需要在用户删除后保留历史', () => {
assert.equal(shouldRetainNotificationHistory('SYSTEM_NOTICE', 'ADMIN_MANUAL'), true)
assert.equal(shouldRetainNotificationHistory('SYSTEM_NOTICE', 'SYSTEM'), false)
assert.equal(shouldRetainNotificationHistory('SONG_PLAYED', 'SYSTEM'), false)
})
test('只有管理员角色可以发送系统通知', () => {
assert.equal(canSendSystemNotification('ADMIN'), true)
assert.equal(canSendSystemNotification('SUPER_ADMIN'), true)
assert.equal(canSendSystemNotification('SONG_ADMIN'), false)
assert.equal(canSendSystemNotification('USER'), false)
})
test('important 只接受布尔值,缺省值为 false', () => {
assert.equal(resolveImportantFlag(undefined), false)
assert.equal(resolveImportantFlag(true), true)
assert.equal(resolveImportantFlag(false), false)
assert.equal(resolveImportantFlag('true'), null)
assert.equal(resolveImportantFlag(1), null)
})
test('系统通知不保存发送人,管理员通知保存发送时的名称快照', () => {
const systemSender = createNotificationSenderSnapshot()
assert.deepEqual(systemSender, {
senderId: null,
senderName: null,
senderUsername: null
})
assert.equal(serializeNotificationSender(systemSender), null)
assert.equal(resolveNotificationSource(null), 'SYSTEM')
const adminSender = createNotificationSenderSnapshot({
id: 7,
name: ' 张老师 ',
username: ' admin '
})
assert.deepEqual(adminSender, {
senderId: 7,
senderName: '张老师',
senderUsername: 'admin'
})
assert.deepEqual(serializeNotificationSender(adminSender), {
id: 7,
name: '张老师',
username: 'admin'
})
assert.equal(
resolveNotificationSource({ id: 7, name: '张老师', username: 'admin' }),
'ADMIN_MANUAL'
)
})
@@ -0,0 +1,86 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import {
canSetInitialPassword,
getPasswordSetupState
} from '../../server/utils/initial-password-policy.ts'
test('历史有密码用户应进入普通改密流程', () => {
assert.deepEqual(
getPasswordSetupState(
{ password: 'existing-hash', passwordChangedAt: null },
false
),
{ hasSetPassword: true, needsInitialPasswordSetup: false }
)
})
test('首次强制改密用户应进入初始设置流程', () => {
assert.deepEqual(
getPasswordSetupState(
{ password: 'temporary-hash', passwordChangedAt: null },
true
),
{ hasSetPassword: true, needsInitialPasswordSetup: true }
)
})
test('无密码 OAuth 用户应进入初始设置流程', () => {
assert.deepEqual(
getPasswordSetupState(
{ password: null, passwordChangedAt: null },
false
),
{ hasSetPassword: false, needsInitialPasswordSetup: true }
)
})
test('已完成密码设置的用户应进入普通改密流程', () => {
assert.deepEqual(
getPasswordSetupState(
{ password: 'existing-hash', passwordChangedAt: new Date() },
true
),
{ hasSetPassword: true, needsInitialPasswordSetup: false }
)
})
test('强制改密账号可以设置初始密码', () => {
assert.equal(
canSetInitialPassword(
{ requirePasswordChange: true },
{ password: 'existing-hash', passwordChangedAt: null }
),
true
)
})
test('真正无密码的账号可以设置初始密码', () => {
assert.equal(
canSetInitialPassword(
{ requirePasswordChange: false },
{ password: null, passwordChangedAt: null }
),
true
)
})
test('非强制状态的有密码账号不能免旧密码设置', () => {
assert.equal(
canSetInitialPassword(
{ requirePasswordChange: false },
{ password: 'existing-hash', passwordChangedAt: null }
),
false
)
})
test('已经完成密码设置的账号不能重复使用初始设置接口', () => {
assert.equal(
canSetInitialPassword(
{ requirePasswordChange: true },
{ password: 'existing-hash', passwordChangedAt: new Date() }
),
false
)
})
@@ -0,0 +1,90 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import {
resolveNotificationBatchReference,
resolveNotificationHistoryFilters,
resolveNotificationHistoryPagination,
resolveNotificationHistoryStatus
} from '../../server/utils/notification-history-policy.ts'
test('通知发送历史支持关键词、类型、发送人和时间排序筛选', () => {
assert.deepEqual(
resolveNotificationHistoryFilters({
keyword: ' 放假通知 ',
type: 'important',
sender: '7',
sortOrder: 'asc'
}),
{
keyword: '放假通知',
type: 'IMPORTANT',
senderId: 7,
sortOrder: 'ASC'
}
)
assert.deepEqual(resolveNotificationHistoryFilters({}), {
keyword: '',
type: 'ALL',
senderId: null,
sortOrder: 'DESC'
})
})
test('通知发送历史拒绝无效筛选参数', () => {
assert.equal(resolveNotificationHistoryFilters({ keyword: ['通知'] }), null)
assert.equal(resolveNotificationHistoryFilters({ keyword: 'a'.repeat(101) }), null)
assert.equal(resolveNotificationHistoryFilters({ type: 'SYSTEM' }), null)
assert.equal(resolveNotificationHistoryFilters({ sender: '0' }), null)
assert.equal(resolveNotificationHistoryFilters({ sender: 'admin' }), null)
assert.equal(resolveNotificationHistoryFilters({ sortOrder: 'latest' }), null)
})
test('通知历史状态筛选支持全部、已读和未读', () => {
assert.equal(resolveNotificationHistoryStatus(undefined), 'ALL')
assert.equal(resolveNotificationHistoryStatus('read'), 'READ')
assert.equal(resolveNotificationHistoryStatus(' UNREAD '), 'UNREAD')
})
test('通知历史状态筛选拒绝未知值和非字符串', () => {
assert.equal(resolveNotificationHistoryStatus('ARCHIVED'), null)
assert.equal(resolveNotificationHistoryStatus(true), null)
})
test('通知历史分页使用安全缺省值并限制最大页大小', () => {
assert.deepEqual(resolveNotificationHistoryPagination(undefined, undefined), {
page: 1,
limit: 20,
offset: 0
})
assert.deepEqual(resolveNotificationHistoryPagination('3', '500'), {
page: 3,
limit: 100,
offset: 200
})
assert.deepEqual(resolveNotificationHistoryPagination('-1', '0'), {
page: 1,
limit: 20,
offset: 0
})
})
test('通知批次引用支持新批次和历史单条记录', () => {
assert.deepEqual(resolveNotificationBatchReference('550E8400-E29B-41D4-A716-446655440000'), {
batchId: '550e8400-e29b-41d4-a716-446655440000',
notificationId: null
})
assert.deepEqual(resolveNotificationBatchReference('legacy-42'), {
batchId: null,
notificationId: 42
})
})
test('通知批次引用拒绝无效值', () => {
assert.equal(resolveNotificationBatchReference('legacy-0'), null)
assert.equal(
resolveNotificationBatchReference('legacy-group-550e8400e29b41d4a716446655440000'),
null
)
assert.equal(resolveNotificationBatchReference('not-a-batch'), null)
assert.equal(resolveNotificationBatchReference(undefined), null)
})
+50
View File
@@ -0,0 +1,50 @@
import assert from 'node:assert/strict'
import { randomBytes } from 'node:crypto'
import test from 'node:test'
import {
decodeOAuthStateCookie,
encodeOAuthStateCookie,
generateState,
parseState
} from '../../server/utils/oauth.ts'
test('OAuth state Cookie 可以安全保存标准 Base64 特殊字符', () => {
const state = Buffer.from([251, 255]).toString('base64')
assert.equal(state, '+/8=')
const encoded = encodeOAuthStateCookie(state)
assert.equal(encoded, 'b64url:-_8')
assert.equal(decodeOAuthStateCookie(encoded), state)
})
test('OAuth state Cookie 对多组随机 Base64 保持往返一致', () => {
for (let length = 1; length <= 64; length += 1) {
const state = randomBytes(length).toString('base64')
assert.equal(decodeOAuthStateCookie(encodeOAuthStateCookie(state)), state)
}
})
test('OAuth state Cookie 兼容旧版无前缀值', () => {
const legacyState = '+/8='
assert.equal(decodeOAuthStateCookie(legacyState), legacyState)
})
test('OAuth state Cookie 拒绝空值、非法字符和非规范 Base64URL', () => {
assert.equal(decodeOAuthStateCookie('b64url:'), '')
assert.equal(decodeOAuthStateCookie('b64url:abc='), '')
assert.equal(decodeOAuthStateCookie('b64url:abc+def'), '')
assert.equal(decodeOAuthStateCookie('b64url:a'), '')
})
test('生成的 OAuth state 经过 Cookie 编解码后仍可验证', () => {
const origin = 'https://voicehub.example.com'
const secret = 'oauth-state-cookie-test-secret'
const { state, csrf } = generateState(origin, 'aggregate', secret, '/account', 'qq')
const restoredState = decodeOAuthStateCookie(encodeOAuthStateCookie(state))
const parsed = parseState(restoredState, origin, csrf, secret)
assert.equal(parsed?.target, origin)
assert.equal(parsed?.provider, 'aggregate')
assert.equal(parsed?.returnTo, '/account')
assert.equal(parsed?.loginType, 'qq')
})
+48
View File
@@ -0,0 +1,48 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import {
getInitialPasswordPolicyViolation,
validateInitialPasswordPolicy,
validatePasswordPolicy
} from '../../app/utils/password-policy.ts'
test('密码策略接受包含三类字符的安全密码', () => {
assert.equal(validatePasswordPolicy('StrongPass123'), null)
assert.equal(validatePasswordPolicy('安全Pass123!'), null)
})
test('密码策略允许6位以上的安全密码', () => {
assert.equal(validatePasswordPolicy('Pass12'), null)
})
test('密码策略拒绝少于6位的密码', () => {
assert.match(validatePasswordPolicy('Pas12') || '', /长度至少为6位/)
})
test('密码策略拒绝常见弱口令和字符类型不足的密码', () => {
assert.match(validatePasswordPolicy('12345678') || '', /过于常见/)
assert.match(validatePasswordPolicy('abcdefgh') || '', /三类/)
})
test('密码策略拒绝超过 bcrypt 有效字节长度的密码', () => {
assert.match(validatePasswordPolicy('Aa1' + '!'.repeat(70)) || '', /72字节/)
})
test('初始密码策略接受6位以上且包含两类字符的密码', () => {
assert.equal(validateInitialPasswordPolicy('abcdef7'), null)
assert.equal(validateInitialPasswordPolicy('ABCDEF1'), null)
assert.equal(validateInitialPasswordPolicy('abcdefg!'), null)
assert.equal(validateInitialPasswordPolicy('abcde1'), null)
})
test('初始密码策略拒绝不足6位的密码', () => {
const violation = getInitialPasswordPolicyViolation('Pas12')
assert.equal(violation?.code, 'AUTH_PASSWORD_TOO_SHORT')
assert.match(violation?.message || '', /至少为6位/)
})
test('初始密码策略拒绝仅包含一类字符的密码', () => {
const violation = getInitialPasswordPolicyViolation('abcdefg')
assert.equal(violation?.code, 'AUTH_INITIAL_PASSWORD_COMPLEXITY_REQUIRED')
assert.match(violation?.message || '', /两类/)
})
+162
View File
@@ -0,0 +1,162 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import {
buildQqCommentRequestParam,
normalizeQqCommentPage,
normalizeQqCommentList
} from '../../server/utils/qqComment.ts'
test('QQ评论分页参数透传上一页游标', () => {
assert.deepEqual(
buildQqCommentRequestParam({
topid: '12345',
cursor: 'cursor-20',
page: 1,
pageSize: 20,
type: 'latest'
}),
{
BizType: 1,
BizId: '12345',
LastCommentSeqNo: 'cursor-20',
PageSize: 20,
PageNum: 1,
PicEnable: 1,
HashTagID: '',
SelfSeeEnable: 1,
AudioEnable: 1
}
)
})
test('QQ评论只保留根评论并将回复挂到根评论下', () => {
const reply = {
CmId: 'reply-1',
Content: '回复内容',
Nick: '回复者',
Location: '广东'
}
const result = normalizeQqCommentList([
{
CmId: 'root-1',
Content: '评论正文',
Nick: '评论者',
SubComments: [reply]
},
reply
])
assert.equal(result.length, 1)
assert.equal(result[0]?.commentId, 'root-1')
assert.equal(result[0]?.content, '评论正文')
assert.equal(result[0]?.replies.length, 1)
assert.equal(result[0]?.replies[0]?.commentId, 'reply-1')
assert.equal(result[0]?.replies[0]?.user.location, '广东')
})
test('QQ评论去重并归一化图片与表情图片地址', () => {
const result = normalizeQqCommentList([
{
CmId: 'root-1',
Content: '带图评论',
Nick: '评论者',
Pic: 'http://music-file.y.qq.com/comment/example.jpg?a=1&amp;b=2',
EmoPic: '//y.gtimg.cn/music/emoji/example.png'
},
{
CmId: 'root-1',
Content: '重复评论',
Nick: '评论者'
}
])
assert.equal(result.length, 1)
assert.deepEqual(result[0]?.images, ['https://music-file.y.qq.com/comment/example.jpg?a=1&b=2'])
assert.deepEqual(result[0]?.emojis, ['https://y.gtimg.cn/music/emoji/example.png'])
})
test('QQ评论过滤只有头像和昵称但没有正文的无效项', () => {
const result = normalizeQqCommentList([
{ CmId: 'empty-1', Content: '', Nick: '空评论', Avatar: 'https://example.com/a.jpg' },
{ CmId: 'valid-1', Content: '有效正文', Nick: '正常评论' }
])
assert.equal(result.length, 1)
assert.equal(result[0]?.commentId, 'valid-1')
})
test('QQ评论按根评论字段收拢顶层回复', () => {
const result = normalizeQqCommentList([
{ CmId: 'root-1', Content: '根评论', Nick: '评论者' },
{ CmId: 'reply-1', RootCmId: 'root-1', Content: '一级回复', Nick: '回复者' },
{ CmId: 'reply-2', ParentCmId: 'root-1', Content: '另一条回复', Nick: '回复者二' }
])
assert.equal(result.length, 1)
assert.deepEqual(
result[0]?.replies.map((reply) => reply.commentId),
['reply-1', 'reply-2']
)
})
test('QQ评论支持 SubCmListV1 回复列表', () => {
const result = normalizeQqCommentList([
{
CmId: 'root-1',
Content: '根评论',
SubCmListV1: [{ CmId: 'reply-1', Content: '新版回复', Nick: '回复者' }]
}
])
assert.equal(result.length, 1)
assert.equal(result[0]?.replies[0]?.content, '新版回复')
})
test('QQ评论保留多层回复供前端完整展示', () => {
const result = normalizeQqCommentList([
{
CmId: 'root-1',
Content: '根评论',
SubComments: [
{
CmId: 'reply-1',
Content: '一级回复',
SubComments: [{ CmId: 'reply-2', Content: '二级回复' }]
}
]
}
])
assert.equal(result[0]?.replies[0]?.commentId, 'reply-1')
assert.equal(result[0]?.replies[0]?.replies[0]?.commentId, 'reply-2')
})
test('QQ评论分页中根评论缺失时保留孤儿回复供后续归并', () => {
const result = normalizeQqCommentPage([
{
CmId: 'reply-1',
RootCmId: 'root-1',
Content: '跨页回复',
Nick: '回复者'
}
])
assert.equal(result.comments.length, 0)
assert.equal(result.orphanReplies.length, 1)
assert.equal(result.orphanReplies[0]?.commentId, 'reply-1')
assert.equal(result.orphanReplies[0]?.rootCommentId, 'root-1')
})
test('QQ评论正文字面量换行还原为真实换行', () => {
const result = normalizeQqCommentList([
{
CmId: 'root-1',
Content: '第一行\\r\\n第二行\\n第三行\\r结尾',
Nick: '评论者',
SubComments: [{ CmId: 'reply-1', Content: '回复\\n换行', Nick: '回复者' }]
}
])
assert.equal(result[0]?.content, '第一行\n第二行\n第三行\n结尾')
assert.equal(result[0]?.replies[0]?.content, '回复\n换行')
})
+29
View File
@@ -0,0 +1,29 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { validateGradeClassPair, resolveGradeClassError, REMARK_MAX_LENGTH } from '../../server/utils/register-validation.ts'
test('年级和班级必须成对填写', () => {
assert.equal(validateGradeClassPair('高一', '一班'), null)
assert.equal(validateGradeClassPair('', ''), null)
assert.equal(validateGradeClassPair(undefined, undefined), null)
const onlyGrade = validateGradeClassPair('高一', '')
assert.equal(onlyGrade?.code, 'AUTH_GRADE_CLASS_TOGETHER')
const onlyClass = validateGradeClassPair('', '一班')
assert.equal(onlyClass?.code, 'AUTH_GRADE_CLASS_TOGETHER')
})
test('站点开启注册必选年级班级时任一缺失按必填拒绝', () => {
assert.equal(resolveGradeClassError('高一', '一班'), null)
assert.equal(resolveGradeClassError('高一', '')?.code, 'AUTH_GRADE_CLASS_TOGETHER')
assert.equal(resolveGradeClassError('高一', '一班', true), null)
assert.equal(resolveGradeClassError('高一', '', true)?.code, 'AUTH_GRADE_CLASS_REQUIRED')
assert.equal(resolveGradeClassError('', '一班', true)?.code, 'AUTH_GRADE_CLASS_REQUIRED')
assert.equal(resolveGradeClassError('', '', true)?.code, 'AUTH_GRADE_CLASS_REQUIRED')
assert.equal(resolveGradeClassError(undefined, undefined, true)?.code, 'AUTH_GRADE_CLASS_REQUIRED')
})
test('注册备注长度上限为 200', () => {
assert.equal(REMARK_MAX_LENGTH, 200)
})
+43
View File
@@ -0,0 +1,43 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { decideDurationOutcome, normalizeStoredDuration } from '../../server/utils/song-duration-policy.ts'
test('往期导入搬运时长时按 0–7200 秒区间归一化', () => {
assert.equal(normalizeStoredDuration(null), null)
assert.equal(normalizeStoredDuration(undefined), null)
assert.equal(normalizeStoredDuration(''), null)
assert.equal(normalizeStoredDuration('abc'), null)
assert.equal(normalizeStoredDuration(NaN), null)
assert.equal(normalizeStoredDuration(-1), null)
assert.equal(normalizeStoredDuration(7201), null)
assert.equal(normalizeStoredDuration(0), 0)
assert.equal(normalizeStoredDuration(29), 29)
assert.equal(normalizeStoredDuration('215'), 215)
assert.equal(normalizeStoredDuration(3600), 3600)
assert.equal(normalizeStoredDuration(5400), 5400)
assert.equal(normalizeStoredDuration(7200), 7200)
})
test('管理员标注的长曲目不再被导入流程静默清空', () => {
// 旧口径为 30–3600 秒,超 1 小时的曲目会被写成 null
assert.equal(normalizeStoredDuration(4200), 4200)
})
test('库中无时长且平台可查时补齐', () => {
assert.deepEqual(decideDurationOutcome(null, 215), { outcome: 'fill', durationSeconds: 215 })
})
test('平台查询失败时保留原值不写库', () => {
assert.deepEqual(decideDurationOutcome(215, null), { outcome: 'nocheck', durationSeconds: 215 })
assert.deepEqual(decideDurationOutcome(null, null), { outcome: 'nocheck', durationSeconds: null })
})
test('提交时长与平台差异在容差内视为一致', () => {
assert.deepEqual(decideDurationOutcome(215, 213), { outcome: 'keep', durationSeconds: 215 })
assert.deepEqual(decideDurationOutcome(215, 220), { outcome: 'keep', durationSeconds: 215 })
})
test('提交时长与平台差异超过容差时清空', () => {
assert.deepEqual(decideDurationOutcome(215, 221), { outcome: 'clear', durationSeconds: null })
assert.deepEqual(decideDurationOutcome(215, 60), { outcome: 'clear', durationSeconds: null })
})
+46
View File
@@ -0,0 +1,46 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { normalizeForMatch, toSimplifiedChinese } from '../../app/utils/song-name-normalize.ts'
test('繁体标题与简体库存归一化后相等', () => {
assert.equal(normalizeForMatch('海闊天空'), normalizeForMatch('海阔天空'))
})
test('繁体歌手名归一化后相等', () => {
assert.equal(normalizeForMatch('周杰倫'), normalizeForMatch('周杰伦'))
assert.equal(normalizeForMatch('陳奕迅'), normalizeForMatch('陈奕迅'))
})
test('归一化移除 feat./ft. 词条与空白标点', () => {
// 口径沿用现网版本:仅移除 feat/ft 词条本体,其后标点由标点规则处理
assert.equal(normalizeForMatch('一首歌 ft. 某人'), '一首歌.某人')
assert.equal(
normalizeForMatch('晴天 (feat. 周杰伦)'),
normalizeForMatch('晴天.周杰伦')
)
assert.equal(normalizeForMatch('《红玫瑰》'), normalizeForMatch('红玫瑰'))
})
test('&/& 统一为 and', () => {
assert.equal(normalizeForMatch('A&B'), normalizeForMatch('A&B'))
assert.equal(normalizeForMatch('A&B'), 'aandb')
})
test('弯引号被视为可去除标点', () => {
assert.equal(normalizeForMatch('“回忆”'), normalizeForMatch('回忆'))
})
test('大小写不敏感', () => {
assert.equal(normalizeForMatch('LOVE Story'), normalizeForMatch('love story'))
})
test('回归: 含 feat 字母的普通英文单词不被误删', () => {
// 旧实现使用字符类 [feat\.?|ft\.?] 会删掉所有 f/e/a/t 字母
assert.equal(toSimplifiedChinese(normalizeForMatch('Coffee')), 'coffee')
assert.equal(toSimplifiedChinese(normalizeForMatch('Eason Chan')), 'easonchan')
})
test('空值与原样返回', () => {
assert.equal(normalizeForMatch(''), '')
assert.equal(toSimplifiedChinese(''), '')
})
+164
View File
@@ -0,0 +1,164 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import {
normalizeLanguageLabels,
parseNeteaseWikiTypes,
parseTencentSongTypes,
mapQqLanguage,
matchBlacklistGenre,
matchBlacklistLanguage
} from '../../server/utils/song-type-resolver.ts'
import {
BLACKLIST_LANGUAGE_VALUES,
BLACKLIST_GENRE_VALUES,
QQ_LANGUAGE_CODE_MAP,
QQ_GENRE_NAME_MAP
} from '../../server/config/constants.ts'
test('网易云语种文本拆分与同义词归一', () => {
assert.deepEqual(normalizeLanguageLabels('粤语'), ['粤语'])
assert.deepEqual(normalizeLanguageLabels('英语、韩语'), ['英语', '韩语'])
assert.deepEqual(normalizeLanguageLabels('国语'), ['华语'])
assert.deepEqual(normalizeLanguageLabels('普通话,英语'), ['华语', '英语'])
assert.deepEqual(normalizeLanguageLabels(''), [])
assert.deepEqual(normalizeLanguageLabels(null), [])
})
test('解析网易云歌曲百科的语种与曲风', () => {
const payload = {
code: 200,
data: {
blocks: [
{
rnData: {
songName: '海阔天空',
blocks: [
{
blockInfo: {
blockCode: 2,
wikiSubElementVos: [
{ pattern: 1, title: '曲风', wikiSubMetaVos: [{ id: 1098, text: '摇滚-流行摇滚' }, { id: 1000, text: '流行' }] },
{ pattern: 0, title: '语种', content: '粤语' },
{ pattern: 0, title: '发行时间', content: '1993-09-09' }
]
}
}
]
}
}
]
}
}
const types = parseNeteaseWikiTypes(payload)
assert.deepEqual(types.languages, ['粤语'])
assert.deepEqual(types.genres, ['摇滚-流行摇滚', '流行'])
// 空结构容错
assert.deepEqual(parseNeteaseWikiTypes(null), { languages: [], genres: [] })
assert.deepEqual(parseNeteaseWikiTypes({ code: 200, data: {} }), { languages: [], genres: [] })
})
test('QQ 语种数字码映射', () => {
assert.equal(mapQqLanguage(0), '华语')
assert.equal(mapQqLanguage(1), '粤语')
assert.equal(mapQqLanguage(2), '闽南语')
assert.equal(mapQqLanguage(3), '日语')
assert.equal(mapQqLanguage(4), '韩语')
assert.equal(mapQqLanguage(5), '英语')
assert.equal(mapQqLanguage(19), null)
assert.equal(mapQqLanguage('abc'), null)
assert.equal(mapQqLanguage(undefined), null)
})
test('解析 QQ 详情的官网分类名(语种/曲风)', () => {
const payload = {
req: {
data: {
info: {
lan: { content: [{ value: '国语' }, { value: ' 英语' }] },
genre: { content: [{ value: 'Pop' }, { value: 'Rock' }] }
},
track_info: { language: 0, genre: 1 }
}
}
}
const types = parseTencentSongTypes(payload)
assert.deepEqual(types.languages, ['华语', '英语'])
assert.deepEqual(types.genres, ['流行', '摇滚'])
// 未收录流派名归「其他」,重复值去重
const other = parseTencentSongTypes({
req: { data: { info: { genre: { content: [{ value: 'Animation' }, { value: 'Rock ' }] } } } }
})
assert.deepEqual(other.languages, [])
assert.deepEqual(other.genres, ['其他', '摇滚'])
// 空结构容错
assert.deepEqual(parseTencentSongTypes(null), { languages: [], genres: [] })
assert.deepEqual(parseTencentSongTypes({ req: { data: {} } }), { languages: [], genres: [] })
assert.deepEqual(parseTencentSongTypes({ req: { data: { info: {} } } }), { languages: [], genres: [] })
})
test('QQ 语种:info.lan 缺失时回退数值码,未收录整数码归「其他」', () => {
const fallback = parseTencentSongTypes({ req: { data: { track_info: { language: 5 } } } })
assert.deepEqual(fallback.languages, ['英语'])
const unknownCode = parseTencentSongTypes({ req: { data: { track_info: { language: 99 } } } })
assert.deepEqual(unknownCode.languages, ['其他'])
const missing = parseTencentSongTypes({ req: { data: { track_info: {} } } })
assert.deepEqual(missing.languages, [])
})
test('曲风与黑名单值按一级分类匹配', () => {
assert.equal(matchBlacklistGenre('摇滚', ['摇滚-流行摇滚', '流行']), true)
assert.equal(matchBlacklistGenre('流行', ['流行-华语流行']), true)
assert.equal(matchBlacklistGenre('原声带', ['原声带-动画片原声']), true)
assert.equal(matchBlacklistGenre('电子', ['流行-华语流行']), false)
assert.equal(matchBlacklistGenre('R&B', ['R&B']), true)
assert.equal(matchBlacklistGenre('', ['流行']), false)
assert.equal(matchBlacklistGenre('流行', []), false)
})
test('曲风匹配:「其他」命中候选列表之外的一级分类', () => {
assert.equal(matchBlacklistGenre('其他', ['金属-重金属']), true)
assert.equal(matchBlacklistGenre('其他', ['嘻哈说唱-硬核说唱', '原声带-电影原声']), true)
assert.equal(matchBlacklistGenre('其他', ['中国传统特色-京剧']), true)
assert.equal(matchBlacklistGenre('其他', ['流行-华语流行']), false)
assert.equal(matchBlacklistGenre('其他', ['古典-巴洛克时期']), false)
assert.equal(matchBlacklistGenre('其他', []), false) // 无法判定不命中
})
test('语种匹配:「其他」命中主要候选值之外的小语种', () => {
assert.equal(matchBlacklistLanguage('英语', ['英语']), true)
assert.equal(matchBlacklistLanguage('英语', ['法语']), false)
assert.equal(matchBlacklistLanguage('其他', ['法语']), true)
assert.equal(matchBlacklistLanguage('其他', ['俄语', '西班牙语']), true)
assert.equal(matchBlacklistLanguage('其他', ['华语']), false)
assert.equal(matchBlacklistLanguage('其他', ['英语', '韩语']), false)
assert.equal(matchBlacklistLanguage('其他', []), false) // 无法判定不命中
assert.equal(matchBlacklistLanguage('', ['法语']), false)
})
test('QQ 语种映射值必须落在语种候选值集合内', () => {
for (const label of Object.values(QQ_LANGUAGE_CODE_MAP)) {
assert.ok(
(BLACKLIST_LANGUAGE_VALUES as readonly string[]).includes(label),
`QQ 语种映射值 ${label} 不在候选值集合中`
)
}
})
test('QQ 流派映射值必须落在曲风候选值集合内', () => {
for (const label of Object.values(QQ_GENRE_NAME_MAP)) {
assert.ok(
(BLACKLIST_GENRE_VALUES as readonly string[]).includes(label),
`QQ 流派映射值 ${label} 不在候选值集合中`
)
}
})
test('候选值集合无重复', () => {
assert.equal(new Set(BLACKLIST_LANGUAGE_VALUES).size, BLACKLIST_LANGUAGE_VALUES.length)
assert.equal(new Set(BLACKLIST_GENRE_VALUES).size, BLACKLIST_GENRE_VALUES.length)
})
@@ -0,0 +1,83 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import {
normalizeRestrictionHours,
resolveSubmissionRestrictionPolicy
} from '../../server/utils/submission-restriction-policy.ts'
test('时长归一化只接受正整数,其余视为未配置', () => {
assert.equal(normalizeRestrictionHours(null), null)
assert.equal(normalizeRestrictionHours(undefined), null)
assert.equal(normalizeRestrictionHours(''), null)
assert.equal(normalizeRestrictionHours('abc'), null)
assert.equal(normalizeRestrictionHours(0), null)
assert.equal(normalizeRestrictionHours(-3), null)
assert.equal(normalizeRestrictionHours(1), 1)
assert.equal(normalizeRestrictionHours('24'), 24)
assert.equal(normalizeRestrictionHours(24.7), 24)
})
test('关闭重复投稿限制后不做任何限制,普通用户可重复投稿已排期歌曲', () => {
const policy = resolveSubmissionRestrictionPolicy({
enableSubmissionRestriction: false,
sameSongRestrictionHours: null,
sameArtistRestrictionHours: null,
submissionRestrictionScope: 'all'
})
assert.equal(policy.mode, 'none')
})
test('开关关闭但残留时长也不生效,避免脏数据绕过开关', () => {
const policy = resolveSubmissionRestrictionPolicy({
enableSubmissionRestriction: false,
sameSongRestrictionHours: 48,
sameArtistRestrictionHours: null
})
assert.equal(policy.mode, 'none')
})
test('系统设置缺失时按不限制降级', () => {
assert.equal(resolveSubmissionRestrictionPolicy(null).mode, 'none')
assert.equal(resolveSubmissionRestrictionPolicy(undefined).mode, 'none')
assert.equal(resolveSubmissionRestrictionPolicy({}).mode, 'none')
})
test('开启限制但未配置时长时沿用本学期查重规则', () => {
const policy = resolveSubmissionRestrictionPolicy({
enableSubmissionRestriction: true,
sameSongRestrictionHours: null,
sameArtistRestrictionHours: null
})
assert.equal(policy.mode, 'semester')
})
test('开启限制且时长非法时退回本学期查重规则', () => {
const policy = resolveSubmissionRestrictionPolicy({
enableSubmissionRestriction: true,
sameSongRestrictionHours: 0,
sameArtistRestrictionHours: 'abc'
})
assert.equal(policy.mode, 'semester')
})
test('开启限制且配置时长时按冷却窗口判定', () => {
const policy = resolveSubmissionRestrictionPolicy({
enableSubmissionRestriction: true,
sameSongRestrictionHours: 24,
sameArtistRestrictionHours: 72,
submissionRestrictionScope: 'self'
})
assert.equal(policy.mode, 'window')
assert.equal(policy.sameSongHours, 24)
assert.equal(policy.sameArtistHours, 72)
assert.equal(policy.scope, 'self')
})
test('限制范围非法值回退为所有人', () => {
const policy = resolveSubmissionRestrictionPolicy({
enableSubmissionRestriction: true,
sameSongRestrictionHours: 24,
submissionRestrictionScope: 'everyone'
})
assert.equal(policy.scope, 'all')
})
+14
View File
@@ -0,0 +1,14 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { JWTEnhanced } from '../../server/utils/jwt-enhanced.ts'
test('预认证令牌必须携带当前 tokenVersion', () => {
assert.equal(JWTEnhanced.hasCurrentTokenVersion({ tokenVersion: 3 }, 3), true)
assert.equal(JWTEnhanced.hasCurrentTokenVersion({ tokenVersion: 2 }, 3), false)
})
test('迁移前或非法 tokenVersion 的预认证令牌按失效处理', () => {
assert.equal(JWTEnhanced.hasCurrentTokenVersion({}, 0), false)
assert.equal(JWTEnhanced.hasCurrentTokenVersion({ tokenVersion: '0' }, 0), false)
assert.equal(JWTEnhanced.hasCurrentTokenVersion(null, 0), false)
})
+41
View File
@@ -0,0 +1,41 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import {
ARCHIVED_USER_STATUSES,
isArchivedStatus,
resolveArchivedFilter
} from '../../app/utils/user-archive.ts'
test('归档状态包含 graduate(限制访问-毕业生)与 withdrawn(限制访问-退学)', () => {
assert.deepEqual([...ARCHIVED_USER_STATUSES], ['graduate', 'withdrawn'])
})
test('isArchivedStatus 识别归档状态', () => {
assert.equal(isArchivedStatus('graduate'), true)
assert.equal(isArchivedStatus('withdrawn'), true)
assert.equal(isArchivedStatus('active'), false)
assert.equal(isArchivedStatus('pending'), false)
assert.equal(isArchivedStatus(null), false)
assert.equal(isArchivedStatus(undefined), false)
})
test('archived=1 解析为仅查已归档', () => {
assert.equal(resolveArchivedFilter('1'), 'archived')
assert.equal(resolveArchivedFilter('true'), 'archived')
})
test('archived=0 解析为排除已归档', () => {
assert.equal(resolveArchivedFilter('0'), 'unarchived')
assert.equal(resolveArchivedFilter('false'), 'unarchived')
})
test('缺省或未知值不限制归档', () => {
assert.equal(resolveArchivedFilter(undefined), 'all')
assert.equal(resolveArchivedFilter(''), 'all')
assert.equal(resolveArchivedFilter('yes'), 'all')
})
test('数组参数取首项解析', () => {
assert.equal(resolveArchivedFilter(['1']), 'archived')
assert.equal(resolveArchivedFilter(['0']), 'unarchived')
})
+135
View File
@@ -0,0 +1,135 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { getIdentityAvatarUrl, resolveAvatarSource } from '../../server/utils/user-avatar.ts'
test('getIdentityAvatarUrl 返回身份头像', () => {
assert.equal(
getIdentityAvatarUrl({
provider: 'github',
providerUserId: '1',
avatar: 'https://example.com/a.png'
}),
'https://example.com/a.png'
)
})
test('getIdentityAvatarUrl 拒绝非 http(s) 头像', () => {
assert.equal(
getIdentityAvatarUrl({
provider: 'github',
providerUserId: '1',
avatar: 'javascript:alert(1)'
}),
null
)
assert.equal(
getIdentityAvatarUrl({
provider: 'qq',
providerUserId: '1',
avatar: 'data:image/png;base64,abc'
}),
null
)
})
test('GitHub 存量身份无 avatar 时回退用户名图片', () => {
assert.equal(
getIdentityAvatarUrl({
provider: 'github',
providerUserId: '1',
providerUsername: 'octocat',
avatar: null
}),
'https://github.com/octocat.png'
)
})
test('WebAuthn / TOTP 身份不参与头像', () => {
assert.equal(
getIdentityAvatarUrl({ provider: 'webauthn', providerUserId: 'x', avatar: null }),
null
)
assert.equal(getIdentityAvatarUrl({ provider: 'totp', providerUserId: 'x', avatar: null }), null)
})
test('resolveAvatarSource 优先返回选中的身份', () => {
const user = { avatarProvider: 'github', avatarProviderUserId: '2' }
const identities = [
{
id: 1,
provider: 'github',
providerUserId: '1',
providerUsername: 'a',
avatar: 'https://a.png',
createdAt: new Date('2024-01-01')
},
{
id: 2,
provider: 'github',
providerUserId: '2',
providerUsername: 'b',
avatar: 'https://b.png',
createdAt: new Date('2024-01-02')
}
]
assert.deepEqual(resolveAvatarSource(user, identities), {
provider: 'github',
providerUserId: '2',
url: 'https://b.png'
})
})
test('resolveAvatarSource 未选择时按创建顺序取第一个可用头像', () => {
const identities = [
{
id: 2,
provider: 'github',
providerUserId: '2',
providerUsername: 'b',
avatar: 'https://b.png',
createdAt: new Date('2024-01-02')
},
{
id: 1,
provider: 'qq',
providerUserId: '1',
providerUsername: 'a',
avatar: 'https://a.png',
createdAt: new Date('2024-01-01')
}
]
assert.deepEqual(resolveAvatarSource({}, identities), {
provider: 'qq',
providerUserId: '1',
url: 'https://a.png'
})
})
test('resolveAvatarSource 全部为空时返回 null', () => {
assert.equal(
resolveAvatarSource({}, [
{ provider: 'webauthn', providerUserId: 'x', avatar: null },
{ provider: 'totp', providerUserId: 'y', avatar: null }
]),
null
)
})
test('解绑回退:删除当前来源后取剩余身份第一个可用头像', () => {
const user = { avatarProvider: 'qq', avatarProviderUserId: '1' }
const remaining = [
{
id: 2,
provider: 'github',
providerUserId: '2',
providerUsername: 'octocat',
avatar: null,
createdAt: new Date('2024-01-03')
}
]
assert.deepEqual(resolveAvatarSource(user, remaining), {
provider: 'github',
providerUserId: '2',
url: 'https://github.com/octocat.png'
})
})