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