mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
用户要求的四类测试全部落地,且每类都抓到了真问题: ## 0) 测试暴露的真实缺陷(先修后测) **保险库明文落盘**(加密解密测试的目标缺陷):.secrets/<plugin>.json 此前 是明文 JSON。接入 SecretBox(与 config.yaml 同一把 master key、同 enc:v1 形态):SetSecretBox 注入(secretSealer 接口,测试可桩);写盘逐值加密、 读盘逐值解密;无前缀旧值原样通过 —— 旧明文库可读、下次写整体重封。 core 注入点在 store 创建之后(NewPlugins 时 box 还不存在 —— 顺序 load-bearing, 修掉了一个 nil-deref)。 ## 1) 加密解密(internal/lua/secrets_crypto_test.go) - EncryptedAtRest:磁盘字节不含明文 token、带 enc:v1、文件 0600/目录 0700 - PlaintextFallbackAndUpgrade:旧明文库可读 + 任意一次写整体重封为密文 - WrongMasterKey:换 key 后读库必须报错(静默空会让插件重登覆盖凭据) - ConcurrentPlugins:8 插件×2 goroutine 并发读写,无串台/丢写/文件数正确 ## 2) 变异(internal/core/device_mutate_test.go + device_harness_test.go) - 决策表抽成 deviceHarness.runDecisionTable(行为测试与变异测试共用, 单一事实源) - 5 个语义变异逐个杀:slow_down 不加 interval / denied 不识别 / expired_token 不识别 / authorization_pending 当失败 / 成功分支禁用 - 行3/行4 断言 message 区分(用户拒绝/过期)—— 初版只断 state=failed 时 两个变异体幸存(落进通用 failed 分支),加 message 后全杀。 这正是变异测试的价值:暴露断言太弱。 - 基准(未变异)必须全过 —— 防止测试自身失效的假阳性 ## 3) 压力(internal/core/channels_stress_test.go) - ConcurrentLogins:12 轮并发 device-poll,会话互不串台、全部终态 - ClaimStress:30 并发领签到,上游收到恰 30 次(无登录态丢失) ## 4) 旧通路(channels_stress_test.go 后半 + 复用 lua 包测试) - LegacyVaultPlaintextLoad:老版本明文库 → 新版本可读 → 下次写重封 - LegacySourceSealedConfig:明文 api_key 的旧 config.yaml → 运行期解密、 落盘 seal、master.key 0600 生成 全量 lua/core/gateway/types/scheduler/provider/config 测试通过;build+vet 绿。
181 lines
5.5 KiB
Go
181 lines
5.5 KiB
Go
package core
|
||
|
||
import (
|
||
"fmt"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"strings"
|
||
"sync/atomic"
|
||
"testing"
|
||
|
||
"llmsproxy/internal/lua"
|
||
)
|
||
|
||
// readBundledChannels 读内置 channels 插件源(供行为/变异测试共用)。
|
||
func readBundledChannels(t *testing.T) (string, error) {
|
||
t.Helper()
|
||
return lua.ReadBundledPlugin("channels")
|
||
}
|
||
|
||
// deviceHarness 是 device-poll 决策表的可复用测试台:假上游(脚本化逐次响应)
|
||
// + channels 插件加载指定源码 + minimax 端点指到假上游。
|
||
// 行为测试(TestChannelsDevicePollBehavior)与变异测试共用它 —— 决策表只有
|
||
// 一份,两处不会漂移。
|
||
type deviceHarness struct {
|
||
t *testing.T
|
||
ps *lua.Plugins
|
||
ts *httptest.Server
|
||
step int32
|
||
// script 持续被 HTTP handler 原子读取
|
||
script atomic.Value // []scriptStep
|
||
}
|
||
|
||
type scriptStep struct {
|
||
http int
|
||
body string
|
||
}
|
||
|
||
func newDeviceHarness(t *testing.T, src string) *deviceHarness {
|
||
t.Helper()
|
||
h := &deviceHarness{t: t}
|
||
h.script.Store([]scriptStep{})
|
||
|
||
h.ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
switch r.URL.Path {
|
||
case "/oauth2/device/code":
|
||
w.Header().Set("Content-Type", "application/json")
|
||
fmt.Fprint(w, `{"device_code":"DEV1","user_code":"ABCD-EFGH","verification_uri":"https://example/activate","interval":1,"expires_in":600}`)
|
||
case "/oauth2/token":
|
||
i := int(atomic.AddInt32(&h.step, 1)) - 1
|
||
script := h.script.Load().([]scriptStep)
|
||
w.Header().Set("Content-Type", "application/json")
|
||
if i >= len(script) {
|
||
w.WriteHeader(500)
|
||
fmt.Fprint(w, `{"error":"unexpected"}`)
|
||
return
|
||
}
|
||
if script[i].http != 0 && script[i].http != 200 {
|
||
w.WriteHeader(script[i].http)
|
||
}
|
||
fmt.Fprint(w, script[i].body)
|
||
default:
|
||
w.WriteHeader(404)
|
||
}
|
||
}))
|
||
t.Cleanup(h.ts.Close)
|
||
|
||
c := kernelTestCore(t)
|
||
h.ps = c.Plugins()
|
||
if err := h.ps.LoadSource("channels", src); err != nil {
|
||
t.Fatalf("加载: %v", err)
|
||
}
|
||
if _, err := h.ps.Call("channels", "set_endpoints", map[string]interface{}{
|
||
"platform": "minimax",
|
||
"device": map[string]interface{}{
|
||
"code_url": h.ts.URL + "/oauth2/device/code",
|
||
"token_url": h.ts.URL + "/oauth2/token",
|
||
},
|
||
}); err != nil {
|
||
t.Fatalf("set_endpoints: %v", err)
|
||
}
|
||
return h
|
||
}
|
||
|
||
// setScript 设置 token 端点的逐次响应脚本。`400 {...}` 形式表示非 200。
|
||
func (h *deviceHarness) setScript(steps ...string) {
|
||
var out []scriptStep
|
||
for _, b := range steps {
|
||
code := 200
|
||
switch {
|
||
case strings.HasPrefix(b, "400 "):
|
||
code, b = 400, b[4:]
|
||
case strings.HasPrefix(b, "403 "):
|
||
code, b = 403, b[4:]
|
||
}
|
||
out = append(out, scriptStep{http: code, body: b})
|
||
}
|
||
h.script.Store(out)
|
||
atomic.StoreInt32(&h.step, 0)
|
||
}
|
||
|
||
func (h *deviceHarness) begin() map[string]interface{} {
|
||
out, err := h.ps.InvokeHandler("channels", "start_login", &lua.HandlerRequest{
|
||
Method: "POST", Body: `{"platform":"minimax"}`,
|
||
})
|
||
if err != nil {
|
||
h.t.Fatal(err)
|
||
}
|
||
return out.JSON.(map[string]interface{})
|
||
}
|
||
|
||
func (h *deviceHarness) poll(id string) map[string]interface{} {
|
||
out, _ := h.ps.InvokeHandler("channels", "poll_login", &lua.HandlerRequest{
|
||
Method: "GET", Query: map[string]string{"id": id},
|
||
})
|
||
return out.JSON.(map[string]interface{})
|
||
}
|
||
|
||
// runDecisionTable 跑完整 8 行决策表,全部符合预期返回 true。
|
||
// 这是行为契约的单一事实来源:行为测试用它断言通过;变异测试用它杀变异体。
|
||
func (h *deviceHarness) runDecisionTable() bool {
|
||
ok := func(cond bool) bool { return cond }
|
||
st := func(steps ...string) map[string]interface{} {
|
||
h.setScript(steps...)
|
||
j := h.begin()
|
||
sid := j["session"].(map[string]interface{})["id"].(string)
|
||
return h.poll(sid)
|
||
}
|
||
|
||
// 行1: 200+pending → 等
|
||
if j := st(`{"status":"pending"}`); j["state"] != "pending" {
|
||
return false
|
||
}
|
||
// 行2: 200+slow_down → 等 + interval+5
|
||
h.setScript(`{"status":"slow_down"}`)
|
||
j := h.begin()
|
||
sid := j["session"].(map[string]interface{})["id"].(string)
|
||
if j2 := h.poll(sid); j2["state"] != "pending" {
|
||
return false
|
||
} else if iv, _ := j2["interval"].(float64); iv < 6 {
|
||
return false
|
||
}
|
||
// 行3: 200+denied → failed,message 区分于通用错误(杀"denied 不识别"变异体)
|
||
if j := st(`{"status":"denied"}`); j["state"] != "failed" {
|
||
return false
|
||
} else if msg, _ := j["message"].(string); !strings.Contains(msg, "用户拒绝") {
|
||
return false
|
||
}
|
||
// 行4: 200+expired_token → failed,message 带过期字样
|
||
if j := st(`{"status":"expired_token"}`); j["state"] != "failed" {
|
||
return false
|
||
} else if msg, _ := j["message"].(string); !strings.Contains(msg, "过期") {
|
||
return false
|
||
}
|
||
// 行5: 400+authorization_pending → 等
|
||
if j := st(`400 {"error":"authorization_pending"}`); j["state"] != "pending" {
|
||
return false
|
||
}
|
||
// 行6: 403+slow_down → 等 + interval+5
|
||
h.setScript(`403 {"error":"slow_down"}`)
|
||
j = h.begin()
|
||
sid = j["session"].(map[string]interface{})["id"].(string)
|
||
if j2 := h.poll(sid); j2["state"] != "pending" {
|
||
return false
|
||
} else if iv, _ := j2["interval"].(float64); iv < 6 {
|
||
return false
|
||
}
|
||
// 行7: 200+access_token → completed(需先授权 add_source)
|
||
_ = h.ps.SetPluginKernelPermission("channels", []string{"add_source", "status", "list_sources"}, false)
|
||
h.setScript(`{"access_token":"MM-ACCESS-1","refresh_token":"MM-RF-1","nickname":"mm-user"}`)
|
||
j = h.begin()
|
||
sid = j["session"].(map[string]interface{})["id"].(string)
|
||
if j2 := h.poll(sid); j2["state"] != "completed" || j2["account"] != "mm-user" {
|
||
return false
|
||
}
|
||
// 行8: 其它 error → failed 带文本
|
||
if j := st(`{"error":"server_error"}`); j["state"] != "failed" {
|
||
return false
|
||
}
|
||
return ok(true)
|
||
}
|