mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
产品代码统一为纯文本风格(照参考实现),去掉 emoji:billing/quota/usage 插件、 channels 的提示文案、WebUI 文案,以及 go 测试、docs、packaging 脚本中的 注意/OK/FAIL 等符号化写法。第三方目录(vendor、node_modules、.probe)不动。 另:packaging/upload-release.sh 的仓库名改为真实全名 JianFeeeee/ModelRouter → HeNanMU-EEA/ModelRouter(前者是重定向,POST/PUT 不跟随 会 404)。
166 lines
5.0 KiB
Go
166 lines
5.0 KiB
Go
package core
|
||
|
||
import (
|
||
"fmt"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"strings"
|
||
"sync/atomic"
|
||
"testing"
|
||
|
||
"llmsproxy/internal/lua"
|
||
)
|
||
|
||
// raccoonHarness:Raccoon 内置二维码扫码登录的测试台。
|
||
//
|
||
// 参考语义(raccoon-oauth.ts 点名):
|
||
//
|
||
// ① qrcode_code **本地随机生成**(16 字节 hex),服务端接受任意自造 code
|
||
// ② 轮询 POST /login_with_qrcode_code {qrcode_code}
|
||
// → data.status ∈ pending / logging / canceled / success
|
||
// ③ success 但**缺 token** 视为未完成(否则产出空凭据并卡死)
|
||
// ④ **任何异常降级为 pending**(网络抖动/畸形响应/未知 status)
|
||
type raccoonHarness struct {
|
||
t *testing.T
|
||
ps *lua.Plugins
|
||
ts *httptest.Server
|
||
polls int32
|
||
step int32 // 第几次轮询开始 success
|
||
gotCode string
|
||
}
|
||
|
||
func newRaccoonHarness(t *testing.T) *raccoonHarness {
|
||
t.Helper()
|
||
h := &raccoonHarness{t: t, step: 3}
|
||
h.ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
if strings.HasSuffix(r.URL.Path, "/login_with_qrcode_code") {
|
||
h.polls++
|
||
if h.polls < atomic.LoadInt32(&h.step) {
|
||
fmt.Fprint(w, `{"code":0,"data":{"status":"logging"}}`)
|
||
return
|
||
}
|
||
fmt.Fprint(w, `{"code":0,"data":{"status":"success","access_token":"RC-AT-1","refresh_token":"RC-RT-1"}}`)
|
||
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)
|
||
if _, err := h.ps.Call("channels", "set_endpoints", map[string]interface{}{
|
||
"platform": "raccoon",
|
||
"qr": map[string]interface{}{
|
||
"api_base": h.ts.URL,
|
||
"auth_prefix": "/api/web/auth/v1",
|
||
"poll_path": "/login_with_qrcode_code",
|
||
"login_page": "/login/mp",
|
||
},
|
||
}); err != nil {
|
||
t.Fatalf("set_endpoints: %v", err)
|
||
}
|
||
return h
|
||
}
|
||
|
||
func (h *raccoonHarness) begin() (map[string]interface{}, string) {
|
||
out, err := h.ps.InvokeHandler("channels", "start_login", &lua.HandlerRequest{
|
||
Method: "POST", Body: `{"platform":"raccoon"}`, 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 *raccoonHarness) 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{})
|
||
}
|
||
|
||
// TestRaccoonQrPollBehavior 逐行锁定参考的扫码登录语义。
|
||
func TestRaccoonQrPollBehavior(t *testing.T) {
|
||
h := newRaccoonHarness(t)
|
||
|
||
t.Run("行1 本地生成code并给出二维码URL", func(t *testing.T) {
|
||
atomic.StoreInt32(&h.polls, 0)
|
||
j, _ := h.begin()
|
||
qr, _ := j["qr_url"].(string)
|
||
if !strings.Contains(qr, "code=") || !strings.Contains(qr, "login/mp") {
|
||
t.Fatalf("应给出带 code 的二维码 URL, 实得 %q", qr)
|
||
}
|
||
// code 应是 32 位 hex(本地随机生成)
|
||
code := qr[strings.Index(qr, "code=")+5:]
|
||
if i := strings.IndexAny(code, "&"); i > 0 {
|
||
code = code[:i]
|
||
}
|
||
if len(code) != 32 {
|
||
t.Fatalf("qrcode_code 应是 32 位 hex, 实得 %q", code)
|
||
}
|
||
})
|
||
|
||
t.Run("行2 logging保持pending", func(t *testing.T) {
|
||
atomic.StoreInt32(&h.polls, 0)
|
||
atomic.StoreInt32(&h.step, 99)
|
||
_, sid := h.begin()
|
||
for i := 0; i < 3; i++ {
|
||
if st := h.pollOnce(sid); st["state"] != "pending" {
|
||
t.Fatalf("logging 应 pending, 实得 %v", st["state"])
|
||
}
|
||
}
|
||
})
|
||
|
||
t.Run("行3 success且带token才完成", func(t *testing.T) {
|
||
atomic.StoreInt32(&h.polls, 0)
|
||
atomic.StoreInt32(&h.step, 2)
|
||
_, sid := h.begin()
|
||
h.pollOnce(sid)
|
||
st := h.pollOnce(sid)
|
||
if st["state"] != "completed" {
|
||
t.Fatalf("success+token 应 completed, 实得 %v", st)
|
||
}
|
||
v, ok, _ := h.ps.GetSecret("channels", "channels:raccoon")
|
||
if !ok || !strings.Contains(v, "RC-AT-1") {
|
||
t.Fatalf("凭据应含 RC-AT-1, 实得 %q", v)
|
||
}
|
||
})
|
||
|
||
t.Run("行4 canceled是终态失败", func(t *testing.T) {
|
||
hh := newRaccoonHarness(t)
|
||
hh.ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
fmt.Fprint(w, `{"code":0,"data":{"status":"canceled"}}`)
|
||
})
|
||
hh.ps.SetSecret("channels", "channels:raccoon", "")
|
||
_, sid := hh.begin()
|
||
if st := hh.pollOnce(sid); st["state"] != "failed" {
|
||
t.Fatalf("canceled 应 failed, 实得 %v", st["state"])
|
||
}
|
||
})
|
||
|
||
t.Run("行5 success缺token视为未完成", func(t *testing.T) {
|
||
hh := newRaccoonHarness(t)
|
||
hh.ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
fmt.Fprint(w, `{"code":0,"data":{"status":"success"}}`)
|
||
})
|
||
_, sid := hh.begin()
|
||
st := hh.pollOnce(sid)
|
||
if st["state"] != "pending" {
|
||
t.Fatalf("缺 token 的 success 必须 pending(否则产出空凭据并卡死), 实得 %v", st["state"])
|
||
}
|
||
})
|
||
}
|