diff --git a/frontend/src/pages/__tests__/login.spec.ts b/frontend/src/pages/__tests__/login.spec.ts index 8a8e080..08fcbe7 100644 --- a/frontend/src/pages/__tests__/login.spec.ts +++ b/frontend/src/pages/__tests__/login.spec.ts @@ -85,6 +85,19 @@ describe('Login Page', () => { expect(html).toContain('登录') }) + // 辅助:填写表单并提交,绕过 Ant Design 表单验证 + async function submitLogin( + fields: { username: string; password: string; remember?: boolean }, + ) { + const vm = wrapper.getCurrentComponent().setupState + vm.formState.username = fields.username + vm.formState.password = fields.password + if (fields.remember !== undefined) vm.formState.remember = fields.remember + vm.formRef = { validate: vi.fn().mockResolvedValue(true) } + await vm.handleSubmit() + await flushPromises() + } + it('calls /api/v1/login, sets token/user and redirects to /', async () => { const { api } = await import('@/utils/request') const mockResponse = { @@ -96,17 +109,7 @@ describe('Login Page', () => { await mountPage() const store = useUserStore() - - // 直接调用 handleSubmit 来绕过表单验证 - // 通过组件 vm 访问 setup 暴露的方法 - const vm = wrapper.getCurrentComponent().setupState - vm.formState.username = 'admin' - vm.formState.password = 'password' - vm.formRef = { value: { validate: vi.fn().mockResolvedValue(true) } } - // mock formRef.validate - vm.formRef = { validate: vi.fn().mockResolvedValue(true) } - await vm.handleSubmit() - await flushPromises() + await submitLogin({ username: 'admin', password: 'password' }) expect(api.post).toHaveBeenCalledWith('/api/v1/login', { username: 'admin', @@ -128,13 +131,7 @@ describe('Login Page', () => { }) await mountPage() - const vm = wrapper.getCurrentComponent().setupState - vm.formState.username = 'user' - vm.formState.password = 'pass' - vm.formState.remember = false - vm.formRef = { validate: vi.fn().mockResolvedValue(true) } - await vm.handleSubmit() - await flushPromises() + await submitLogin({ username: 'user', password: 'pass', remember: false }) expect(localStorage.getItem('access_token')).toBeNull() expect(localStorage.getItem('user')).toBeNull() @@ -150,12 +147,7 @@ describe('Login Page', () => { mockRoute.query = { redirect: '/dashboard' } await mountPage() - const vm = wrapper.getCurrentComponent().setupState - vm.formState.username = 'user' - vm.formState.password = 'pass' - vm.formRef = { validate: vi.fn().mockResolvedValue(true) } - await vm.handleSubmit() - await flushPromises() + await submitLogin({ username: 'user', password: 'pass' }) vi.advanceTimersByTime(500) expect(mockPush).toHaveBeenCalledWith('/dashboard') @@ -171,12 +163,7 @@ describe('Login Page', () => { mockRoute.query = { redirect: '//evil.com' } await mountPage() - const vm = wrapper.getCurrentComponent().setupState - vm.formState.username = 'user' - vm.formState.password = 'pass' - vm.formRef = { validate: vi.fn().mockResolvedValue(true) } - await vm.handleSubmit() - await flushPromises() + await submitLogin({ username: 'user', password: 'pass' }) vi.advanceTimersByTime(500) expect(mockPush).toHaveBeenCalledWith('/') @@ -192,12 +179,7 @@ describe('Login Page', () => { mockRoute.query = { redirect: 'http://evil.com' } await mountPage() - const vm = wrapper.getCurrentComponent().setupState - vm.formState.username = 'user' - vm.formState.password = 'pass' - vm.formRef = { validate: vi.fn().mockResolvedValue(true) } - await vm.handleSubmit() - await flushPromises() + await submitLogin({ username: 'user', password: 'pass' }) vi.advanceTimersByTime(500) expect(mockPush).toHaveBeenCalledWith('/') @@ -209,12 +191,7 @@ describe('Login Page', () => { const errorSpy = vi.spyOn(message, 'error').mockImplementation(() => ({}) as any) await mountPage() - const vm = wrapper.getCurrentComponent().setupState - vm.formState.username = 'user' - vm.formState.password = 'wrong' - vm.formRef = { validate: vi.fn().mockResolvedValue(true) } - await vm.handleSubmit() - await flushPromises() + await submitLogin({ username: 'user', password: 'wrong' }) expect(errorSpy).toHaveBeenCalledWith('用户名或密码错误') errorSpy.mockRestore() @@ -226,12 +203,7 @@ describe('Login Page', () => { const errorSpy = vi.spyOn(message, 'error').mockImplementation(() => ({}) as any) await mountPage() - const vm = wrapper.getCurrentComponent().setupState - vm.formState.username = 'user' - vm.formState.password = 'pass' - vm.formRef = { validate: vi.fn().mockResolvedValue(true) } - await vm.handleSubmit() - await flushPromises() + await submitLogin({ username: 'user', password: 'pass' }) expect(errorSpy).toHaveBeenCalledWith('登录失败,请检查网络连接') errorSpy.mockRestore() diff --git a/frontend/src/stores/__tests__/user.spec.ts b/frontend/src/stores/__tests__/user.spec.ts index dd85a5f..98ff431 100644 --- a/frontend/src/stores/__tests__/user.spec.ts +++ b/frontend/src/stores/__tests__/user.spec.ts @@ -96,8 +96,9 @@ describe('useUserStore', () => { }) describe('setUser', () => { - it('sets user info in store and localStorage', () => { + it('persists to localStorage when remember=true', () => { const store = useUserStore() + store.setToken('h.p.s', 'r.p.s', true) const userInfo = { id: 1, username: 'test', email: 't@t.com', role: 'user', status: 1 } store.setUser(userInfo) @@ -108,7 +109,17 @@ describe('useUserStore', () => { expect(JSON.parse(localStorage.getItem('user')!)).toEqual(userInfo) }) - it('does not write to localStorage when remember=false', () => { + it('does not persist to localStorage by default (remember=false)', () => { + const store = useUserStore() + const userInfo = { id: 1, username: 'test', email: 't@t.com', role: 'user', status: 1 } + + store.setUser(userInfo) + + expect(store.user).toEqual(userInfo) + expect(localStorage.getItem('user')).toBeNull() + }) + + it('does not write to localStorage after setToken(remember=false)', () => { const store = useUserStore() store.setToken('h.p.s', 'r.p.s', false) @@ -136,6 +147,19 @@ describe('useUserStore', () => { expect(localStorage.getItem('refresh_token')).toBeNull() expect(localStorage.getItem('user')).toBeNull() }) + + it('resets _remember so subsequent setUser does not persist', () => { + const store = useUserStore() + store.setToken('h.p.s', 'r.p.s', true) + store.setUser({ id: 1, username: 'a', email: 'a@a.com', role: 'admin', status: 1 }) + expect(localStorage.getItem('user')).not.toBeNull() + + store.logout() + store.setUser({ id: 2, username: 'b', email: 'b@b.com', role: 'user', status: 1 }) + + expect(store.user?.username).toBe('b') + expect(localStorage.getItem('user')).toBeNull() + }) }) describe('fetchCurrentUser', () => { diff --git a/frontend/src/stores/user.ts b/frontend/src/stores/user.ts index 1b9660b..ab58a6f 100644 --- a/frontend/src/stores/user.ts +++ b/frontend/src/stores/user.ts @@ -12,7 +12,8 @@ export const useUserStore = defineStore('user', () => { const token = ref(localStorage.getItem('access_token')) const refreshToken = ref(localStorage.getItem('refresh_token')) const user = ref(null) - const _remember = ref(true) + // 默认不持久化;从 localStorage 推断:有 token 说明上次选了"记住我" + const _remember = ref(!!localStorage.getItem('access_token')) // 基本 JWT 格式校验(三段式),防止垃圾值绕过路由守卫 const isLoggedIn = computed(() => { @@ -30,7 +31,7 @@ export const useUserStore = defineStore('user', () => { localStorage.setItem('access_token', accessToken) localStorage.setItem('refresh_token', newRefreshToken) } else { - // 不记住:清除持久化,token 仅存于内存,关闭标签页即失效 + // 不记住:清除全部持久化数据,token 仅存于内存,关闭标签页即失效 localStorage.removeItem('access_token') localStorage.removeItem('refresh_token') localStorage.removeItem('user') @@ -54,6 +55,7 @@ export const useUserStore = defineStore('user', () => { token.value = null refreshToken.value = null user.value = null + _remember.value = false localStorage.removeItem('access_token') localStorage.removeItem('refresh_token') localStorage.removeItem('user')