Files
Cyrene_v2/internal/services/gateway/admin_channels_platform_test.go
T
Aska 5ec4c9814d
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(kook): P2 KOOK 专属页+后端 admin 端点(design/17 §2/§4)
后端(KOOK REST, 复用 httpclient WithAuthScheme(Bot), token不打印):
- kook/admin.go(新): ListFriends/HandleFriendRequest/DeleteFriend/BlockFriend/
  ListUserChats/CreateUserChat/ListDirectMessages
- kook/admin_guild.go(新): ListGuilds/ListGuildChannels/ListGuildUsers/ListChannelMsgs/SendTestMessage
- gateway/admin_channels_platform.go(新): kookAdmin(反射解包namedAdapter命中内嵌PlatformAdapter)
  + 4 handler + adminKookView聚合(子项失败记errors不伪造)
- server.go 注册 4 条 ownerAuth 路由: /view /friends /friend/handle /message/test
- admin.go/admin_write_channel_config.go: AdminChannelAccount 加 Capabilities
前端(KOOK 专属页):
- KookPanel.tsx(新): 好友&申请/服务器/频道/成员/私信会话/发消息测试卡片
- AdapterDetailPage.tsx: platform==='kook' 渲染 KookPanel, 否则通用
- types.ts/api.ts: KOOK 类型 + adminChannelPlatformView/Friends/FriendHandle/MessageTest
文档: Docs/api-client.md 新增4端点契约(owner-only)+capabilities+v0.17
验收: go build/vet/test ./... 全绿 + npm run build 97模块全绿; 零新依赖
注: api.ts/types.ts 集中契约文件>500行属既有结构(未新增违规); KOOK其余REST方法留能力面待用
2026-09-07 18:19:31 +08:00

158 lines
5.5 KiB
Go

