package static import ( "net/http" "net/http/httptest" "testing" ) // 缓存策略的判据。 // // 这套规则守的是一个**没有报错、只有肉眼可见**的故障:换了新前端,用户却长期 // 看到旧界面。原因是入口页被缓存 —— Vite 的资源名带内容哈希,新构建生成新文件名, // 而用户拿到的 HTML 里引用的是旧文件名,于是整站钉死在那一代资源上。 func TestIsImmutableAsset(t *testing.T) { cases := []struct { path string want bool why string }{ {"/assets/index-DUb2s9Ly.css", true, "Vite 产出的 CSS(主名-8 位哈希.扩展名)"}, {"/assets/index-ziu1EtZt.js", true, "Vite 产出的 JS"}, {"/assets/CalendarView-BQ1MXzlA.js", true, "懒加载 chunk"}, {"/assets/agentmail.svg", false, "没有内容哈希:改名不变,缓存了就更新不了"}, {"/assets/favicon.ico", false, "同上(public/ 直接拷贝的资源)"}, {"/assets/foo.css", false, "无哈希"}, {"/assets/short-ab12.css", false, "哈希长度不足 8 位:更像版本号或缩写,不当作内容哈希"}, {"/index.html", false, "入口页必须每次回源校验"}, {"/", false, "根路径"}, {"/api/v1/me", false, "API 不该被静态缓存规则碰到"}, } for _, c := range cases { if got := IsImmutableAsset(c.path); got != c.want { t.Errorf("%s:IsImmutableAsset(%q) = %v,应为 %v —— %s", c.path, c.path, got, c.want, c.why) } } } func TestCacheControlHeaders(t *testing.T) { inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("ok")) }) h := CacheControl(inner) get := func(path string) string { req := httptest.NewRequest(http.MethodGet, path, nil) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) return rec.Header().Get("Cache-Control") } // 带哈希 → 永久缓存 if got := get("/assets/index-DUb2s9Ly.css"); got != "public, max-age=31536000, immutable" { t.Errorf("带哈希资源应 immutable,得到 %q", got) } // 不带哈希 → 必须回源校验(给它 immutable 会让改动永远不生效) if got := get("/assets/agentmail.svg"); got != "no-cache" { t.Errorf("无哈希资源应 no-cache,得到 %q", got) } // 其他路径(含入口页可能的回退)→ 同样回源校验 if got := get("/"); got != "no-cache" { t.Errorf("根路径应 no-cache,得到 %q", got) } } func TestSetIndexCacheControl(t *testing.T) { rec := httptest.NewRecorder() SetIndexCacheControl(rec) // 不能是 no-store:那会连磁盘缓存都不用,每次全量重取; // no-cache 的语义是「可以存,但每次必须先回源确认」 if got := rec.Header().Get("Cache-Control"); got != "no-cache" { t.Errorf("入口页应 no-cache,得到 %q", got) } }