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)) }