82 lines
3.2 KiB
Go
82 lines
3.2 KiB
Go
// 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)
|
||
}
|
||
}
|