2026-05-13 16:07:25 +08:00

171 lines
5.4 KiB
Go

package upload
import (
"bytes"
"context"
"encoding/json"
"io"
"mime/multipart"
"net/http"
"net/http/httptest"
"strings"
"testing"
"hyapp-admin-server/internal/model"
"hyapp-admin-server/internal/modules/shared"
"github.com/gin-gonic/gin"
)
type fakeUploader struct {
contentType string
key string
sizeBytes int64
body []byte
}
func (u *fakeUploader) PutObject(_ context.Context, key string, reader io.Reader, sizeBytes int64, contentType string) (string, error) {
body, err := io.ReadAll(reader)
if err != nil {
return "", err
}
u.key = key
u.body = body
u.sizeBytes = sizeBytes
u.contentType = contentType
return "https://media.example.com/" + key, nil
}
type fakeAudit struct{}
func (fakeAudit) CreateOperationLog(model.OperationLog) error { return nil }
func (fakeAudit) LogOperation(shared.OperationEvent) error { return nil }
func TestUploadImageUsesAdminPrefix(t *testing.T) {
uploader := &fakeUploader{}
handler := New(uploader, "admin", fakeAudit{})
router := ginRouter(handler)
payload := append([]byte{0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n'}, bytes.Repeat([]byte{1}, 32)...)
body, contentType := multipartUploadBody(t, "file", "avatar.png", "image/png", payload)
req := httptest.NewRequest(http.MethodPost, "/upload-image", body)
req.Header.Set("Content-Type", contentType)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d body=%s", rec.Code, rec.Body.String())
}
if !strings.HasPrefix(uploader.key, "admin/images/7/") || !strings.HasSuffix(uploader.key, ".png") {
t.Fatalf("unexpected key: %s", uploader.key)
}
if !bytes.Equal(uploader.body, payload) || uploader.sizeBytes != int64(len(payload)) || uploader.contentType != "image/png" {
t.Fatalf("unexpected upload content: size=%d type=%s", uploader.sizeBytes, uploader.contentType)
}
var envelope struct {
Code int `json:"code"`
Data struct {
ObjectKey string `json:"object_key"`
URL string `json:"url"`
} `json:"data"`
}
if err := json.NewDecoder(rec.Body).Decode(&envelope); err != nil {
t.Fatalf("decode response: %v", err)
}
if envelope.Data.ObjectKey != uploader.key || envelope.Data.URL != "https://media.example.com/"+uploader.key {
t.Fatalf("unexpected response: %+v", envelope.Data)
}
}
func TestUploadImageRejectsSpoofedContent(t *testing.T) {
uploader := &fakeUploader{}
handler := New(uploader, "admin", fakeAudit{})
router := ginRouter(handler)
body, contentType := multipartUploadBody(t, "file", "avatar.png", "image/png", []byte("not an image"))
req := httptest.NewRequest(http.MethodPost, "/upload-image", body)
req.Header.Set("Content-Type", contentType)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("status = %d body=%s", rec.Code, rec.Body.String())
}
if uploader.key != "" {
t.Fatalf("invalid image must not upload: %s", uploader.key)
}
}
func TestUploadImageAllowsAnimatedAssetFormats(t *testing.T) {
uploader := &fakeUploader{}
handler := New(uploader, "admin", fakeAudit{})
router := ginRouter(handler)
body, contentType := multipartUploadBody(t, "file", "cover.pag", "application/octet-stream", []byte("PAG animated asset"))
req := httptest.NewRequest(http.MethodPost, "/upload-image", body)
req.Header.Set("Content-Type", contentType)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d body=%s", rec.Code, rec.Body.String())
}
if !strings.HasPrefix(uploader.key, "admin/images/7/") || !strings.HasSuffix(uploader.key, ".pag") {
t.Fatalf("unexpected key: %s", uploader.key)
}
if uploader.contentType != "application/vnd.tencent.pag" {
t.Fatalf("unexpected content type: %s", uploader.contentType)
}
}
func TestUploadImageAllowsPayloadOverPreviousImageLimit(t *testing.T) {
uploader := &fakeUploader{}
handler := New(uploader, "admin", fakeAudit{})
router := ginRouter(handler)
payload := append([]byte{0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n'}, bytes.Repeat([]byte{1}, (5<<20)+1)...)
body, contentType := multipartUploadBody(t, "file", "large.png", "image/png", payload)
req := httptest.NewRequest(http.MethodPost, "/upload-image", body)
req.Header.Set("Content-Type", contentType)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d body=%s", rec.Code, rec.Body.String())
}
if uploader.sizeBytes != int64(len(payload)) {
t.Fatalf("unexpected upload size: %d", uploader.sizeBytes)
}
}
func ginRouter(handler *Handler) http.Handler {
router := gin.New()
router.Use(func(c *gin.Context) {
c.Set("userID", uint(7))
c.Set("username", "admin")
c.Next()
})
router.POST("/upload-image", handler.UploadImage)
return router
}
func multipartUploadBody(t *testing.T, fieldName string, filename string, contentType string, payload []byte) (*bytes.Buffer, string) {
t.Helper()
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreatePart(map[string][]string{
"Content-Disposition": {`form-data; name="` + fieldName + `"; filename="` + filename + `"`},
"Content-Type": {contentType},
})
if err != nil {
t.Fatalf("create multipart part: %v", err)
}
if _, err := part.Write(payload); err != nil {
t.Fatalf("write multipart part: %v", err)
}
if err := writer.Close(); err != nil {
t.Fatalf("close multipart writer: %v", err)
}
return body, writer.FormDataContentType()
}