util_test.go 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. package main
  2. import (
  3. "net"
  4. "os"
  5. "path/filepath"
  6. "strings"
  7. "testing"
  8. "time"
  9. )
  10. // TestRedact 覆盖 token 脱敏的各种边界(含多 token、终止符、无 token)。
  11. func TestRedact(t *testing.T) {
  12. cases := []struct{ in, want string }{
  13. {"http://127.0.0.1:3099/?token=abc123", "http://127.0.0.1:3099/?token=***"},
  14. {"no token here", "no token here"},
  15. {"a token=x&b=1", "a token=***&b=1"},
  16. {"token=", "token=***"},
  17. {"token=a token=b", "token=*** token=***"},
  18. {"token=abc\"x", "token=***\"x"},
  19. {"", ""},
  20. }
  21. for _, c := range cases {
  22. if got := Redact(c.in); got != c.want {
  23. t.Errorf("Redact(%q) = %q, want %q", c.in, got, c.want)
  24. }
  25. }
  26. }
  27. // TestExtractHTTPURL 覆盖 URL 提取与误报拒绝。
  28. func TestExtractHTTPURL(t *testing.T) {
  29. cases := []struct{ in, want string }{
  30. {"dsh web: http://127.0.0.1:3099/?token=abc", "http://127.0.0.1:3099/?token=abc"},
  31. {"[dsh out] https://example.com/x", "https://example.com/x"},
  32. {"no url at all", ""},
  33. {"httpx not a url", ""},
  34. {"prefix http://a.b c", "http://a.b"},
  35. }
  36. for _, c := range cases {
  37. if got := extractHTTPURL(c.in); got != c.want {
  38. t.Errorf("extractHTTPURL(%q) = %q, want %q", c.in, got, c.want)
  39. }
  40. }
  41. }
  42. func TestShortURL(t *testing.T) {
  43. if got := shortURL("http://127.0.0.1:3080/?token=x"); got != "http://127.0.0.1:3080/" {
  44. t.Errorf("shortURL 去除 query 失败: %q", got)
  45. }
  46. if got := shortURL("http://127.0.0.1:3080/"); got != "http://127.0.0.1:3080/" {
  47. t.Errorf("shortURL 无 query 不应改变: %q", got)
  48. }
  49. }
  50. func TestHumanDuration(t *testing.T) {
  51. cases := []struct {
  52. d time.Duration
  53. want string
  54. }{
  55. {0, "0 秒"},
  56. {-time.Second, "0 秒"},
  57. {45 * time.Second, "45 秒"},
  58. {90 * time.Second, "1 分钟"},
  59. {2*time.Hour + 5*time.Minute, "2 小时 5 分"},
  60. }
  61. for _, c := range cases {
  62. if got := humanDuration(c.d); got != c.want {
  63. t.Errorf("humanDuration(%v) = %q, want %q", c.d, got, c.want)
  64. }
  65. }
  66. }
  67. // TestConfigNormalized 覆盖越界字段回退默认值。
  68. func TestConfigNormalized(t *testing.T) {
  69. c := &Config{WebPort: -1, WebHost: "", UpdateIntervalH: 0, RestartDelaySec: 0}
  70. n := c.normalized()
  71. if n.WebPort != 3080 {
  72. t.Errorf("WebPort 应回退 3080,得到 %d", n.WebPort)
  73. }
  74. if n.WebHost != "127.0.0.1" {
  75. t.Errorf("WebHost 应回退 127.0.0.1,得到 %q", n.WebHost)
  76. }
  77. if n.UpdateIntervalH != 24 {
  78. t.Errorf("UpdateIntervalH 应回退 24,得到 %d", n.UpdateIntervalH)
  79. }
  80. if n.RestartDelaySec != 5 {
  81. t.Errorf("RestartDelaySec 应回退 5,得到 %d", n.RestartDelaySec)
  82. }
  83. over := &Config{WebPort: 70000, WebHost: "127.0.0.1", UpdateIntervalH: 1, RestartDelaySec: 1}
  84. if got := over.normalized().WebPort; got != 3080 {
  85. t.Errorf("超范围 WebPort 应回退 3080,得到 %d", got)
  86. }
  87. }
  88. // TestIsPortOpen 用真实监听端口验证探测。
  89. func TestIsPortOpen(t *testing.T) {
  90. ln, err := net.Listen("tcp", "127.0.0.1:0")
  91. if err != nil {
  92. t.Skipf("无法创建监听: %v", err)
  93. }
  94. port := ln.Addr().(*net.TCPAddr).Port
  95. if !isPortOpen("127.0.0.1", port) {
  96. t.Errorf("已监听端口 %d 应判定为 open", port)
  97. }
  98. _ = ln.Close()
  99. time.Sleep(100 * time.Millisecond)
  100. if isPortOpen("127.0.0.1", port) {
  101. t.Errorf("已关闭端口 %d 应判定为 closed", port)
  102. }
  103. }
  104. // TestConfigRoundTrip 覆盖配置的首次生成、保存重载、BOM 容忍与损坏回退。
  105. // 通过 dataDirForced 注入临时目录,避免触碰真实用户配置。
  106. func TestConfigRoundTrip(t *testing.T) {
  107. dataDirForced = t.TempDir()
  108. defer func() { dataDirForced = "" }()
  109. // 首次加载:生成默认配置
  110. c := loadConfig()
  111. if c.WebPort != 3080 || c.AutoUpdate || c.RestartDelaySec != 5 {
  112. t.Fatalf("默认配置异常: %+v", c)
  113. }
  114. if _, err := os.Stat(configPath()); err != nil {
  115. t.Errorf("首次加载应生成配置文件: %v", err)
  116. }
  117. // 保存后重载
  118. c.WebPort = 3099
  119. c.AutoUpdate = true
  120. if err := saveConfig(c); err != nil {
  121. t.Fatalf("保存失败: %v", err)
  122. }
  123. c2 := loadConfig()
  124. if c2.WebPort != 3099 || !c2.AutoUpdate {
  125. t.Errorf("重载不一致: %+v", c2)
  126. }
  127. // BOM 容忍
  128. bom := append([]byte{0xEF, 0xBB, 0xBF}, []byte(`{"webPort":1234}`)...)
  129. if err := os.WriteFile(configPath(), bom, 0o600); err != nil {
  130. t.Fatal(err)
  131. }
  132. if got := loadConfig().WebPort; got != 1234 {
  133. t.Errorf("BOM 配置应解析为 1234,得到 %d", got)
  134. }
  135. // 损坏配置回退默认值
  136. if err := os.WriteFile(configPath(), []byte(`{ broken`), 0o600); err != nil {
  137. t.Fatal(err)
  138. }
  139. if got := loadConfig().WebPort; got != 3080 {
  140. t.Errorf("损坏配置应回退 3080,得到 %d", got)
  141. }
  142. }
  143. // TestSupervisorLifecycle 用"立即退出的假命令"验证守护状态机:启动、自动重启、停止。
  144. func TestSupervisorLifecycle(t *testing.T) {
  145. cmdExe := os.Getenv("COMSPEC")
  146. if cmdExe == "" {
  147. t.Skip("缺少 COMSPEC,跳过守护生命周期测试")
  148. }
  149. dir := t.TempDir()
  150. dataDirForced = dir
  151. defer func() { dataDirForced = "" }()
  152. // 假 dsh:立即退出(触发守护重启)
  153. bat := filepath.Join(dir, "fake-dsh.bat")
  154. if err := os.WriteFile(bat, []byte("@echo off\r\nexit /b 1\r\n"), 0o600); err != nil {
  155. t.Fatal(err)
  156. }
  157. cfg := defaultConfig()
  158. cfg.NodePath = cmdExe
  159. cfg.DshBinJS = bat
  160. cfg.WebPort = 1 // 无监听端口,避免误判"端口被占用"
  161. cfg.RestartDelaySec = 1
  162. log := NewLogger()
  163. defer log.Close()
  164. s := NewSupervisor(cfg, log)
  165. s.Start()
  166. if !s.isDesired() {
  167. t.Errorf("Start 后 desired 应为 true")
  168. }
  169. time.Sleep(2500 * time.Millisecond)
  170. if got := s.Status().Restarts; got < 1 {
  171. t.Errorf("假命令立即退出应至少触发一次自动重启,得到 %d", got)
  172. }
  173. s.Stop()
  174. if s.isDesired() {
  175. t.Errorf("Stop 后 desired 应为 false")
  176. }
  177. time.Sleep(600 * time.Millisecond)
  178. if s.Status().Running {
  179. t.Errorf("Stop 后不应处于运行态")
  180. }
  181. }
  182. // TestSetAutoUpdatePersists 验证线程安全 setter 同时更新内存与磁盘。
  183. func TestSetAutoUpdatePersists(t *testing.T) {
  184. dataDirForced = t.TempDir()
  185. defer func() { dataDirForced = "" }()
  186. log := NewLogger()
  187. defer log.Close()
  188. s := NewSupervisor(loadConfig(), log)
  189. if err := s.SetAutoUpdate(true); err != nil {
  190. t.Fatalf("SetAutoUpdate 失败: %v", err)
  191. }
  192. if !loadConfig().AutoUpdate {
  193. t.Errorf("配置未持久化到磁盘")
  194. }
  195. s.mu.Lock()
  196. inMemory := s.cfg.AutoUpdate
  197. s.mu.Unlock()
  198. if !inMemory {
  199. t.Errorf("内存配置未更新")
  200. }
  201. if err := s.SetAutoUpdate(false); err != nil {
  202. t.Fatalf("SetAutoUpdate(false) 失败: %v", err)
  203. }
  204. if loadConfig().AutoUpdate {
  205. t.Errorf("关闭后配置仍为 true")
  206. }
  207. }
  208. // TestLoggerRotate 验证日志按天轮转(切换文件),并清理测试产物。
  209. func TestLoggerRotate(t *testing.T) {
  210. l := NewLogger()
  211. defer l.Close()
  212. base := l.Path()
  213. l.rotate("19990101")
  214. rotated := l.Path()
  215. if rotated == base {
  216. t.Errorf("rotate 未切换日志路径: %q", rotated)
  217. }
  218. if !strings.HasSuffix(rotated, "tray-19990101.log") {
  219. t.Errorf("轮转文件名不符合预期: %q", rotated)
  220. }
  221. t.Cleanup(func() { _ = os.Remove(rotated) })
  222. }
  223. // TestBackoff 验证指数退避序列与上限。
  224. func TestBackoff(t *testing.T) {
  225. s := NewSupervisor(&Config{RestartDelaySec: 3, WebHost: "127.0.0.1", WebPort: 1}, nil)
  226. // 无日志时 Logger 为 nil 会 panic,这里只测纯计算:手动构造 failures
  227. _ = s
  228. want := []time.Duration{3 * time.Second, 6 * time.Second, 12 * time.Second, 24 * time.Second, 48 * time.Second}
  229. for i, w := range want {
  230. s.failures = i
  231. if got := s.backoff(); got != w {
  232. t.Errorf("failures=%d backoff=%v, want %v", i, got, w)
  233. }
  234. }
  235. // 上限 2 分钟
  236. s.failures = 10
  237. if got := s.backoff(); got != 2*time.Minute {
  238. t.Errorf("退避上限应为 2 分钟,得到 %v", got)
  239. }
  240. // 端口占用温和退避上限 30 秒
  241. s.failures = 10
  242. if got := s.portBusyBackoff(); got != 30*time.Second {
  243. t.Errorf("端口占用退避上限应为 30 秒,得到 %v", got)
  244. }
  245. }