Files
picoclaw/web/backend/api/channels_test.go
T
wenjie a8d0b03515 fix(web): save channel configs with nested channel_list patches (#2530)
Persist channel settings through the current channel_list schema, keeping common
channel fields at the top level and channel-specific fields under settings.
Return common fields and default config shapes from channel config endpoints, and
add coverage for nested patches, missing channel defaults, and secret handling.
2026-04-16 10:30:16 +08:00

196 lines
5.7 KiB
Go

package api
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/sipeed/picoclaw/pkg/config"
)
func TestHandleGetChannelConfig_ReturnsSecretPresenceWithoutLeakingSecrets(t *testing.T) {
configPath, cleanup := setupOAuthTestEnv(t)
defer cleanup()
cfg, err := config.LoadConfig(configPath)
if err != nil {
t.Fatalf("LoadConfig() error = %v", err)
}
bc := cfg.Channels[config.ChannelFeishu]
bc.Enabled = true
decoded, err := bc.GetDecoded()
if err != nil {
t.Fatalf("GetDecoded() error = %v", err)
}
bcfg := decoded.(*config.FeishuSettings)
bcfg.AppID = "cli_test_app"
bcfg.AppSecret = *config.NewSecureString("feishu-secret-from-security")
bc.AllowFrom = config.FlexibleStringSlice{"ou_test_user"}
if err := config.SaveConfig(configPath, cfg); err != nil {
t.Fatalf("SaveConfig() error = %v", err)
}
h := NewHandler(configPath)
mux := http.NewServeMux()
h.RegisterRoutes(mux)
req := httptest.NewRequest(http.MethodGet, "/api/channels/feishu/config", nil)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf(
"GET /api/channels/feishu/config status = %d, want %d, body=%s",
rec.Code,
http.StatusOK,
rec.Body.String(),
)
}
if strings.Contains(rec.Body.String(), "feishu-secret-from-security") {
t.Fatalf("response leaked secret value: %s", rec.Body.String())
}
var resp struct {
Config map[string]any `json:"config"`
ConfiguredSecrets []string `json:"configured_secrets"`
ConfigKey string `json:"config_key"`
Variant string `json:"variant"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("json.Unmarshal() error = %v", err)
}
if got := resp.ConfigKey; got != "feishu" {
t.Fatalf("config_key = %q, want %q", got, "feishu")
}
if got := resp.Config["app_id"]; got != "cli_test_app" {
t.Fatalf("config.app_id = %#v, want %q", got, "cli_test_app")
}
if got := resp.Config["enabled"]; got != true {
t.Fatalf("config.enabled = %#v, want true", got)
}
allowFrom, ok := resp.Config["allow_from"].([]any)
if !ok || len(allowFrom) != 1 || allowFrom[0] != "ou_test_user" {
t.Fatalf("config.allow_from = %#v, want [\"ou_test_user\"]", resp.Config["allow_from"])
}
if _, exists := resp.Config["app_secret"]; exists {
t.Fatalf("config should omit app_secret, got %#v", resp.Config["app_secret"])
}
if len(resp.ConfiguredSecrets) != 1 || resp.ConfiguredSecrets[0] != "app_secret" {
t.Fatalf("configured_secrets = %#v, want [\"app_secret\"]", resp.ConfiguredSecrets)
}
}
func TestHandleGetChannelConfig_ReturnsNotFoundForUnknownChannel(t *testing.T) {
configPath, cleanup := setupOAuthTestEnv(t)
defer cleanup()
h := NewHandler(configPath)
mux := http.NewServeMux()
h.RegisterRoutes(mux)
req := httptest.NewRequest(http.MethodGet, "/api/channels/not-a-channel/config", nil)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusNotFound {
t.Fatalf("GET /api/channels/not-a-channel/config status = %d, want %d", rec.Code, http.StatusNotFound)
}
}
func TestHandleGetChannelConfig_ReturnsCommonFieldsWhenSettingsEmpty(t *testing.T) {
configPath, cleanup := setupOAuthTestEnv(t)
defer cleanup()
cfg, err := config.LoadConfig(configPath)
if err != nil {
t.Fatalf("LoadConfig() error = %v", err)
}
bc := cfg.Channels[config.ChannelFeishu]
bc.Enabled = true
bc.AllowFrom = config.FlexibleStringSlice{"ou_common_user"}
if err := config.SaveConfig(configPath, cfg); err != nil {
t.Fatalf("SaveConfig() error = %v", err)
}
h := NewHandler(configPath)
mux := http.NewServeMux()
h.RegisterRoutes(mux)
req := httptest.NewRequest(http.MethodGet, "/api/channels/feishu/config", nil)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf(
"GET /api/channels/feishu/config status = %d, want %d, body=%s",
rec.Code,
http.StatusOK,
rec.Body.String(),
)
}
var resp struct {
Config map[string]any `json:"config"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("json.Unmarshal() error = %v", err)
}
if got := resp.Config["enabled"]; got != true {
t.Fatalf("config.enabled = %#v, want true", got)
}
allowFrom, ok := resp.Config["allow_from"].([]any)
if !ok || len(allowFrom) != 1 || allowFrom[0] != "ou_common_user" {
t.Fatalf("config.allow_from = %#v, want [\"ou_common_user\"]", resp.Config["allow_from"])
}
}
func TestHandleGetChannelConfig_ReturnsDefaultShapeForMissingChannel(t *testing.T) {
configPath, cleanup := setupOAuthTestEnv(t)
defer cleanup()
cfg, err := config.LoadConfig(configPath)
if err != nil {
t.Fatalf("LoadConfig() error = %v", err)
}
delete(cfg.Channels, config.ChannelIRC)
if err := config.SaveConfig(configPath, cfg); err != nil {
t.Fatalf("SaveConfig() error = %v", err)
}
h := NewHandler(configPath)
mux := http.NewServeMux()
h.RegisterRoutes(mux)
req := httptest.NewRequest(http.MethodGet, "/api/channels/irc/config", nil)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf(
"GET /api/channels/irc/config status = %d, want %d, body=%s",
rec.Code,
http.StatusOK,
rec.Body.String(),
)
}
var resp struct {
Config map[string]any `json:"config"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("json.Unmarshal() error = %v", err)
}
if got := resp.Config["server"]; got != "" {
t.Fatalf("config.server = %#v, want empty string", got)
}
if got := resp.Config["nick"]; got != "picoclaw" {
t.Fatalf("config.nick = %#v, want %q", got, "picoclaw")
}
if got := resp.Config["enabled"]; got != false {
t.Fatalf("config.enabled = %#v, want false", got)
}
}