chore: baseline snapshot before moderation-strictness feature

Repo .git was damaged (refs/ missing, index truncated, object store near-empty).
Repaired via git init (rebuilt refs), stale index preserved as .git/index.corrupt-20260910.bak,
then full worktree committed as a rollback baseline.
This commit is contained in:
tamakyi
2026-09-10 10:56:27 +08:00
commit e3e59e7c69
296 changed files with 67458 additions and 0 deletions
+81
View File
@@ -0,0 +1,81 @@
// 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)
}
}