test: 四类验收 —— 加密解密 / 变异 / 压力 / 旧通路

用户要求的四类测试全部落地,且每类都抓到了真问题:

## 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 绿。
This commit is contained in:
JianFeeeee
2026-10-11 20:50:52 +08:00
parent 6f2b6b8d3d
commit da0282a3db
9 changed files with 977 additions and 161 deletions

View File

@ -1,181 +1,40 @@
package core
import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"
"llmsproxy/internal/lua"
)
// TestChannelsDevicePollBehavior 是 device-poll 流的行为对齐测试。
//
// 决策表逐行搬自参考实现 pollMinimaxDeviceToken(minimax-oauth.ts),
// 上游用假服务器模拟,逐次响应由测试控制。断言的是**行为**(会话状态推进),
// 不是实现细节。
// 不是实现细节。决策表本体在 deviceHarness.runDecisionTable(与变异测试共用)。
func TestChannelsDevicePollBehavior(t *testing.T) {
// 假 MiniMax:token 端点按脚本逐次响应;code 端点固定发一个码。
var step int32
var script atomic.Value
script.Store([]map[string]interface{}{})
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(&step, 1)) - 1
scripted := script.Load().([]map[string]interface{})
w.Header().Set("Content-Type", "application/json")
if i >= len(scripted) {
w.WriteHeader(500)
fmt.Fprint(w, `{"error":"unexpected"}`)
return
}
st := scripted[i]
if code, _ := st["http"].(int); code != 0 && code != 200 {
w.WriteHeader(code)
}
fmt.Fprint(w, st["body"].(string))
default:
w.WriteHeader(404)
}
}))
defer ts.Close()
c := kernelTestCore(t)
ps := c.Plugins()
src, err := lua.ReadBundledPlugin("channels")
if err != nil {
t.Fatal(err)
}
if err := ps.LoadSource("channels", src); err != nil {
t.Fatal(err)
}
// 把 minimax 平台的端点指到假上游(登录主机也要放行:httptest 用 127.0.0.1,
// 白名单对 loopback 天然放行)。set_endpoints 是插件导出的合法运维能力。
if _, err := ps.Call("channels", "set_endpoints", map[string]interface{}{
"platform": "minimax",
"device": map[string]interface{}{
"code_url": ts.URL + "/oauth2/device/code",
"token_url": ts.URL + "/oauth2/token",
},
}); err != nil {
t.Fatalf("set_endpoints: %v", err)
}
begin := func() map[string]interface{} {
out, err := ps.InvokeHandler("channels", "start_login", &lua.HandlerRequest{
Method: "POST", Body: `{"platform":"minimax"}`,
})
if err != nil {
t.Fatal(err)
}
return out.JSON.(map[string]interface{})
}
poll := func(id string) map[string]interface{} {
out, _ := ps.InvokeHandler("channels", "poll_login", &lua.HandlerRequest{
Method: "GET", Query: map[string]string{"id": id},
})
return out.JSON.(map[string]interface{})
}
setScript := func(steps ...string) {
var ms []map[string]interface{}
for _, b := range steps {
code := 200
if strings.HasPrefix(b, "403 ") || strings.HasPrefix(b, "400 ") {
code = 400
if strings.HasPrefix(b, "403 ") {
code = 403
}
b = b[4:]
}
ms = append(ms, map[string]interface{}{"http": code, "body": b})
}
script.Store(ms)
atomic.StoreInt32(&step, 0)
}
// ---- begin:设备码申请成功,user_code 透传,verifier 不回显 ----
j := begin()
h := newDeviceHarness(t, mustChannelsSrc(t))
// begin 直查:user_code 透传、授权 URL、verifier 不回显
j := h.begin()
sess := j["session"].(map[string]interface{})
sid := sess["id"].(string)
if sess["user_code"] != "ABCD-EFGH" {
t.Fatalf("begin 应透传 user_code, 实得 %v", sess["user_code"])
}
if j["url"] != "https://example/activate" {
t.Fatalf("begin 应给授权 URL, 实得 %v", j["url"])
}
// ---- 行 1: 200+status=pending → 继续等 ----
setScript(`{"status":"pending"}`)
if st := poll(sid); st["state"] != "pending" {
t.Fatalf("行1 pending: 应仍 pending, 实得 %v", st)
if _, has := j["device_verifier"]; has {
t.Fatal("verifier 绝不能回显")
}
// ---- 行 2: 200+status=slow_down → interval+5,仍 pending ----
setScript(`{"status":"slow_down"}`)
if st := poll(sid); st["state"] != "pending" {
t.Fatalf("行2 slow_down: 应仍 pending, 实得 %v", st)
} else if iv, _ := st["interval"].(float64); iv < 6 {
t.Fatalf("行2 slow_down: interval 应 +5, 实得 %v", st["interval"])
}
// ---- 行 3: 200+status=denied → failed(用户拒绝)----
setScript(`{"status":"denied"}`)
if st := poll(sid); st["state"] != "failed" {
t.Fatalf("行3 denied: 应 failed, 实得 %v", st)
}
// ---- 行 4: 200+status=expired → failed(码过期)----
j = begin()
sid = j["session"].(map[string]interface{})["id"].(string)
setScript(`{"status":"expired_token"}`)
if st := poll(sid); st["state"] != "failed" {
t.Fatalf("行4 expired_token: 应 failed, 实得 %v", st)
}
// ---- 行 5: 非200+error=authorization_pending → 继续等(OAuth 标准形态)----
j = begin()
sid = j["session"].(map[string]interface{})["id"].(string)
setScript(`400 {"error":"authorization_pending"}`)
if st := poll(sid); st["state"] != "pending" {
t.Fatalf("行5 authorization_pending: 应仍 pending, 实得 %v", st)
}
// ---- 行 6: 非200+error=slow_down → interval+5 ----
setScript(`403 {"error":"slow_down"}`)
if st := poll(sid); st["state"] != "pending" {
t.Fatalf("行6 slow_down(400): 应仍 pending, 实得 %v", st)
} else if iv, _ := st["interval"].(float64); iv < 6 {
t.Fatalf("行6: interval 应 +5, 实得 %v", st["interval"])
}
// ---- 行 7: 200+access_token → completed,凭据入库+接入源 ----
j = begin()
sid = j["session"].(map[string]interface{})["id"].(string)
_ = ps.SetPluginKernelPermission("channels", []string{"add_source", "status", "list_sources"}, false)
setScript(`{"access_token":"MM-ACCESS-1","refresh_token":"MM-RF-1","nickname":"mm-user"}`)
st := poll(sid)
if st["state"] != "completed" {
t.Fatalf("行7 成功: 应 completed, 实得 %v", st)
}
if st["account"] != "mm-user" {
t.Fatalf("行7: account 应取 nickname, 实得 %v", st)
}
// ---- 行 8: 其它错误 → failed 且带 error 文本 ----
j = begin()
sid = j["session"].(map[string]interface{})["id"].(string)
setScript(`{"error":"server_error"}`)
st = poll(sid)
if st["state"] != "failed" {
t.Fatalf("行8 error: 应 failed, 实得 %v", st)
}
if msg, _ := st["message"].(string); !strings.Contains(msg, "server_error") {
t.Fatalf("行8: message 应带 error 文本, 实得 %v", st["message"])
// 决策表全过
if !h.runDecisionTable() {
t.Fatal("device-poll 决策表未全过(见 runDecisionTable 内的行号)")
}
}
// mustChannelsSrc 读 bundled channels 源码。
func mustChannelsSrc(t *testing.T) string {
t.Helper()
src, err := readBundledChannels(t)
if err != nil {
t.Fatal(err)
}
return src
}