mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
refactor: seperate security.yml for store keys
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"regexp"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/config"
|
||||
"github.com/sipeed/picoclaw/pkg/logger"
|
||||
)
|
||||
|
||||
// registerConfigRoutes binds configuration management endpoints to the ServeMux.
|
||||
@@ -45,7 +46,7 @@ func (h *Handler) handleUpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
|
||||
var cfg config.Config
|
||||
if err := json.Unmarshal(body, &cfg); err != nil {
|
||||
if err = json.Unmarshal(body, &cfg); err != nil {
|
||||
http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
@@ -63,6 +64,14 @@ func (h *Handler) handleUpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Infof("new config: %+v", cfg)
|
||||
oldCfg, err := config.LoadConfig(h.configPath)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to load config: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
cfg.SecurityCopyFrom(oldCfg)
|
||||
|
||||
if err := config.SaveConfig(h.configPath, &cfg); err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to save config: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
@@ -150,6 +159,8 @@ func (h *Handler) handlePatchConfig(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
newCfg.SecurityCopyFrom(cfg)
|
||||
|
||||
if err := config.SaveConfig(h.configPath, &newCfg); err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to save config: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
@@ -175,17 +186,17 @@ func validateConfig(cfg *config.Config) []string {
|
||||
}
|
||||
|
||||
// Pico channel: token required when enabled
|
||||
if cfg.Channels.Pico.Enabled && cfg.Channels.Pico.Token == "" {
|
||||
if cfg.Channels.Pico.Enabled && cfg.Channels.Pico.Token() == "" {
|
||||
errs = append(errs, "channels.pico.token is required when pico channel is enabled")
|
||||
}
|
||||
|
||||
// Telegram: token required when enabled
|
||||
if cfg.Channels.Telegram.Enabled && cfg.Channels.Telegram.Token == "" {
|
||||
if cfg.Channels.Telegram.Enabled && cfg.Channels.Telegram.Token() == "" {
|
||||
errs = append(errs, "channels.telegram.token is required when telegram channel is enabled")
|
||||
}
|
||||
|
||||
// Discord: token required when enabled
|
||||
if cfg.Channels.Discord.Enabled && cfg.Channels.Discord.Token == "" {
|
||||
if cfg.Channels.Discord.Enabled && cfg.Channels.Discord.Token() == "" {
|
||||
errs = append(errs, "channels.discord.token is required when discord channel is enabled")
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ func TestHandleUpdateConfig_PreservesExecAllowRemoteDefaultWhenOmitted(t *testin
|
||||
h.RegisterRoutes(mux)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPut, "/api/config", bytes.NewBufferString(`{
|
||||
"version": 1,
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"workspace": "~/.picoclaw/workspace"
|
||||
@@ -27,7 +28,7 @@ func TestHandleUpdateConfig_PreservesExecAllowRemoteDefaultWhenOmitted(t *testin
|
||||
{
|
||||
"model_name": "custom-default",
|
||||
"model": "openai/gpt-4o",
|
||||
"api_key": "sk-default"
|
||||
"api_keys": ["sk-default"]
|
||||
}
|
||||
]
|
||||
}`))
|
||||
|
||||
@@ -159,10 +159,10 @@ func (h *Handler) gatewayStartReady() (bool, string, error) {
|
||||
return false, fmt.Sprintf("default model %q is invalid", modelName), nil
|
||||
}
|
||||
|
||||
if !hasModelConfiguration(*modelCfg) {
|
||||
if !hasModelConfiguration(modelCfg) {
|
||||
return false, fmt.Sprintf("default model %q has no credentials configured", modelName), nil
|
||||
}
|
||||
if requiresRuntimeProbe(*modelCfg) && !probeLocalModelAvailability(*modelCfg) {
|
||||
if requiresRuntimeProbe(modelCfg) && !probeLocalModelAvailability(modelCfg) {
|
||||
return false, fmt.Sprintf("default model %q is not reachable", modelName), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ func TestGatewayStartReady_ValidDefaultModel(t *testing.T) {
|
||||
configPath := filepath.Join(t.TempDir(), "config.json")
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Agents.Defaults.ModelName = cfg.ModelList[0].ModelName
|
||||
cfg.ModelList[0].APIKey = "test-key"
|
||||
cfg.ModelList[0].SetAPIKey("test-key")
|
||||
err := config.SaveConfig(configPath, cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("SaveConfig() error = %v", err)
|
||||
@@ -144,7 +144,7 @@ func TestGatewayStartReady_DefaultModelWithoutCredential(t *testing.T) {
|
||||
configPath := filepath.Join(t.TempDir(), "config.json")
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Agents.Defaults.ModelName = cfg.ModelList[0].ModelName
|
||||
cfg.ModelList[0].APIKey = ""
|
||||
cfg.ModelList[0].SetAPIKey("")
|
||||
cfg.ModelList[0].AuthMethod = ""
|
||||
err := config.SaveConfig(configPath, cfg)
|
||||
if err != nil {
|
||||
@@ -177,7 +177,7 @@ func TestGatewayStartReady_LocalModelWithoutAPIKey(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig() error = %v", err)
|
||||
}
|
||||
cfg.ModelList = []config.ModelConfig{{
|
||||
cfg.ModelList = []*config.ModelConfig{{
|
||||
ModelName: "local-vllm",
|
||||
Model: "vllm/custom-model",
|
||||
APIBase: "http://localhost:8000/v1",
|
||||
@@ -214,7 +214,7 @@ func TestGatewayStartReady_LocalModelWithRunningService(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig() error = %v", err)
|
||||
}
|
||||
cfg.ModelList = []config.ModelConfig{{
|
||||
cfg.ModelList = []*config.ModelConfig{{
|
||||
ModelName: "local-vllm",
|
||||
Model: "vllm/custom-model",
|
||||
APIBase: "http://127.0.0.1:8000/v1",
|
||||
@@ -249,12 +249,12 @@ func TestGatewayStartReady_RemoteVLLMWithAPIKeyDoesNotProbe(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig() error = %v", err)
|
||||
}
|
||||
cfg.ModelList = []config.ModelConfig{{
|
||||
cfg.ModelList = []*config.ModelConfig{{
|
||||
ModelName: "remote-vllm",
|
||||
Model: "vllm/custom-model",
|
||||
APIBase: "https://models.example.com/v1",
|
||||
APIKey: "remote-key",
|
||||
}}
|
||||
cfg.ModelList[0o0].SetAPIKey("remote-key")
|
||||
cfg.Agents.Defaults.ModelName = "remote-vllm"
|
||||
err = config.SaveConfig(configPath, cfg)
|
||||
if err != nil {
|
||||
@@ -284,7 +284,7 @@ func TestGatewayStartReady_LocalOllamaUsesDefaultProbeBase(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig() error = %v", err)
|
||||
}
|
||||
cfg.ModelList = []config.ModelConfig{{
|
||||
cfg.ModelList = []*config.ModelConfig{{
|
||||
ModelName: "local-ollama",
|
||||
Model: "ollama/llama3",
|
||||
}}
|
||||
@@ -312,7 +312,7 @@ func TestGatewayStartReady_OAuthModelRequiresStoredCredential(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig() error = %v", err)
|
||||
}
|
||||
cfg.ModelList = []config.ModelConfig{{
|
||||
cfg.ModelList = []*config.ModelConfig{{
|
||||
ModelName: "openai-oauth",
|
||||
Model: "openai/gpt-5.4",
|
||||
AuthMethod: "oauth",
|
||||
@@ -483,12 +483,12 @@ func TestGatewayStatusRequiresRestartAfterDefaultModelChange(t *testing.T) {
|
||||
configPath := filepath.Join(t.TempDir(), "config.json")
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Agents.Defaults.ModelName = cfg.ModelList[0].ModelName
|
||||
cfg.ModelList[0].APIKey = "test-key"
|
||||
cfg.ModelList = append(cfg.ModelList, config.ModelConfig{
|
||||
cfg.ModelList[0].SetAPIKey("test-key")
|
||||
cfg.ModelList = append(cfg.ModelList, &config.ModelConfig{
|
||||
ModelName: "second-model",
|
||||
Model: "openai/gpt-4.1",
|
||||
APIKey: "second-key",
|
||||
})
|
||||
cfg.ModelList[len(cfg.ModelList)-1].SetAPIKey("second-key")
|
||||
if err := config.SaveConfig(configPath, cfg); err != nil {
|
||||
t.Fatalf("SaveConfig() error = %v", err)
|
||||
}
|
||||
@@ -627,7 +627,7 @@ func TestGatewayRestartKeepsRunningProcessWhenPreconditionsFail(t *testing.T) {
|
||||
configPath := filepath.Join(t.TempDir(), "config.json")
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Agents.Defaults.ModelName = cfg.ModelList[0].ModelName
|
||||
cfg.ModelList[0].APIKey = ""
|
||||
cfg.ModelList[0].SetAPIKey("")
|
||||
cfg.ModelList[0].AuthMethod = ""
|
||||
if err := config.SaveConfig(configPath, cfg); err != nil {
|
||||
t.Fatalf("SaveConfig() error = %v", err)
|
||||
@@ -680,7 +680,7 @@ func TestGatewayRestartKeepsOldProcessWhenItDoesNotExitInTime(t *testing.T) {
|
||||
configPath := filepath.Join(t.TempDir(), "config.json")
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Agents.Defaults.ModelName = cfg.ModelList[0].ModelName
|
||||
cfg.ModelList[0].APIKey = "test-key"
|
||||
cfg.ModelList[0].SetAPIKey("test-key")
|
||||
if err := config.SaveConfig(configPath, cfg); err != nil {
|
||||
t.Fatalf("SaveConfig() error = %v", err)
|
||||
}
|
||||
@@ -741,7 +741,7 @@ func TestGatewayRestartReturnsErrorStatusWhenReplacementFailsToStart(t *testing.
|
||||
configPath := filepath.Join(t.TempDir(), "config.json")
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Agents.Defaults.ModelName = cfg.ModelList[0].ModelName
|
||||
cfg.ModelList[0].APIKey = "test-key"
|
||||
cfg.ModelList[0].SetAPIKey("test-key")
|
||||
if err := config.SaveConfig(configPath, cfg); err != nil {
|
||||
t.Fatalf("SaveConfig() error = %v", err)
|
||||
}
|
||||
|
||||
@@ -20,9 +20,9 @@ var (
|
||||
probeOpenAICompatibleModelFunc = probeOpenAICompatibleModel
|
||||
)
|
||||
|
||||
func hasModelConfiguration(m config.ModelConfig) bool {
|
||||
func hasModelConfiguration(m *config.ModelConfig) bool {
|
||||
authMethod := strings.ToLower(strings.TrimSpace(m.AuthMethod))
|
||||
apiKey := strings.TrimSpace(m.APIKey)
|
||||
apiKey := strings.TrimSpace(m.APIKey())
|
||||
|
||||
if authMethod == "oauth" || authMethod == "token" {
|
||||
if provider, ok := oauthProviderForModel(m.Model); ok {
|
||||
@@ -44,7 +44,7 @@ func hasModelConfiguration(m config.ModelConfig) bool {
|
||||
|
||||
// isModelConfigured reports whether a model is currently available to use.
|
||||
// Local models must be reachable; remote/API-key models only need saved config.
|
||||
func isModelConfigured(m config.ModelConfig) bool {
|
||||
func isModelConfigured(m *config.ModelConfig) bool {
|
||||
if !hasModelConfiguration(m) {
|
||||
return false
|
||||
}
|
||||
@@ -54,7 +54,7 @@ func isModelConfigured(m config.ModelConfig) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func requiresRuntimeProbe(m config.ModelConfig) bool {
|
||||
func requiresRuntimeProbe(m *config.ModelConfig) bool {
|
||||
authMethod := strings.ToLower(strings.TrimSpace(m.AuthMethod))
|
||||
if authMethod == "local" {
|
||||
return true
|
||||
@@ -75,7 +75,7 @@ func requiresRuntimeProbe(m config.ModelConfig) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func probeLocalModelAvailability(m config.ModelConfig) bool {
|
||||
func probeLocalModelAvailability(m *config.ModelConfig) bool {
|
||||
apiBase := modelProbeAPIBase(m)
|
||||
protocol, modelID := splitModel(m.Model)
|
||||
switch protocol {
|
||||
@@ -95,7 +95,7 @@ func probeLocalModelAvailability(m config.ModelConfig) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func modelProbeAPIBase(m config.ModelConfig) string {
|
||||
func modelProbeAPIBase(m *config.ModelConfig) string {
|
||||
if apiBase := strings.TrimSpace(m.APIBase); apiBase != "" {
|
||||
return normalizeModelProbeAPIBase(apiBase)
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ func (h *Handler) handleListModels(w http.ResponseWriter, r *http.Request) {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(cfg.ModelList))
|
||||
for i, m := range cfg.ModelList {
|
||||
go func(i int, m config.ModelConfig) {
|
||||
go func(i int, m *config.ModelConfig) {
|
||||
defer wg.Done()
|
||||
configured[i] = isModelConfigured(m)
|
||||
}(i, m)
|
||||
@@ -72,7 +72,7 @@ func (h *Handler) handleListModels(w http.ResponseWriter, r *http.Request) {
|
||||
ModelName: m.ModelName,
|
||||
Model: m.Model,
|
||||
APIBase: m.APIBase,
|
||||
APIKey: maskAPIKey(m.APIKey),
|
||||
APIKey: maskAPIKey(m.APIKey()),
|
||||
Proxy: m.Proxy,
|
||||
AuthMethod: m.AuthMethod,
|
||||
ConnectMode: m.ConnectMode,
|
||||
@@ -122,7 +122,7 @@ func (h *Handler) handleAddModel(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
cfg.ModelList = append(cfg.ModelList, mc)
|
||||
cfg.ModelList = append(cfg.ModelList, &mc)
|
||||
|
||||
if err := config.SaveConfig(h.configPath, cfg); err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to save config: %v", err), http.StatusInternalServerError)
|
||||
@@ -180,11 +180,11 @@ func (h *Handler) handleUpdateModel(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Preserve the existing API key when the caller omits it (empty string).
|
||||
// This lets the UI update api_base / proxy without clearing the stored secret.
|
||||
if mc.APIKey == "" {
|
||||
mc.APIKey = cfg.ModelList[idx].APIKey
|
||||
if mc.APIKey() == "" {
|
||||
mc.SetAPIKey(cfg.ModelList[idx].APIKey())
|
||||
}
|
||||
|
||||
cfg.ModelList[idx] = mc
|
||||
cfg.ModelList[idx] = &mc
|
||||
|
||||
if err := config.SaveConfig(h.configPath, cfg); err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to save config: %v", err), http.StatusInternalServerError)
|
||||
|
||||
@@ -59,7 +59,7 @@ func TestHandleListModels_ConfiguredStatusUsesRuntimeProbesForLocalModels(t *tes
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig() error = %v", err)
|
||||
}
|
||||
cfg.ModelList = []config.ModelConfig{
|
||||
cfg.ModelList = []*config.ModelConfig{
|
||||
{
|
||||
ModelName: "openai-oauth",
|
||||
Model: "openai/gpt-5.4",
|
||||
@@ -78,7 +78,6 @@ func TestHandleListModels_ConfiguredStatusUsesRuntimeProbesForLocalModels(t *tes
|
||||
ModelName: "vllm-remote",
|
||||
Model: "vllm/custom-model",
|
||||
APIBase: "https://models.example.com/v1",
|
||||
APIKey: "remote-key",
|
||||
},
|
||||
{
|
||||
ModelName: "copilot-gpt-5.4",
|
||||
@@ -87,6 +86,11 @@ func TestHandleListModels_ConfiguredStatusUsesRuntimeProbesForLocalModels(t *tes
|
||||
AuthMethod: "oauth",
|
||||
},
|
||||
}
|
||||
cfg.WithSecurity(&config.SecurityConfig{ModelList: map[string]config.ModelSecurityEntry{
|
||||
"vllm-remote": {
|
||||
APIKeys: []string{"remote-key"},
|
||||
},
|
||||
}})
|
||||
cfg.Agents.Defaults.ModelName = "openai-oauth"
|
||||
if err := config.SaveConfig(configPath, cfg); err != nil {
|
||||
t.Fatalf("SaveConfig() error = %v", err)
|
||||
@@ -152,7 +156,7 @@ func TestHandleListModels_ConfiguredStatusForOAuthModelWithCredential(t *testing
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig() error = %v", err)
|
||||
}
|
||||
cfg.ModelList = []config.ModelConfig{{
|
||||
cfg.ModelList = []*config.ModelConfig{{
|
||||
ModelName: "claude-oauth",
|
||||
Model: "anthropic/claude-sonnet-4.6",
|
||||
AuthMethod: "oauth",
|
||||
@@ -215,7 +219,7 @@ func TestHandleListModels_ProbesLocalModelsConcurrently(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig() error = %v", err)
|
||||
}
|
||||
cfg.ModelList = []config.ModelConfig{
|
||||
cfg.ModelList = []*config.ModelConfig{
|
||||
{
|
||||
ModelName: "local-vllm-a",
|
||||
Model: "vllm/custom-a",
|
||||
@@ -274,7 +278,7 @@ func TestHandleListModels_NormalizesWildcardLocalAPIBaseForProbe(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig() error = %v", err)
|
||||
}
|
||||
cfg.ModelList = []config.ModelConfig{{
|
||||
cfg.ModelList = []*config.ModelConfig{{
|
||||
ModelName: "vllm-local",
|
||||
Model: "vllm/custom-model",
|
||||
APIBase: "http://0.0.0.0:8000/v1",
|
||||
|
||||
@@ -776,28 +776,28 @@ func modelBelongsToProvider(provider, model string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func defaultModelConfigForProvider(provider, authMethod string) config.ModelConfig {
|
||||
func defaultModelConfigForProvider(provider, authMethod string) *config.ModelConfig {
|
||||
switch provider {
|
||||
case oauthProviderOpenAI:
|
||||
return config.ModelConfig{
|
||||
return &config.ModelConfig{
|
||||
ModelName: "gpt-5.4",
|
||||
Model: "openai/gpt-5.4",
|
||||
AuthMethod: authMethod,
|
||||
}
|
||||
case oauthProviderAnthropic:
|
||||
return config.ModelConfig{
|
||||
return &config.ModelConfig{
|
||||
ModelName: "claude-sonnet-4.6",
|
||||
Model: "anthropic/claude-sonnet-4.6",
|
||||
AuthMethod: authMethod,
|
||||
}
|
||||
case oauthProviderGoogleAntigravity:
|
||||
return config.ModelConfig{
|
||||
return &config.ModelConfig{
|
||||
ModelName: "gemini-flash",
|
||||
Model: "antigravity/gemini-3-flash",
|
||||
AuthMethod: authMethod,
|
||||
}
|
||||
default:
|
||||
return config.ModelConfig{}
|
||||
return &config.ModelConfig{}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -166,7 +166,7 @@ func TestOAuthLogoutClearsCredentialAndConfig(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig error: %v", err)
|
||||
}
|
||||
cfg.ModelList = append(cfg.ModelList, config.ModelConfig{
|
||||
cfg.ModelList = append(cfg.ModelList, &config.ModelConfig{
|
||||
ModelName: "gpt-5.4",
|
||||
Model: "openai/gpt-5.4",
|
||||
AuthMethod: "oauth",
|
||||
@@ -229,12 +229,18 @@ func setupOAuthTestEnv(t *testing.T) (string, func()) {
|
||||
}
|
||||
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.ModelList = []config.ModelConfig{{
|
||||
cfg.ModelList = []*config.ModelConfig{{
|
||||
ModelName: "custom-default",
|
||||
Model: "openai/gpt-4o",
|
||||
APIKey: "sk-default",
|
||||
}}
|
||||
cfg.Agents.Defaults.ModelName = "custom-default"
|
||||
cfg.WithSecurity(&config.SecurityConfig{
|
||||
ModelList: map[string]config.ModelSecurityEntry{
|
||||
"custom-default": {
|
||||
APIKeys: []string{"sk-default"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
configPath := filepath.Join(tmp, "config.json")
|
||||
if err := config.SaveConfig(configPath, cfg); err != nil {
|
||||
|
||||
@@ -57,7 +57,7 @@ func (h *Handler) handleGetPicoToken(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"token": cfg.Channels.Pico.Token,
|
||||
"token": cfg.Channels.Pico.Token(),
|
||||
"ws_url": wsURL,
|
||||
"enabled": cfg.Channels.Pico.Enabled,
|
||||
})
|
||||
@@ -74,7 +74,7 @@ func (h *Handler) handleRegenPicoToken(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
token := generateSecureToken()
|
||||
cfg.Channels.Pico.Token = token
|
||||
cfg.Channels.Pico.SetToken(token)
|
||||
|
||||
if err := config.SaveConfig(h.configPath, cfg); err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to save config: %v", err), http.StatusInternalServerError)
|
||||
@@ -110,8 +110,8 @@ func (h *Handler) ensurePicoChannel(callerOrigin string) (bool, error) {
|
||||
changed = true
|
||||
}
|
||||
|
||||
if cfg.Channels.Pico.Token == "" {
|
||||
cfg.Channels.Pico.Token = generateSecureToken()
|
||||
if cfg.Channels.Pico.Token() == "" {
|
||||
cfg.Channels.Pico.SetToken(generateSecureToken())
|
||||
changed = true
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ func (h *Handler) handlePicoSetup(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"token": cfg.Channels.Pico.Token,
|
||||
"token": cfg.Channels.Pico.Token(),
|
||||
"ws_url": wsURL,
|
||||
"enabled": true,
|
||||
"changed": changed,
|
||||
|
||||
@@ -33,7 +33,7 @@ func TestEnsurePicoChannel_FreshConfig(t *testing.T) {
|
||||
if !cfg.Channels.Pico.Enabled {
|
||||
t.Error("expected Pico to be enabled after setup")
|
||||
}
|
||||
if cfg.Channels.Pico.Token == "" {
|
||||
if cfg.Channels.Pico.Token() == "" {
|
||||
t.Error("expected a non-empty token after setup")
|
||||
}
|
||||
}
|
||||
@@ -121,7 +121,7 @@ func TestEnsurePicoChannel_PreservesUserSettings(t *testing.T) {
|
||||
// Pre-configure with custom user settings
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Channels.Pico.Enabled = true
|
||||
cfg.Channels.Pico.Token = "user-custom-token"
|
||||
cfg.Channels.Pico.SetToken("user-custom-token")
|
||||
cfg.Channels.Pico.AllowTokenQuery = true
|
||||
cfg.Channels.Pico.AllowOrigins = []string{"https://myapp.example.com"}
|
||||
if err := config.SaveConfig(configPath, cfg); err != nil {
|
||||
@@ -143,8 +143,8 @@ func TestEnsurePicoChannel_PreservesUserSettings(t *testing.T) {
|
||||
t.Fatalf("LoadConfig() error = %v", err)
|
||||
}
|
||||
|
||||
if cfg.Channels.Pico.Token != "user-custom-token" {
|
||||
t.Errorf("token = %q, want %q", cfg.Channels.Pico.Token, "user-custom-token")
|
||||
if cfg.Channels.Pico.Token() != "user-custom-token" {
|
||||
t.Errorf("token = %q, want %q", cfg.Channels.Pico.Token(), "user-custom-token")
|
||||
}
|
||||
if !cfg.Channels.Pico.AllowTokenQuery {
|
||||
t.Error("user's allow_token_query=true must be preserved")
|
||||
@@ -166,7 +166,7 @@ func TestEnsurePicoChannel_Idempotent(t *testing.T) {
|
||||
}
|
||||
|
||||
cfg1, _ := config.LoadConfig(configPath)
|
||||
token1 := cfg1.Channels.Pico.Token
|
||||
token1 := cfg1.Channels.Pico.Token()
|
||||
|
||||
// Second call should be a no-op
|
||||
changed, err := h.ensurePicoChannel(origin)
|
||||
@@ -178,7 +178,7 @@ func TestEnsurePicoChannel_Idempotent(t *testing.T) {
|
||||
}
|
||||
|
||||
cfg2, _ := config.LoadConfig(configPath)
|
||||
if cfg2.Channels.Pico.Token != token1 {
|
||||
if cfg2.Channels.Pico.Token() != token1 {
|
||||
t.Error("token should not change on subsequent calls")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user