// Copyright 2026 tamakyi. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package route import ( "net/http" "net/http/httptest" "strings" "testing" "github.com/flamego/flamego" "github.com/stretchr/testify/require" ) // TestRecoveryMiddlewareRendersHTML 锁住 panic 的对外表现。 // // 改动前用的是 flamego.Recovery():它直接写纯文本 "Internal Server Error", // 用户看到白底黑字两行英文,没有返回入口,也拿不到可以报给我们的编号。 func TestRecoveryMiddlewareRendersHTML(t *testing.T) { f := flamego.New() f.Use(recoveryMiddleware()) f.Get("/boom", func() { panic("boom") }) req := httptest.NewRequest(http.MethodGet, "/boom", nil) resp := httptest.NewRecorder() f.ServeHTTP(resp, req) require.Equal(t, http.StatusInternalServerError, resp.Code) require.True(t, strings.HasPrefix(resp.Header().Get("Content-Type"), "text/html"), "必须是 HTML,不能退回纯文本") require.Equal(t, "no-store", resp.Header().Get("Cache-Control")) body := resp.Body.String() require.Contains(t, body, "") // 中文环境(default lang)下应给出可读文案与返回入口 require.Contains(t, body, "服务出了点问题") require.Contains(t, body, `href="/"`, "错误页必须有返回首页的入口") require.NotContains(t, body, "boom", "不能把 panic 内容暴露给用户") } // TestRecoveryMiddlewareDoesNotDoubleWrite 响应已写出时不能再写头。 // // 场景:handler 先写了一部分内容再 panic。这时 HTTP 头已经发出去了, // 再 WriteHeader 会被 net/http 忽略(并打 superfluous 警告), // 强行写还会把已经发出去的响应体盖掉。 func TestRecoveryMiddlewareDoesNotDoubleWrite(t *testing.T) { f := flamego.New() f.Use(recoveryMiddleware()) f.Get("/half", func(ctx flamego.Context) { _, _ = ctx.ResponseWriter().Write([]byte("partial")) panic("boom after write") }) req := httptest.NewRequest(http.MethodGet, "/half", nil) resp := httptest.NewRecorder() f.ServeHTTP(resp, req) require.Equal(t, http.StatusOK, resp.Code, "头已发出,状态码不能变") require.Equal(t, "partial", resp.Body.String(), "不能把已写出的内容覆盖掉") } // TestPickErrorPageLang 错误页语言只能靠 Accept-Language 猜 // (sessioner 是组级中间件,NotFound 不经过它)。 func TestPickErrorPageLang(t *testing.T) { cases := []struct { name string header string want string }{ {"空 header 用站点默认", "", ""}, {"英文", "en-US,en;q=0.9", "en"}, {"日文", "ja-JP,ja;q=0.9", "ja"}, {"繁体", "zh-TW,zh;q=0.9", "zh-TW"}, {"zh-Hant 也算繁体", "zh-Hant", "zh-TW"}, {"简体", "zh-CN,zh;q=0.9", "zh-CN"}, // 浏览器常把首选语言排在前面,这里按出现顺序取第一个能识别的 {"首选英文后跟日文", "en-GB,en;q=0.9,ja;q=0.5", "en"}, {"无法识别时回退默认", "de-DE,fr;q=0.8", ""}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/nope", nil) if c.header != "" { req.Header.Set("Accept-Language", c.header) } got := pickErrorPageLang(req) if c.want == "" { // 回退到站点默认语言,不是空串 require.NotEmpty(t, got) return } require.Equal(t, c.want, got) }) } } // TestWriteErrorPageStatusCodes 404 与 500 各自的状态码与 noindex。 func TestWriteErrorPageStatusCodes(t *testing.T) { for _, status := range []int{http.StatusNotFound, http.StatusInternalServerError} { f := flamego.New() f.Get("/x", func(ctx flamego.Context) { writeErrorPage(ctx, status, "error.notFoundTitle", "error.notFoundDesc") }) req := httptest.NewRequest(http.MethodGet, "/x", nil) resp := httptest.NewRecorder() f.ServeHTTP(resp, req) require.Equal(t, status, resp.Code) require.Contains(t, resp.Body.String(), `name="robots" content="noindex"`, "错误页不该被搜索引擎收录") } } // TestEscErrorPageText 字典文本里出现引号/尖括号时不能把页面结构弄坏。 func TestEscErrorPageText(t *testing.T) { require.Equal(t, "a"b", esc(`a"b`)) require.Equal(t, "<script>", esc("