|
@@ -0,0 +1,150 @@
|
|
|
|
|
+package main
|
|
|
|
|
+
|
|
|
|
|
+import (
|
|
|
|
|
+ "net"
|
|
|
|
|
+ "os"
|
|
|
|
|
+ "strings"
|
|
|
|
|
+ "testing"
|
|
|
|
|
+ "time"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+// TestRedact 覆盖 token 脱敏的各种边界(含多 token、终止符、无 token)。
|
|
|
|
|
+func TestRedact(t *testing.T) {
|
|
|
|
|
+ cases := []struct{ in, want string }{
|
|
|
|
|
+ {"http://127.0.0.1:3099/?token=abc123", "http://127.0.0.1:3099/?token=***"},
|
|
|
|
|
+ {"no token here", "no token here"},
|
|
|
|
|
+ {"a token=x&b=1", "a token=***&b=1"},
|
|
|
|
|
+ {"token=", "token=***"},
|
|
|
|
|
+ {"token=a token=b", "token=*** token=***"},
|
|
|
|
|
+ {"token=abc\"x", "token=***\"x"},
|
|
|
|
|
+ {"", ""},
|
|
|
|
|
+ }
|
|
|
|
|
+ for _, c := range cases {
|
|
|
|
|
+ if got := Redact(c.in); got != c.want {
|
|
|
|
|
+ t.Errorf("Redact(%q) = %q, want %q", c.in, got, c.want)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestExtractHTTPURL 覆盖 URL 提取与误报拒绝。
|
|
|
|
|
+func TestExtractHTTPURL(t *testing.T) {
|
|
|
|
|
+ cases := []struct{ in, want string }{
|
|
|
|
|
+ {"dsh web: http://127.0.0.1:3099/?token=abc", "http://127.0.0.1:3099/?token=abc"},
|
|
|
|
|
+ {"[dsh out] https://example.com/x", "https://example.com/x"},
|
|
|
|
|
+ {"no url at all", ""},
|
|
|
|
|
+ {"httpx not a url", ""},
|
|
|
|
|
+ {"prefix http://a.b c", "http://a.b"},
|
|
|
|
|
+ }
|
|
|
|
|
+ for _, c := range cases {
|
|
|
|
|
+ if got := extractHTTPURL(c.in); got != c.want {
|
|
|
|
|
+ t.Errorf("extractHTTPURL(%q) = %q, want %q", c.in, got, c.want)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestShortURL(t *testing.T) {
|
|
|
|
|
+ if got := shortURL("http://127.0.0.1:3080/?token=x"); got != "http://127.0.0.1:3080/" {
|
|
|
|
|
+ t.Errorf("shortURL 去除 query 失败: %q", got)
|
|
|
|
|
+ }
|
|
|
|
|
+ if got := shortURL("http://127.0.0.1:3080/"); got != "http://127.0.0.1:3080/" {
|
|
|
|
|
+ t.Errorf("shortURL 无 query 不应改变: %q", got)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestHumanDuration(t *testing.T) {
|
|
|
|
|
+ cases := []struct {
|
|
|
|
|
+ d time.Duration
|
|
|
|
|
+ want string
|
|
|
|
|
+ }{
|
|
|
|
|
+ {0, "0 秒"},
|
|
|
|
|
+ {-time.Second, "0 秒"},
|
|
|
|
|
+ {45 * time.Second, "45 秒"},
|
|
|
|
|
+ {90 * time.Second, "1 分钟"},
|
|
|
|
|
+ {2*time.Hour + 5*time.Minute, "2 小时 5 分"},
|
|
|
|
|
+ }
|
|
|
|
|
+ for _, c := range cases {
|
|
|
|
|
+ if got := humanDuration(c.d); got != c.want {
|
|
|
|
|
+ t.Errorf("humanDuration(%v) = %q, want %q", c.d, got, c.want)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestConfigNormalized 覆盖越界字段回退默认值。
|
|
|
|
|
+func TestConfigNormalized(t *testing.T) {
|
|
|
|
|
+ c := &Config{WebPort: -1, WebHost: "", UpdateIntervalH: 0, RestartDelaySec: 0}
|
|
|
|
|
+ n := c.normalized()
|
|
|
|
|
+ if n.WebPort != 3080 {
|
|
|
|
|
+ t.Errorf("WebPort 应回退 3080,得到 %d", n.WebPort)
|
|
|
|
|
+ }
|
|
|
|
|
+ if n.WebHost != "127.0.0.1" {
|
|
|
|
|
+ t.Errorf("WebHost 应回退 127.0.0.1,得到 %q", n.WebHost)
|
|
|
|
|
+ }
|
|
|
|
|
+ if n.UpdateIntervalH != 24 {
|
|
|
|
|
+ t.Errorf("UpdateIntervalH 应回退 24,得到 %d", n.UpdateIntervalH)
|
|
|
|
|
+ }
|
|
|
|
|
+ if n.RestartDelaySec != 5 {
|
|
|
|
|
+ t.Errorf("RestartDelaySec 应回退 5,得到 %d", n.RestartDelaySec)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ over := &Config{WebPort: 70000, WebHost: "127.0.0.1", UpdateIntervalH: 1, RestartDelaySec: 1}
|
|
|
|
|
+ if got := over.normalized().WebPort; got != 3080 {
|
|
|
|
|
+ t.Errorf("超范围 WebPort 应回退 3080,得到 %d", got)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestIsPortOpen 用真实监听端口验证探测。
|
|
|
|
|
+func TestIsPortOpen(t *testing.T) {
|
|
|
|
|
+ ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Skipf("无法创建监听: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ port := ln.Addr().(*net.TCPAddr).Port
|
|
|
|
|
+ if !isPortOpen("127.0.0.1", port) {
|
|
|
|
|
+ t.Errorf("已监听端口 %d 应判定为 open", port)
|
|
|
|
|
+ }
|
|
|
|
|
+ _ = ln.Close()
|
|
|
|
|
+ time.Sleep(100 * time.Millisecond)
|
|
|
|
|
+ if isPortOpen("127.0.0.1", port) {
|
|
|
|
|
+ t.Errorf("已关闭端口 %d 应判定为 closed", port)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestLoggerRotate 验证日志按天轮转(切换文件),并清理测试产物。
|
|
|
|
|
+func TestLoggerRotate(t *testing.T) {
|
|
|
|
|
+ l := NewLogger()
|
|
|
|
|
+ defer l.Close()
|
|
|
|
|
+ base := l.Path()
|
|
|
|
|
+ l.rotate("19990101")
|
|
|
|
|
+ rotated := l.Path()
|
|
|
|
|
+ if rotated == base {
|
|
|
|
|
+ t.Errorf("rotate 未切换日志路径: %q", rotated)
|
|
|
|
|
+ }
|
|
|
|
|
+ if !strings.HasSuffix(rotated, "tray-19990101.log") {
|
|
|
|
|
+ t.Errorf("轮转文件名不符合预期: %q", rotated)
|
|
|
|
|
+ }
|
|
|
|
|
+ t.Cleanup(func() { _ = os.Remove(rotated) })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestBackoff 验证指数退避序列与上限。
|
|
|
|
|
+func TestBackoff(t *testing.T) {
|
|
|
|
|
+ s := NewSupervisor(&Config{RestartDelaySec: 3, WebHost: "127.0.0.1", WebPort: 1}, nil)
|
|
|
|
|
+ // 无日志时 Logger 为 nil 会 panic,这里只测纯计算:手动构造 failures
|
|
|
|
|
+ _ = s
|
|
|
|
|
+ want := []time.Duration{3 * time.Second, 6 * time.Second, 12 * time.Second, 24 * time.Second, 48 * time.Second}
|
|
|
|
|
+ for i, w := range want {
|
|
|
|
|
+ s.failures = i
|
|
|
|
|
+ if got := s.backoff(); got != w {
|
|
|
|
|
+ t.Errorf("failures=%d backoff=%v, want %v", i, got, w)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // 上限 2 分钟
|
|
|
|
|
+ s.failures = 10
|
|
|
|
|
+ if got := s.backoff(); got != 2*time.Minute {
|
|
|
|
|
+ t.Errorf("退避上限应为 2 分钟,得到 %v", got)
|
|
|
|
|
+ }
|
|
|
|
|
+ // 端口占用温和退避上限 30 秒
|
|
|
|
|
+ s.failures = 10
|
|
|
|
|
+ if got := s.portBusyBackoff(); got != 30*time.Second {
|
|
|
|
|
+ t.Errorf("端口占用退避上限应为 30 秒,得到 %v", got)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|