node_shared_guid_test.go 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. package service
  2. import (
  3. "fmt"
  4. "testing"
  5. "time"
  6. "github.com/mhsanaei/3x-ui/v3/internal/database"
  7. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  8. )
  9. // Cloned node servers ship an identical panelGuid in their copied settings, and
  10. // a node cloned from the master shares the master's own GUID. effectiveNodeGuid
  11. // must keep each physical node in its own attribution bucket by falling back to
  12. // the node-unique id for both collision kinds, while leaving a uniquely-named
  13. // node on its real GUID and never folding transitive (Id 0) nodes.
  14. func TestEffectiveNodeGuid_DisambiguatesAmbiguousGuids(t *testing.T) {
  15. nodes := []*model.Node{
  16. {Id: 1, Guid: "dup"},
  17. {Id: 2, Guid: "dup"},
  18. {Id: 3, Guid: "uniq"},
  19. {Id: 4, Guid: ""},
  20. {Id: 5, Guid: "master"},
  21. {Id: 0, Guid: "transitive"},
  22. }
  23. ambiguous := ambiguousNodeGuids(nodes, "master")
  24. if _, ok := ambiguous["dup"]; !ok {
  25. t.Fatalf("dup must be flagged ambiguous, got %v", ambiguous)
  26. }
  27. if _, ok := ambiguous["master"]; !ok {
  28. t.Fatalf("a node sharing the master GUID must be flagged, got %v", ambiguous)
  29. }
  30. if _, ok := ambiguous["uniq"]; ok {
  31. t.Fatalf("uniq must not be flagged, got %v", ambiguous)
  32. }
  33. if _, ok := ambiguous["transitive"]; ok {
  34. t.Fatalf("transitive (Id 0) must not count, got %v", ambiguous)
  35. }
  36. cases := map[*model.Node]string{
  37. nodes[0]: "node:1",
  38. nodes[1]: "node:2",
  39. nodes[2]: "uniq",
  40. nodes[3]: "node:4",
  41. nodes[4]: "node:5",
  42. nodes[5]: "transitive",
  43. }
  44. for n, want := range cases {
  45. if got := effectiveNodeGuid(n, ambiguous); got != want {
  46. t.Errorf("effectiveNodeGuid(Id=%d, Guid=%q) = %q, want %q", n.Id, n.Guid, got, want)
  47. }
  48. }
  49. }
  50. // effectiveNodeKey (the no-preloaded-list variant used by the write paths) must
  51. // agree with the slice helper: fall back to the node-unique id when a GUID is
  52. // shared with another node or with the master, else keep the real GUID.
  53. func TestEffectiveNodeKey_FallsBackOnCollision(t *testing.T) {
  54. setupConflictDB(t)
  55. db := database.GetDB()
  56. selfGuid, _ := (&SettingService{}).GetPanelGuid()
  57. if selfGuid == "" {
  58. t.Fatal("expected a panel guid")
  59. }
  60. mk := func(id int, name, guid string) *model.Node {
  61. n := &model.Node{Id: id, Name: name, Address: fmt.Sprintf("10.0.0.%d", id), Port: 2053, ApiToken: "t", Guid: guid, Status: "online"}
  62. if err := db.Create(n).Error; err != nil {
  63. t.Fatalf("create %s: %v", name, err)
  64. }
  65. return n
  66. }
  67. dupA := mk(1, "a", "shared")
  68. mk(2, "b", "shared")
  69. uniq := mk(3, "c", "solo")
  70. masterClone := mk(4, "d", selfGuid)
  71. if got := effectiveNodeKey(dupA); got != "node:1" {
  72. t.Errorf("node-node collision: got %q, want node:1", got)
  73. }
  74. if got := effectiveNodeKey(uniq); got != "solo" {
  75. t.Errorf("unique node: got %q, want solo", got)
  76. }
  77. if got := effectiveNodeKey(masterClone); got != "node:4" {
  78. t.Errorf("master collision: got %q, want node:4", got)
  79. }
  80. }
  81. // recountByGuid must split per-node counts even when two direct nodes share a
  82. // GUID and their inbounds still carry that shared GUID as origin (pre-backfill).
  83. func TestRecountByGuid_SplitsClonedNodesWithSharedGuid(t *testing.T) {
  84. setupConflictDB(t)
  85. db := database.GetDB()
  86. svc := NodeService{}
  87. selfGuid, _ := (&SettingService{}).GetPanelGuid()
  88. n1 := &model.Node{Id: 1, Name: "A", Address: "10.0.0.1", Port: 2053, ApiToken: "t", Guid: "dup", Status: "online"}
  89. n2 := &model.Node{Id: 2, Name: "B", Address: "10.0.0.2", Port: 2053, ApiToken: "t", Guid: "dup", Status: "online"}
  90. n3 := &model.Node{Id: 3, Name: "C", Address: "10.0.0.3", Port: 2053, ApiToken: "t", Guid: "uniq", Status: "online"}
  91. for _, n := range []*model.Node{n1, n2, n3} {
  92. if err := db.Create(n).Error; err != nil {
  93. t.Fatalf("create node %s: %v", n.Name, err)
  94. }
  95. }
  96. id1, id2, id3 := 1, 2, 3
  97. inbounds := []*model.Inbound{
  98. {Tag: "a", Port: 1001, Protocol: model.VLESS, Settings: `{"clients":[]}`, Enable: true, NodeID: &id1, OriginNodeGuid: "dup"},
  99. {Tag: "b", Port: 1002, Protocol: model.VLESS, Settings: `{"clients":[]}`, Enable: true, NodeID: &id2, OriginNodeGuid: "dup"},
  100. {Tag: "c", Port: 1003, Protocol: model.VLESS, Settings: `{"clients":[]}`, Enable: true, NodeID: &id3, OriginNodeGuid: "uniq"},
  101. }
  102. for _, ib := range inbounds {
  103. if err := db.Create(ib).Error; err != nil {
  104. t.Fatalf("create inbound %s: %v", ib.Tag, err)
  105. }
  106. }
  107. nodes := []*model.Node{n1, n2, n3}
  108. svc.recountByGuid(nodes, selfGuid)
  109. if n1.InboundCount != 1 || n2.InboundCount != 1 {
  110. t.Errorf("cloned nodes must not share inbound counts: n1=%d n2=%d, want 1,1", n1.InboundCount, n2.InboundCount)
  111. }
  112. if n3.InboundCount != 1 {
  113. t.Errorf("unique node InboundCount = %d, want 1", n3.InboundCount)
  114. }
  115. }
  116. // A cloned node's IP-attribution subtree must be stored under its node-unique
  117. // key, so a second clone sharing the GUID can't overwrite it in node_client_ips.
  118. func TestMergeClientIpsByGuid_RemapsClonedNodeSubtree(t *testing.T) {
  119. setupClientIpTestDB(t)
  120. db := database.GetDB()
  121. svc := &InboundService{}
  122. now := time.Now().Unix()
  123. n1 := &model.Node{Id: 1, Name: "A", Address: "10.0.0.1", Port: 2053, ApiToken: "t", Guid: "dup", Status: "online"}
  124. n2 := &model.Node{Id: 2, Name: "B", Address: "10.0.0.2", Port: 2053, ApiToken: "t", Guid: "dup", Status: "online"}
  125. for _, n := range []*model.Node{n1, n2} {
  126. if err := db.Create(n).Error; err != nil {
  127. t.Fatalf("create node: %v", err)
  128. }
  129. }
  130. if err := svc.MergeClientIpsByGuid(n1, map[string]map[string][]model.ClientIpEntry{
  131. "dup": {"u@x": {{IP: "1.1.1.1", Timestamp: now}}},
  132. }); err != nil {
  133. t.Fatalf("merge n1: %v", err)
  134. }
  135. var rows []model.NodeClientIp
  136. if err := db.Find(&rows).Error; err != nil {
  137. t.Fatalf("load rows: %v", err)
  138. }
  139. if len(rows) != 1 {
  140. t.Fatalf("want 1 attribution row, got %d", len(rows))
  141. }
  142. if rows[0].NodeGuid != "node:1" {
  143. t.Errorf("cloned node IPs must be stored under node-unique key, got %q", rows[0].NodeGuid)
  144. }
  145. }