package gateway import ( "encoding/json" "net/http" "strings" "testing" "git.yeij.top/AskaEth/Cyrene_v2/internal/auth" capreg "git.yeij.top/AskaEth/Cyrene_v2/internal/capability/registry" "git.yeij.top/AskaEth/Cyrene_v2/internal/config" ) // 本文件锁定管理列表的**按账号**连接状态:多账号平台上,每行连接/断开的门控 // 必须看该账号自己的状态;没有活适配器的账号状态**未知**(字段缺省), // 而不是被读成「未连接」——把「读不到」画成「没连上」会让主人盲点「断开」。 // TestAdminListChannelsReportsPerAccountConnection 断言每个账号各报自己的状态, // 且没有活适配器的账号**不带** connected 字段(未知 ≠ 未连接)。 func TestAdminListChannelsReportsPerAccountConnection(t *testing.T) { t.Parallel() caps := capreg.New() live := &spyChannel{name: "kook:3fc8"} live.connected.Store(true) down := &spyChannel{name: "kook:conf1"} for _, ch := range []*spyChannel{live, down} { if err := caps.RegisterChannel(ch); err != nil { t.Fatalf("register %s: %v", ch.Name(), err) } } store := &stubChannelConfigStore{ path: t.TempDir() + "/channels.json", accounts: map[string][]config.ChannelAccount{ "kook": { {ID: "3fc8", Config: map[string]any{"bot_token": "tk-a"}}, {ID: "conf1", Config: map[string]any{"bot_token": "tk-b"}}, {ID: "conf2", Config: map[string]any{"bot_token": "tk-c"}}, // 无活适配器 ⇒ 状态未知 }, }, } _, ts, jwt := buildEnv(t, cfgDefault(), Deps{Registry: caps, Channels: store}) tok := signAny(t, jwt, "acct", auth.RoleOwner) resp, body := doReq(t, http.MethodGet, ts.URL+"/api/v1/admin/channels", nil, tok) if resp.StatusCode != http.StatusOK { t.Fatalf("list status = %d, want 200 (body=%s)", resp.StatusCode, body) } var items []ChannelSummary if err := json.Unmarshal(decodeBody(t, body)["data"], &items); err != nil { t.Fatalf("decode list: %v (body=%s)", err, body) } if len(items) != 1 || items[0].Name != "kook" { t.Fatalf("items = %+v, want a single kook row", items) } if len(items[0].Accounts) != 3 { t.Fatalf("accounts = %+v, want all three configured accounts listed", items[0].Accounts) } state := accountConnections(items[0].Accounts) if got := state["3fc8"]; got == nil || !*got { t.Fatalf("account 3fc8 connected = %v, want true", got) } if got := state["conf1"]; got == nil || *got { t.Fatalf("account conf1 connected = %v, want false", got) } // nil = 字段缺省:没有活适配器 ⇒ 状态未知,绝不能画成「未连接」。 if got := state["conf2"]; got != nil { t.Fatalf("account conf2 connected = %v, want the field omitted(未知 ≠ 未连接)", *got) } // 凭据绝不进列表响应。 if bytes := string(body); containsToken(bytes) { t.Fatalf("list response leaked a token: %s", bytes) } } // accountConnections 把账号列表映射成 id → 连接状态(字段缺省时值为 nil)。 func accountConnections(accts []AdminChannelAccount) map[string]*bool { out := make(map[string]*bool, len(accts)) for _, a := range accts { out[a.ID] = a.Connected } return out } // containsToken 报告响应体里是否出现了测试用的原始 token 片段。 func containsToken(body string) bool { for _, tk := range []string{"tk-a", "tk-b", "tk-c"} { if strings.Contains(body, tk) { return true } } return false }