Forráskód Böngészése

fix(nodes): report probe heartbeat persistence failures (#6207)

Co-authored-by: n0ctal <[email protected]>
n0ctal 13 órája
szülő
commit
4a5f6771b3

+ 4 - 1
internal/web/controller/node.go

@@ -314,7 +314,10 @@ func (a *NodeController) probe(c *gin.Context) {
 	} else {
 		patch.Status = "online"
 	}
-	_ = a.nodeService.UpdateHeartbeat(id, patch)
+	if err := a.nodeService.UpdateHeartbeat(id, patch); err != nil {
+		jsonMsg(c, I18nWeb(c, "pages.nodes.toasts.test"), err)
+		return
+	}
 	jsonObj(c, patch.ToUI(probeErr == nil), nil)
 }
 

+ 49 - 0
internal/web/controller/node_credentials_writeonly_test.go

@@ -2,6 +2,7 @@ package controller
 
 import (
 	"encoding/json"
+	"errors"
 	"net"
 	"net/http"
 	"net/http/httptest"
@@ -12,6 +13,8 @@ import (
 
 	"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"
@@ -66,6 +69,52 @@ func TestNodeControllerResponsesDoNotLeakApiToken(t *testing.T) {
 	}
 }
 
+func TestNodeControllerProbeReportsHeartbeatPersistenceFailure(t *testing.T) {
+	engine := newNodeCredentialTestEngine(t)
+	remote := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Content-Type", "application/json")
+		_, _ = w.Write([]byte(`{"success":true,"obj":{"cpu":1,"mem":{"current":1,"total":2},"xray":{"version":"1","state":"running"},"panelVersion":"v3.6.0","panelGuid":"guid","uptime":7,"netIO":{"up":3,"down":4}}}`))
+	}))
+	defer remote.Close()
+	host, portString, err := net.SplitHostPort(strings.TrimPrefix(remote.URL, "http://"))
+	if err != nil {
+		t.Fatalf("split remote addr: %v", err)
+	}
+	port, err := strconv.Atoi(portString)
+	if err != nil {
+		t.Fatalf("parse remote port: %v", err)
+	}
+	node := &model.Node{Scheme: "http", Address: host, Port: port, BasePath: "/", Enable: true, AllowPrivateAddress: true}
+	if err := database.GetDB().Create(node).Error; err != nil {
+		t.Fatalf("seed node: %v", err)
+	}
+
+	db := database.GetDB()
+	const callback = "test:fail_node_heartbeat_update"
+	errInjected := errors.New("injected heartbeat persistence failure")
+	if err := db.Callback().Update().Before("gorm:update").Register(callback, func(tx *gorm.DB) {
+		if tx.Statement != nil && tx.Statement.Table == "nodes" {
+			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)
+		}
+	})
+
+	w := httptest.NewRecorder()
+	engine.ServeHTTP(w, httptest.NewRequest(http.MethodPost, "/panel/api/nodes/probe/"+strconv.Itoa(node.Id), nil))
+	if !strings.Contains(w.Body.String(), `"success":false`) {
+		t.Fatalf("probe reported success despite heartbeat persistence failure: %s", w.Body.String())
+	}
+	if !strings.Contains(w.Body.String(), errInjected.Error()) {
+		t.Fatalf("probe response omitted persistence error: %s", w.Body.String())
+	}
+}
+
 func TestNodeControllerAddAcceptsTokenButReturnsView(t *testing.T) {
 	engine := newNodeCredentialTestEngine(t)