node_heartbeat_descendants_test.go 3.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. package job
  2. import (
  3. "net/http"
  4. "net/http/httptest"
  5. "path/filepath"
  6. "strconv"
  7. "strings"
  8. "testing"
  9. "github.com/op/go-logging"
  10. "github.com/mhsanaei/3x-ui/v3/internal/database"
  11. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  12. xuilogger "github.com/mhsanaei/3x-ui/v3/internal/logger"
  13. "github.com/mhsanaei/3x-ui/v3/internal/web/runtime"
  14. "github.com/mhsanaei/3x-ui/v3/internal/web/service"
  15. )
  16. func transitiveGuids(t *testing.T) []string {
  17. t.Helper()
  18. tree, err := (&service.NodeService{}).GetNodeTree()
  19. if err != nil {
  20. t.Fatalf("GetNodeTree: %v", err)
  21. }
  22. var out []string
  23. for _, n := range tree {
  24. if n.Transitive {
  25. out = append(out, n.Guid)
  26. }
  27. }
  28. return out
  29. }
  30. // The heartbeat skips a disabled node and never sees a deleted one, so the
  31. // sub-nodes it had learned from them stayed on the Nodes page for good.
  32. func TestHeartbeatDropsSubNodesOfNodesItNoLongerProbes(t *testing.T) {
  33. cases := []struct {
  34. name string
  35. retire func(t *testing.T, nodeID int)
  36. }{
  37. {"disabled", func(t *testing.T, nodeID int) {
  38. if err := (&service.NodeService{}).SetEnable(nodeID, false); err != nil {
  39. t.Fatalf("SetEnable: %v", err)
  40. }
  41. }},
  42. {"deleted", func(t *testing.T, nodeID int) {
  43. if err := (&service.NodeService{}).Delete(nodeID); err != nil {
  44. t.Fatalf("Delete: %v", err)
  45. }
  46. }},
  47. }
  48. for _, tc := range cases {
  49. t.Run(tc.name, func(t *testing.T) {
  50. xuilogger.InitLogger(logging.ERROR)
  51. if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil {
  52. t.Fatalf("InitDB: %v", err)
  53. }
  54. t.Cleanup(func() { _ = database.CloseDB() })
  55. runtime.SetManager(runtime.NewManager(runtime.LocalDeps{APIPort: func() int { return 0 }, SetNeedRestart: func() {}}))
  56. t.Cleanup(func() { runtime.SetManager(nil) })
  57. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  58. w.Header().Set("Content-Type", "application/json")
  59. switch {
  60. case strings.HasSuffix(r.URL.Path, "server/status"):
  61. _, _ = w.Write([]byte(`{"success":true,"obj":{"panelGuid":"direct-guid","xray":{"state":"running"}}}`))
  62. case strings.HasSuffix(r.URL.Path, "server/descendants"):
  63. _, _ = w.Write([]byte(`{"success":true,"obj":[{"guid":"sub-guid","parentGuid":"direct-guid","name":"sub","status":"online"}]}`))
  64. default:
  65. _, _ = w.Write([]byte(`{"success":true}`))
  66. }
  67. }))
  68. t.Cleanup(srv.Close)
  69. host, port, _ := strings.Cut(strings.TrimPrefix(srv.URL, "http://"), ":")
  70. portNum, _ := strconv.Atoi(port)
  71. node := &model.Node{
  72. Name: "direct", Scheme: "http", Address: host, Port: portNum, BasePath: "/", ApiToken: "tok",
  73. Enable: true, Status: "unknown", AllowPrivateAddress: true, TlsVerifyMode: "verify",
  74. }
  75. if err := database.GetDB().Create(node).Error; err != nil {
  76. t.Fatalf("create node: %v", err)
  77. }
  78. hb := NewNodeHeartbeatJob()
  79. hb.Run()
  80. if got := transitiveGuids(t); len(got) != 1 || got[0] != "sub-guid" {
  81. t.Fatalf("sub-nodes after first heartbeat = %v, want [sub-guid]", got)
  82. }
  83. tc.retire(t, node.Id)
  84. hb.Run()
  85. if got := transitiveGuids(t); len(got) != 0 {
  86. t.Fatalf("sub-nodes after the node was %s = %v, want none", tc.name, got)
  87. }
  88. })
  89. }
  90. }