feat: 新增账户资料页与头像上传
将工作人员和用户的详情、编辑改为独立账户资料页。 增加受控头像上传与读取、图片安全校验、接口测试,并优化只读及编辑布局。 同步更新平台需求、接口安全说明、项目文档和操作日志。
This commit is contained in:
70
backend/api/internal/logic/upload/avatar_test.go
Normal file
70
backend/api/internal/logic/upload/avatar_test.go
Normal file
@@ -0,0 +1,70 @@
|
||||
// Package upload 测试头像上传的格式、尺寸与路径安全边界。
|
||||
// 版本:v1.0.0
|
||||
package upload
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// pngBytes 生成指定尺寸的有效 PNG 测试图片。
|
||||
func pngBytes(t *testing.T, width, height int) []byte {
|
||||
t.Helper()
|
||||
picture := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||
picture.Set(0, 0, color.RGBA{R: 32, G: 96, B: 192, A: 255})
|
||||
var buffer bytes.Buffer
|
||||
if err := png.Encode(&buffer, picture); err != nil {
|
||||
t.Fatalf("encode PNG: %v", err)
|
||||
}
|
||||
return buffer.Bytes()
|
||||
}
|
||||
|
||||
// TestValidateAvatarAcceptsRealPNG 验证真实 PNG 可通过并规范化类型。
|
||||
func TestValidateAvatarAcceptsRealPNG(t *testing.T) {
|
||||
extension, contentType, err := validateAvatar("头像.PNG", pngBytes(t, 2, 2))
|
||||
if err != nil {
|
||||
t.Fatalf("valid PNG was rejected: %v", err)
|
||||
}
|
||||
if extension != ".png" || contentType != "image/png" {
|
||||
t.Fatalf("avatar metadata = (%q, %q)", extension, contentType)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateAvatarRejectsSpoofedAndOversizedImage 验证伪造扩展名与超大像素尺寸会被拒绝。
|
||||
func TestValidateAvatarRejectsSpoofedAndOversizedImage(t *testing.T) {
|
||||
if _, _, err := validateAvatar("fake.png", []byte("not an image")); err == nil {
|
||||
t.Fatal("spoofed PNG was accepted")
|
||||
}
|
||||
if _, _, err := validateAvatar("wide.png", pngBytes(t, maxAvatarDimension+1, 1)); err == nil {
|
||||
t.Fatal("oversized image dimensions were accepted")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAvatarPathFromURIStaysInsideControlledRoot 验证头像 URI 不能逃逸受控目录。
|
||||
func TestAvatarPathFromURIStaysInsideControlledRoot(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
t.Setenv("HEQI_UPLOAD_DIR", root)
|
||||
path, err := avatarPathFromURI("/uploads/avatars/2026/08/10/example.png")
|
||||
if err != nil {
|
||||
t.Fatalf("controlled URI was rejected: %v", err)
|
||||
}
|
||||
expectedRoot := filepath.Join(root, "avatars")
|
||||
relative, err := filepath.Rel(expectedRoot, path)
|
||||
if err != nil || relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) {
|
||||
t.Fatalf("avatar path escaped root: %q", path)
|
||||
}
|
||||
for _, uri := range []string{
|
||||
"https://example.com/avatar.png",
|
||||
"/uploads/example.png",
|
||||
"/uploads/avatars/../../secret.png",
|
||||
} {
|
||||
if _, err := avatarPathFromURI(uri); err == nil {
|
||||
t.Fatalf("unsafe avatar URI was accepted: %q", uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user