fix login and register

This commit is contained in:
2026-03-18 16:23:58 +08:00
parent 9cba09d731
commit 2d063870fe
6 changed files with 480 additions and 5 deletions
+247
View File
@@ -0,0 +1,247 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { mount, flushPromises } from '@vue/test-utils'
import { nextTick } from 'vue'
import { message } from 'ant-design-vue'
import { ApiError } from '@/types/api'
import { useUserStore } from '@/stores/user'
// jsdom 不支持 matchMediaAnt Design Vue 响应式布局需要
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: (query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: () => {},
removeListener: () => {},
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: () => false,
}),
})
vi.mock('@/utils/request', () => ({
api: {
get: vi.fn(),
post: vi.fn(),
},
}))
const mockPush = vi.fn()
const mockRoute = { query: {} }
vi.mock('vue-router', () => ({
useRouter: () => ({ push: mockPush }),
useRoute: () => mockRoute,
}))
// Stub Brand component to avoid asset loading
vi.mock('@/components/Brand.vue', () => ({
default: { template: '<div class="brand-stub" />' },
}))
describe('Login Page', () => {
let wrapper: ReturnType<typeof mount>
beforeEach(() => {
setActivePinia(createPinia())
localStorage.clear()
vi.restoreAllMocks()
mockPush.mockReset()
mockRoute.query = {}
vi.useFakeTimers()
})
afterEach(() => {
wrapper?.unmount()
vi.useRealTimers()
})
async function mountPage() {
const { default: LoginPage } = await import('../login.vue')
wrapper = mount(LoginPage, {
attachTo: document.body,
global: {
stubs: {
UserOutlined: { template: '<span />' },
LockOutlined: { template: '<span />' },
},
},
})
await flushPromises()
await nextTick()
return wrapper
}
it('renders login form with username, password inputs and submit button', async () => {
await mountPage()
const html = wrapper.html()
// 验证输入框存在
expect(html).toContain('请输入用户名')
expect(html).toContain('请输入密码')
// 验证提交按钮存在
expect(html).toContain('登录')
})
it('calls /api/v1/login, sets token/user and redirects to /', async () => {
const { api } = await import('@/utils/request')
const mockResponse = {
access_token: 'a.b.c',
refresh_token: 'r.r.r',
user: { id: 1, username: 'admin', email: 'a@b.com', role: 'admin', status: 1 },
}
vi.mocked(api.post).mockResolvedValueOnce(mockResponse)
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()
expect(api.post).toHaveBeenCalledWith('/api/v1/login', {
username: 'admin',
password: 'password',
})
expect(store.token).toBe('a.b.c')
expect(store.user).toEqual(mockResponse.user)
vi.advanceTimersByTime(500)
expect(mockPush).toHaveBeenCalledWith('/')
})
it('passes remember=false to setToken when unchecked', async () => {
const { api } = await import('@/utils/request')
vi.mocked(api.post).mockResolvedValueOnce({
access_token: 'a.b.c',
refresh_token: 'r.r.r',
user: { id: 1, username: 'u', email: 'u@u.com', role: 'user', status: 1 },
})
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()
expect(localStorage.getItem('access_token')).toBeNull()
expect(localStorage.getItem('user')).toBeNull()
})
it('redirects to safe path from query param', async () => {
const { api } = await import('@/utils/request')
vi.mocked(api.post).mockResolvedValueOnce({
access_token: 'a.b.c',
refresh_token: 'r.r.r',
user: { id: 1, username: 'u', email: 'u@u.com', role: 'user', status: 1 },
})
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()
vi.advanceTimersByTime(500)
expect(mockPush).toHaveBeenCalledWith('/dashboard')
})
it('blocks //evil redirect and falls back to /', async () => {
const { api } = await import('@/utils/request')
vi.mocked(api.post).mockResolvedValueOnce({
access_token: 'a.b.c',
refresh_token: 'r.r.r',
user: { id: 1, username: 'u', email: 'u@u.com', role: 'user', status: 1 },
})
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()
vi.advanceTimersByTime(500)
expect(mockPush).toHaveBeenCalledWith('/')
})
it('blocks http:// redirect and falls back to /', async () => {
const { api } = await import('@/utils/request')
vi.mocked(api.post).mockResolvedValueOnce({
access_token: 'a.b.c',
refresh_token: 'r.r.r',
user: { id: 1, username: 'u', email: 'u@u.com', role: 'user', status: 1 },
})
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()
vi.advanceTimersByTime(500)
expect(mockPush).toHaveBeenCalledWith('/')
})
it('shows error message on ApiError', async () => {
const { api } = await import('@/utils/request')
vi.mocked(api.post).mockRejectedValueOnce(new ApiError('用户名或密码错误', 401))
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()
expect(errorSpy).toHaveBeenCalledWith('用户名或密码错误')
errorSpy.mockRestore()
})
it('shows network error message on generic error', async () => {
const { api } = await import('@/utils/request')
vi.mocked(api.post).mockRejectedValueOnce(new Error('Network failure'))
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()
expect(errorSpy).toHaveBeenCalledWith('登录失败,请检查网络连接')
errorSpy.mockRestore()
})
it('navigates to register page', async () => {
await mountPage()
const vm = wrapper.getCurrentComponent().setupState
vm.goToRegister()
expect(mockPush).toHaveBeenCalledWith('/register')
})
})