Ctrl K
ComunidadGuías…

server/internal/handlers/admin_config_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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))
}