util_test.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. package main
  2. import (
  3. "net"
  4. "os"
  5. "strings"
  6. "testing"
  7. "time"
  8. )
  9. // TestRedact 覆盖 token 脱敏的各种边界(含多 token、终止符、无 token)。
  10. func TestRedact(t *testing.T) {
  11. cases := []struct{ in, want string }{
  12. {"http://127.0.0.1:3099/?token=abc123", "http://127.0.0.1:3099/?token=***"},
  13. {"no token here", "no token here"},
  14. {"a token=x&b=1", "a token=***&b=1"},
  15. {"token=", "token=***"},
  16. {"token=a token=b", "token=*** token=***"},
  17. {"token=abc\"x", "token=***\"x"},
  18. {"", ""},
  19. }
  20. for _, c := range cases {
  21. if got := Redact(c.in); got != c.want {
  22. t.Errorf("Redact(%q) = %q, want %q", c.in, got, c.want)
  23. }
  24. }
  25. }
  26. // TestExtractHTTPURL 覆盖 URL 提取与误报拒绝。
  27. func TestExtractHTTPURL(t *testing.T) {
  28. cases := []struct{ in, want string }{
  29. {"dsh web: http://127.0.0.1:3099/?token=abc", "http://127.0.0.1:3099/?token=abc"},
  30. {"[dsh out] https://example.com/x", "https://example.com/x"},
  31. {"no url at all", ""},
  32. {"httpx not a url", ""},
  33. {"prefix http://a.b c", "http://a.b"},
  34. }
  35. for _, c := range cases {
  36. if got := extractHTTPURL(c.in); got != c.want {
  37. t.Errorf("extractHTTPURL(%q) = %q, want %q", c.in, got, c.want)
  38. }
  39. }
  40. }
  41. func TestShortURL(t *testing.T) {
  42. if got := shortURL("http://127.0.0.1:3080/?token=x"); got != "http://127.0.0.1:3080/" {
  43. t.Errorf("shortURL 去除 query 失败: %q", got)
  44. }
  45. if got := shortURL("http://127.0.0.1:3080/"); got != "http://127.0.0.1:3080/" {
  46. t.Errorf("shortURL 无 query 不应改变: %q", got)
  47. }
  48. }
  49. func TestHumanDuration(t *testing.T) {
  50. cases := []struct {
  51. d time.Duration
  52. want string
  53. }{
  54. {0, "0 秒"},
  55. {-time.Second, "0 秒"},
  56. {45 * time.Second, "45 秒"},
  57. {90 * time.Second, "1 分钟"},
  58. {2*time.Hour + 5*time.Minute, "2 小时 5 分"},
  59. }
  60. for _, c := range cases {
  61. if got := humanDuration(c.d); got != c.want {
  62. t.Errorf("humanDuration(%v) = %q, want %q", c.d, got, c.want)
  63. }
  64. }
  65. }
  66. // TestConfigNormalized 覆盖越界字段回退默认值。
  67. func TestConfigNormalized(t *testing.T) {
  68. c := &Config{WebPort: -1, WebHost: "", UpdateIntervalH: 0, RestartDelaySec: 0}
  69. n := c.normalized()
  70. if n.WebPort != 3080 {
  71. t.Errorf("WebPort 应回退 3080,得到 %d", n.WebPort)
  72. }
  73. if n.WebHost != "127.0.0.1" {
  74. t.Errorf("WebHost 应回退 127.0.0.1,得到 %q", n.WebHost)
  75. }
  76. if n.UpdateIntervalH != 24 {
  77. t.Errorf("UpdateIntervalH 应回退 24,得到 %d", n.UpdateIntervalH)
  78. }
  79. if n.RestartDelaySec != 5 {
  80. t.Errorf("RestartDelaySec 应回退 5,得到 %d", n.RestartDelaySec)
  81. }
  82. over := &Config{WebPort: 70000, WebHost: "127.0.0.1", UpdateIntervalH: 1, RestartDelaySec: 1}
  83. if got := over.normalized().WebPort; got != 3080 {
  84. t.Errorf("超范围 WebPort 应回退 3080,得到 %d", got)
  85. }
  86. }
  87. // TestIsPortOpen 用真实监听端口验证探测。
  88. func TestIsPortOpen(t *testing.T) {
  89. ln, err := net.Listen("tcp", "127.0.0.1:0")
  90. if err != nil {
  91. t.Skipf("无法创建监听: %v", err)
  92. }
  93. port := ln.Addr().(*net.TCPAddr).Port
  94. if !isPortOpen("127.0.0.1", port) {
  95. t.Errorf("已监听端口 %d 应判定为 open", port)
  96. }
  97. _ = ln.Close()
  98. time.Sleep(100 * time.Millisecond)
  99. if isPortOpen("127.0.0.1", port) {
  100. t.Errorf("已关闭端口 %d 应判定为 closed", port)
  101. }
  102. }
  103. // TestLoggerRotate 验证日志按天轮转(切换文件),并清理测试产物。
  104. func TestLoggerRotate(t *testing.T) {
  105. l := NewLogger()
  106. defer l.Close()
  107. base := l.Path()
  108. l.rotate("19990101")
  109. rotated := l.Path()
  110. if rotated == base {
  111. t.Errorf("rotate 未切换日志路径: %q", rotated)
  112. }
  113. if !strings.HasSuffix(rotated, "tray-19990101.log") {
  114. t.Errorf("轮转文件名不符合预期: %q", rotated)
  115. }
  116. t.Cleanup(func() { _ = os.Remove(rotated) })
  117. }
  118. // TestBackoff 验证指数退避序列与上限。
  119. func TestBackoff(t *testing.T) {
  120. s := NewSupervisor(&Config{RestartDelaySec: 3, WebHost: "127.0.0.1", WebPort: 1}, nil)
  121. // 无日志时 Logger 为 nil 会 panic,这里只测纯计算:手动构造 failures
  122. _ = s
  123. want := []time.Duration{3 * time.Second, 6 * time.Second, 12 * time.Second, 24 * time.Second, 48 * time.Second}
  124. for i, w := range want {
  125. s.failures = i
  126. if got := s.backoff(); got != w {
  127. t.Errorf("failures=%d backoff=%v, want %v", i, got, w)
  128. }
  129. }
  130. // 上限 2 分钟
  131. s.failures = 10
  132. if got := s.backoff(); got != 2*time.Minute {
  133. t.Errorf("退避上限应为 2 分钟,得到 %v", got)
  134. }
  135. // 端口占用温和退避上限 30 秒
  136. s.failures = 10
  137. if got := s.portBusyBackoff(); got != 30*time.Second {
  138. t.Errorf("端口占用退避上限应为 30 秒,得到 %v", got)
  139. }
  140. }