fix: reload after signing out an expired account session and check password lengths

This commit is contained in:
DiheChen committed 2026-11-08 22:48:13 +08:00
1 parent 9338c65877
commit 4e2c24bdca
2 files changed
+139 -2

No files matched your search

@@ -41,6 +41,11 @@ export function AccountSection() {
switchIdentity(null)
}
catch (error) {
// 账号在这台设备上的会话已经失效,客户端也已经换成新的游客,退出登录的目的已经达到。
if (error instanceof IdentityRenewedError) {
window.location.reload()
return
}
notificationStore.error(errorMessage(error))
setBusy(false)
}
@@ -160,7 +165,7 @@ function PasswordDialog({ onClose }: { onClose: () => void }) {
onClose()
}
catch (error) {
notificationStore.error(error instanceof IdentityRenewedError ? '密码没有修改,再试一次。' : errorMessage(error, { INVALID_ARGUMENT: '新密码要 8 到 128 位。' }))
notificationStore.error(error instanceof IdentityRenewedError ? '密码没有修改,再试一次。' : errorMessage(error, { INVALID_ARGUMENT: '密码要 8 到 128 位。' }))
setBusy(false)
}
}
@@ -169,7 +174,7 @@ function PasswordDialog({ onClose }: { onClose: () => void }) {
<form className="form-stack" onSubmit={event => submitted(event, submit)}>
<label>
当前密码
<input type="password" data-autofocus required autoComplete="current-password" maxLength={128} value={currentPassword} onChange={event => setCurrentPassword(event.target.value)} />
<input type="password" data-autofocus required autoComplete="current-password" minLength={8} maxLength={128} value={currentPassword} onChange={event => setCurrentPassword(event.target.value)} />
</label>
<label>
新密码
@@ -0,0 +1,132 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
class FakeStorage {
readonly #items = new Map<string, string>()
get length(): number {
return this.#items.size
}
key(index: number): string | null {
return [...this.#items.keys()][index] ?? null
}
getItem(key: string): string | null {
return this.#items.get(key) ?? null
}
setItem(key: string, value: string): void {
this.#items.set(key, value)
}
removeItem(key: string): void {
this.#items.delete(key)
}
}
const memberTokenKey = 'alisten.member-token'
const emailKey = 'alisten.email'
const member = { id: 'm1', name: 'alice', avatarHash: '' }
const renewed = { id: 'm2', name: '游客-1a2b', avatarHash: '' }
let local: FakeStorage
let reload: ReturnType<typeof vi.fn>
let me: unknown
async function load() {
const [{ profileStore, switchIdentity }, { identityEvents }] = await Promise.all([import('./profile-store'), import('@/core/api')])
return { profileStore, switchIdentity, identityEvents }
}
beforeEach(() => {
vi.resetModules()
// 通知按计时器消失。
vi.useFakeTimers()
local = new FakeStorage()
reload = vi.fn()
me = { member }
vi.stubGlobal('localStorage', local)
vi.stubGlobal('sessionStorage', new FakeStorage())
vi.stubGlobal('window', { location: { reload } })
vi.stubGlobal('fetch', (path: string, init: RequestInit) => {
if (`${init.method} ${path}` !== 'GET /api/me')
throw new Error(`没有预期的请求 ${init.method} ${path}`)
return Promise.resolve(new Response(JSON.stringify(me), { status: 200 }))
})
})
afterEach(() => {
vi.useRealTimers()
vi.unstubAllGlobals()
})
describe('switchIdentity', () => {
it('换成新的 token,删除所有票据和本机的邮箱,重新加载页面', async () => {
local.setItem(memberTokenKey, 'guest')
local.setItem('alisten.ticket.r1', 't1')
local.setItem('alisten.ticket.r2', 't2')
local.setItem(emailKey, 'alice@example.com')
local.setItem('alisten.volume', '80')
const { switchIdentity } = await load()
switchIdentity('account')
expect(local.getItem(memberTokenKey)).toBe('account')
expect(local.getItem('alisten.ticket.r1')).toBeNull()
expect(local.getItem('alisten.ticket.r2')).toBeNull()
expect(local.getItem(emailKey)).toBeNull()
expect(local.getItem('alisten.volume')).toBe('80')
expect(reload).toHaveBeenCalledOnce()
})
it('退出登录时删除 token,重新加载之后换成新的游客', async () => {
local.setItem(memberTokenKey, 'account')
local.setItem('alisten.ticket.r1', 't1')
local.setItem(emailKey, 'alice@example.com')
const { switchIdentity } = await load()
switchIdentity(null)
expect(local.getItem(memberTokenKey)).toBeNull()
expect(local.getItem('alisten.ticket.r1')).toBeNull()
expect(local.getItem(emailKey)).toBeNull()
expect(reload).toHaveBeenCalledOnce()
})
})
describe('账号状态', () => {
beforeEach(() => {
local.setItem(memberTokenKey, 'good')
})
it('取回资料时一并取回账号', async () => {
me = { member, account: { username: 'alice' } }
const { profileStore } = await load()
await profileStore.load()
expect(profileStore.state).toMatchObject({ member, account: { username: 'alice' } })
})
it('游客没有账号', async () => {
const { profileStore } = await load()
profileStore.setAccount({ username: 'alice' })
await profileStore.load()
expect(profileStore.state).toMatchObject({ member, account: null })
})
it('注册之后设置账号', async () => {
const { profileStore } = await load()
await profileStore.load()
profileStore.setAccount({ username: 'alice' })
expect(profileStore.state).toMatchObject({ member, account: { username: 'alice' } })
})
it('身份被拒绝或更换之后没有账号', async () => {
const { profileStore, identityEvents } = await load()
profileStore.start()
profileStore.setAccount({ username: 'alice' })
identityEvents.emit({ kind: 'rejected' })
expect(profileStore.state).toMatchObject({ member: null, account: null })
profileStore.setAccount({ username: 'alice' })
identityEvents.emit({ kind: 'renewed', member: renewed })
expect(profileStore.state).toMatchObject({ member: renewed, account: null })
})
})