diff --git a/internal/web/controller/xray_setting.go b/internal/web/controller/xray_setting.go index 9f1d006d5..958c16ba1 100644 --- a/internal/web/controller/xray_setting.go +++ b/internal/web/controller/xray_setting.go @@ -190,7 +190,7 @@ func (a *XraySettingController) warp(c *gin.Context) { a.XrayService.SetToNeedRestart() // Restart the auto-update clock so a scheduled rotation // doesn't fire right after this manual one. - _ = a.SettingService.SetWarpLastUpdate(time.Now().Unix()) + err = a.SettingService.SetWarpLastUpdate(time.Now().Unix()) } case "license": license := c.PostForm("license") @@ -202,7 +202,7 @@ func (a *XraySettingController) warp(c *gin.Context) { } else if err = a.SettingService.SetWarpUpdateInterval(interval); err == nil && interval > 0 { // Count the interval from now rather than from epoch 0, // otherwise the job would rotate on its next tick. - _ = a.SettingService.SetWarpLastUpdate(time.Now().Unix()) + err = a.SettingService.SetWarpLastUpdate(time.Now().Unix()) } } diff --git a/internal/web/controller/xray_setting_warp_test.go b/internal/web/controller/xray_setting_warp_test.go new file mode 100644 index 000000000..200a0f609 --- /dev/null +++ b/internal/web/controller/xray_setting_warp_test.go @@ -0,0 +1,71 @@ +package controller + +import ( + "errors" + "net/http" + "net/http/httptest" + "net/url" + "path/filepath" + "strings" + "testing" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" + + "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/model" + "github.com/mhsanaei/3x-ui/v3/internal/web/locale" +) + +func TestWarpIntervalReportsClockPersistenceFailure(t *testing.T) { + gin.SetMode(gin.TestMode) + dbDir := t.TempDir() + t.Setenv("XUI_DB_FOLDER", dbDir) + if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { + t.Fatalf("InitDB: %v", err) + } + t.Cleanup(func() { _ = database.CloseDB() }) + db := database.GetDB() + for _, setting := range []*model.Setting{ + {Key: "warpUpdateInterval", Value: "0"}, + {Key: "warpLastUpdate", Value: "0"}, + } { + if err := db.Create(setting).Error; err != nil { + t.Fatalf("seed %s: %v", setting.Key, err) + } + } + + const callback = "test:fail_warp_clock_update" + errInjected := errors.New("injected WARP clock persistence failure") + if err := db.Callback().Update().Before("gorm:update").Register(callback, func(tx *gorm.DB) { + setting, ok := tx.Statement.Model.(*model.Setting) + if ok && setting.Key == "warpLastUpdate" { + tx.AddError(errInjected) + } + }); err != nil { + t.Fatalf("register update callback: %v", err) + } + t.Cleanup(func() { + if err := db.Callback().Update().Remove(callback); err != nil { + t.Errorf("remove update callback: %v", err) + } + }) + + engine := gin.New() + engine.Use(func(c *gin.Context) { + c.Set("I18n", func(_ locale.I18nType, key string, _ ...string) string { return key }) + c.Next() + }) + NewXraySettingController(engine.Group("/panel/api")) + form := url.Values{"interval": {"7"}} + req := httptest.NewRequest(http.MethodPost, "/panel/api/xray/warp/interval", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + engine.ServeHTTP(w, req) + if !strings.Contains(w.Body.String(), `"success":false`) { + t.Fatalf("interval update reported success after clock persistence failure: %s", w.Body.String()) + } + if !strings.Contains(w.Body.String(), errInjected.Error()) { + t.Fatalf("response omitted clock persistence error: %s", w.Body.String()) + } +} diff --git a/internal/web/job/warp_ip_job.go b/internal/web/job/warp_ip_job.go index 7df0bb385..7355ae040 100644 --- a/internal/web/job/warp_ip_job.go +++ b/internal/web/job/warp_ip_job.go @@ -28,13 +28,19 @@ func (j *WarpIpJob) Run() { return } - lastUpdate, _ := j.settingService.GetWarpLastUpdate() + lastUpdate, err := j.settingService.GetWarpLastUpdate() + if err != nil { + logger.Warning("Failed to read scheduled WARP IP update time: ", err) + return + } now := time.Now().Unix() // First run after the feature is enabled (e.g. interval set via direct // DB edit): establish a baseline instead of rotating immediately. if lastUpdate == 0 { - _ = j.settingService.SetWarpLastUpdate(now) + if err := j.settingService.SetWarpLastUpdate(now); err != nil { + logger.Warning("Failed to establish scheduled WARP IP update time: ", err) + } return } @@ -46,7 +52,9 @@ func (j *WarpIpJob) Run() { return } - _ = j.settingService.SetWarpLastUpdate(now) + if err := j.settingService.SetWarpLastUpdate(now); err != nil { + logger.Warning("WARP IP changed but the next-update time was not saved: ", err) + } j.xrayService.SetToNeedRestart() logger.Info("Successfully updated WARP IP and scheduled Xray restart") } diff --git a/internal/web/job/warp_ip_job_test.go b/internal/web/job/warp_ip_job_test.go new file mode 100644 index 000000000..7fc6e5e28 --- /dev/null +++ b/internal/web/job/warp_ip_job_test.go @@ -0,0 +1,29 @@ +package job + +import ( + "testing" + + "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/model" + "github.com/mhsanaei/3x-ui/v3/internal/web/service" +) + +func TestWarpIpJobInitializesMissingLastUpdate(t *testing.T) { + setupIntegrationDB(t) + + settings := service.SettingService{} + if err := settings.SetWarpUpdateInterval(1); err != nil { + t.Fatalf("enable scheduled WARP rotation: %v", err) + } + + job := &WarpIpJob{settingService: settings} + job.Run() + + var stored model.Setting + if err := database.GetDB().Where("key = ?", "warpLastUpdate").First(&stored).Error; err != nil { + t.Fatalf("scheduled WARP rotation did not establish its baseline: %v", err) + } + if stored.Value == "" || stored.Value == "0" { + t.Fatalf("warpLastUpdate = %q, want a non-zero baseline", stored.Value) + } +} diff --git a/internal/web/service/setting.go b/internal/web/service/setting.go index 61366522b..1cedef037 100644 --- a/internal/web/service/setting.go +++ b/internal/web/service/setting.go @@ -391,11 +391,17 @@ func (s *SettingService) setInt(key string, value int) error { } func (s *SettingService) GetWarpLastUpdate() (int64, error) { - val, err := s.getString("warpLastUpdate") - if err != nil || val == "" { + setting, err := s.getSetting("warpLastUpdate") + if database.IsNotFound(err) { + return 0, nil + } + if err != nil { return 0, err } - return strconv.ParseInt(val, 10, 64) + if setting.Value == "" { + return 0, nil + } + return strconv.ParseInt(setting.Value, 10, 64) } func (s *SettingService) SetWarpLastUpdate(val int64) error {