package core import ( "fmt" "net/http" "net/http/httptest" "strings" "sync/atomic" "testing" "llmsproxy/internal/lua" ) // TestChannelsDevicePollBehavior 是 device-poll 流的行为对齐测试。 // // 决策表逐行搬自参考实现 pollMinimaxDeviceToken(minimax-oauth.ts), // 上游用假服务器模拟,逐次响应由测试控制。断言的是**行为**(会话状态推进), // 不是实现细节。 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() 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) } // ---- 行 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"]) } }