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("越权读取")
|
|||
|
|
}
|
|||
|
|
}
|