package core import ( "fmt" "net/http" "net/http/httptest" "strings" "sync/atomic" "testing" "modelrouter/internal/lua" ) // qoderHarness:Qoder PKCE 设备码轮的测试台。 // // 参考语义(qoder-oauth.ts / qoder.ts 注释点名): // // ① 授权页 = {authBase}/device/selectAccounts?challenge&challenge_method=S256 // &nonce&machine_id&client_id —— **本地拼装,不打网络** // ② 轮询 = GET {openApiBase}/api/v1/deviceToken/poll?nonce&verifier&challenge_method=S256 // ③ 404 = 尚无会话(继续等);2xx 无 token = 继续等;有 token 即完成 // // 此前的实现在这里标了 flow="manual",理由是"登录要 WASM 签名头"—— // 把聊天加密与登录混为一谈,导致该平台完全不可用。 type qoderHarness struct { t *testing.T ps *lua.Plugins ts *httptest.Server poll int32 // 已发生的轮询次数 // step: 第几次轮询开始返回 token(0 = 一直 404) tokenAt int32 gotQuery string } func newQoderHarness(t *testing.T) *qoderHarness { t.Helper() h := &qoderHarness{t: t, tokenAt: 2} h.ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") if r.URL.Path == "/api/v1/deviceToken/poll" { h.gotQuery = r.URL.RawQuery n := atomic.AddInt32(&h.poll, 1) if n < atomic.LoadInt32(&h.tokenAt) { w.WriteHeader(404) // 尚无会话 fmt.Fprint(w, `{"code":404,"msg":"no session"}`) return } fmt.Fprint(w, `{"token":"QD-AT-1","refresh_token":"QD-RT-1","user_id":"u9","user_name":"qoder-dev"}`) return } w.WriteHeader(404) })) t.Cleanup(h.ts.Close) c := kernelTestCore(t) h.ps = c.Plugins() src, err := readBundledChannels(t) if err != nil { t.Fatal(err) } if err := h.ps.LoadSource("channels", src); err != nil { t.Fatal(err) } _ = h.ps.SetPluginKernelPermission("channels", []string{"add_source", "list_sources", "status"}, false) // 把 poll 指到假上游(授权页是本地拼装,不经网络) if _, err := h.ps.Call("channels", "set_endpoints", map[string]interface{}{ "platform": "qoder", "device": map[string]interface{}{"token_url": h.ts.URL + "/api/v1/deviceToken/poll"}, }); err != nil { t.Fatalf("set_endpoints: %v", err) } return h } func (h *qoderHarness) begin() (map[string]interface{}, string) { out, err := h.ps.InvokeHandler("channels", "start_login", &lua.HandlerRequest{ Method: "POST", Body: `{"platform":"qoder"}`, Role: "admin", }) if err != nil { h.t.Fatal(err) } j := out.JSON.(map[string]interface{}) return j, j["session"].(map[string]interface{})["id"].(string) } func (h *qoderHarness) pollOnce(sid string) map[string]interface{} { out, err := h.ps.InvokeHandler("channels", "poll_login", &lua.HandlerRequest{ Method: "GET", Query: map[string]string{"id": sid}, Role: "admin", }) if err != nil { h.t.Fatal(err) } return out.JSON.(map[string]interface{}) } // TestQoderDevicePollBehavior 逐行锁定参考的 Qoder 登录语义。 func TestQoderDevicePollBehavior(t *testing.T) { h := newQoderHarness(t) t.Run("行1 授权页本地拼装_含PKCE四要素", func(t *testing.T) { j, _ := h.begin() url, _ := j["url"].(string) for _, want := range []string{ "qoder.com/device/selectAccounts", "challenge=", "challenge_method=S256", "nonce=", "machine_id=", // client_id 必须是 prod 那个(非 prod 会在授权回调阶段被拒) "client_id=e883ade2-e6e3-4d6d-adf7-f92ceff5fdcb", } { if !strings.Contains(url, want) { t.Fatalf("授权页缺 %q:\n%s", want, url) } } }) t.Run("行2 404视为继续_不误报失败", func(t *testing.T) { atomic.StoreInt32(&h.poll, 0) atomic.StoreInt32(&h.tokenAt, 99) // 一直 404 _, sid := h.begin() for i := 0; i < 3; i++ { st := h.pollOnce(sid) if st["state"] != "pending" { t.Fatalf("404 应保持 pending, 实得 %v", st["state"]) } } }) t.Run("行3 轮询带verifier与nonce_不带device_code", func(t *testing.T) { atomic.StoreInt32(&h.poll, 0) atomic.StoreInt32(&h.tokenAt, 99) _, sid := h.begin() h.pollOnce(sid) q := h.gotQuery if !strings.Contains(q, "verifier=") || !strings.Contains(q, "nonce=") { t.Fatalf("轮询 query 应含 verifier 与 nonce, 实得 %q", q) } if !strings.Contains(q, "challenge_method=S256") { t.Fatalf("轮询应带 challenge_method=S256, 实得 %q", q) } }) t.Run("行4 拿到token即完成_落凭据", func(t *testing.T) { atomic.StoreInt32(&h.poll, 0) atomic.StoreInt32(&h.tokenAt, 2) // 第二次轮询返回 token _, sid := h.begin() if st := h.pollOnce(sid); st["state"] != "pending" { t.Fatalf("首拍应 pending: %v", st) } st := h.pollOnce(sid) if st["state"] != "completed" { t.Fatalf("拿到 token 后应 completed, 实得 %v", st) } // 凭据落保险库(旧路径键) v, ok, _ := h.ps.GetSecret("channels", "channels:qoder") if !ok || !strings.Contains(v, "QD-AT-1") { t.Fatalf("凭据应含 QD-AT-1, 实得 %q", v) } }) t.Run("行5 池路径_凭据落账号ref", func(t *testing.T) { atomic.StoreInt32(&h.poll, 0) atomic.StoreInt32(&h.tokenAt, 1) add, err := h.ps.InvokeHandler("channels", "account_add", &lua.HandlerRequest{ Method: "POST", Body: `{"platform":"qoder"}`, Role: "admin", }) if err != nil { t.Fatal(err) } aid := add.JSON.(map[string]interface{})["account_id"].(string) out, _ := h.ps.InvokeHandler("channels", "start_login", &lua.HandlerRequest{ Method: "POST", Body: fmt.Sprintf(`{"platform":"qoder","account_id":%q}`, aid), Role: "admin", }) sid := out.JSON.(map[string]interface{})["session"].(map[string]interface{})["id"].(string) st := h.pollOnce(sid) if st["state"] != "completed" { t.Fatalf("池路径应完成: %v", st) } if _, ok, _ := h.ps.GetSecret("channels", "channels:qoder:"+aid); !ok { t.Fatalf("池路径凭据未落到 channels:qoder:%s", aid) } }) }