xray_metrics_test.go 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. package service
  2. import (
  3. "path/filepath"
  4. "strings"
  5. "testing"
  6. "time"
  7. "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest"
  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. dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db"))
  55. tests := []struct {
  56. name string
  57. threshold int
  58. seq []probe
  59. want []eventbus.EventType
  60. }{
  61. {
  62. name: "notifies only after threshold consecutive failed probes",
  63. threshold: 3,
  64. seq: []probe{
  65. {true, 1},
  66. {false, 2},
  67. {false, 3},
  68. {false, 4},
  69. {false, 5},
  70. {true, 6},
  71. {false, 7},
  72. {true, 8},
  73. },
  74. want: []eventbus.EventType{eventbus.EventOutboundDown, eventbus.EventOutboundUp},
  75. },
  76. {
  77. name: "repeated samples of the same probe do not advance the streak",
  78. threshold: 3,
  79. seq: []probe{{false, 2}, {false, 2}, {false, 2}, {false, 2}, {false, 2}},
  80. want: nil,
  81. },
  82. {
  83. name: "single-probe blip never notifies",
  84. threshold: 3,
  85. seq: []probe{{true, 1}, {false, 2}, {true, 3}},
  86. want: nil,
  87. },
  88. {
  89. name: "threshold 1 keeps the legacy notify-on-first-failure behaviour",
  90. threshold: 1,
  91. seq: []probe{{true, 1}, {false, 2}},
  92. want: []eventbus.EventType{eventbus.EventOutboundDown},
  93. },
  94. }
  95. for _, tt := range tests {
  96. t.Run(tt.name, func(t *testing.T) {
  97. got := runObservatory(t, tt.threshold, tt.seq)
  98. if len(got) != len(tt.want) {
  99. t.Fatalf("events = %v, want %v", got, tt.want)
  100. }
  101. for i := range got {
  102. if got[i] != tt.want[i] {
  103. t.Fatalf("event[%d] = %q, want %q (full: %v)", i, got[i], tt.want[i], got)
  104. }
  105. }
  106. })
  107. }
  108. }
  109. func TestValidObsTag(t *testing.T) {
  110. tests := []struct {
  111. name string
  112. tag string
  113. want bool
  114. }{
  115. {"plain ascii", "proxy-1", true},
  116. {"dots and underscores", "warp_us.east", true},
  117. {"flag emoji", "🇩🇪 Germany", true},
  118. {"cyrillic", "Германия", true},
  119. {"spaces allowed", "US proxy 2", true},
  120. {"empty rejected", "", false},
  121. {"control char rejected", "bad\x00tag", false},
  122. {"newline rejected", "bad\ntag", false},
  123. {"invalid utf8 rejected", string([]byte{0xff, 0xfe}), false},
  124. {"overlong rejected", strings.Repeat("a", maxObsTagLength+1), false},
  125. }
  126. for _, tc := range tests {
  127. t.Run(tc.name, func(t *testing.T) {
  128. if got := validObsTag(tc.tag); got != tc.want {
  129. t.Fatalf("validObsTag(%q) = %v, want %v", tc.tag, got, tc.want)
  130. }
  131. })
  132. }
  133. }
  134. func TestApplyObservatoryKeepsUnicodeTags(t *testing.T) {
  135. dbDir := t.TempDir()
  136. dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db"))
  137. s := &XrayMetricsService{settingService: SettingService{}}
  138. s.applyObservatory(time.Unix(1000, 0), map[string]rawObsEntry{
  139. "🇩🇪 Berlin": {Alive: true, Delay: 42, LastTryTime: 1},
  140. })
  141. if !s.HasObservatoryTag("🇩🇪 Berlin") {
  142. t.Fatal("emoji-tagged outbound must appear in the observatory")
  143. }
  144. snaps := s.ObservatorySnapshot()
  145. if len(snaps) != 1 || snaps[0].Tag != "🇩🇪 Berlin" {
  146. t.Fatalf("snapshot = %+v, want the emoji tag", snaps)
  147. }
  148. }