mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-05-05 06:01:45 +00:00
系统性地修复、补充和强化项目的自动化测试能力: 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>
240 lines
7.0 KiB
TypeScript
240 lines
7.0 KiB
TypeScript
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()
|
||
})
|
||
})
|
||
})
|