Files
tamabox/internal/route/bodylimit_test.go
T
2026-09-08 10:10:44 +08:00

82 lines
3.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 (
"bytes"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/flamego/flamego"
"github.com/tamakyi/TamaBox/internal/form"
)
// TestRequestBodyLimitEnforced 是 P1-1(请求体 12MiB 上限必须在任何解析之前全局生效)
// 的回归测试。
//
// 背景:此前 MaxBytesReader 只装在 form.Bind(路由级),但 Contexter 组级 CSRF 校验
// (无 X-CSRF-Token 头时经 FormValue("_csrf") 惰性解析整包 body)先于 Bind 执行,
// 超大 multipart 会在路由级限制生效前无上限落盘临时文件耗尽磁盘;chat /send、
// vote /submit 等不走 Bind 的入口更是完全没有上限。修复为全局中间件
// requestBodyLimitMiddleware(挂载于 f.Use 链,先于所有 Group/CSRF)。
//
// 本测试验证三件事(用内存 flamego 实例,无需 DB/Redis):
// 1. 合法请求(≤8MiB 单图上限 + 表单开销)不被误伤 → 200;
// 2. Content-Length 明确超限 → 中间件直接 413 快速失败,不进入 handler;
// 3. chunked/未知长度超限(绕过 Content-Length 预检)→ MaxBytesReader 在读超限时报错,
// 服务端拒绝,而非默默读入/落盘后 200。
func TestRequestBodyLimitEnforced(t *testing.T) {
f := flamego.New()
// 与 internal/route/route.go 一致:全局 Use 挂载先于任何路由/组 handler。
f.Use(requestBodyLimitMiddleware())
f.Post("/echo", func(ctx flamego.Context) {
body, err := io.ReadAll(ctx.Request().Request.Body)
if err != nil {
// MaxBytesReader 超限读报错:服务端拒绝,不得返回 200。
ctx.ResponseWriter().WriteHeader(http.StatusRequestEntityTooLarge)
return
}
ctx.ResponseWriter().WriteHeader(http.StatusOK)
_, _ = ctx.ResponseWriter().Write(body)
})
ts := httptest.NewServer(f)
defer ts.Close()
post := func(payload []byte, chunked bool) int {
req, _ := http.NewRequest(http.MethodPost, ts.URL+"/echo", bytes.NewReader(payload))
req.ContentLength = int64(len(payload))
if chunked {
req.ContentLength = -1 // 强制 chunked,绕过 Content-Length 预检
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("POST failed: %v", err)
}
defer func() { _ = resp.Body.Close() }()
_, _ = io.Copy(io.Discard, resp.Body)
return resp.StatusCode
}
// 1. 合法:8MiB(≈单图上限 storage.MaxUploadBytes + 表单开销)→ 必须放行。
legal := bytes.Repeat([]byte{'a'}, 8<<20)
if code := post(legal, false); code != http.StatusOK {
t.Errorf("合法 8MiB 请求被拒绝:got %d, want 200", code)
}
// 2. Content-Length 明确超限 → 413(中间件预检,不进入 handler)。
huge := bytes.Repeat([]byte{'b'}, form.MaxRequestBodySize+1)
if code := post(huge, false); code != http.StatusRequestEntityTooLarge {
t.Errorf("Content-Length 超限请求未被 413:got %d, want 413", code)
}
// 3. chunked 超限(无 Content-Length,必须靠 MaxBytesReader 惰性封顶)→ 413。
if code := post(huge, true); code != http.StatusRequestEntityTooLarge {
t.Errorf("chunked 超限请求未被拒绝:got %d, want 413", code)
}
}