package handler import ( "bytes" "encoding/json" "net/http/httptest" "os" "path/filepath" "testing" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" "nex/backend/internal/config" ) func init() { gin.SetMode(gin.TestMode) } func createTestConfig(t *testing.T) (*config.Config, string) { t.Helper() dir := t.TempDir() configPath := filepath.Join(dir, "config.yaml") cfg := config.DefaultConfig() cfg.Database.Path = filepath.Join(dir, "test.db") cfg.Log.Path = filepath.Join(dir, "log") data, err := yaml.Marshal(cfg) require.NoError(t, err) require.NoError(t, os.WriteFile(configPath, data, 0o600)) return cfg, configPath } func TestSettingsHandler_GetStartupSettings_Desktop(t *testing.T) { cfg, configPath := createTestConfig(t) h := NewSettingsHandler(cfg, "desktop", true, configPath) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("GET", "/api/settings/startup", nil) h.GetStartupSettings(c) assert.Equal(t, 200, w.Code) var resp startupSettingsResponse require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) assert.Equal(t, "desktop", resp.Mode) assert.True(t, resp.Editable) assert.True(t, resp.RestartRequired) assert.Equal(t, configPath, resp.ConfigPath) assert.Equal(t, cfg.Server.Port, resp.Config.Server.Port) assert.Equal(t, "30s", resp.Config.Server.ReadTimeout) assert.Equal(t, cfg.Database.Driver, resp.Config.Database.Driver) } func TestSettingsHandler_GetStartupSettings_Server(t *testing.T) { cfg, configPath := createTestConfig(t) h := NewSettingsHandler(cfg, "server", false, configPath) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("GET", "/api/settings/startup", nil) h.GetStartupSettings(c) assert.Equal(t, 200, w.Code) var resp startupSettingsResponse require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) assert.Equal(t, "server", resp.Mode) assert.False(t, resp.Editable) assert.False(t, resp.RestartRequired) assert.Equal(t, configPath, resp.ConfigPath) } func TestSettingsHandler_SaveStartupSettings_Desktop(t *testing.T) { cfg, configPath := createTestConfig(t) h := NewSettingsHandler(cfg, "desktop", true, configPath) newPort := 9999 body, _ := json.Marshal(map[string]interface{}{ "config": map[string]interface{}{ "server": map[string]interface{}{ "port": newPort, "read_timeout": "30s", "write_timeout": "30s", }, "database": map[string]interface{}{ "driver": "sqlite", "path": filepath.Join(t.TempDir(), "new.db"), "port": 3306, "dbname": "nex", "max_idle_conns": 10, "max_open_conns": 100, "conn_max_lifetime": "1h", }, "log": map[string]interface{}{ "level": "info", "path": filepath.Join(t.TempDir(), "log"), "max_size": 100, "max_backups": 10, "max_age": 30, "compress": true, }, }, }) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("PUT", "/api/settings/startup", bytes.NewReader(body)) c.Request.Header.Set("Content-Type", "application/json") h.SaveStartupSettings(c) assert.Equal(t, 200, w.Code) var resp startupSettingsResponse require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) assert.Equal(t, newPort, resp.Config.Server.Port) assert.True(t, resp.Editable) assert.True(t, resp.RestartRequired) savedCfg, err := config.LoadDesktopConfigAtPath(configPath) require.NoError(t, err) assert.Equal(t, newPort, savedCfg.Server.Port) } func TestSettingsHandler_SaveStartupSettings_Server_Forbidden(t *testing.T) { cfg, configPath := createTestConfig(t) h := NewSettingsHandler(cfg, "server", false, configPath) body, _ := json.Marshal(map[string]interface{}{ "config": map[string]interface{}{ "server": map[string]interface{}{ "port": 9999, "read_timeout": "30s", "write_timeout": "30s", }, "database": map[string]interface{}{ "driver": "sqlite", "path": "/tmp/test.db", "port": 3306, "dbname": "nex", "max_idle_conns": 10, "max_open_conns": 100, "conn_max_lifetime": "1h", }, "log": map[string]interface{}{ "level": "info", "path": "/tmp/log", "max_size": 100, "max_backups": 10, "max_age": 30, "compress": true, }, }, }) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("PUT", "/api/settings/startup", bytes.NewReader(body)) c.Request.Header.Set("Content-Type", "application/json") h.SaveStartupSettings(c) assert.Equal(t, 403, w.Code) assert.Contains(t, w.Body.String(), "不允许保存") } func TestSettingsHandler_SaveStartupSettings_Desktop_InvalidConfig(t *testing.T) { cfg, configPath := createTestConfig(t) h := NewSettingsHandler(cfg, "desktop", true, configPath) originalData, err := os.ReadFile(configPath) require.NoError(t, err) body, _ := json.Marshal(map[string]interface{}{ "config": map[string]interface{}{ "server": map[string]interface{}{ "port": 0, "read_timeout": "30s", "write_timeout": "30s", }, "database": map[string]interface{}{ "driver": "sqlite", "path": "/tmp/test.db", "port": 3306, "dbname": "nex", "max_idle_conns": 10, "max_open_conns": 100, "conn_max_lifetime": "1h", }, "log": map[string]interface{}{ "level": "info", "path": "/tmp/log", "max_size": 100, "max_backups": 10, "max_age": 30, "compress": true, }, }, }) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("PUT", "/api/settings/startup", bytes.NewReader(body)) c.Request.Header.Set("Content-Type", "application/json") h.SaveStartupSettings(c) assert.Equal(t, 400, w.Code) currentData, err := os.ReadFile(configPath) require.NoError(t, err) assert.Equal(t, originalData, currentData) } func TestSettingsHandler_SaveStartupSettings_Desktop_InvalidDuration(t *testing.T) { cfg, configPath := createTestConfig(t) h := NewSettingsHandler(cfg, "desktop", true, configPath) body, _ := json.Marshal(map[string]interface{}{ "config": map[string]interface{}{ "server": map[string]interface{}{ "port": 9826, "read_timeout": "not-a-duration", "write_timeout": "30s", }, "database": map[string]interface{}{ "driver": "sqlite", "path": "/tmp/test.db", "port": 3306, "dbname": "nex", "max_idle_conns": 10, "max_open_conns": 100, "conn_max_lifetime": "1h", }, "log": map[string]interface{}{ "level": "info", "path": "/tmp/log", "max_size": 100, "max_backups": 10, "max_age": 30, "compress": true, }, }, }) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("PUT", "/api/settings/startup", bytes.NewReader(body)) c.Request.Header.Set("Content-Type", "application/json") h.SaveStartupSettings(c) assert.Equal(t, 400, w.Code) assert.Contains(t, w.Body.String(), "read_timeout") } func TestSettingsHandler_SaveStartupSettings_Desktop_CreatesConfigFile(t *testing.T) { cfg := config.DefaultConfig() dir := t.TempDir() configPath := filepath.Join(dir, "nex", "config.yaml") _, err := os.Stat(configPath) assert.True(t, os.IsNotExist(err)) h := NewSettingsHandler(cfg, "desktop", true, configPath) body, _ := json.Marshal(map[string]interface{}{ "config": map[string]interface{}{ "server": map[string]interface{}{ "port": 9826, "read_timeout": "30s", "write_timeout": "30s", }, "database": map[string]interface{}{ "driver": "sqlite", "path": filepath.Join(dir, "test.db"), "port": 3306, "dbname": "nex", "max_idle_conns": 10, "max_open_conns": 100, "conn_max_lifetime": "1h", }, "log": map[string]interface{}{ "level": "info", "path": filepath.Join(dir, "log"), "max_size": 100, "max_backups": 10, "max_age": 30, "compress": true, }, }, }) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("PUT", "/api/settings/startup", bytes.NewReader(body)) c.Request.Header.Set("Content-Type", "application/json") h.SaveStartupSettings(c) assert.Equal(t, 200, w.Code) _, err = os.Stat(configPath) assert.NoError(t, err) } func TestSettingsHandler_SaveStartupSettings_Desktop_PasswordIncluded(t *testing.T) { cfg, configPath := createTestConfig(t) h := NewSettingsHandler(cfg, "desktop", true, configPath) body, _ := json.Marshal(map[string]interface{}{ "config": map[string]interface{}{ "server": map[string]interface{}{ "port": 9826, "read_timeout": "30s", "write_timeout": "30s", }, "database": map[string]interface{}{ "driver": "mysql", "host": "localhost", "port": 3306, "user": "root", "password": "secret123", "dbname": "nex", "path": "", "max_idle_conns": 10, "max_open_conns": 100, "conn_max_lifetime": "1h", }, "log": map[string]interface{}{ "level": "info", "path": filepath.Join(t.TempDir(), "log"), "max_size": 100, "max_backups": 10, "max_age": 30, "compress": true, }, }, }) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("PUT", "/api/settings/startup", bytes.NewReader(body)) c.Request.Header.Set("Content-Type", "application/json") h.SaveStartupSettings(c) assert.Equal(t, 200, w.Code) var resp startupSettingsResponse require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) assert.Equal(t, "secret123", resp.Config.Database.Password) } func TestSettingsHandler_GetStartupSettings_DesktopReadsConfigFile(t *testing.T) { cfg, configPath := createTestConfig(t) savedCfg := config.DefaultConfig() savedCfg.Server.Port = 5555 data, err := yaml.Marshal(savedCfg) require.NoError(t, err) require.NoError(t, os.WriteFile(configPath, data, 0o600)) h := NewSettingsHandler(cfg, "desktop", true, configPath) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("GET", "/api/settings/startup", nil) h.GetStartupSettings(c) assert.Equal(t, 200, w.Code) var resp startupSettingsResponse require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) assert.Equal(t, 5555, resp.Config.Server.Port) } func TestSettingsHandler_GetStartupSettings_ServerReturnsRuntime(t *testing.T) { cfg, configPath := createTestConfig(t) cfg.Server.Port = 7777 h := NewSettingsHandler(cfg, "server", false, configPath) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("GET", "/api/settings/startup", nil) h.GetStartupSettings(c) assert.Equal(t, 200, w.Code) var resp startupSettingsResponse require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) assert.Equal(t, 7777, resp.Config.Server.Port) } func TestSettingsHandler_SaveStartupSettings_InvalidJSON(t *testing.T) { cfg, configPath := createTestConfig(t) h := NewSettingsHandler(cfg, "desktop", true, configPath) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("PUT", "/api/settings/startup", bytes.NewReader([]byte("{invalid"))) c.Request.Header.Set("Content-Type", "application/json") h.SaveStartupSettings(c) assert.Equal(t, 400, w.Code) }