package assetstore import ( "bytes" "context" "encoding/base64" "errors" "image" "image/color" "image/jpeg" "image/png" "io" "os" "path/filepath" "strings" "testing" "cmroubao/backend-api/internal/usecase" ) func TestPutNormalizesPNGToBoundedWhiteJPEG(t *testing.T) { root := t.TempDir() store := mustStore(t, root) source := image.NewNRGBA(image.Rect(0, 0, 3000, 1200)) for y := 0; y < source.Bounds().Dy(); y++ { for x := 0; x < source.Bounds().Dx(); x++ { source.SetNRGBA(x, y, color.NRGBA{R: 20, G: 80, B: 140, A: 0}) } } var input bytes.Buffer if err := png.Encode(&input, source); err != nil { t.Fatalf("png.Encode() error = %v", err) } result, err := store.Put( context.Background(), "ignored-object-id", "image/png", bytes.NewReader(input.Bytes()), ) if err != nil { t.Fatalf("Put() error = %v", err) } if result.InputMediaType != "image/png" || result.MediaType != "image/jpeg" || len(result.InputSHA256) != 64 || len(result.SHA256) != 64 || result.SizeBytes <= 0 || strings.Contains(result.StorageKey, `\`) || strings.Contains(result.StorageKey, "..") { t.Fatalf("Put() result = %+v", result) } content, err := store.Open(context.Background(), result.StorageKey) if err != nil { t.Fatalf("Open() error = %v", err) } defer content.Close() normalized, format, err := image.Decode(content) if err != nil { t.Fatalf("image.Decode() error = %v", err) } if format != "jpeg" || normalized.Bounds().Dx() != MaxOutputDimension || normalized.Bounds().Dy() != 819 { t.Fatalf("normalized bounds/format = %v/%s", normalized.Bounds(), format) } red, green, blue, _ := normalized.At(20, 20).RGBA() if red < 0xf000 || green < 0xf000 || blue < 0xf000 { t.Fatalf( "transparent pixel was not composited on white: %x %x %x", red, green, blue, ) } } func TestPutAcceptsDecodableJPEGAndWebP(t *testing.T) { store := mustStore(t, t.TempDir()) var jpegInput bytes.Buffer if err := jpeg.Encode( &jpegInput, image.NewRGBA(image.Rect(0, 0, 4, 3)), &jpeg.Options{Quality: 80}, ); err != nil { t.Fatalf("jpeg.Encode() error = %v", err) } if _, err := store.Put( context.Background(), "jpeg", "image/jpeg", bytes.NewReader(jpegInput.Bytes()), ); err != nil { t.Fatalf("JPEG Put() error = %v", err) } webpBytes, err := base64.StdEncoding.DecodeString( "UklGRrIBAABXRUJQVlA4TKUBAAAvSsAYAA8w//M///MfeJAkbXvaSG7m8Q3GfYSB" + "JekwQztm/IcZlgwnmWImn2BK7aFmBtnVir6q//8VOkFE/xm4baTIu8c48ArEo6+B" + "3zFKYln3pqClSCKX0begFTAXFOLXHSyF8cCNcZEG4OywuA4KVVfJCiArU7GAgJI8" + "+lJP/OKMT/fBAjevg1cYB7YVkFuWga2lyPi5I0HFy5YTpWIHg0RZpkniRVW9odHA" + "KOwosWuOGdxIyn2OvaCDvhg/we6TwadPBPbqBV58MsLmMJ8yZnOWk8SRz4N+QoyP" + "L+MnamzMvcE1rHNEr91F9GKZPVUcS9w7PhhH36suB9qPeYb/oLk6cuTiJ0wOK3m" + "5h1cKjW6EVZCYMK7dxcKCBdgP9HkKr9gkAO2P8GKZGWVdIAatQa+1IDpt6qyorVw" + "dy01xdW8Jkfk6xjEXmVQQ+HQdFr6OKhIN34dXWq0+0qr6EJSCeeVLH9+gvGTLyqM" + "65PQ44ihzlTXxQKjKbAvshXgir7Lil9w4L2bvMycmjQcqXaMCO6BlY28i+FOLzbf" + "I1vEqxAhotocAAA==", ) if err != nil { t.Fatalf("decode WebP fixture: %v", err) } result, err := store.Put( context.Background(), "webp", "image/webp", bytes.NewReader(webpBytes), ) if err != nil { t.Fatalf("WebP Put() error = %v", err) } if result.InputMediaType != "image/webp" { t.Fatalf("WebP input type = %q", result.InputMediaType) } } func TestPutRejectsMismatchCorruptionAndOversizeAndCleansTemps(t *testing.T) { root := t.TempDir() store := mustStore(t, root) var valid bytes.Buffer if err := png.Encode( &valid, image.NewRGBA(image.Rect(0, 0, 2, 2)), ); err != nil { t.Fatalf("png.Encode() error = %v", err) } _, err := store.Put( context.Background(), "mismatch", "image/jpeg", bytes.NewReader(valid.Bytes()), ) assertStoreError(t, err, usecase.ImageStoreErrorUnsupported) _, err = store.Put( context.Background(), "corrupt", "image/png", bytes.NewReader([]byte("not an image")), ) assertStoreError(t, err, usecase.ImageStoreErrorInvalid) _, err = store.Put( context.Background(), "oversize", "image/png", io.LimitReader(zeroReader{}, MaxInputBytes+1), ) assertStoreError(t, err, usecase.ImageStoreErrorTooLarge) entries, err := os.ReadDir(root) if err != nil { t.Fatalf("ReadDir() error = %v", err) } for _, entry := range entries { if strings.HasPrefix(entry.Name(), ".input-") || strings.HasPrefix(entry.Name(), ".normalized-") { t.Fatalf("temporary file remained: %s", entry.Name()) } } } func TestOpenDeleteRejectTraversalAndDeleteIsIdempotent(t *testing.T) { store := mustStore(t, t.TempDir()) for _, key := range []string{ "../secret", "/absolute", `nested\file`, } { if _, err := store.Open(context.Background(), key); err == nil { t.Fatalf("Open(%q) error = nil", key) } } var input bytes.Buffer if err := jpeg.Encode( &input, image.NewRGBA(image.Rect(0, 0, 2, 2)), nil, ); err != nil { t.Fatalf("jpeg.Encode() error = %v", err) } result, err := store.Put( context.Background(), "delete", "image/jpeg", bytes.NewReader(input.Bytes()), ) if err != nil { t.Fatalf("Put() error = %v", err) } if err := store.Delete(context.Background(), result.StorageKey); err != nil { t.Fatalf("Delete() error = %v", err) } if err := store.Delete(context.Background(), result.StorageKey); err != nil { t.Fatalf("second Delete() error = %v", err) } if _, err := store.Open(context.Background(), result.StorageKey); err == nil { t.Fatal("Open() after Delete() error = nil") } } func TestNewRejectsFileAsRoot(t *testing.T) { path := filepath.Join(t.TempDir(), "root-file") if err := os.WriteFile(path, []byte("x"), 0o600); err != nil { t.Fatalf("WriteFile() error = %v", err) } if _, err := New(path); err == nil { t.Fatal("New(file) error = nil") } } func mustStore(t *testing.T, root string) *Store { t.Helper() store, err := New(root) if err != nil { t.Fatalf("New() error = %v", err) } return store } func assertStoreError( t *testing.T, err error, kind usecase.ImageStoreErrorKind, ) { t.Helper() var typed *usecase.ImageStoreError if !errors.As(err, &typed) { t.Fatalf("error = %v, want ImageStoreError", err) } if typed.Kind != kind { t.Fatalf("error kind = %s, want %s", typed.Kind, kind) } } type zeroReader struct{} func (zeroReader) Read(buffer []byte) (int, error) { for index := range buffer { buffer[index] = 0 } return len(buffer), nil }