已完成用户APP首期功能开发
交付用户端首期页面、配套接口、后台资源及测试文档。用户APP构建、静态分析和三个管理后台构建通过;完整测试仍有2项失败,后端模型注释检查未通过,详见交付记录。
This commit is contained in:
119
backend/api/internal/logic/upload/ticket_photo_test.go
Normal file
119
backend/api/internal/logic/upload/ticket_photo_test.go
Normal file
@@ -0,0 +1,119 @@
|
||||
// 功能描述:报修上传的内容幂等、格式、跨账户和路径边界回归。
|
||||
// 版本: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("越权读取")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user