120 lines
3.7 KiB
Go
120 lines
3.7 KiB
Go
// 功能描述:报修上传的内容幂等、格式、跨账户和路径边界回归。
|
||
// 版本: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("越权读取")
|
||
}
|
||
}
|