package gateway
// Exercises the per-platform KOOK adapter panel endpoints (design/17 §2). The
// adapter is registered through a wrapper that embeds adapter.PlatformAdapter —
// mirroring cmd/cyrene's namedAdapter — so these tests also cover the reflection
// unwrap the gateway uses to reach the concrete *kook.Adapter.
import (
"encoding/json"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"testing"
"git.yeij.top/AskaEth/Cyrene_v2/internal/auth"
"git.yeij.top/AskaEth/Cyrene_v2/internal/capability/registry"
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
"git.yeij.top/AskaEth/Cyrene_v2/plugins/channels/kook"
)
func gatewayKookLogger() *slog.Logger {
return slog.New(slog.NewTextHandler(io.Discard, nil))
}
// gatewayKookWrap embeds adapter.PlatformAdapter the way cmd/cyrene's namedAdapter
// does, so the gateway's reflection-based unwrap (asKookAdmin) is exercised.
type gatewayKookWrap struct {
adapter.PlatformAdapter
name string
}
func (w *gatewayKookWrap) Name() string { return w.name }
// gatewayKookServer answers the KOOK REST surface a panel view touches.
func gatewayKookServer(t *testing.T) *httptest.Server {
t.Helper()
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch r.URL.Path {
case "/friend":
_ = json.NewEncoder(w).Encode(map[string]any{"code": 0, "data": map[string]any{
"friend": []any{map[string]any{"id": 1, "friend_info": map[string]any{"id": "u1", "username": "alice"}}},
}})
case "/user-chat/list":
_ = json.NewEncoder(w).Encode(map[string]any{"code": 0, "data": map[string]any{
"items": []any{map[string]any{"code": "c1", "target_info": map[string]any{"id": "u9", "username": "zed"}}},
}})
case "/guild/list":
_ = json.NewEncoder(w).Encode(map[string]any{"code": 0, "data": map[string]any{
"items": []any{map[string]any{"id": "g1", "name": "server"}},
}})
case "/channel/list":
_ = json.NewEncoder(w).Encode(map[string]any{"code": 0, "data": map[string]any{
"items": []any{map[string]any{"id": "c1", "guild_id": "g1", "name": "general", "type": 1}},
}})
case "/guild/user-list":
_ = json.NewEncoder(w).Encode(map[string]any{"code": 0, "data": map[string]any{
"items": []any{map[string]any{"id": "u1", "username": "alice", "online": true}},
}})
case "/friend/handle-request":
_ = json.NewEncoder(w).Encode(map[string]any{"code": 0, "data": map[string]any{}})
case "/message/create":
_ = json.NewEncoder(w).Encode(map[string]any{"code": 0, "data": map[string]any{"msg_id": "m1"}})
default:
http.NotFound(w, r)
}
})
srv := httptest.NewServer(h)
t.Cleanup(srv.Close)
return srv
}
// newKookGateway wires a registry whose kook channel wraps a real adapter pointed
// at an httptest KOOK server, and returns the owner JWT.
func newKookGateway(t *testing.T, name string) (string, string) {
t.Helper()
kookSrv := gatewayKookServer(t)
ka := kook.New(
kook.Config{BotToken: "testtoken"},
kook.WithBaseURL(kookSrv.URL),
kook.WithHTTPClient(kookSrv.Client()),
kook.WithLogger(gatewayKookLogger()),
)
reg := registry.New()
if err := reg.RegisterChannel(&gatewayKookWrap{PlatformAdapter: ka, name: name}); err != nil {
t.Fatalf("register channel: %v", err)
}
_, ts, jwt := buildEnv(t, cfgDefault(), Deps{Registry: reg})
tok := signAny(t, jwt, "acct", auth.RoleOwner)
return ts.URL, tok
}
func TestAdminChannelPlatformView_Kook(t *testing.T) {
t.Parallel()
base, tok := newKookGateway(t, "kook:conf0")
resp, body := doReq(t, http.MethodGet, base+"/api/v1/admin/channels/kook/view?accountId=conf0", nil, tok)
if resp.StatusCode != http.StatusOK {
t.Fatalf("view status = %d, body=%s", resp.StatusCode, body)
}
var out struct {
Data struct {
Platform string `json:"platform"`
Friends *struct {
Friends []struct{ UserID string `json:"user_id"` } `json:"friends"`
} `json:"friends"`
Guilds []struct{ ID string `json:"id"` } `json:"guilds"`
} `json:"data"`
}
if err := json.Unmarshal(body, &out); err != nil {
t.Fatalf("decode view: %v", err)
}
if out.Data.Platform != "kook" {
t.Errorf("platform = %q, want kook", out.Data.Platform)
}
if out.Data.Friends == nil || len(out.Data.Friends.Friends) != 1 || out.Data.Friends.Friends[0].UserID != "u1" {
t.Errorf("friends = %+v, want [u1]", out.Data.Friends)
}
if len(out.Data.Guilds) != 1 || out.Data.Guilds[0].ID != "g1" {
t.Errorf("guilds = %+v, want [g1]", out.Data.Guilds)
}
}
func TestAdminChannelPlatformView_NonKookNotImplemented(t *testing.T) {
t.Parallel()
_, ts, jwt := buildEnv(t, cfgDefault(), Deps{})
tok := signAny(t, jwt, "acct", auth.RoleOwner)
resp, body := doReq(t, http.MethodGet, ts.URL+"/api/v1/admin/channels/telegram/view", nil, tok)
if resp.StatusCode != http.StatusNotImplemented {
t.Fatalf("status = %d, want 501 (body=%s)", resp.StatusCode, body)
}
}
func TestAdminChannelFriendHandle_Kook(t *testing.T) {
t.Parallel()
base, tok := newKookGateway(t, "kook:conf0")
resp, body := doReq(t, http.MethodPost, base+"/api/v1/admin/channels/kook/friend/handle?accountId=conf0",
map[string]any{"id": 77, "accept": true}, tok)
if resp.StatusCode != http.StatusOK {
t.Fatalf("handle status = %d, body=%s", resp.StatusCode, body)
}
var out struct {
Data struct {
Handled bool `json:"handled"`
ID int64 `json:"id"`
} `json:"data"`
}
if err := json.Unmarshal(body, &out); err != nil {
t.Fatalf("decode handle: %v", err)
}
if !out.Data.Handled || out.Data.ID != 77 {
t.Errorf("handle = %+v, want handled id 77", out.Data)
}
}