Files
Cyrene_v2/cmd/cyrene/channels_test.go
T
Aska aedae2190b
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(config): 运行时配置体系②——渠道配置落 channels.json + 面板落盘 + 热更新
- 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)——如实标注待凭据
2026-09-07 13:29:41 +08:00

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
}