Files

120 lines
3.7 KiB
Go
Raw Permalink Normal View History

// 功能描述:报修上传的内容幂等、格式、跨账户和路径边界回归。
// 版本1.0.0。
package upload
import (
"bytes"
"encoding/json"
"github.com/gin-gonic/gin"
"mime/multipart"
"net/http/httptest"
"path"
"strings"
"testing"
)
func uploadPhotoForTest(t *testing.T, owner, filename string, content []byte) (string, int) {
t.Helper()
var body bytes.Buffer
writer := multipart.NewWriter(&body)
part, err := writer.CreateFormFile("file", filename)
if err != nil {
t.Fatal(err)
}
part.Write(content)
writer.Close()
response := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(response)
ctx.Request = httptest.NewRequest("POST", "/ticket-photos", &body)
ctx.Request.Header.Set("Content-Type", writer.FormDataContentType())
UploadTicketPhoto(ctx, owner)
var result struct {
Code int `json:"code"`
Details json.RawMessage `json:"details"`
}
if err := json.Unmarshal(response.Body.Bytes(), &result); err != nil {
t.Fatal(err)
}
var details struct {
URI string `json:"uri"`
}
if result.Code == 0 {
if err := json.Unmarshal(result.Details, &details); err != nil {
t.Fatal(err)
}
}
return details.URI, result.Code
}
// TestDraftPhotoRead 验证草稿读取只能访问当前账户已上传的文件名。
func TestDraftPhotoRead(t *testing.T) {
t.Setenv("HEQI_UPLOAD_DIR", t.TempDir())
picture := pngBytes(t, 2, 2)
uri, code := uploadPhotoForTest(t, "alice", "image.png", picture)
if code != 0 {
t.Fatal("上传失败", code)
}
for _, scenario := range []struct {
owner, name string
status int
}{
{"alice", path.Base(uri), 200},
{"bob", path.Base(uri), 404},
{"alice", "../" + path.Base(uri), 404},
{"alice", "unknown.png", 404},
} {
response := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(response)
ctx.Request = httptest.NewRequest("GET", "/ticket-photos/file", nil)
ServeOwnedTicketPhoto(ctx, scenario.owner, scenario.name)
ctx.Writer.WriteHeaderNow()
if response.Code != scenario.status {
t.Fatalf("%s: 状态 %d", scenario.owner, response.Code)
}
if scenario.status == 200 && !bytes.Equal(response.Body.Bytes(), picture) {
t.Fatal("图片内容不一致")
}
}
}
func TestTicketPhotoStorage(t *testing.T) {
t.Setenv("HEQI_UPLOAD_DIR", t.TempDir())
picture := pngBytes(t, 2, 2)
first, code := uploadPhotoForTest(t, "alice", "image.png", picture)
if code != 0 || !OwnsTicketPhoto("alice", first) {
t.Fatal("上传失败", code)
}
second, code := uploadPhotoForTest(t, "alice", "other.png", picture)
if code != 0 || first != second {
t.Fatal("重复上传未复用")
}
if OwnsTicketPhoto("bob", first) {
t.Fatal("跨账户引用")
}
for _, uri := range []string{first + "/../secret.png", strings.ReplaceAll(first, "/", "\\"), "https://example.com/a.png", "/uploads/avatars/a.png"} {
if OwnsTicketPhoto("alice", uri) {
t.Fatal("非法路径", uri)
}
}
for _, data := range [][]byte{[]byte("fake"), pngBytes(t, 4097, 1), make([]byte, (2<<20)+1)} {
if _, code := uploadPhotoForTest(t, "alice", "x.png", data); code == 0 {
t.Fatal("错误图片被接受")
}
}
response := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(response)
ctx.Request = httptest.NewRequest("GET", "/photo", nil)
ServeTicketPhoto(ctx, "alice", first)
if response.Code != 200 || response.Header().Get("Cache-Control") != "private, no-store" || !bytes.Equal(response.Body.Bytes(), picture) {
t.Fatal("读取或缓存保护失败")
}
response = httptest.NewRecorder()
ctx, _ = gin.CreateTestContext(response)
ctx.Request = httptest.NewRequest("GET", "/photo", nil)
ServeTicketPhoto(ctx, "bob", first)
ctx.Writer.WriteHeaderNow()
if response.Code != 404 {
t.Fatal("越权读取")
}
}