node_tree_test.go 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
  1. package service
  2. import (
  3. "testing"
  4. "github.com/mhsanaei/3x-ui/v3/database"
  5. "github.com/mhsanaei/3x-ui/v3/database/model"
  6. )
  7. // #4983: a transitive sub-node learned from a direct node must surface as its
  8. // own read-only entry nested under its parent, and per-GUID counts must split a
  9. // direct node's own inbounds from its sub-nodes'.
  10. func TestGetNodeTree_SurfacesTransitiveNodeNestedUnderParent(t *testing.T) {
  11. setupConflictDB(t)
  12. db := database.GetDB()
  13. svc := NodeService{}
  14. selfGuid, _ := (&SettingService{}).GetPanelGuid()
  15. if err := db.Create(&model.Node{
  16. Id: 1, Name: "Node2", Address: "10.0.0.2", Port: 2053,
  17. ApiToken: "t", Guid: "node2-guid", Status: "online",
  18. }).Error; err != nil {
  19. t.Fatalf("create node: %v", err)
  20. }
  21. // Node2's own inbound and a transitive inbound physically on Node3
  22. // (managed through Node2, so node_id = Node2 but origin = Node3).
  23. nid := 1
  24. if err := db.Create(&model.Inbound{Tag: "n1-own", Enable: true, Port: 443, Protocol: model.VLESS, Settings: `{"clients":[]}`, NodeID: &nid, OriginNodeGuid: "node2-guid"}).Error; err != nil {
  25. t.Fatalf("create own inbound: %v", err)
  26. }
  27. if err := db.Create(&model.Inbound{Tag: "n1-via", Enable: true, Port: 8443, Protocol: model.VLESS, Settings: `{"clients":[]}`, NodeID: &nid, OriginNodeGuid: "node3-guid"}).Error; err != nil {
  28. t.Fatalf("create transitive inbound: %v", err)
  29. }
  30. // The heartbeat learned that Node2 manages Node3.
  31. nodeDescendantsMu.Lock()
  32. nodeDescendantsCache[1] = []model.NodeSummary{{
  33. Guid: "node3-guid", ParentGuid: "node2-guid", Name: "Node3", Address: "10.0.0.3", Status: "online",
  34. }}
  35. nodeDescendantsMu.Unlock()
  36. t.Cleanup(func() {
  37. nodeDescendantsMu.Lock()
  38. nodeDescendantsCache = map[int][]model.NodeSummary{}
  39. nodeDescendantsMu.Unlock()
  40. })
  41. tree, err := svc.GetNodeTree()
  42. if err != nil {
  43. t.Fatalf("GetNodeTree: %v", err)
  44. }
  45. var node2, node3 *model.Node
  46. for _, n := range tree {
  47. switch n.Guid {
  48. case "node2-guid":
  49. node2 = n
  50. case "node3-guid":
  51. node3 = n
  52. }
  53. }
  54. if node2 == nil || node3 == nil {
  55. t.Fatalf("expected Node2 + transitive Node3, got %d nodes", len(tree))
  56. }
  57. if node2.ParentGuid != selfGuid {
  58. t.Errorf("Node2 parent = %q, want this panel's GUID %q", node2.ParentGuid, selfGuid)
  59. }
  60. if !node3.Transitive || node3.ParentGuid != "node2-guid" {
  61. t.Errorf("Node3 should be transitive under node2-guid, got transitive=%v parent=%q", node3.Transitive, node3.ParentGuid)
  62. }
  63. if node3.Id != 0 {
  64. t.Errorf("transitive node must be a read-only projection (Id 0), got Id=%d", node3.Id)
  65. }
  66. if node2.InboundCount != 1 {
  67. t.Errorf("Node2 should host only its own inbound, got InboundCount=%d", node2.InboundCount)
  68. }
  69. if node3.InboundCount != 1 {
  70. t.Errorf("transitive Node3 should host its 1 inbound, got %d", node3.InboundCount)
  71. }
  72. }