Why: the forced password gate still submitted an empty current password after the backend began requiring it (current_required), locking every account seeded with an initial password out of the workbench, and the admin user panel had no client for the owner-confirmed recovery proposal contract that replaced one-time key issuing (HTTP 410). Evidence: npm test 300 passed; tsc -b && vite build ok; knip issues 54 <= baseline 55 (SELF). Impact: mode-aware ChangePasswordGate with current-password input, admin recovery proposal panel, owner-side account settings section, recovery proposal client methods; no backend changes. Refs: backend 8dfc223, b9c0a34; frontend #94
55 lines
2.3 KiB
TypeScript
55 lines
2.3 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { recoveryProposalErrorMessage, recoveryProposalHint, recoveryStatusLabel } from './recovery'
|
|
import { humanChangePasswordError, humanRedeemError, requiresCurrentPassword } from './components/LoginPage'
|
|
|
|
// 账户链路文案与模式判定(2026-10-14 current_required 加固后前后端契约对齐的
|
|
// 防漂移锁):改密门必须区分 grant_setup / current_required,恢复提案 409 的
|
|
// 两种语义必须分开表达。
|
|
|
|
describe('requiresCurrentPassword', () => {
|
|
it('current_required 必须要当前密码,grant_setup 免当前密码', () => {
|
|
expect(requiresCurrentPassword('current_required')).toBe(true)
|
|
expect(requiresCurrentPassword('grant_setup')).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('humanChangePasswordError', () => {
|
|
it('后端 current password incorrect 映射为当前密码不正确', () => {
|
|
expect(humanChangePasswordError('401: current password incorrect')).toBe('当前密码不正确')
|
|
})
|
|
})
|
|
|
|
describe('humanRedeemError', () => {
|
|
it('401/409/429 语义分离', () => {
|
|
expect(humanRedeemError('409: login key already used')).toContain('已被使用')
|
|
expect(humanRedeemError('401: invalid or expired login key')).toContain('无效或已过期')
|
|
expect(humanRedeemError('429: too many failed attempts')).toContain('稍后再试')
|
|
})
|
|
})
|
|
|
|
describe('recoveryStatusLabel', () => {
|
|
it('覆盖全部五种提案状态', () => {
|
|
for (const status of ['pending', 'confirmed', 'completed', 'revoked', 'expired'] as const) {
|
|
expect(recoveryStatusLabel[status]).toBeTruthy()
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('recoveryProposalHint', () => {
|
|
it('pending/confirmed 给出下一步,终态不给', () => {
|
|
expect(recoveryProposalHint('pending')).toContain('本人确认')
|
|
expect(recoveryProposalHint('confirmed')).toContain('新密码')
|
|
expect(recoveryProposalHint('completed')).toBe('')
|
|
})
|
|
})
|
|
|
|
describe('recoveryProposalErrorMessage', () => {
|
|
it('OWNER_PROOF_UNAVAILABLE 与活动提案冲突分开表达', () => {
|
|
expect(recoveryProposalErrorMessage('409: OWNER_PROOF_UNAVAILABLE')).toContain('会话证明')
|
|
expect(recoveryProposalErrorMessage('409: store: conflict')).toContain('已有进行中的恢复提案')
|
|
})
|
|
it('未识别错误剥状态码原样透出', () => {
|
|
expect(recoveryProposalErrorMessage('500: boom')).toBe('boom')
|
|
})
|
|
})
|