You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
191 lines
6.7 KiB
191 lines
6.7 KiB
package console
|
|
|
|
// 巡检的纯逻辑单测:配置校验规则、状态码判定、签名编码、探测去重键。
|
|
// 这些是巡检结论的判据,判错会直接变成误报/漏报,值得钉死。
|
|
// 真实探针(会打第三方接口)不在单测覆盖范围内。
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"yunyan/comm"
|
|
)
|
|
|
|
func TestCheckSvcConfig(t *testing.T) {
|
|
encField := SvcField{Key: "api_key", Encrypted: true}
|
|
plainField := SvcField{Key: "model"}
|
|
|
|
tests := []struct {
|
|
name string
|
|
svc *ThirdSvcConfig
|
|
ovr *SvcRegionOverride
|
|
cats map[int32]bool
|
|
fields map[string]string
|
|
wantFatal bool
|
|
wantHas string // 期望问题清单里包含的关键字,空=不检查
|
|
}{
|
|
{
|
|
name: "凭据齐全且勾了类别,无任何问题",
|
|
svc: &ThirdSvcConfig{Categories: "5", Fields: []SvcField{encField, plainField}},
|
|
cats: map[int32]bool{svcCatLLM: true},
|
|
fields: map[string]string{"api_key": "sk-xxx", "model": "gpt-4o"},
|
|
},
|
|
{
|
|
name: "加密字段为空判致命",
|
|
svc: &ThirdSvcConfig{Categories: "5", Fields: []SvcField{encField}},
|
|
cats: map[int32]bool{svcCatLLM: true},
|
|
fields: map[string]string{"api_key": " "},
|
|
wantFatal: true,
|
|
wantHas: "api_key",
|
|
},
|
|
{
|
|
name: "被区域停用的加密字段不算缺失",
|
|
svc: &ThirdSvcConfig{Categories: "5", Fields: []SvcField{encField}},
|
|
ovr: &SvcRegionOverride{DisabledKeys: []string{"api_key"}},
|
|
cats: map[int32]bool{svcCatLLM: true},
|
|
// 该区域本就不用这个字段,为空是正常的。
|
|
fields: map[string]string{},
|
|
},
|
|
{
|
|
name: "区域专属新增的加密字段为空同样判致命",
|
|
svc: &ThirdSvcConfig{Categories: "5", Fields: []SvcField{encField}},
|
|
ovr: &SvcRegionOverride{ExtraFields: []SvcField{{Key: "region_key", Encrypted: true}}},
|
|
cats: map[int32]bool{svcCatLLM: true},
|
|
fields: map[string]string{"api_key": "sk-xxx", "region_key": ""},
|
|
wantFatal: true,
|
|
wantHas: "region_key",
|
|
},
|
|
{
|
|
name: "MCP 缺 url 判致命",
|
|
svc: &ThirdSvcConfig{Categories: "10"},
|
|
cats: map[int32]bool{svcCatMCP: true},
|
|
fields: map[string]string{},
|
|
wantFatal: true,
|
|
wantHas: "MCP",
|
|
},
|
|
{
|
|
name: "未勾类别只是隐患,不致命",
|
|
svc: &ThirdSvcConfig{Categories: "", Fields: []SvcField{encField}},
|
|
cats: map[int32]bool{},
|
|
fields: map[string]string{"api_key": "sk-xxx"},
|
|
wantHas: "未勾选服务类别",
|
|
},
|
|
{
|
|
name: "TTS 缺 languages 只是隐患",
|
|
svc: &ThirdSvcConfig{Categories: "2", Fields: []SvcField{encField}},
|
|
cats: map[int32]bool{svcCatTTS: true},
|
|
fields: map[string]string{"api_key": "sk-xxx", "languages": ""},
|
|
wantHas: "languages",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
issues := checkSvcConfig(tc.svc, tc.ovr, tc.cats, tc.fields)
|
|
if got := hasFatalIssue(issues); got != tc.wantFatal {
|
|
t.Fatalf("hasFatalIssue=%v, want %v;问题清单=%v", got, tc.wantFatal, issues)
|
|
}
|
|
if tc.wantHas != "" && !strings.Contains(strings.Join(issues, "|"), tc.wantHas) {
|
|
t.Fatalf("问题清单 %v 中未包含 %q", issues, tc.wantHas)
|
|
}
|
|
if tc.wantHas == "" && !tc.wantFatal && len(issues) != 0 {
|
|
t.Fatalf("期望无问题,实得 %v", issues)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClassify(t *testing.T) {
|
|
tests := []struct {
|
|
status int
|
|
wantOk bool
|
|
wantKind string
|
|
}{
|
|
{200, true, comm.ProbeCredential},
|
|
{204, true, comm.ProbeCredential},
|
|
{401, false, comm.ProbeCredential}, // 凭据无效——要运营去换密钥
|
|
{403, false, comm.ProbeCredential}, //
|
|
{429, true, comm.ProbeCredential}, // 限流说明凭据是被接受的
|
|
{404, true, comm.ProbeNone}, // 端点不适用,不能算故障,但也没验证到
|
|
{405, true, comm.ProbeNone}, //
|
|
{500, false, comm.ProbeCredential}, // 服务商故障
|
|
{418, false, comm.ProbeCredential}, // 其它异常
|
|
}
|
|
for _, tc := range tests {
|
|
got := classify(tc.status, "body", "测试服务")
|
|
if got.Ok != tc.wantOk || got.Kind != tc.wantKind {
|
|
t.Errorf("classify(%d) = {Ok:%v Kind:%s}, want {Ok:%v Kind:%s}",
|
|
tc.status, got.Ok, got.Kind, tc.wantOk, tc.wantKind)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestAliyunEncode 阿里云 RPC 签名要求的 percent-encode 与 Go 默认 QueryEscape 有三处差异,
|
|
// 编码错会让签名恒不匹配,把「AK 正常」误报成「AK 无效」。
|
|
func TestAliyunEncode(t *testing.T) {
|
|
tests := [][2]string{
|
|
{"a b", "a%20b"}, // 空格必须是 %20 而不是 +
|
|
{"a*b", "a%2Ab"}, // * 必须转义
|
|
{"a~b", "a~b"}, // ~ 必须保持原样
|
|
{"a/b", "a%2Fb"}, //
|
|
{"/", "%2F"}, //
|
|
{"k=v&x=y", "k%3Dv%26x%3Dy"},
|
|
}
|
|
for _, tc := range tests {
|
|
if got := aliyunEncode(tc[0]); got != tc[1] {
|
|
t.Errorf("aliyunEncode(%q) = %q, want %q", tc[0], got, tc[1])
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestProbeCacheKey 去重键必须与字段顺序无关(map 遍历顺序随机),
|
|
// 且任一字段值不同就必须是不同的键——否则会把 A 区域的探测结论错安到 B 区域头上。
|
|
func TestProbeCacheKey(t *testing.T) {
|
|
a := probeCacheKey("openai", map[string]string{"api_key": "k1", "model": "gpt-4o"})
|
|
b := probeCacheKey("openai", map[string]string{"model": "gpt-4o", "api_key": "k1"})
|
|
if a != b {
|
|
t.Fatal("同一份配置在不同 map 遍历顺序下应得到相同的键")
|
|
}
|
|
if a == probeCacheKey("openai", map[string]string{"api_key": "k2", "model": "gpt-4o"}) {
|
|
t.Fatal("凭据不同必须得到不同的键")
|
|
}
|
|
if a == probeCacheKey("deepseek", map[string]string{"api_key": "k1", "model": "gpt-4o"}) {
|
|
t.Fatal("服务商不同必须得到不同的键")
|
|
}
|
|
// 键分隔符不能被字段值本身伪造出碰撞。
|
|
x := probeCacheKey("p", map[string]string{"a": "1", "b": "2"})
|
|
y := probeCacheKey("p", map[string]string{"a": "1\x01b\x012"})
|
|
if x == y {
|
|
t.Fatal("不同字段结构不应碰撞")
|
|
}
|
|
}
|
|
|
|
func TestCatSet(t *testing.T) {
|
|
got := catSet("5, 9 ,10")
|
|
for _, want := range []int32{svcCatLLM, svcCatLLMVision, svcCatMCP} {
|
|
if !got[want] {
|
|
t.Errorf("catSet 未解析出类别 %d", want)
|
|
}
|
|
}
|
|
if len(catSet("")) != 0 || len(catSet("abc")) != 0 {
|
|
t.Error("空值/非法值应解析为空集")
|
|
}
|
|
}
|
|
|
|
// TestSortHealth 巡检页与告警条都靠它排序:不可用的必须排在最前,同级里一直坏着的优先。
|
|
func TestSortHealth(t *testing.T) {
|
|
rows := []*comm.SvcHealth{
|
|
{Name: "c", Status: comm.HealthOK},
|
|
{Name: "d", Status: comm.HealthSkip},
|
|
{Name: "a", Status: comm.HealthError, FailStreak: 1},
|
|
{Name: "b", Status: comm.HealthWarn},
|
|
{Name: "e", Status: comm.HealthError, FailStreak: 9},
|
|
}
|
|
sortHealth(rows)
|
|
want := []string{"e", "a", "b", "c", "d"}
|
|
for i, w := range want {
|
|
if rows[i].Name != w {
|
|
t.Fatalf("排序结果第 %d 位是 %s,期望 %s", i, rows[i].Name, w)
|
|
}
|
|
}
|
|
}
|
|
|