90 lines
4.1 KiB
TypeScript
90 lines
4.1 KiB
TypeScript
// 登录/改密门错误文案:登录与改密的 401 语义不同,映射分开锁定
|
|
// (后端错误串是现行 HTTP 契约的一部分,这里逐字对拍防漂移)。
|
|
import { createElement } from 'react'
|
|
import { renderToStaticMarkup } from 'react-dom/server'
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import { api } from '../src/api'
|
|
import { auth } from '../src/auth'
|
|
import {
|
|
ForcedPasswordModeSection,
|
|
isForcedPasswordMode,
|
|
requiresCurrentPassword,
|
|
submitForcedPassword,
|
|
} from '../src/components/passwordChangeMode'
|
|
import { humanChangePasswordError, humanLoginError } from '../src/components/loginErrors'
|
|
|
|
describe('humanLoginError(登录)', () => {
|
|
it('401 统一为用户名或密码不正确', () => {
|
|
expect(humanLoginError('401: invalid owner or password')).toBe('用户名或密码不正确')
|
|
})
|
|
it('429/412 各有文案,其余剥状态码前缀', () => {
|
|
expect(humanLoginError('429: too many failed attempts; retry later')).toBe('失败次数过多,请稍后再试')
|
|
expect(humanLoginError('412: auth disabled (embedded single-tenant mode)')).toBe('本服务未开启登录(嵌入式单租户档)')
|
|
expect(humanLoginError('400: bad')).toBe('bad')
|
|
})
|
|
})
|
|
|
|
describe('humanChangePasswordError(改密门)', () => {
|
|
it('当前密码错误不再伪装成登录失败', () => {
|
|
expect(humanChangePasswordError('401: current password incorrect')).toBe('当前密码不正确')
|
|
})
|
|
it('无会话(API key 凭证)给出可行动指引', () => {
|
|
expect(humanChangePasswordError('401: session login required (API keys do not own passwords)')).toBe('请先用账号密码登录再改密(API 密钥不能修改密码)')
|
|
})
|
|
it('会话过期与失效 key 各有文案', () => {
|
|
expect(humanChangePasswordError('401: session expired')).toBe('会话已过期,请重新登录')
|
|
expect(humanChangePasswordError('401: unauthorized: invalid or revoked API key')).toBe('本地残留的 API 密钥已失效,请退出登录后重试')
|
|
})
|
|
it('未知 401 回落通用文案,其余错误剥前缀透传', () => {
|
|
expect(humanChangePasswordError('401: whatever')).toBe('认证失败,请重新登录')
|
|
expect(humanChangePasswordError('400: password must be 8-256 characters')).toBe('password must be 8-256 characters')
|
|
})
|
|
})
|
|
|
|
describe('humanChangePasswordError(登录恢复新增文案)', () => {
|
|
it('并发凭据变化给出重新登录指引', () => {
|
|
expect(humanChangePasswordError('401: credentials changed concurrently; retry login')).toBe('登录状态已变化(密码在其他地方被更新),请重新登录')
|
|
})
|
|
})
|
|
|
|
describe('one_time_password 强制改密流程', () => {
|
|
afterEach(() => {
|
|
auth.clear()
|
|
vi.unstubAllGlobals()
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
it('识别 one_time_password 模式且无需当前密码字段', () => {
|
|
expect(isForcedPasswordMode('one_time_password')).toBe(true)
|
|
expect(requiresCurrentPassword('one_time_password')).toBe(false)
|
|
const html = renderToStaticMarkup(createElement(ForcedPasswordModeSection, {
|
|
mode: 'one_time_password',
|
|
current: '',
|
|
onCurrentChange: () => undefined,
|
|
}))
|
|
expect(html).not.toContain('forced-current')
|
|
expect(html).toContain('无需输入当前密码')
|
|
})
|
|
|
|
it('提交 one_time_password 改密时走 /api/auth/password', async () => {
|
|
const fetchSpy = vi.fn().mockResolvedValue(new Response(JSON.stringify({
|
|
owner: 'alice',
|
|
name: 'Alice',
|
|
isAdmin: false,
|
|
mustChangePassword: false,
|
|
reloginRequired: true,
|
|
}), { status: 200, headers: { 'Content-Type': 'application/json' } }))
|
|
vi.stubGlobal('fetch', fetchSpy)
|
|
const changeSpy = vi.spyOn(api, 'changePassword')
|
|
|
|
await submitForcedPassword('one_time_password', 'should-be-ignored', 'new-password-123')
|
|
|
|
expect(changeSpy).toHaveBeenCalledWith('', 'new-password-123')
|
|
expect(fetchSpy).toHaveBeenCalledWith('/api/auth/password', expect.objectContaining({
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ current: '', next: 'new-password-123' }),
|
|
}))
|
|
})
|
|
})
|