后端(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方法留能力面待用
158 lines
5.5 KiB
Go
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)
|
|
}
|
|
}
|