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"]) } } } // TestNamedAdapterForwardsWebhook verifies the production wrapper forwards a // kook webhook-mode adapter's InboundHandler/WebhookPath to the channel bridge, // so bridge.WebhookHandlers() mounts the KOOK callback route on the gateway. func TestNamedAdapterForwardsWebhook(t *testing.T) { t.Setenv("CHANNEL_KOOK_TOKEN", "kook-secret") t.Setenv("CHANNEL_KOOK_RECEIVE_MODE", "webhook") t.Setenv("CHANNEL_KOOK_WEBHOOK_PATH", "/webhook/kook") adapters := buildChannelAdapters(testFactory(t)) var found *namedAdapter for _, a := range adapters { if nwa, ok := a.(*namedAdapter); ok && barePlatform(nwa.Name()) == "kook" { found = nwa break } } if found == nil { t.Fatalf("kook named adapter not found, got %v", names(adapters)) } if p := found.WebhookPath(); p != "/webhook/kook" { t.Errorf("WebhookPath = %q, want /webhook/kook", p) } if found.InboundHandler() == nil { t.Error("InboundHandler() = nil, want a mounted handler") } } // TestNamedAdapterNoWebhookInWSMode verifies a ws-mode adapter (all non-webhook // platforms) does not surface a webhook handler through the wrapper. func TestNamedAdapterNoWebhookInWSMode(t *testing.T) { t.Setenv("CHANNEL_KOOK_TOKEN", "kook-secret") t.Setenv("CHANNEL_KOOK_RECEIVE_MODE", "ws") adapters := buildChannelAdapters(testFactory(t)) for _, a := range adapters { if barePlatform(a.Name()) != "kook" { continue } if nwa, ok := a.(*namedAdapter); ok { if p := nwa.WebhookPath(); p != "" { t.Errorf("ws-mode kook WebhookPath = %q, want empty", p) } } } } func names(adapters []adapter.PlatformAdapter) []string { out := make([]string, 0, len(adapters)) for _, a := range adapters { out = append(out, a.Name()) } return out }