- runtimeconfig/channels.go: channels.json 承载 map[platform][]ChannelAccount(token/baseURL/黑白名单/群聊限流), LoadChannels 优先文件/回退env, SaveChannels(0600+tmp原子写), Upsert/Delete/Masked脱敏
- channelbridge/reload.go: adapterManager 热替换——工厂重建新adapter先连接, 再原子断旧adapter(独立readCtx/进行中不中断); Bridge.Reload(platform) 逐账号增删
- 面板 PUT/DELETE /admin/channels/{platform}/config(owner+审计) 落盘channels.json+Reload热更新; GET /admin/channels 附账号掩码不泄裸token
- 前端 Channels.tsx 配置表单(id/token/baseURL/黑白名单/限流+config JSON)+保存热更新提示+掩码展示/删除
- .gitignore 加 channels.json; .env 退兜底(.env.example注明); token不打印/Config()脱敏
- channelbridge.go 拆 logic→reload.go(742→731); 全仓 go build/vet/test绿+前端build绿
- 未做真token端到端(需渠道凭据,不读.env)——如实标注待凭据
107 lines
3.3 KiB
Go
107 lines
3.3 KiB
Go
package main
|
|
|
|
import (
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
|
|
)
|
|
|
|
func testLog() *slog.Logger {
|
|
return logger.New(logger.Options{Out: io.Discard})
|
|
}
|
|
|
|
// testFactory builds a channelFactory pointing at a temp (absent) channels.json,
|
|
// seeded by the env fallback, so the test reads the env the same way boot does.
|
|
func testFactory(t *testing.T) *channelFactory {
|
|
t.Helper()
|
|
return &channelFactory{
|
|
path: filepath.Join(t.TempDir(), "channels.json"),
|
|
fallback: channelAccountsFromEnv(),
|
|
log: testLog(),
|
|
}
|
|
}
|
|
|
|
func TestBuildChannelAdapters_IncludesKook(t *testing.T) {
|
|
t.Setenv("CHANNEL_KOOK_TOKEN", "kook-secret")
|
|
adapters := buildChannelAdapters(testFactory(t))
|
|
if !hasAdapter(adapters, "kook") {
|
|
t.Fatalf("expected a kook adapter, got %v", names(adapters))
|
|
}
|
|
}
|
|
|
|
func TestBuildChannelAdapters_KookMultiAccount(t *testing.T) {
|
|
t.Setenv("CHANNELS_JSON", `{"kook":[{"id":"conf0","config":{"bot_token":"kook-secret"},"allowUsers":["u-1"],"denyGroups":["g-9"]}]}`)
|
|
adapters := buildChannelAdapters(testFactory(t))
|
|
if !hasAdapter(adapters, "kook:conf0") {
|
|
t.Fatalf("expected a kook:conf0 adapter, got %v", names(adapters))
|
|
}
|
|
// The named adapter must expose its allow/deny lists via Config().
|
|
for _, a := range adapters {
|
|
if a.Name() == "kook:conf0" {
|
|
cfg := a.Config()
|
|
if got, _ := cfg["allowUsers"].([]string); len(got) != 1 || got[0] != "u-1" {
|
|
t.Errorf("allowUsers = %v, want [u-1]", cfg["allowUsers"])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func hasAdapter(adapters []adapter.PlatformAdapter, name string) bool {
|
|
for _, a := range adapters {
|
|
if a.Name() == name {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// TestChannelFactoryBuildFromFile exercises the reload factory against a real
|
|
// channels.json file: the factory reads the file (file beats env) and builds a
|
|
// named adapter carrying the account's token and black-or-white lists, so a panel
|
|
// save → Reload path uses exactly this builder.
|
|
func TestChannelFactoryBuildFromFile(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "channels.json")
|
|
if err := os.WriteFile(path, []byte(`{"kook":[{"id":"conf0","config":{"bot_token":"bt"},"denyGroups":["g-1"],"allowUsers":["u-1"],"groupMsgPerMin":3}]}`), 0o600); err != nil {
|
|
t.Fatalf("write channels.json: %v", err)
|
|
}
|
|
f := &channelFactory{path: path, fallback: channelAccountsFromEnv(), log: testLog()}
|
|
adpts, err := f.Build("kook")
|
|
if err != nil {
|
|
t.Fatalf("Build: %v", err)
|
|
}
|
|
if !hasAdapter(adpts, "kook:conf0") {
|
|
t.Fatalf("kook:conf0 not built, got %v", names(adpts))
|
|
}
|
|
for _, a := range adpts {
|
|
if a.Name() != "kook:conf0" {
|
|
continue
|
|
}
|
|
cfg := a.Config()
|
|
if cfg["bot_token"] != "bt" {
|
|
t.Errorf("bot_token = %v, want bt", cfg["bot_token"])
|
|
}
|
|
if got, _ := cfg["denyGroups"].([]string); len(got) != 1 || got[0] != "g-1" {
|
|
t.Errorf("denyGroups = %v, want [g-1]", cfg["denyGroups"])
|
|
}
|
|
if got, _ := cfg["allowUsers"].([]string); len(got) != 1 || got[0] != "u-1" {
|
|
t.Errorf("allowUsers = %v, want [u-1]", cfg["allowUsers"])
|
|
}
|
|
if got, _ := cfg["groupMsgPerMin"].(int); got != 3 {
|
|
t.Errorf("groupMsgPerMin = %v, want 3", cfg["groupMsgPerMin"])
|
|
}
|
|
}
|
|
}
|
|
|
|
func names(adapters []adapter.PlatformAdapter) []string {
|
|
out := make([]string, 0, len(adapters))
|
|
for _, a := range adapters {
|
|
out = append(out, a.Name())
|
|
}
|
|
return out
|
|
}
|