108 lines
5.4 KiB
TypeScript
108 lines
5.4 KiB
TypeScript
// 忘记密码唯一处置守护:管理员生成一次性登录密码 → 用户登录 → 强制改密。
|
|
// 恢复提案 UI/API 已整体移除;登录页只保留用户名 + 密码一种凭据入口。
|
|
import { describe, expect, it } from 'vitest'
|
|
import { readFileSync, readdirSync, statSync } from 'node:fs'
|
|
import { join, relative, dirname } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const SRC = join(dirname(fileURLToPath(import.meta.url)), '..', 'src')
|
|
const read = (...parts: string[]) => readFileSync(join(SRC, ...parts), 'utf8')
|
|
|
|
function walk(dir: string, out: string[] = []): string[] {
|
|
for (const name of readdirSync(dir)) {
|
|
const p = join(dir, name)
|
|
if (statSync(p).isDirectory()) walk(p, out)
|
|
else if (/\.(ts|tsx)$/.test(p)) out.push(p)
|
|
}
|
|
return out
|
|
}
|
|
|
|
describe('恢复提案已移除', () => {
|
|
it('src 不再调用恢复提案或登录 Key 兑换端点', () => {
|
|
const hits = walk(SRC).filter(p => /recovery-proposals|RecoveryProposal|login-key\/redeem|redeemLoginKey/.test(readFileSync(p, 'utf8')))
|
|
expect(hits.map(p => relative(SRC, p))).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe('一次性登录密码 API 契约', () => {
|
|
const apiSrc = read('api.ts')
|
|
it('POST /api/admin/users/{owner}/one-time-password 并消费 oneTimePassword 字段', () => {
|
|
const line = apiSrc.split('\n').find(l => l.includes('adminIssueOneTimePassword:')) ?? ''
|
|
expect(line).toContain('/api/admin/users/${encodeURIComponent(owner)}/one-time-password`')
|
|
expect(line).toContain("method: 'POST'")
|
|
expect(line).toContain('oneTimePassword: string')
|
|
})
|
|
it('旧的临时密码重置端点与字段已移除', () => {
|
|
expect(apiSrc).not.toContain('adminResetUserPassword')
|
|
expect(apiSrc).not.toContain('temporaryPassword')
|
|
expect(apiSrc).not.toMatch(/\/api\/admin\/users\/\$\{[^}]+\}\/password`/)
|
|
})
|
|
})
|
|
|
|
describe('管理员用户管理:唯一忘记密码动作', () => {
|
|
const panel = read('components', 'adminSections.tsx')
|
|
it('调用一次性登录密码端点并展示 oneTimePassword', () => {
|
|
expect(panel.match(/api\.adminIssueOneTimePassword\(/g)).toHaveLength(1)
|
|
expect(panel).toContain('out.oneTimePassword')
|
|
expect(panel).not.toContain('temporaryPassword')
|
|
})
|
|
it('每行只有「生成一次性登录密码」一个按钮,无恢复提案或重置临时密码入口', () => {
|
|
expect(panel.match(/>生成一次性登录密码</g)).toHaveLength(1)
|
|
expect(panel).not.toContain('恢复提案')
|
|
expect(panel).not.toContain('重置临时密码')
|
|
})
|
|
it('一次性密码文案标明使用后强制改密,明文只在一次性展示框出现', () => {
|
|
expect(panel).toContain('使用后必须强制改密')
|
|
expect(panel.match(/aria-label="一次性密码"/g)).toHaveLength(1)
|
|
expect(panel).not.toMatch(/localStorage|sessionStorage|clipboard/)
|
|
})
|
|
it('文案标明生成不使原密码失效;原密码登录则作废未用的一次性密码且不强制改密', () => {
|
|
expect(panel).not.toMatch(/原密码(已|立即|随生成即)失效/)
|
|
expect(panel).toContain('原密码仍可正常登录')
|
|
expect(panel).toContain('未使用的一次性登录密码随即作废')
|
|
expect(panel).toContain('不会被要求改密')
|
|
})
|
|
})
|
|
|
|
describe('登录页:仅用户名 + 密码', () => {
|
|
const login = read('components', 'LoginPage.tsx')
|
|
const loginPage = login.slice(login.indexOf('export function LoginPage'), login.indexOf('export function requiresCurrentPassword'))
|
|
it('只有一个用户名输入与一个密码输入,无第二凭据入口', () => {
|
|
expect(loginPage.match(/<input /g)).toHaveLength(2)
|
|
expect(loginPage.match(/type="password"/g)).toHaveLength(1)
|
|
expect(loginPage).toContain('api.login(')
|
|
expect(loginPage).not.toMatch(/role="tab"|login-key|alk_|redeem/i)
|
|
})
|
|
it('忘记密码指引指向一次性登录密码且标明须改密', () => {
|
|
expect(loginPage).toContain('生成一次性登录密码,使用后须立即改密')
|
|
})
|
|
})
|
|
|
|
describe('强制改密门:one_time_password 模式', () => {
|
|
const login = read('components', 'LoginPage.tsx')
|
|
const gate = login.slice(login.indexOf('export function ChangePasswordGate'), login.indexOf('export function humanLoginError'))
|
|
it('门放行判定经 isPasswordChangeMode,接受 one_time_password', () => {
|
|
expect(gate).toContain('isPasswordChangeMode(me.passwordChangeMode)')
|
|
expect(gate).toContain("mode === 'one_time_password'")
|
|
})
|
|
it('当前密码输入只在 current_required 下渲染,且提示不再指向一次性登录密码', () => {
|
|
const currentBlock = gate.slice(gate.indexOf("{mode === 'current_required' && <>"), gate.indexOf('</>}'))
|
|
expect(currentBlock).toContain('id="forced-current"')
|
|
expect(currentBlock).not.toContain('一次性登录密码')
|
|
expect(gate.match(/id="forced-current"/g)).toHaveLength(1)
|
|
})
|
|
})
|
|
|
|
describe('我的账号:无恢复提案确认', () => {
|
|
const settings = read('components', 'MyAccountSettings.tsx')
|
|
it('只给出联系管理员生成一次性登录密码的指引,没有可操作的恢复按钮', () => {
|
|
expect(settings).toContain('请联系管理员生成一次性登录密码')
|
|
expect(settings).toContain('强制改密')
|
|
expect(settings).not.toMatch(/由本人确认|重新生成设密能力|兑换/)
|
|
})
|
|
it('不声称生成一次性登录密码会使原密码失效', () => {
|
|
expect(settings).not.toMatch(/原密码(已|立即|随生成即)失效/)
|
|
expect(settings).toContain('原密码仍可正常登录')
|
|
})
|
|
})
|