系统性地修复、补充和强化项目的自动化测试能力: 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>
213 lines
4.9 KiB
Go
213 lines
4.9 KiB
Go
//go:build unit
|
|
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
var _ OpsRepository = (*stubOpsRepo)(nil)
|
|
|
|
type stubOpsRepo struct {
|
|
OpsRepository
|
|
overview *OpsDashboardOverview
|
|
err error
|
|
}
|
|
|
|
func (s *stubOpsRepo) GetDashboardOverview(ctx context.Context, filter *OpsDashboardFilter) (*OpsDashboardOverview, error) {
|
|
if s.err != nil {
|
|
return nil, s.err
|
|
}
|
|
if s.overview != nil {
|
|
return s.overview, nil
|
|
}
|
|
return &OpsDashboardOverview{}, nil
|
|
}
|
|
|
|
func TestComputeGroupAvailableRatio(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("正常情况: 10个账号, 8个可用 = 80%", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
got := computeGroupAvailableRatio(&GroupAvailability{
|
|
TotalAccounts: 10,
|
|
AvailableCount: 8,
|
|
})
|
|
require.InDelta(t, 80.0, got, 0.0001)
|
|
})
|
|
|
|
t.Run("边界情况: TotalAccounts = 0 应返回 0", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
got := computeGroupAvailableRatio(&GroupAvailability{
|
|
TotalAccounts: 0,
|
|
AvailableCount: 8,
|
|
})
|
|
require.Equal(t, 0.0, got)
|
|
})
|
|
|
|
t.Run("边界情况: AvailableCount = 0 应返回 0%", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
got := computeGroupAvailableRatio(&GroupAvailability{
|
|
TotalAccounts: 10,
|
|
AvailableCount: 0,
|
|
})
|
|
require.Equal(t, 0.0, got)
|
|
})
|
|
}
|
|
|
|
func TestCountAccountsByCondition(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("测试限流账号统计: acc.IsRateLimited", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
accounts := map[int64]*AccountAvailability{
|
|
1: {IsRateLimited: true},
|
|
2: {IsRateLimited: false},
|
|
3: {IsRateLimited: true},
|
|
}
|
|
|
|
got := countAccountsByCondition(accounts, func(acc *AccountAvailability) bool {
|
|
return acc.IsRateLimited
|
|
})
|
|
require.Equal(t, int64(2), got)
|
|
})
|
|
|
|
t.Run("测试错误账号统计(排除临时不可调度): acc.HasError && acc.TempUnschedulableUntil == nil", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
until := time.Now().UTC().Add(5 * time.Minute)
|
|
accounts := map[int64]*AccountAvailability{
|
|
1: {HasError: true},
|
|
2: {HasError: true, TempUnschedulableUntil: &until},
|
|
3: {HasError: false},
|
|
}
|
|
|
|
got := countAccountsByCondition(accounts, func(acc *AccountAvailability) bool {
|
|
return acc.HasError && acc.TempUnschedulableUntil == nil
|
|
})
|
|
require.Equal(t, int64(1), got)
|
|
})
|
|
|
|
t.Run("边界情况: 空 map 应返回 0", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
got := countAccountsByCondition(map[int64]*AccountAvailability{}, func(acc *AccountAvailability) bool {
|
|
return acc.IsRateLimited
|
|
})
|
|
require.Equal(t, int64(0), got)
|
|
})
|
|
}
|
|
|
|
func TestComputeRuleMetricNewIndicators(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
groupID := int64(101)
|
|
platform := "openai"
|
|
|
|
availability := &OpsAccountAvailability{
|
|
Group: &GroupAvailability{
|
|
GroupID: groupID,
|
|
TotalAccounts: 10,
|
|
AvailableCount: 8,
|
|
},
|
|
Accounts: map[int64]*AccountAvailability{
|
|
1: {IsRateLimited: true},
|
|
2: {IsRateLimited: true},
|
|
3: {HasError: true},
|
|
4: {HasError: true, TempUnschedulableUntil: timePtr(time.Now().UTC().Add(2 * time.Minute))},
|
|
5: {HasError: false, IsRateLimited: false},
|
|
},
|
|
}
|
|
|
|
opsService := &OpsService{
|
|
getAccountAvailability: func(_ context.Context, _ string, _ *int64) (*OpsAccountAvailability, error) {
|
|
return availability, nil
|
|
},
|
|
}
|
|
|
|
svc := &OpsAlertEvaluatorService{
|
|
opsService: opsService,
|
|
opsRepo: &stubOpsRepo{overview: &OpsDashboardOverview{}},
|
|
}
|
|
|
|
start := time.Now().UTC().Add(-5 * time.Minute)
|
|
end := time.Now().UTC()
|
|
ctx := context.Background()
|
|
|
|
tests := []struct {
|
|
name string
|
|
metricType string
|
|
groupID *int64
|
|
wantValue float64
|
|
wantOK bool
|
|
}{
|
|
{
|
|
name: "group_available_accounts",
|
|
metricType: "group_available_accounts",
|
|
groupID: &groupID,
|
|
wantValue: 8,
|
|
wantOK: true,
|
|
},
|
|
{
|
|
name: "group_available_ratio",
|
|
metricType: "group_available_ratio",
|
|
groupID: &groupID,
|
|
wantValue: 80.0,
|
|
wantOK: true,
|
|
},
|
|
{
|
|
name: "account_rate_limited_count",
|
|
metricType: "account_rate_limited_count",
|
|
groupID: nil,
|
|
wantValue: 2,
|
|
wantOK: true,
|
|
},
|
|
{
|
|
name: "account_error_count",
|
|
metricType: "account_error_count",
|
|
groupID: nil,
|
|
wantValue: 1,
|
|
wantOK: true,
|
|
},
|
|
{
|
|
name: "group_available_accounts without group_id returns false",
|
|
metricType: "group_available_accounts",
|
|
groupID: nil,
|
|
wantValue: 0,
|
|
wantOK: false,
|
|
},
|
|
{
|
|
name: "group_available_ratio without group_id returns false",
|
|
metricType: "group_available_ratio",
|
|
groupID: nil,
|
|
wantValue: 0,
|
|
wantOK: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
rule := &OpsAlertRule{
|
|
MetricType: tt.metricType,
|
|
}
|
|
gotValue, gotOK := svc.computeRuleMetric(ctx, rule, nil, start, end, platform, tt.groupID)
|
|
require.Equal(t, tt.wantOK, gotOK)
|
|
if !tt.wantOK {
|
|
return
|
|
}
|
|
require.InDelta(t, tt.wantValue, gotValue, 0.0001)
|
|
})
|
|
}
|
|
}
|