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:
yangjianbo
2026-02-08 12:05:39 +08:00
parent 53e1c8b268
commit bb5a5dd65e
41 changed files with 5101 additions and 182 deletions

View File

@@ -0,0 +1,239 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useSubscriptionStore } from '@/stores/subscriptions'
// Mock subscriptions API
const mockGetActiveSubscriptions = vi.fn()
vi.mock('@/api/subscriptions', () => ({
default: {
getActiveSubscriptions: (...args: any[]) => mockGetActiveSubscriptions(...args),
},
}))
const fakeSubscriptions = [
{
id: 1,
user_id: 1,
group_id: 1,
status: 'active' as const,
daily_usage_usd: 5,
weekly_usage_usd: 20,
monthly_usage_usd: 50,
daily_window_start: null,
weekly_window_start: null,
monthly_window_start: null,
created_at: '2024-01-01',
updated_at: '2024-01-01',
expires_at: '2025-01-01',
},
{
id: 2,
user_id: 1,
group_id: 2,
status: 'active' as const,
daily_usage_usd: 10,
weekly_usage_usd: 40,
monthly_usage_usd: 100,
daily_window_start: null,
weekly_window_start: null,
monthly_window_start: null,
created_at: '2024-02-01',
updated_at: '2024-02-01',
expires_at: '2025-02-01',
},
]
describe('useSubscriptionStore', () => {
beforeEach(() => {
setActivePinia(createPinia())
vi.useFakeTimers()
vi.clearAllMocks()
})
afterEach(() => {
vi.useRealTimers()
})
// --- fetchActiveSubscriptions ---
describe('fetchActiveSubscriptions', () => {
it('成功获取活跃订阅', async () => {
mockGetActiveSubscriptions.mockResolvedValue(fakeSubscriptions)
const store = useSubscriptionStore()
const result = await store.fetchActiveSubscriptions()
expect(result).toEqual(fakeSubscriptions)
expect(store.activeSubscriptions).toEqual(fakeSubscriptions)
expect(store.loading).toBe(false)
})
it('缓存有效时返回缓存数据', async () => {
mockGetActiveSubscriptions.mockResolvedValue(fakeSubscriptions)
const store = useSubscriptionStore()
// 第一次请求
await store.fetchActiveSubscriptions()
expect(mockGetActiveSubscriptions).toHaveBeenCalledTimes(1)
// 第二次请求60秒内- 应返回缓存
const result = await store.fetchActiveSubscriptions()
expect(mockGetActiveSubscriptions).toHaveBeenCalledTimes(1) // 没有新请求
expect(result).toEqual(fakeSubscriptions)
})
it('缓存过期后重新请求', async () => {
mockGetActiveSubscriptions.mockResolvedValue(fakeSubscriptions)
const store = useSubscriptionStore()
await store.fetchActiveSubscriptions()
expect(mockGetActiveSubscriptions).toHaveBeenCalledTimes(1)
// 推进 61 秒让缓存过期
vi.advanceTimersByTime(61_000)
const updatedSubs = [fakeSubscriptions[0]]
mockGetActiveSubscriptions.mockResolvedValue(updatedSubs)
const result = await store.fetchActiveSubscriptions()
expect(mockGetActiveSubscriptions).toHaveBeenCalledTimes(2)
expect(result).toEqual(updatedSubs)
})
it('force=true 强制重新请求', async () => {
mockGetActiveSubscriptions.mockResolvedValue(fakeSubscriptions)
const store = useSubscriptionStore()
await store.fetchActiveSubscriptions()
const updatedSubs = [fakeSubscriptions[0]]
mockGetActiveSubscriptions.mockResolvedValue(updatedSubs)
const result = await store.fetchActiveSubscriptions(true)
expect(mockGetActiveSubscriptions).toHaveBeenCalledTimes(2)
expect(result).toEqual(updatedSubs)
})
it('并发请求共享同一个 Promise去重', async () => {
let resolvePromise: (v: any) => void
mockGetActiveSubscriptions.mockImplementation(
() => new Promise((resolve) => { resolvePromise = resolve })
)
const store = useSubscriptionStore()
// 并发发起两个请求
const p1 = store.fetchActiveSubscriptions()
const p2 = store.fetchActiveSubscriptions()
// 只调用了一次 API
expect(mockGetActiveSubscriptions).toHaveBeenCalledTimes(1)
// 解决 Promise
resolvePromise!(fakeSubscriptions)
const [r1, r2] = await Promise.all([p1, p2])
expect(r1).toEqual(fakeSubscriptions)
expect(r2).toEqual(fakeSubscriptions)
})
it('API 错误时抛出异常', async () => {
mockGetActiveSubscriptions.mockRejectedValue(new Error('Network error'))
const store = useSubscriptionStore()
await expect(store.fetchActiveSubscriptions()).rejects.toThrow('Network error')
})
})
// --- hasActiveSubscriptions ---
describe('hasActiveSubscriptions', () => {
it('有订阅时返回 true', async () => {
mockGetActiveSubscriptions.mockResolvedValue(fakeSubscriptions)
const store = useSubscriptionStore()
await store.fetchActiveSubscriptions()
expect(store.hasActiveSubscriptions).toBe(true)
})
it('无订阅时返回 false', () => {
const store = useSubscriptionStore()
expect(store.hasActiveSubscriptions).toBe(false)
})
it('清除后返回 false', async () => {
mockGetActiveSubscriptions.mockResolvedValue(fakeSubscriptions)
const store = useSubscriptionStore()
await store.fetchActiveSubscriptions()
expect(store.hasActiveSubscriptions).toBe(true)
store.clear()
expect(store.hasActiveSubscriptions).toBe(false)
})
})
// --- invalidateCache ---
describe('invalidateCache', () => {
it('失效缓存后下次请求重新获取数据', async () => {
mockGetActiveSubscriptions.mockResolvedValue(fakeSubscriptions)
const store = useSubscriptionStore()
await store.fetchActiveSubscriptions()
expect(mockGetActiveSubscriptions).toHaveBeenCalledTimes(1)
store.invalidateCache()
await store.fetchActiveSubscriptions()
expect(mockGetActiveSubscriptions).toHaveBeenCalledTimes(2)
})
})
// --- clear ---
describe('clear', () => {
it('清除所有订阅数据', async () => {
mockGetActiveSubscriptions.mockResolvedValue(fakeSubscriptions)
const store = useSubscriptionStore()
await store.fetchActiveSubscriptions()
expect(store.activeSubscriptions).toHaveLength(2)
store.clear()
expect(store.activeSubscriptions).toHaveLength(0)
expect(store.hasActiveSubscriptions).toBe(false)
})
})
// --- polling ---
describe('startPolling / stopPolling', () => {
it('startPolling 不会创建重复 interval', () => {
const store = useSubscriptionStore()
mockGetActiveSubscriptions.mockResolvedValue([])
store.startPolling()
store.startPolling() // 重复调用
// 推进5分钟只触发一次
vi.advanceTimersByTime(5 * 60 * 1000)
expect(mockGetActiveSubscriptions).toHaveBeenCalledTimes(1)
store.stopPolling()
})
it('stopPolling 停止定期刷新', () => {
const store = useSubscriptionStore()
mockGetActiveSubscriptions.mockResolvedValue([])
store.startPolling()
store.stopPolling()
vi.advanceTimersByTime(10 * 60 * 1000)
expect(mockGetActiveSubscriptions).not.toHaveBeenCalled()
})
})
})