Files
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

181 lines
5.4 KiB
Go

package kook
import (
"context"
"net/http"
"sync"
"testing"
)
func TestListFriends_ParsesAndSendsType(t *testing.T) {
t.Parallel()
var mu sync.Mutex
var query string
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != friendPath {
http.Error(w, "unexpected path: "+r.URL.Path, http.StatusNotFound)
return
}
mu.Lock()
query = r.URL.RawQuery
mu.Unlock()
kookOK(w, map[string]any{
"friend": []any{
map[string]any{"id": 1, "friend_info": map[string]any{"id": "u1", "username": "alice", "nickname": "Ali", "online": true}},
},
"request": []any{
map[string]any{"id": 77, "friend_info": map[string]any{"id": "u2", "username": "bob"}},
},
"blocked": []any{
map[string]any{"id": 2, "friend_info": map[string]any{"id": "u3", "username": "carol"}},
},
})
})
a, _ := newTestAdapter(t, h)
fl, err := a.ListFriends(context.Background(), FriendTypeRequest)
if err != nil {
t.Fatalf("ListFriends: %v", err)
}
mu.Lock()
defer mu.Unlock()
if query != "type=request" {
t.Errorf("query = %q, want type=request", query)
}
if len(fl.Friends) != 1 || fl.Friends[0].UserID != "u1" || !fl.Friends[0].Online || fl.Friends[0].Nickname != "Ali" {
t.Errorf("friends = %+v, want [u1 Ali online]", fl.Friends)
}
if len(fl.Requests) != 1 || fl.Requests[0].ID != 77 || fl.Requests[0].Friend.UserID != "u2" {
t.Errorf("requests = %+v, want [77 u2]", fl.Requests)
}
if len(fl.Blocked) != 1 || fl.Blocked[0].UserID != "u3" {
t.Errorf("blocked = %+v, want [u3]", fl.Blocked)
}
}
func TestHandleFriendRequest_Body(t *testing.T) {
t.Parallel()
var mu sync.Mutex
var body map[string]any
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != friendHandlePath {
http.Error(w, "unexpected path", http.StatusNotFound)
return
}
mu.Lock()
body = decodeBody(r)
mu.Unlock()
kookOK(w, map[string]any{})
})
a, _ := newTestAdapter(t, h)
if err := a.HandleFriendRequest(context.Background(), 42, true); err != nil {
t.Fatalf("HandleFriendRequest: %v", err)
}
mu.Lock()
defer mu.Unlock()
if body["id"] != float64(42) || body["accept"] != true {
t.Errorf("body = %v, want id 42 accept true", body)
}
}
func TestListUserChats(t *testing.T) {
t.Parallel()
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != userChatListPath {
http.Error(w, "unexpected path", http.StatusNotFound)
return
}
kookOK(w, map[string]any{
"items": []any{
map[string]any{
"code": "c1", "last_read_time": 1, "latest_msg_time": 2, "unread_count": 3,
"target_info": map[string]any{"id": "u9", "username": "zed"},
},
},
})
})
a, _ := newTestAdapter(t, h)
chats, err := a.ListUserChats(context.Background(), 1, 50)
if err != nil {
t.Fatalf("ListUserChats: %v", err)
}
if len(chats) != 1 || chats[0].Code != "c1" || chats[0].TargetID != "u9" || chats[0].TargetName != "zed" {
t.Errorf("chats = %+v, want [c1 u9 zed]", chats)
}
}
func TestListGuildsAndChannels(t *testing.T) {
t.Parallel()
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case guildListPath:
kookOK(w, map[string]any{
"items": []any{map[string]any{"id": "g1", "name": "server", "icon": "i", "topic": "t"}},
})
case channelListPath:
if r.URL.Query().Get("guild_id") != "g1" || r.URL.Query().Get("type") != "1" {
http.Error(w, "missing guild/type", http.StatusBadRequest)
return
}
kookOK(w, map[string]any{
"items": []any{map[string]any{"id": "c1", "guild_id": "g1", "name": "general", "type": 1, "is_category": false}},
})
default:
http.Error(w, "unexpected path", http.StatusNotFound)
}
})
a, _ := newTestAdapter(t, h)
guilds, err := a.ListGuilds(context.Background(), 1, 50)
if err != nil {
t.Fatalf("ListGuilds: %v", err)
}
if len(guilds) != 1 || guilds[0].ID != "g1" || guilds[0].Name != "server" {
t.Fatalf("guilds = %+v, want [g1 server]", guilds)
}
channels, err := a.ListGuildChannels(context.Background(), "g1", 1)
if err != nil {
t.Fatalf("ListGuildChannels: %v", err)
}
if len(channels) != 1 || channels[0].ID != "c1" || channels[0].Name != "general" || channels[0].IsCategory {
t.Errorf("channels = %+v, want [c1 general]", channels)
}
}
func TestSendTestMessage(t *testing.T) {
t.Parallel()
var mu sync.Mutex
var body map[string]any
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != guildChannelMsgPath {
http.Error(w, "unexpected path: "+r.URL.Path, http.StatusNotFound)
return
}
mu.Lock()
body = decodeBody(r)
mu.Unlock()
kookOK(w, map[string]any{"msg_id": "m1", "msg_timestamp": 123})
})
a, _ := newTestAdapter(t, h)
res, err := a.SendTestMessage(context.Background(), "chan-1", "hello")
if err != nil {
t.Fatalf("SendTestMessage: %v", err)
}
if res.MsgID != "m1" || res.MsgTimestamp != 123 {
t.Errorf("res = %+v, want m1/123", res)
}
mu.Lock()
defer mu.Unlock()
if body["target_id"] != "chan-1" || body["content"] != "hello" {
t.Errorf("body = %v, want target_id chan-1 content hello", body)
}
}
func TestSendTestMessage_NoTarget(t *testing.T) {
t.Parallel()
a, _ := newTestAdapter(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "unexpected", http.StatusNotFound)
}))
if _, err := a.SendTestMessage(context.Background(), "", "hi"); err == nil {
t.Fatal("expected error for empty target")
}
}