bb5a5dd65e
系统性地修复、补充和强化项目的自动化测试能力: 1. 测试基础设施修复 - 修复 stubConcurrencyCache 缺失方法和构造函数参数不匹配 - 创建 testutil 共享包(stubs.go, fixtures.go, httptest.go) - 为所有 Stub 添加编译期接口断言 2. 中间件测试补充 - 新增 JWT 认证中间件测试(有效/过期/篡改/缺失 Token) - 补充 rate_limiter 和 recovery 中间件测试场景 3. 网关核心路径测试 - 新增账户选择、等待队列、流式响应、并发控制、计费、Claude Code 检测测试 - 覆盖负载均衡、粘性会话、SSE 转发、槽位管理等关键逻辑 4. 前端测试体系(11个新测试文件,163个测试用例) - Pinia stores: auth, app, subscriptions - API client: 请求拦截器、响应拦截器、401 刷新 - Router guards: 认证重定向、管理员权限、简易模式限制 - Composables: useForm, useTableLoader, useClipboard - Components: LoginForm, ApiKeyCreate, Dashboard 5. CI/CD 流水线重构 - 重构 backend-ci.yml 为统一的 ci.yml - 前后端 4 个并行 Job + Postgres/Redis services - Race 检测、覆盖率收集与门禁、Docker 构建验证 6. E2E 自动化测试 - e2e-test.sh 自动化脚本(Docker 启动→健康检查→测试→清理) - 用户注册→登录→API Key→网关调用完整链路测试 - Mock 模式和 API Key 脱敏支持 7. 修复预存问题 - tlsfingerprint dialer_test.go 缺失 build tag 导致集成测试编译冲突 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
179 lines
5.4 KiB
TypeScript
179 lines
5.4 KiB
TypeScript
/**
|
|
* LoginView 组件核心逻辑测试
|
|
* 测试登录表单提交、验证、2FA 等场景
|
|
*/
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import { mount, flushPromises } from '@vue/test-utils'
|
|
import { setActivePinia, createPinia } from 'pinia'
|
|
import { defineComponent, reactive, ref } from 'vue'
|
|
import { useAuthStore } from '@/stores/auth'
|
|
|
|
// Mock 所有外部依赖
|
|
const mockLogin = vi.fn()
|
|
const mockLogin2FA = vi.fn()
|
|
const mockPush = vi.fn()
|
|
|
|
vi.mock('@/api', () => ({
|
|
authAPI: {
|
|
login: (...args: any[]) => mockLogin(...args),
|
|
login2FA: (...args: any[]) => mockLogin2FA(...args),
|
|
logout: vi.fn(),
|
|
getCurrentUser: vi.fn().mockResolvedValue({ data: {} }),
|
|
register: vi.fn(),
|
|
refreshToken: vi.fn(),
|
|
},
|
|
isTotp2FARequired: (response: any) => response?.requires_2fa === true,
|
|
}))
|
|
|
|
vi.mock('@/api/admin/system', () => ({
|
|
checkUpdates: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('@/api/auth', () => ({
|
|
getPublicSettings: vi.fn().mockResolvedValue({}),
|
|
}))
|
|
|
|
/**
|
|
* 创建一个简化的测试组件来封装登录逻辑
|
|
* 避免引入 LoginView.vue 的全部依赖(AuthLayout、i18n、Icon 等)
|
|
*/
|
|
const LoginFormTestComponent = defineComponent({
|
|
setup() {
|
|
const authStore = useAuthStore()
|
|
const formData = reactive({ email: '', password: '' })
|
|
const isLoading = ref(false)
|
|
const errorMessage = ref('')
|
|
|
|
const handleLogin = async () => {
|
|
if (!formData.email || !formData.password) {
|
|
errorMessage.value = '请输入邮箱和密码'
|
|
return
|
|
}
|
|
|
|
isLoading.value = true
|
|
errorMessage.value = ''
|
|
|
|
try {
|
|
const response = await authStore.login({
|
|
email: formData.email,
|
|
password: formData.password,
|
|
})
|
|
|
|
// 2FA 流程由调用方处理
|
|
if ((response as any)?.requires_2fa) {
|
|
errorMessage.value = '需要 2FA 验证'
|
|
return
|
|
}
|
|
|
|
mockPush('/dashboard')
|
|
} catch (error: any) {
|
|
errorMessage.value = error.message || '登录失败'
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
return { formData, isLoading, errorMessage, handleLogin }
|
|
},
|
|
template: `
|
|
<form @submit.prevent="handleLogin">
|
|
<input id="email" v-model="formData.email" type="email" />
|
|
<input id="password" v-model="formData.password" type="password" />
|
|
<p v-if="errorMessage" class="error">{{ errorMessage }}</p>
|
|
<button type="submit" :disabled="isLoading">登录</button>
|
|
</form>
|
|
`,
|
|
})
|
|
|
|
describe('LoginForm 核心逻辑', () => {
|
|
beforeEach(() => {
|
|
setActivePinia(createPinia())
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it('成功登录后跳转到 dashboard', async () => {
|
|
mockLogin.mockResolvedValue({
|
|
access_token: 'token',
|
|
token_type: 'Bearer',
|
|
user: { id: 1, username: 'test', email: 'test@example.com', role: 'user', balance: 0, concurrency: 5, status: 'active', allowed_groups: null, created_at: '', updated_at: '' },
|
|
})
|
|
|
|
const wrapper = mount(LoginFormTestComponent)
|
|
|
|
await wrapper.find('#email').setValue('test@example.com')
|
|
await wrapper.find('#password').setValue('password123')
|
|
await wrapper.find('form').trigger('submit')
|
|
await flushPromises()
|
|
|
|
expect(mockLogin).toHaveBeenCalledWith({
|
|
email: 'test@example.com',
|
|
password: 'password123',
|
|
})
|
|
expect(mockPush).toHaveBeenCalledWith('/dashboard')
|
|
})
|
|
|
|
it('登录失败时显示错误信息', async () => {
|
|
mockLogin.mockRejectedValue(new Error('Invalid credentials'))
|
|
|
|
const wrapper = mount(LoginFormTestComponent)
|
|
|
|
await wrapper.find('#email').setValue('test@example.com')
|
|
await wrapper.find('#password').setValue('wrong')
|
|
await wrapper.find('form').trigger('submit')
|
|
await flushPromises()
|
|
|
|
expect(wrapper.find('.error').text()).toBe('Invalid credentials')
|
|
})
|
|
|
|
it('空表单提交显示验证错误', async () => {
|
|
const wrapper = mount(LoginFormTestComponent)
|
|
|
|
await wrapper.find('form').trigger('submit')
|
|
await flushPromises()
|
|
|
|
expect(wrapper.find('.error').text()).toBe('请输入邮箱和密码')
|
|
expect(mockLogin).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('需要 2FA 时不跳转', async () => {
|
|
mockLogin.mockResolvedValue({
|
|
requires_2fa: true,
|
|
temp_token: 'temp-123',
|
|
})
|
|
|
|
const wrapper = mount(LoginFormTestComponent)
|
|
|
|
await wrapper.find('#email').setValue('test@example.com')
|
|
await wrapper.find('#password').setValue('password123')
|
|
await wrapper.find('form').trigger('submit')
|
|
await flushPromises()
|
|
|
|
expect(mockPush).not.toHaveBeenCalled()
|
|
expect(wrapper.find('.error').text()).toBe('需要 2FA 验证')
|
|
})
|
|
|
|
it('提交过程中按钮被禁用', async () => {
|
|
let resolveLogin: (v: any) => void
|
|
mockLogin.mockImplementation(
|
|
() => new Promise((resolve) => { resolveLogin = resolve })
|
|
)
|
|
|
|
const wrapper = mount(LoginFormTestComponent)
|
|
|
|
await wrapper.find('#email').setValue('test@example.com')
|
|
await wrapper.find('#password').setValue('password123')
|
|
await wrapper.find('form').trigger('submit')
|
|
|
|
expect(wrapper.find('button').attributes('disabled')).toBeDefined()
|
|
|
|
resolveLogin!({
|
|
access_token: 'token',
|
|
token_type: 'Bearer',
|
|
user: { id: 1, username: 'test', email: 'test@example.com', role: 'user', balance: 0, concurrency: 5, status: 'active', allowed_groups: null, created_at: '', updated_at: '' },
|
|
})
|
|
await flushPromises()
|
|
|
|
expect(wrapper.find('button').attributes('disabled')).toBeUndefined()
|
|
})
|
|
})
|