xray_metrics_test.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. package service
  2. import (
  3. "path/filepath"
  4. "strings"
  5. "testing"
  6. "time"
  7. "github.com/mhsanaei/3x-ui/v3/internal/database"
  8. "github.com/mhsanaei/3x-ui/v3/internal/eventbus"
  9. )
  10. // probe is one observatory sample: whether the outbound is alive and the
  11. // last_try_time xray reports for it (a new probe advances lastTry).
  12. type probe struct {
  13. alive bool
  14. lastTry int64
  15. }
  16. const testSentinel eventbus.EventType = "test.sentinel"
  17. // runObservatory feeds a probe sequence through applyObservatory with the given
  18. // threshold and returns the outbound.* events it published, in order.
  19. func runObservatory(t *testing.T, threshold int, seq []probe) []eventbus.EventType {
  20. t.Helper()
  21. ss := SettingService{}
  22. if err := ss.SetOutboundDownThreshold(threshold); err != nil {
  23. t.Fatalf("set threshold: %v", err)
  24. }
  25. bus := eventbus.New(256)
  26. events := make(chan eventbus.Event, 256)
  27. bus.Subscribe("test", func(e eventbus.Event) { events <- e })
  28. SetEventBus(bus)
  29. t.Cleanup(func() {
  30. SetEventBus(nil)
  31. bus.Stop()
  32. })
  33. s := &XrayMetricsService{settingService: ss}
  34. for _, p := range seq {
  35. s.applyObservatory(time.Unix(p.lastTry, 0), map[string]rawObsEntry{
  36. "proxy": {Alive: p.alive, Delay: 10, LastTryTime: p.lastTry, OutboundTag: "proxy"},
  37. })
  38. }
  39. bus.Publish(eventbus.Event{Type: testSentinel, Source: "x"})
  40. var got []eventbus.EventType
  41. for {
  42. select {
  43. case e := <-events:
  44. if e.Type == testSentinel {
  45. return got
  46. }
  47. got = append(got, e.Type)
  48. case <-time.After(2 * time.Second):
  49. t.Fatal("timed out waiting for events to drain")
  50. }
  51. }
  52. }
  53. func TestApplyObservatoryDebounce(t *testing.T) {
  54. if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil {
  55. t.Fatalf("init db: %v", err)
  56. }
  57. t.Cleanup(func() { _ = database.CloseDB() })
  58. tests := []struct {
  59. name string
  60. threshold int
  61. seq []probe
  62. want []eventbus.EventType
  63. }{
  64. {
  65. name: "notifies only after threshold consecutive failed probes",
  66. threshold: 3,
  67. seq: []probe{
  68. {true, 1},
  69. {false, 2},
  70. {false, 3},
  71. {false, 4},
  72. {false, 5},
  73. {true, 6},
  74. {false, 7},
  75. {true, 8},
  76. },
  77. want: []eventbus.EventType{eventbus.EventOutboundDown, eventbus.EventOutboundUp},
  78. },
  79. {
  80. name: "repeated samples of the same probe do not advance the streak",
  81. threshold: 3,
  82. seq: []probe{{false, 2}, {false, 2}, {false, 2}, {false, 2}, {false, 2}},
  83. want: nil,
  84. },
  85. {
  86. name: "single-probe blip never notifies",
  87. threshold: 3,
  88. seq: []probe{{true, 1}, {false, 2}, {true, 3}},
  89. want: nil,
  90. },
  91. {
  92. name: "threshold 1 keeps the legacy notify-on-first-failure behaviour",
  93. threshold: 1,
  94. seq: []probe{{true, 1}, {false, 2}},
  95. want: []eventbus.EventType{eventbus.EventOutboundDown},
  96. },
  97. }
  98. for _, tt := range tests {
  99. t.Run(tt.name, func(t *testing.T) {
  100. got := runObservatory(t, tt.threshold, tt.seq)
  101. if len(got) != len(tt.want) {
  102. t.Fatalf("events = %v, want %v", got, tt.want)
  103. }
  104. for i := range got {
  105. if got[i] != tt.want[i] {
  106. t.Fatalf("event[%d] = %q, want %q (full: %v)", i, got[i], tt.want[i], got)
  107. }
  108. }
  109. })
  110. }
  111. }
  112. func TestValidObsTag(t *testing.T) {
  113. tests := []struct {
  114. name string
  115. tag string
  116. want bool
  117. }{
  118. {"plain ascii", "proxy-1", true},
  119. {"dots and underscores", "warp_us.east", true},
  120. {"flag emoji", "🇩🇪 Germany", true},
  121. {"cyrillic", "Германия", true},
  122. {"spaces allowed", "US proxy 2", true},
  123. {"empty rejected", "", false},
  124. {"control char rejected", "bad\x00tag", false},
  125. {"newline rejected", "bad\ntag", false},
  126. {"invalid utf8 rejected", string([]byte{0xff, 0xfe}), false},
  127. {"overlong rejected", strings.Repeat("a", maxObsTagLength+1), false},
  128. }
  129. for _, tc := range tests {
  130. t.Run(tc.name, func(t *testing.T) {
  131. if got := validObsTag(tc.tag); got != tc.want {
  132. t.Fatalf("validObsTag(%q) = %v, want %v", tc.tag, got, tc.want)
  133. }
  134. })
  135. }
  136. }
  137. func TestApplyObservatoryKeepsUnicodeTags(t *testing.T) {
  138. dbDir := t.TempDir()
  139. if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil {
  140. t.Fatalf("InitDB: %v", err)
  141. }
  142. t.Cleanup(func() { _ = database.CloseDB() })
  143. s := &XrayMetricsService{settingService: SettingService{}}
  144. s.applyObservatory(time.Unix(1000, 0), map[string]rawObsEntry{
  145. "🇩🇪 Berlin": {Alive: true, Delay: 42, LastTryTime: 1},
  146. })
  147. if !s.HasObservatoryTag("🇩🇪 Berlin") {
  148. t.Fatal("emoji-tagged outbound must appear in the observatory")
  149. }
  150. snaps := s.ObservatorySnapshot()
  151. if len(snaps) != 1 || snaps[0].Tag != "🇩🇪 Berlin" {
  152. t.Fatalf("snapshot = %+v, want the emoji tag", snaps)
  153. }
  154. }