Files
xinghuoapi/backend/internal/service/claude_code_validator_test.go
QTom 4280aca82c feat(gateway): 添加 Claude Code 客户端最低版本检查功能
- 通过 User-Agent 识别 Claude Code 客户端并提取版本号
- 在网关层验证客户端版本是否满足管理员配置的最低要求
- 在管理后台提供版本要求配置选项(英文/中文双语)
- 实现原子缓存 + singleflight 防止并发问题和 thundering herd
- 使用 context.WithoutCancel 隔离 DB 查询,避免客户端断连影响缓存
- 双 TTL 策略:60s 正常、5s 错误恢复,保证性能与可用性
- 仅检查 Claude Code 客户端,其他客户端不受影响
- 添加完整单元测试覆盖版本提取、比对、上下文操作
2026-03-01 15:45:44 +08:00

107 lines
3.3 KiB
Go

package service
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/Wei-Shaw/sub2api/internal/pkg/ctxkey"
"github.com/stretchr/testify/require"
)
func TestClaudeCodeValidator_ProbeBypass(t *testing.T) {
validator := NewClaudeCodeValidator()
req := httptest.NewRequest(http.MethodPost, "http://example.com/v1/messages", nil)
req.Header.Set("User-Agent", "claude-cli/1.2.3 (darwin; arm64)")
req = req.WithContext(context.WithValue(req.Context(), ctxkey.IsMaxTokensOneHaikuRequest, true))
ok := validator.Validate(req, map[string]any{
"model": "claude-haiku-4-5",
"max_tokens": 1,
})
require.True(t, ok)
}
func TestClaudeCodeValidator_ProbeBypassRequiresUA(t *testing.T) {
validator := NewClaudeCodeValidator()
req := httptest.NewRequest(http.MethodPost, "http://example.com/v1/messages", nil)
req.Header.Set("User-Agent", "curl/8.0.0")
req = req.WithContext(context.WithValue(req.Context(), ctxkey.IsMaxTokensOneHaikuRequest, true))
ok := validator.Validate(req, map[string]any{
"model": "claude-haiku-4-5",
"max_tokens": 1,
})
require.False(t, ok)
}
func TestClaudeCodeValidator_MessagesWithoutProbeStillNeedStrictValidation(t *testing.T) {
validator := NewClaudeCodeValidator()
req := httptest.NewRequest(http.MethodPost, "http://example.com/v1/messages", nil)
req.Header.Set("User-Agent", "claude-cli/1.2.3 (darwin; arm64)")
ok := validator.Validate(req, map[string]any{
"model": "claude-haiku-4-5",
"max_tokens": 1,
})
require.False(t, ok)
}
func TestClaudeCodeValidator_NonMessagesPathUAOnly(t *testing.T) {
validator := NewClaudeCodeValidator()
req := httptest.NewRequest(http.MethodPost, "http://example.com/v1/models", nil)
req.Header.Set("User-Agent", "claude-cli/1.2.3 (darwin; arm64)")
ok := validator.Validate(req, nil)
require.True(t, ok)
}
func TestExtractVersion(t *testing.T) {
v := NewClaudeCodeValidator()
tests := []struct {
ua string
want string
}{
{"claude-cli/2.1.22 (darwin; arm64)", "2.1.22"},
{"claude-cli/1.0.0", "1.0.0"},
{"Claude-CLI/3.10.5 (linux; x86_64)", "3.10.5"}, // 大小写不敏感
{"curl/8.0.0", ""}, // 非 Claude CLI
{"", ""}, // 空字符串
{"claude-cli/", ""}, // 无版本号
{"claude-cli/2.1.22-beta", "2.1.22"}, // 带后缀仍提取主版本号
}
for _, tt := range tests {
got := v.ExtractVersion(tt.ua)
require.Equal(t, tt.want, got, "ExtractVersion(%q)", tt.ua)
}
}
func TestCompareVersions(t *testing.T) {
tests := []struct {
a, b string
want int
}{
{"2.1.0", "2.1.0", 0}, // 相等
{"2.1.1", "2.1.0", 1}, // patch 更大
{"2.0.0", "2.1.0", -1}, // minor 更小
{"3.0.0", "2.99.99", 1}, // major 更大
{"1.0.0", "2.0.0", -1}, // major 更小
{"0.0.1", "0.0.0", 1}, // patch 差异
{"", "1.0.0", -1}, // 空字符串 vs 正常版本
{"v2.1.0", "2.1.0", 0}, // v 前缀处理
}
for _, tt := range tests {
got := CompareVersions(tt.a, tt.b)
require.Equal(t, tt.want, got, "CompareVersions(%q, %q)", tt.a, tt.b)
}
}
func TestSetGetClaudeCodeVersion(t *testing.T) {
ctx := context.Background()
require.Equal(t, "", GetClaudeCodeVersion(ctx), "empty context should return empty string")
ctx = SetClaudeCodeVersion(ctx, "2.1.63")
require.Equal(t, "2.1.63", GetClaudeCodeVersion(ctx))
}