mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-05-05 23:59:36 +00:00
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:
239
frontend/src/stores/__tests__/subscriptions.spec.ts
Normal file
239
frontend/src/stores/__tests__/subscriptions.spec.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user