server/internal/handlers/admin_config_test.go
Rama main · réplica de lectura del repositorio original.
package handlers_test
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"blackbox/server/internal/auth"
"blackbox/server/internal/handlers"
"blackbox/server/internal/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAdminConfig_ReturnsWebhookSecret(t *testing.T) {
t.Parallel()
req := httptest.NewRequest(http.MethodGet, "/api/admin/config", nil)
req = req.WithContext(context.WithValue(req.Context(), auth.ClaimsKey, &auth.Claims{
UserID: "admin1",
IsAdmin: true,
}))
w := httptest.NewRecorder()
handlers.AdminConfig(newTestDB(t), "my-secret-value", nil)(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var resp map[string]any
require.NoError(t, json.NewDecoder(w.Body).Decode(&resp))
assert.Equal(t, "my-secret-value", resp["webhook_secret"])
assert.Equal(t, true, resp["file_watcher_redact_secrets"])
assert.Equal(t, "ollama", resp["ai_provider"])
assert.Equal(t, "", resp["ai_url"])
assert.Equal(t, "", resp["ai_model"])
assert.Equal(t, false, resp["ai_api_key_set"])
assert.Equal(t, "analysis", resp["ai_mode"])
assert.Equal(t, "no-store, no-cache, must-revalidate", w.Header().Get("Cache-Control"))
}
func TestAdminConfig_ReturnsAISettings(t *testing.T) {
t.Parallel()
req := httptest.NewRequest(http.MethodGet, "/api/admin/config", nil)
req = req.WithContext(context.WithValue(req.Context(), auth.ClaimsKey, &auth.Claims{
UserID: "admin1",
IsAdmin: true,
}))
w := httptest.NewRecorder()
database := newTestDB(t)
require.NoError(t, database.Create(&models.AppSetting{Key: "file_watcher_redact_secrets", Value: "false"}).Error)
require.NoError(t, database.Create(&models.AppSetting{Key: "ai_provider", Value: "openai_compat"}).Error)
require.NoError(t, database.Create(&models.AppSetting{Key: "ai_url", Value: " https://api.openai.com "}).Error)
require.NoError(t, database.Create(&models.AppSetting{Key: "ai_model", Value: " gpt-4o-mini "}).Error)
require.NoError(t, database.Create(&models.AppSetting{Key: "ai_api_key", Value: "sk-secret"}).Error)
require.NoError(t, database.Create(&models.AppSetting{Key: "ai_mode", Value: " enhanced "}).Error)
handlers.AdminConfig(database, "", nil)(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var resp map[string]any
require.NoError(t, json.NewDecoder(w.Body).Decode(&resp))
assert.Equal(t, "openai_compat", resp["ai_provider"])
assert.Equal(t, "https://api.openai.com", resp["ai_url"])
assert.Equal(t, "gpt-4o-mini", resp["ai_model"])
assert.Equal(t, true, resp["ai_api_key_set"])
assert.Equal(t, "enhanced", resp["ai_mode"])
}
func TestAdminConfig_LegacyOllamaFallback(t *testing.T) {
t.Parallel()
req := httptest.NewRequest(http.MethodGet, "/api/admin/config", nil)
req = req.WithContext(context.WithValue(req.Context(), auth.ClaimsKey, &auth.Claims{
UserID: "admin1",
IsAdmin: true,
}))
w := httptest.NewRecorder()
database := newTestDB(t)
require.NoError(t, database.Create(&models.AppSetting{Key: "ollama_url", Value: "http://localhost:11434"}).Error)
require.NoError(t, database.Create(&models.AppSetting{Key: "ollama_model", Value: "llama3.2"}).Error)
require.NoError(t, database.Create(&models.AppSetting{Key: "ollama_mode", Value: "enhanced"}).Error)
handlers.AdminConfig(database, "", nil)(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var resp map[string]any
require.NoError(t, json.NewDecoder(w.Body).Decode(&resp))
assert.Equal(t, "ollama", resp["ai_provider"])
assert.Equal(t, "http://localhost:11434", resp["ai_url"])
assert.Equal(t, "llama3.2", resp["ai_model"])
assert.Equal(t, false, resp["ai_api_key_set"])
assert.Equal(t, "enhanced", resp["ai_mode"])
}
func TestAdminConfig_DoesNotExposeMCPPort(t *testing.T) {
t.Parallel()
req := httptest.NewRequest(http.MethodGet, "/api/admin/config", nil)
req = req.WithContext(context.WithValue(req.Context(), auth.ClaimsKey, &auth.Claims{
UserID: "admin1",
IsAdmin: true,
}))
w := httptest.NewRecorder()
database := newTestDB(t)
require.NoError(t, database.Create(&models.AppSetting{Key: "mcp_enabled", Value: "true"}).Error)
require.NoError(t, database.Create(&models.AppSetting{Key: "mcp_port", Value: "13001"}).Error)
require.NoError(t, database.Create(&models.AppSetting{Key: "mcp_auth_token", Value: "secret-token-value-abcdef1234"}).Error)
handlers.AdminConfig(database, "", nil)(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var resp map[string]any
require.NoError(t, json.NewDecoder(w.Body).Decode(&resp))
_, exists := resp["mcp_port"]
assert.False(t, exists, "admin config should not expose mcp_port")
assert.Equal(t, true, resp["mcp_enabled"])
assert.Equal(t, true, resp["mcp_auth_token_set"])
}
func TestAdminConfig_ReturnsMCPEndpointURL(t *testing.T) {
t.Parallel()
req := httptest.NewRequest(http.MethodGet, "/api/admin/config", nil)
req = req.WithContext(context.WithValue(req.Context(), auth.ClaimsKey, &auth.Claims{
UserID: "admin1",
IsAdmin: true,
}))
w := httptest.NewRecorder()
database := newTestDB(t)
require.NoError(t, database.Create(&models.AppSetting{Key: "base_url", Value: "https://blackbox.example.com"}).Error)
handlers.AdminConfig(database, "", nil)(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var resp map[string]any
require.NoError(t, json.NewDecoder(w.Body).Decode(&resp))
assert.Equal(t, "https://blackbox.example.com/mcp", resp["mcp_endpoint_url"])
_, exists := resp["mcp_port"]
assert.False(t, exists, "admin config should not expose mcp_port")
}
func TestAdminConfig_ReturnsEmptyMCPEndpointURLWithoutBaseURL(t *testing.T) {
t.Parallel()
req := httptest.NewRequest(http.MethodGet, "/api/admin/config", nil)
req = req.WithContext(context.WithValue(req.Context(), auth.ClaimsKey, &auth.Claims{
UserID: "admin1",
IsAdmin: true,
}))
w := httptest.NewRecorder()
handlers.AdminConfig(newTestDB(t), "", nil)(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var resp map[string]any
require.NoError(t, json.NewDecoder(w.Body).Decode(&resp))
assert.Equal(t, "", resp["mcp_endpoint_url"])
}
func TestAdminConfig_NormalizesTrailingSlashInMCPEndpointURL(t *testing.T) {
t.Parallel()
req := httptest.NewRequest(http.MethodGet, "/api/admin/config", nil)
req = req.WithContext(context.WithValue(req.Context(), auth.ClaimsKey, &auth.Claims{
UserID: "admin1",
IsAdmin: true,
}))
w := httptest.NewRecorder()
database := newTestDB(t)
require.NoError(t, database.Create(&models.AppSetting{Key: "base_url", Value: "https://blackbox.example.com/"}).Error)
handlers.AdminConfig(database, "", nil)(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var resp map[string]any
require.NoError(t, json.NewDecoder(w.Body).Decode(&resp))
assert.Equal(t, "https://blackbox.example.com/mcp", resp["mcp_endpoint_url"])
}
func TestAdminConfig_ReturnsEmptyMCPEndpointURLForLegacyInvalidBaseURL(t *testing.T) {
t.Parallel()
req := httptest.NewRequest(http.MethodGet, "/api/admin/config", nil)
req = req.WithContext(context.WithValue(req.Context(), auth.ClaimsKey, &auth.Claims{
UserID: "admin1",
IsAdmin: true,
}))
w := httptest.NewRecorder()
database := newTestDB(t)
require.NoError(t, database.Create(&models.AppSetting{Key: "base_url", Value: "https://blackbox.example.com?foo=bar"}).Error)
handlers.AdminConfig(database, "", nil)(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var resp map[string]any
require.NoError(t, json.NewDecoder(w.Body).Decode(&resp))
assert.Equal(t, "", resp["mcp_endpoint_url"])
assert.Equal(t, "https://blackbox.example.com?foo=bar", resp["base_url"])
}
func TestAdminConfig_ReturnsMCPMigrationWarningWhenLegacyPortExists(t *testing.T) {
t.Parallel()
req := httptest.NewRequest(http.MethodGet, "/api/admin/config", nil)
req = req.WithContext(context.WithValue(req.Context(), auth.ClaimsKey, &auth.Claims{
UserID: "admin1",
IsAdmin: true,
}))
w := httptest.NewRecorder()
database := newTestDB(t)
require.NoError(t, database.Create(&models.AppSetting{Key: "mcp_port", Value: "13001"}).Error)
handlers.AdminConfig(database, "", nil)(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var resp map[string]any
require.NoError(t, json.NewDecoder(w.Body).Decode(&resp))
assert.Equal(t, true, resp["mcp_migration_warning"])
}
func TestAdminConfig_DoesNotReturnMCPMigrationWarningWithoutLegacyPort(t *testing.T) {
t.Parallel()
req := httptest.NewRequest(http.MethodGet, "/api/admin/config", nil)
req = req.WithContext(context.WithValue(req.Context(), auth.ClaimsKey, &auth.Claims{
UserID: "admin1",
IsAdmin: true,
}))
w := httptest.NewRecorder()
handlers.AdminConfig(newTestDB(t), "", nil)(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var resp map[string]any
require.NoError(t, json.NewDecoder(w.Body).Decode(&resp))
assert.Equal(t, false, resp["mcp_migration_warning"])
}
func TestUpdateAISettings_RejectsInvalidURL(t *testing.T) {
t.Parallel()
database := newTestDB(t)
req := httptest.NewRequest(http.MethodPut, "/api/admin/settings/ai", strings.NewReader(`{"ai_url":"not a url","ai_model":"llama3.2"}`))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
handlers.UpdateAISettings(database)(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
var count int64
require.NoError(t, database.Model(&models.AppSetting{}).Count(&count).Error)
assert.Zero(t, count)
}
func TestUpdateAISettings_RejectsInvalidProvider(t *testing.T) {
t.Parallel()
database := newTestDB(t)
req := httptest.NewRequest(http.MethodPut, "/api/admin/settings/ai", strings.NewReader(`{"ai_provider":"anthropic","ai_url":"http://localhost","ai_model":"m"}`))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
handlers.UpdateAISettings(database)(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
var count int64
require.NoError(t, database.Model(&models.AppSetting{}).Count(&count).Error)
assert.Zero(t, count)
}
func TestUpdateAISettings_PersistsMode(t *testing.T) {
t.Parallel()
database := newTestDB(t)
req := httptest.NewRequest(http.MethodPut, "/api/admin/settings/ai", strings.NewReader(`{"ai_provider":"ollama","ai_url":"http://localhost:11434","ai_model":"llama3.2","ai_mode":"enhanced"}`))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
handlers.UpdateAISettings(database)(w, req)
assert.Equal(t, http.StatusNoContent, w.Code)
var setting models.AppSetting
require.NoError(t, database.First(&setting, "key = ?", "ai_mode").Error)
assert.Equal(t, "enhanced", setting.Value)
}
func TestUpdateAISettings_PreservesAPIKeyWhenBlank(t *testing.T) {
t.Parallel()
database := newTestDB(t)
// Initial save with API key
body1 := `{"ai_provider":"openai_compat","ai_url":"https://api.openai.com","ai_model":"gpt-4o","ai_api_key":"sk-original","ai_mode":"analysis"}`
req1 := httptest.NewRequest(http.MethodPut, "/api/admin/settings/ai", strings.NewReader(body1))
req1.Header.Set("Content-Type", "application/json")
w1 := httptest.NewRecorder()
handlers.UpdateAISettings(database)(w1, req1)
assert.Equal(t, http.StatusNoContent, w1.Code)
// Update without sending API key
body2 := `{"ai_provider":"openai_compat","ai_url":"https://api.openai.com","ai_model":"gpt-4o-mini","ai_api_key":"","ai_mode":"analysis"}`
req2 := httptest.NewRequest(http.MethodPut, "/api/admin/settings/ai", strings.NewReader(body2))
req2.Header.Set("Content-Type", "application/json")
w2 := httptest.NewRecorder()
handlers.UpdateAISettings(database)(w2, req2)
assert.Equal(t, http.StatusNoContent, w2.Code)
var keySetting models.AppSetting
require.NoError(t, database.First(&keySetting, "key = ?", "ai_api_key").Error)
assert.Equal(t, "sk-original", keySetting.Value)
var modelSetting models.AppSetting
require.NoError(t, database.First(&modelSetting, "key = ?", "ai_model").Error)
assert.Equal(t, "gpt-4o-mini", modelSetting.Value)
}
func TestUpdateBaseURLSetting_ValidHTTPS(t *testing.T) {
t.Parallel()
database := newTestDB(t)
body := `{"base_url":"https://blackbox.example.com"}`
req := httptest.NewRequest(http.MethodPut, "/api/admin/settings/base-url", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
handlers.UpdateBaseURLSetting(database)(w, req)
assert.Equal(t, http.StatusNoContent, w.Code)
var setting models.AppSetting
require.NoError(t, database.First(&setting, "key = ?", "base_url").Error)
assert.Equal(t, "https://blackbox.example.com", setting.Value)
}
func TestUpdateBaseURLSetting_InvalidURL(t *testing.T) {
t.Parallel()
database := newTestDB(t)
body := `{"base_url":"not a valid url"}`
req := httptest.NewRequest(http.MethodPut, "/api/admin/settings/base-url", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
handlers.UpdateBaseURLSetting(database)(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
var count int64
require.NoError(t, database.Model(&models.AppSetting{}).Where("key = ?", "base_url").Count(&count).Error)
assert.Zero(t, count)
}
func TestUpdateBaseURLSetting_RejectsUnsupportedScheme(t *testing.T) {
t.Parallel()
database := newTestDB(t)
body := `{"base_url":"javascript:alert(1)"}`
req := httptest.NewRequest(http.MethodPut, "/api/admin/settings/base-url", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
handlers.UpdateBaseURLSetting(database)(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
var count int64
require.NoError(t, database.Model(&models.AppSetting{}).Where("key = ?", "base_url").Count(&count).Error)
assert.Zero(t, count)
}
func TestUpdateBaseURLSetting_RejectsQueryOrFragment(t *testing.T) {
t.Parallel()
for _, body := range []string{
`{"base_url":"https://blackbox.example.com?foo=bar"}`,
`{"base_url":"https://blackbox.example.com#frag"}`,
} {
database := newTestDB(t)
req := httptest.NewRequest(http.MethodPut, "/api/admin/settings/base-url", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
handlers.UpdateBaseURLSetting(database)(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
var count int64
require.NoError(t, database.Model(&models.AppSetting{}).Where("key = ?", "base_url").Count(&count).Error)
assert.Zero(t, count)
}
}
func TestUpdateBaseURLSetting_EmptyStringClears(t *testing.T) {
t.Parallel()
database := newTestDB(t)
// Pre-populate a base_url
require.NoError(t, database.Save(&models.AppSetting{Key: "base_url", Value: "https://old.example.com"}).Error)
body := `{"base_url":""}`
req := httptest.NewRequest(http.MethodPut, "/api/admin/settings/base-url", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
handlers.UpdateBaseURLSetting(database)(w, req)
assert.Equal(t, http.StatusNoContent, w.Code)
var setting models.AppSetting
require.NoError(t, database.First(&setting, "key = ?", "base_url").Error)
assert.Equal(t, "", setting.Value)
}
func TestTestAISettings_Success(t *testing.T) {
t.Parallel()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodPost, r.Method)
assert.Equal(t, "/api/generate", r.URL.Path)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"response":"OK"}`))
}))
t.Cleanup(srv.Close)
database := newTestDB(t)
req := httptest.NewRequest(http.MethodPost, "/api/admin/settings/ai/test", strings.NewReader(`{"ai_provider":"ollama","ai_url":"`+srv.URL+`","ai_model":"llama3.2"}`))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
handlers.TestAISettings(database)(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var resp map[string]any
require.NoError(t, json.NewDecoder(w.Body).Decode(&resp))
assert.Equal(t, true, resp["ok"])
assert.Equal(t, "OK", resp["response"])
}
func TestTestAISettings_UsesStoredAPIKeyWhenBlank(t *testing.T) {
t.Parallel()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodPost, r.Method)
assert.Equal(t, "/v1/chat/completions", r.URL.Path)
assert.Equal(t, "Bearer sk-saved", r.Header.Get("Authorization"))
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"choices":[{"message":{"content":"OK"}}]}`))
}))
t.Cleanup(srv.Close)
database := newTestDB(t)
require.NoError(t, database.Create(&models.AppSetting{Key: "ai_api_key", Value: "sk-saved"}).Error)
req := httptest.NewRequest(http.MethodPost, "/api/admin/settings/ai/test", strings.NewReader(`{"ai_provider":"openai_compat","ai_url":"`+srv.URL+`","ai_model":"gpt-4o-mini","ai_api_key":"","ai_mode":"analysis"}`))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
handlers.TestAISettings(database)(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var resp map[string]any
require.NoError(t, json.NewDecoder(w.Body).Decode(&resp))
assert.Equal(t, true, resp["ok"])
assert.Equal(t, "OK", resp["response"])
}
// --- MCP Settings Tests ---
func TestUpdateMCPSettings_EnableGeneratesToken(t *testing.T) {
t.Parallel()
database := newTestDB(t)
body := `{"mcp_enabled":true}`
req := httptest.NewRequest(http.MethodPut, "/api/admin/settings/mcp", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
handlers.UpdateMCPSettings(database, nil)(w, req)
assert.Equal(t, http.StatusNoContent, w.Code)
var tokenSetting models.AppSetting
require.NoError(t, database.First(&tokenSetting, "key = ?", "mcp_auth_token").Error)
assert.NotEmpty(t, tokenSetting.Value)
var enabledSetting models.AppSetting
require.NoError(t, database.First(&enabledSetting, "key = ?", "mcp_enabled").Error)
assert.Equal(t, "true", enabledSetting.Value)
var portCount int64
require.NoError(t, database.Model(&models.AppSetting{}).Where("key = ?", "mcp_port").Count(&portCount).Error)
assert.Zero(t, portCount)
}
func TestUpdateMCPSettings_RejectsMissingEnabledWithoutAck(t *testing.T) {
t.Parallel()
for _, body := range []string{
`{}`,
`{"foo":"bar"}`,
`{"acknowledge_mcp_migration_warning":false}`,
} {
database := newTestDB(t)
req := httptest.NewRequest(http.MethodPut, "/api/admin/settings/mcp", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
handlers.UpdateMCPSettings(database, nil)(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
var settingsCount int64
require.NoError(t, database.Model(&models.AppSetting{}).Count(&settingsCount).Error)
assert.Zero(t, settingsCount)
}
}
func TestUpdateMCPSettings_DisablePersists(t *testing.T) {
t.Parallel()
database := newTestDB(t)
body := `{"mcp_enabled":false}`
req := httptest.NewRequest(http.MethodPut, "/api/admin/settings/mcp", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
handlers.UpdateMCPSettings(database, nil)(w, req)
assert.Equal(t, http.StatusNoContent, w.Code)
var enabledSetting models.AppSetting
require.NoError(t, database.First(&enabledSetting, "key = ?", "mcp_enabled").Error)
assert.Equal(t, "false", enabledSetting.Value)
}
func TestUpdateMCPSettings_IgnoresLegacyPortField(t *testing.T) {
t.Parallel()
database := newTestDB(t)
body := `{"mcp_enabled":true,"mcp_port":80}`
req := httptest.NewRequest(http.MethodPut, "/api/admin/settings/mcp", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
handlers.UpdateMCPSettings(database, nil)(w, req)
assert.Equal(t, http.StatusNoContent, w.Code)
var portCount int64
require.NoError(t, database.Model(&models.AppSetting{}).Where("key = ?", "mcp_port").Count(&portCount).Error)
assert.Zero(t, portCount)
}
func TestUpdateMCPSettings_PreservesExistingToken(t *testing.T) {
t.Parallel()
database := newTestDB(t)
require.NoError(t, database.Create(&models.AppSetting{Key: "mcp_auth_token", Value: "existing-token-value-abcdef1234"}).Error)
body := `{"mcp_enabled":true}`
req := httptest.NewRequest(http.MethodPut, "/api/admin/settings/mcp", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
handlers.UpdateMCPSettings(database, nil)(w, req)
assert.Equal(t, http.StatusNoContent, w.Code)
var tokenSetting models.AppSetting
require.NoError(t, database.First(&tokenSetting, "key = ?", "mcp_auth_token").Error)
assert.Equal(t, "existing-token-value-abcdef1234", tokenSetting.Value)
}
func TestUpdateMCPSettings_AcknowledgeMigrationWarning(t *testing.T) {
t.Parallel()
database := newTestDB(t)
require.NoError(t, database.Create(&models.AppSetting{Key: "mcp_port", Value: "13001"}).Error)
require.NoError(t, database.Create(&models.AppSetting{Key: "mcp_enabled", Value: "true"}).Error)
require.NoError(t, database.Create(&models.AppSetting{Key: "mcp_auth_token", Value: "existing-token-value-abcdef1234"}).Error)
body := `{"acknowledge_mcp_migration_warning":true}`
req := httptest.NewRequest(http.MethodPut, "/api/admin/settings/mcp", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
handlers.UpdateMCPSettings(database, nil)(w, req)
assert.Equal(t, http.StatusNoContent, w.Code)
var enabledSetting models.AppSetting
require.NoError(t, database.First(&enabledSetting, "key = ?", "mcp_enabled").Error)
assert.Equal(t, "true", enabledSetting.Value)
var configResp map[string]any
configReq := httptest.NewRequest(http.MethodGet, "/api/admin/config", nil)
configReq = configReq.WithContext(context.WithValue(configReq.Context(), auth.ClaimsKey, &auth.Claims{
UserID: "admin1",
IsAdmin: true,
}))
configW := httptest.NewRecorder()
handlers.AdminConfig(database, "", nil)(configW, configReq)
assert.Equal(t, http.StatusOK, configW.Code)
require.NoError(t, json.NewDecoder(configW.Body).Decode(&configResp))
assert.Equal(t, false, configResp["mcp_migration_warning"])
}
func TestUpdateMCPSettings_AcknowledgeMigrationWarningWithoutMCPStateDoesNotCreateSettings(t *testing.T) {
t.Parallel()
database := newTestDB(t)
require.NoError(t, database.Create(&models.AppSetting{Key: "mcp_port", Value: "13001"}).Error)
body := `{"acknowledge_mcp_migration_warning":true}`
req := httptest.NewRequest(http.MethodPut, "/api/admin/settings/mcp", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
handlers.UpdateMCPSettings(database, nil)(w, req)
assert.Equal(t, http.StatusNoContent, w.Code)
var enabledCount int64
require.NoError(t, database.Model(&models.AppSetting{}).Where("key = ?", "mcp_enabled").Count(&enabledCount).Error)
assert.Zero(t, enabledCount)
var tokenCount int64
require.NoError(t, database.Model(&models.AppSetting{}).Where("key = ?", "mcp_auth_token").Count(&tokenCount).Error)
assert.Zero(t, tokenCount)
var ackSetting models.AppSetting
require.NoError(t, database.First(&ackSetting, "key = ?", "mcp_migration_warning_acknowledged").Error)
assert.Equal(t, "true", ackSetting.Value)
}
func TestRegenerateMCPToken_ReturnsNewSuffix(t *testing.T) {
t.Parallel()
database := newTestDB(t)
// Seed initial token and enabled state
require.NoError(t, database.Create(&models.AppSetting{Key: "mcp_auth_token", Value: "old-token-12345678"}).Error)
require.NoError(t, database.Create(&models.AppSetting{Key: "mcp_enabled", Value: "false"}).Error)
req := httptest.NewRequest(http.MethodPost, "/api/admin/settings/mcp/regenerate-token", nil)
w := httptest.NewRecorder()
handlers.RegenerateMCPToken(database, nil)(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var resp map[string]string
require.NoError(t, json.NewDecoder(w.Body).Decode(&resp))
suffix, ok := resp["mcp_auth_token_suffix"]
require.True(t, ok, "response should have mcp_auth_token_suffix")
assert.Len(t, suffix, 8)
var newTokenSetting models.AppSetting
require.NoError(t, database.First(&newTokenSetting, "key = ?", "mcp_auth_token").Error)
assert.NotEqual(t, "old-token-12345678", newTokenSetting.Value)
assert.True(t, strings.HasSuffix(newTokenSetting.Value, suffix))
}