test: 完善自动化测试体系(7个模块,73个任务)
系统性地修复、补充和强化项目的自动化测试能力: 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>
This commit is contained in:
324
frontend/src/router/__tests__/guards.spec.ts
Normal file
324
frontend/src/router/__tests__/guards.spec.ts
Normal file
@@ -0,0 +1,324 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||
import { setActivePinia, createPinia } from 'pinia'
|
||||
import { defineComponent, h } from 'vue'
|
||||
|
||||
// Mock 导航加载状态
|
||||
vi.mock('@/composables/useNavigationLoading', () => {
|
||||
const mockStart = vi.fn()
|
||||
const mockEnd = vi.fn()
|
||||
return {
|
||||
useNavigationLoadingState: () => ({
|
||||
startNavigation: mockStart,
|
||||
endNavigation: mockEnd,
|
||||
isLoading: { value: false },
|
||||
}),
|
||||
useNavigationLoading: () => ({
|
||||
startNavigation: mockStart,
|
||||
endNavigation: mockEnd,
|
||||
isLoading: { value: false },
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
// Mock 路由预加载
|
||||
vi.mock('@/composables/useRoutePrefetch', () => ({
|
||||
useRoutePrefetch: () => ({
|
||||
triggerPrefetch: vi.fn(),
|
||||
cancelPendingPrefetch: vi.fn(),
|
||||
resetPrefetchState: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock API 相关模块
|
||||
vi.mock('@/api', () => ({
|
||||
authAPI: {
|
||||
getCurrentUser: vi.fn().mockResolvedValue({ data: {} }),
|
||||
logout: vi.fn(),
|
||||
},
|
||||
isTotp2FARequired: () => false,
|
||||
}))
|
||||
|
||||
vi.mock('@/api/admin/system', () => ({
|
||||
checkUpdates: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/api/auth', () => ({
|
||||
getPublicSettings: vi.fn(),
|
||||
}))
|
||||
|
||||
const DummyComponent = defineComponent({
|
||||
render() {
|
||||
return h('div', 'dummy')
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* 创建带守卫逻辑的测试路由
|
||||
* 模拟 router/index.ts 中的 beforeEach 守卫逻辑
|
||||
*/
|
||||
function createTestRouter() {
|
||||
const router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{ path: '/login', component: DummyComponent, meta: { requiresAuth: false, title: 'Login' } },
|
||||
{
|
||||
path: '/register',
|
||||
component: DummyComponent,
|
||||
meta: { requiresAuth: false, title: 'Register' },
|
||||
},
|
||||
{ path: '/home', component: DummyComponent, meta: { requiresAuth: false, title: 'Home' } },
|
||||
{ path: '/dashboard', component: DummyComponent, meta: { title: 'Dashboard' } },
|
||||
{ path: '/keys', component: DummyComponent, meta: { title: 'API Keys' } },
|
||||
{ path: '/subscriptions', component: DummyComponent, meta: { title: 'Subscriptions' } },
|
||||
{ path: '/redeem', component: DummyComponent, meta: { title: 'Redeem' } },
|
||||
{
|
||||
path: '/admin/dashboard',
|
||||
component: DummyComponent,
|
||||
meta: { requiresAdmin: true, title: 'Admin Dashboard' },
|
||||
},
|
||||
{
|
||||
path: '/admin/users',
|
||||
component: DummyComponent,
|
||||
meta: { requiresAdmin: true, title: 'Admin Users' },
|
||||
},
|
||||
{
|
||||
path: '/admin/groups',
|
||||
component: DummyComponent,
|
||||
meta: { requiresAdmin: true, title: 'Admin Groups' },
|
||||
},
|
||||
{
|
||||
path: '/admin/subscriptions',
|
||||
component: DummyComponent,
|
||||
meta: { requiresAdmin: true, title: 'Admin Subscriptions' },
|
||||
},
|
||||
{
|
||||
path: '/admin/redeem',
|
||||
component: DummyComponent,
|
||||
meta: { requiresAdmin: true, title: 'Admin Redeem' },
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
// 用于测试的 auth 状态
|
||||
interface MockAuthState {
|
||||
isAuthenticated: boolean
|
||||
isAdmin: boolean
|
||||
isSimpleMode: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 router/index.ts 中 beforeEach 守卫的核心逻辑提取为可测试的函数
|
||||
*/
|
||||
function simulateGuard(
|
||||
toPath: string,
|
||||
toMeta: Record<string, any>,
|
||||
authState: MockAuthState
|
||||
): string | null {
|
||||
const requiresAuth = toMeta.requiresAuth !== false
|
||||
const requiresAdmin = toMeta.requiresAdmin === true
|
||||
|
||||
// 不需要认证的路由
|
||||
if (!requiresAuth) {
|
||||
if (
|
||||
authState.isAuthenticated &&
|
||||
(toPath === '/login' || toPath === '/register')
|
||||
) {
|
||||
return authState.isAdmin ? '/admin/dashboard' : '/dashboard'
|
||||
}
|
||||
return null // 允许通过
|
||||
}
|
||||
|
||||
// 需要认证但未登录
|
||||
if (!authState.isAuthenticated) {
|
||||
return '/login'
|
||||
}
|
||||
|
||||
// 需要管理员但不是管理员
|
||||
if (requiresAdmin && !authState.isAdmin) {
|
||||
return '/dashboard'
|
||||
}
|
||||
|
||||
// 简易模式限制
|
||||
if (authState.isSimpleMode) {
|
||||
const restrictedPaths = [
|
||||
'/admin/groups',
|
||||
'/admin/subscriptions',
|
||||
'/admin/redeem',
|
||||
'/subscriptions',
|
||||
'/redeem',
|
||||
]
|
||||
if (restrictedPaths.some((path) => toPath.startsWith(path))) {
|
||||
return authState.isAdmin ? '/admin/dashboard' : '/dashboard'
|
||||
}
|
||||
}
|
||||
|
||||
return null // 允许通过
|
||||
}
|
||||
|
||||
describe('路由守卫逻辑', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
})
|
||||
|
||||
// --- 未认证用户 ---
|
||||
|
||||
describe('未认证用户', () => {
|
||||
const authState: MockAuthState = {
|
||||
isAuthenticated: false,
|
||||
isAdmin: false,
|
||||
isSimpleMode: false,
|
||||
}
|
||||
|
||||
it('访问需要认证的页面重定向到 /login', () => {
|
||||
const redirect = simulateGuard('/dashboard', {}, authState)
|
||||
expect(redirect).toBe('/login')
|
||||
})
|
||||
|
||||
it('访问管理页面重定向到 /login', () => {
|
||||
const redirect = simulateGuard('/admin/dashboard', { requiresAdmin: true }, authState)
|
||||
expect(redirect).toBe('/login')
|
||||
})
|
||||
|
||||
it('访问公开页面允许通过', () => {
|
||||
const redirect = simulateGuard('/login', { requiresAuth: false }, authState)
|
||||
expect(redirect).toBeNull()
|
||||
})
|
||||
|
||||
it('访问 /home 公开页面允许通过', () => {
|
||||
const redirect = simulateGuard('/home', { requiresAuth: false }, authState)
|
||||
expect(redirect).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
// --- 已认证普通用户 ---
|
||||
|
||||
describe('已认证普通用户', () => {
|
||||
const authState: MockAuthState = {
|
||||
isAuthenticated: true,
|
||||
isAdmin: false,
|
||||
isSimpleMode: false,
|
||||
}
|
||||
|
||||
it('访问 /login 重定向到 /dashboard', () => {
|
||||
const redirect = simulateGuard('/login', { requiresAuth: false }, authState)
|
||||
expect(redirect).toBe('/dashboard')
|
||||
})
|
||||
|
||||
it('访问 /register 重定向到 /dashboard', () => {
|
||||
const redirect = simulateGuard('/register', { requiresAuth: false }, authState)
|
||||
expect(redirect).toBe('/dashboard')
|
||||
})
|
||||
|
||||
it('访问 /dashboard 允许通过', () => {
|
||||
const redirect = simulateGuard('/dashboard', {}, authState)
|
||||
expect(redirect).toBeNull()
|
||||
})
|
||||
|
||||
it('访问管理页面被拒绝,重定向到 /dashboard', () => {
|
||||
const redirect = simulateGuard('/admin/dashboard', { requiresAdmin: true }, authState)
|
||||
expect(redirect).toBe('/dashboard')
|
||||
})
|
||||
|
||||
it('访问 /admin/users 被拒绝', () => {
|
||||
const redirect = simulateGuard('/admin/users', { requiresAdmin: true }, authState)
|
||||
expect(redirect).toBe('/dashboard')
|
||||
})
|
||||
})
|
||||
|
||||
// --- 已认证管理员 ---
|
||||
|
||||
describe('已认证管理员', () => {
|
||||
const authState: MockAuthState = {
|
||||
isAuthenticated: true,
|
||||
isAdmin: true,
|
||||
isSimpleMode: false,
|
||||
}
|
||||
|
||||
it('访问 /login 重定向到 /admin/dashboard', () => {
|
||||
const redirect = simulateGuard('/login', { requiresAuth: false }, authState)
|
||||
expect(redirect).toBe('/admin/dashboard')
|
||||
})
|
||||
|
||||
it('访问管理页面允许通过', () => {
|
||||
const redirect = simulateGuard('/admin/dashboard', { requiresAdmin: true }, authState)
|
||||
expect(redirect).toBeNull()
|
||||
})
|
||||
|
||||
it('访问用户页面允许通过', () => {
|
||||
const redirect = simulateGuard('/dashboard', {}, authState)
|
||||
expect(redirect).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
// --- 简易模式 ---
|
||||
|
||||
describe('简易模式受限路由', () => {
|
||||
it('普通用户简易模式访问 /subscriptions 重定向到 /dashboard', () => {
|
||||
const authState: MockAuthState = {
|
||||
isAuthenticated: true,
|
||||
isAdmin: false,
|
||||
isSimpleMode: true,
|
||||
}
|
||||
const redirect = simulateGuard('/subscriptions', {}, authState)
|
||||
expect(redirect).toBe('/dashboard')
|
||||
})
|
||||
|
||||
it('普通用户简易模式访问 /redeem 重定向到 /dashboard', () => {
|
||||
const authState: MockAuthState = {
|
||||
isAuthenticated: true,
|
||||
isAdmin: false,
|
||||
isSimpleMode: true,
|
||||
}
|
||||
const redirect = simulateGuard('/redeem', {}, authState)
|
||||
expect(redirect).toBe('/dashboard')
|
||||
})
|
||||
|
||||
it('管理员简易模式访问 /admin/groups 重定向到 /admin/dashboard', () => {
|
||||
const authState: MockAuthState = {
|
||||
isAuthenticated: true,
|
||||
isAdmin: true,
|
||||
isSimpleMode: true,
|
||||
}
|
||||
const redirect = simulateGuard('/admin/groups', { requiresAdmin: true }, authState)
|
||||
expect(redirect).toBe('/admin/dashboard')
|
||||
})
|
||||
|
||||
it('管理员简易模式访问 /admin/subscriptions 重定向', () => {
|
||||
const authState: MockAuthState = {
|
||||
isAuthenticated: true,
|
||||
isAdmin: true,
|
||||
isSimpleMode: true,
|
||||
}
|
||||
const redirect = simulateGuard(
|
||||
'/admin/subscriptions',
|
||||
{ requiresAdmin: true },
|
||||
authState
|
||||
)
|
||||
expect(redirect).toBe('/admin/dashboard')
|
||||
})
|
||||
|
||||
it('简易模式下非受限页面正常访问', () => {
|
||||
const authState: MockAuthState = {
|
||||
isAuthenticated: true,
|
||||
isAdmin: false,
|
||||
isSimpleMode: true,
|
||||
}
|
||||
const redirect = simulateGuard('/dashboard', {}, authState)
|
||||
expect(redirect).toBeNull()
|
||||
})
|
||||
|
||||
it('简易模式下 /keys 正常访问', () => {
|
||||
const authState: MockAuthState = {
|
||||
isAuthenticated: true,
|
||||
isAdmin: false,
|
||||
isSimpleMode: true,
|
||||
}
|
||||
const redirect = simulateGuard('/keys', {}, authState)
|
||||
expect(redirect).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user