Files
go_blog/handlers/p3_upload_test.go
T
kevin f307781f58 docs: 全部 Go 代码注释汉化
- 48 个 Go 文件所有注释(行注释/块注释/行尾注释,含 _test.go)翻译为中文
- 保留技术标识符:SECURITY_TODO(n)、unsafe-inline、sqlite/mysql、路由参数等
- 代码、字符串字面量、日志消息保持英文原文,零逻辑改动
- go build/vet 通过,go test -count=1 ./... 全绿
2026-08-27 19:03:03 +08:00

156 lines
5.4 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.
package handlers
import (
"mime/multipart"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"go_blog/models"
)
// TestUploadAttachmentRejectsMismatchedContent 覆盖 SECURITY_TODO #14
// 扩展名白名单仅是头部级别的;字节必须与配置的 MIME 类型匹配
//(携带 PNG 字节的 .txt 文件是伪装载荷)。
func TestUploadAttachmentRejectsMismatchedContent(t *testing.T) {
e := newSecurityTestEnv(t)
alice := e.login(t, "alice")
token := e.csrfTokenFor(t, alice)
var aliceArt models.Article
e.db.Where("slug = ?", "alice-post").First(&aliceArt)
artID := strconv.FormatUint(uint64(aliceArt.ID), 10)
// 声称 .txt、实际 PNG 字节 -> 拒绝 400。
var buf strings.Builder
mw := multipart.NewWriter(&buf)
mw.WriteField("article_id", artID)
mw.WriteField("_csrf", token)
fw, _ := mw.CreateFormFile("file", "photo.txt")
fw.Write(pngBytes(t))
mw.Close()
w := e.do(http.MethodPost, "/my/articles/attachments", alice, strings.NewReader(buf.String()), mw.FormDataContentType())
if w.Code != http.StatusBadRequest {
t.Fatalf("mismatched content: status = %d, want 400 (body %s)", w.Code, w.Body.String())
}
// 真正的文本可通过。
buf.Reset()
mw = multipart.NewWriter(&buf)
mw.WriteField("article_id", artID)
mw.WriteField("_csrf", token)
fw, _ = mw.CreateFormFile("file", "notes.txt")
fw.Write([]byte("hello plain text"))
mw.Close()
w = e.do(http.MethodPost, "/my/articles/attachments", alice, strings.NewReader(buf.String()), mw.FormDataContentType())
if w.Code != http.StatusOK {
t.Fatalf("genuine text upload: status = %d, want 200 (body %s)", w.Code, w.Body.String())
}
}
// TestRSSUsesConfiguredSiteURL 覆盖 SECURITY_TODO #16:配置后 feed 链接使用
// 规范化的站点 URL,否则回退到请求的 Host(并输出日志警告)。
func TestRSSUsesConfiguredSiteURL(t *testing.T) {
e := newSecurityTestEnv(t)
// 未设置:回退到请求的 Host。
req := httptest.NewRequest(http.MethodGet, "/rss", nil)
req.Host = "evil.example.com"
w := httptest.NewRecorder()
e.router.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("GET /rss: status = %d", w.Code)
}
if !strings.Contains(w.Body.String(), "http://evil.example.com") {
t.Fatal("fallback did not use the request Host")
}
// 已配置:固定 URL 生效,Host 头被忽略。
var s models.SiteSetting
if err := e.db.First(&s, 1).Error; err != nil {
t.Fatalf("load site setting: %v", err)
}
s.SiteURL = "https://blog.example.com"
if err := e.db.Save(&s).Error; err != nil {
t.Fatalf("save site setting: %v", err)
}
req = httptest.NewRequest(http.MethodGet, "/rss", nil)
req.Host = "evil.example.com"
w = httptest.NewRecorder()
e.router.ServeHTTP(w, req)
body := w.Body.String()
if !strings.Contains(body, "https://blog.example.com") {
t.Fatal("configured SiteURL was not used in the feed")
}
if strings.Contains(body, "evil.example.com") {
t.Fatal("request Host leaked into RSS link despite SiteURL being set")
}
}
// TestAdminCommentListFollowsGravatarSwitch 覆盖 SECURITY_TODO #15
// 平台开关关闭时后台审核列表不输出 Gravatar URL
// 管理员重新启用后再使用。
func TestAdminCommentListFollowsGravatarSwitch(t *testing.T) {
e := newSecurityTestEnv(t)
admin := e.login(t, "admin")
var art models.Article
e.db.Where("slug = ?", "alice-post").First(&art)
e.db.Create(&models.Comment{
ArticleID: art.ID, AuthorName: "Ann", Email: "ann@example.com",
Content: "hello", Status: models.CommentApproved, IPAddress: "127.0.0.1",
})
// 关闭(新默认):列表中没有 gravatar.com 条目。
w := e.do(http.MethodGet, "/admin/comments?status=all", admin, nil, "")
if w.Code != http.StatusOK {
t.Fatalf("GET /admin/comments: status = %d", w.Code)
}
if strings.Contains(w.Body.String(), "gravatar.com") {
t.Fatal("admin comment list emitted Gravatar URLs while disabled")
}
// 开启:Gravatar URL 出现(遵循平台策略)。
e.db.Model(&models.CommentConfig{}).Where("id = ?", 1).Update("use_gravatar", true)
models.LoadConfigCache(e.db)
w = e.do(http.MethodGet, "/admin/comments?status=all", admin, nil, "")
if w.Code != http.StatusOK {
t.Fatalf("GET /admin/comments (enabled): status = %d", w.Code)
}
if !strings.Contains(w.Body.String(), "gravatar.com") {
t.Fatal("admin comment list missing Gravatar URLs while enabled")
}
}
// TestContentMatchesTypeTable 驱动纯匹配函数(SECURITY_TODO #14)。
func TestContentMatchesTypeTable(t *testing.T) {
txt := &models.UploadFileType{MimeType: "text/plain"}
pngType := &models.UploadFileType{MimeType: "image/png"}
noPolicy := &models.UploadFileType{MimeType: ""}
cases := []struct {
name string
typ *models.UploadFileType
content []byte
want bool
}{
{"txt-real", txt, []byte("just text content"), true},
{"txt-png-bytes", txt, pngBytes(t), false},
{"png-real", pngType, pngBytes(t), true},
{"png-text-bytes", pngType, []byte("not an image at all"), false},
{"empty-policy", noPolicy, pngBytes(t), true},
{"empty-content", txt, nil, true},
{"octet-stream-wildcard", &models.UploadFileType{MimeType: "application/octet-stream"}, pngBytes(t), true},
{"charset-parameter", &models.UploadFileType{MimeType: "text/plain; charset=utf-8"}, []byte("abc"), true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := contentMatchesType(tc.typ, tc.content); got != tc.want {
t.Fatalf("contentMatchesType(%q) = %v, want %v", tc.typ.MimeType, got, tc.want)
}
})
}
}