online_test.go 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. package xray
  2. import (
  3. "slices"
  4. "testing"
  5. )
  6. func newOnlineTestProcess() *Process {
  7. return &Process{newProcess(nil)}
  8. }
  9. func assertSameSet(t *testing.T, label string, got, want []string) {
  10. t.Helper()
  11. g := append([]string(nil), got...)
  12. w := append([]string(nil), want...)
  13. slices.Sort(g)
  14. slices.Sort(w)
  15. if !slices.Equal(g, w) {
  16. t.Errorf("%s = %v, want %v", label, got, want)
  17. }
  18. }
  19. // TestMergedNodeTreesScopesPerGuid pins #4983/#4809: each node's clients stay
  20. // under that node's GUID, so a client on one node is never attributed to
  21. // another — and a sub-node's clients (reported under their own GUID inside a
  22. // parent's tree) compose upward without collapsing onto the parent.
  23. func TestMergedNodeTreesScopesPerGuid(t *testing.T) {
  24. p := newOnlineTestProcess()
  25. // Node A (direct) reports its own clients plus sub-node B's tree.
  26. p.SetNodeOnlineTree(1, map[string][]string{
  27. "guid-a": {"user1", "user2"},
  28. "guid-b": {"user3"}, // B is behind A; still keyed by B's own GUID
  29. })
  30. p.SetNodeOnlineTree(2, map[string][]string{
  31. "guid-c": {"user4"},
  32. })
  33. merged := p.GetMergedNodeTrees()
  34. assertSameSet(t, "guid-a", merged["guid-a"], []string{"user1", "user2"})
  35. assertSameSet(t, "guid-b", merged["guid-b"], []string{"user3"})
  36. assertSameSet(t, "guid-c", merged["guid-c"], []string{"user4"})
  37. if slices.Contains(merged["guid-a"], "user3") {
  38. t.Errorf("user3 (on sub-node B) leaked onto node A: %v", merged["guid-a"])
  39. }
  40. }
  41. // TestMergedNodeTreesOmitsEmpty keeps the payload small: empty GUID sets don't
  42. // appear as keys.
  43. func TestMergedNodeTreesOmitsEmpty(t *testing.T) {
  44. p := newOnlineTestProcess()
  45. p.SetNodeOnlineTree(1, map[string][]string{
  46. "guid-a": {"user1"},
  47. "guid-x": {},
  48. })
  49. if _, ok := p.GetMergedNodeTrees()["guid-x"]; ok {
  50. t.Errorf("empty GUID set should be omitted: %v", p.GetMergedNodeTrees())
  51. }
  52. }
  53. // TestGetOnlineClientsUnionDedupes confirms the flat union (client-centric /
  54. // total-count views) merges local + every node and dedupes.
  55. func TestGetOnlineClientsUnionDedupes(t *testing.T) {
  56. p := newOnlineTestProcess()
  57. p.RefreshLocalOnline([]string{"user1"}, nil, 1000, 20000)
  58. p.SetNodeOnlineTree(1, map[string][]string{"guid-a": {"user1", "user2"}})
  59. assertSameSet(t, "union", p.GetOnlineClients(), []string{"user1", "user2"})
  60. }
  61. // TestRefreshLocalOnlineGraceWindow checks the in-memory local set honours the
  62. // grace window: idle-but-recent clients stay online, stale ones age out, and
  63. // the set is derived only from local activity (never the shared DB column).
  64. func TestRefreshLocalOnlineGraceWindow(t *testing.T) {
  65. p := newOnlineTestProcess()
  66. const grace = 20000
  67. p.RefreshLocalOnline([]string{"user1"}, nil, 1000, grace)
  68. if got := p.GetLocalOnlineClients(); !slices.Contains(got, "user1") {
  69. t.Fatalf("user1 should be online right after activity, got %v", got)
  70. }
  71. p.RefreshLocalOnline([]string{"user2"}, nil, 11000, grace)
  72. got := p.GetLocalOnlineClients()
  73. if !slices.Contains(got, "user1") || !slices.Contains(got, "user2") {
  74. t.Fatalf("both within grace window, got %v", got)
  75. }
  76. p.RefreshLocalOnline(nil, nil, 22000, grace)
  77. got = p.GetLocalOnlineClients()
  78. if slices.Contains(got, "user1") {
  79. t.Errorf("user1 (idle 21s, past grace) should have aged out, got %v", got)
  80. }
  81. if !slices.Contains(got, "user2") {
  82. t.Errorf("user2 (idle 11s, within grace) should still be online, got %v", got)
  83. }
  84. }
  85. // TestGetLocalActiveInboundsTracksGraceWindow pins #4859: a multi-inbound
  86. // client only counts online on inbounds that actually carried traffic, and the
  87. // active-inbound signal honours the same grace window as the online signal.
  88. func TestGetLocalActiveInboundsTracksGraceWindow(t *testing.T) {
  89. p := newOnlineTestProcess()
  90. const grace = 20000
  91. p.RefreshLocalOnline([]string{"alice"}, []string{"inbound-a"}, 1000, grace)
  92. assertSameSet(t, "active after first poll", p.GetLocalActiveInbounds(), []string{"inbound-a"})
  93. p.RefreshLocalOnline([]string{"alice"}, []string{"inbound-b"}, 11000, grace)
  94. assertSameSet(t, "both within grace", p.GetLocalActiveInbounds(), []string{"inbound-a", "inbound-b"})
  95. p.RefreshLocalOnline(nil, nil, 22000, grace)
  96. assertSameSet(t, "inbound-a (idle 21s) aged out, inbound-b kept", p.GetLocalActiveInbounds(), []string{"inbound-b"})
  97. p.RefreshLocalOnline(nil, nil, 40000, grace)
  98. if got := p.GetLocalActiveInbounds(); len(got) != 0 {
  99. t.Errorf("all inbounds idle past grace, want empty, got %v", got)
  100. }
  101. }
  102. func TestMergedActiveInboundTreesScopesPerGuid(t *testing.T) {
  103. p := newOnlineTestProcess()
  104. p.SetNodeActiveInboundTree(1, map[string][]string{
  105. "guid-a": {"in-a", "in-a"},
  106. "guid-b": {"in-b"},
  107. })
  108. p.SetNodeActiveInboundTree(2, map[string][]string{
  109. "guid-a": {"in-c"},
  110. "guid-c": {},
  111. })
  112. merged := p.GetMergedActiveInboundTrees()
  113. assertSameSet(t, "guid-a", merged["guid-a"], []string{"in-a", "in-c"})
  114. assertSameSet(t, "guid-b", merged["guid-b"], []string{"in-b"})
  115. if _, ok := merged["guid-c"]; ok {
  116. t.Errorf("empty active-inbound GUID set should be omitted: %v", merged)
  117. }
  118. }
  119. // TestClearNodeOnlineClientsDropsNode mirrors a failed node probe: the node's
  120. // whole subtree contribution disappears immediately.
  121. func TestClearNodeOnlineClientsDropsNode(t *testing.T) {
  122. p := newOnlineTestProcess()
  123. p.SetNodeOnlineTree(3, map[string][]string{"guid-a": {"user1"}})
  124. p.SetNodeActiveInboundTree(3, map[string][]string{"guid-a": {"in-a"}})
  125. p.ClearNodeOnlineClients(3)
  126. if _, ok := p.GetMergedNodeTrees()["guid-a"]; ok {
  127. t.Errorf("node 3's subtree should be absent after ClearNodeOnlineClients")
  128. }
  129. if _, ok := p.GetMergedActiveInboundTrees()["guid-a"]; ok {
  130. t.Errorf("node 3's active-inbound subtree should be absent after ClearNodeOnlineClients")
  131. }
  132. }
  133. func TestSetNodeTreesEmptyInputDropsNode(t *testing.T) {
  134. p := newOnlineTestProcess()
  135. p.SetNodeOnlineTree(3, map[string][]string{"guid-a": {"user1"}})
  136. p.SetNodeActiveInboundTree(3, map[string][]string{"guid-a": {"in-a"}})
  137. p.SetNodeOnlineTree(3, nil)
  138. p.SetNodeActiveInboundTree(3, nil)
  139. if _, ok := p.GetMergedNodeTrees()["guid-a"]; ok {
  140. t.Errorf("empty online tree should remove node 3's subtree")
  141. }
  142. if _, ok := p.GetMergedActiveInboundTrees()["guid-a"]; ok {
  143. t.Errorf("empty active-inbound tree should remove node 3's subtree")
  144. }
  145. }
  146. // TestOnlineAPISupportTriState pins the lazy capability probe contract: a new
  147. // process starts Unknown (so the first caller probes), and the flag holds
  148. // whatever the probe recorded until the process is replaced on restart.
  149. func TestOnlineAPISupportTriState(t *testing.T) {
  150. p := newOnlineTestProcess()
  151. if got := p.OnlineAPISupport(); got != OnlineAPIUnknown {
  152. t.Fatalf("new process must start with OnlineAPIUnknown, got %v", got)
  153. }
  154. p.SetOnlineAPISupport(OnlineAPISupported)
  155. if got := p.OnlineAPISupport(); got != OnlineAPISupported {
  156. t.Fatalf("expected OnlineAPISupported, got %v", got)
  157. }
  158. p.SetOnlineAPISupport(OnlineAPIUnsupported)
  159. if got := p.OnlineAPISupport(); got != OnlineAPIUnsupported {
  160. t.Fatalf("expected OnlineAPIUnsupported, got %v", got)
  161. }
  162. }