Files
NianAIGC/backend/internal/assets/image_format_test.go
2026-09-23 13:33:47 +08:00

171 lines
6.7 KiB
Go

package assets
import (
"bytes"
"context"
"encoding/base64"
"errors"
"image"
"image/jpeg"
"image/png"
"io"
"strings"
"testing"
)
func imageFormatFixtures(t *testing.T) (pngBytes, jpegBytes, webpBytes []byte) {
t.Helper()
img := image.NewRGBA(image.Rect(0, 0, 2, 2))
var p, j bytes.Buffer
if err := png.Encode(&p, img); err != nil {
t.Fatal(err)
}
if err := jpeg.Encode(&j, img, nil); err != nil {
t.Fatal(err)
}
w, err := base64.StdEncoding.DecodeString("UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEADsD+JaQAA3AAAAAA")
if err != nil {
t.Fatal(err)
}
return p.Bytes(), j.Bytes(), w
}
func TestNormalizeImageFileUsesBytesAndPreservesCompatibleNames(t *testing.T) {
pngData, jpegData, webpData := imageFormatFixtures(t)
for _, test := range []struct {
label, name, mime, wantName, wantMIME string
data []byte
}{
{"opaque PNG", "result.image", "application/octet-stream", "result.png", "image/png", pngData},
{"JPEG", "result.image", "", "result.jpg", "image/jpeg", jpegData},
{"WebP despite bad MIME", "result.image", "image/png", "result.webp", "image/webp", webpData},
{"wrong extension", "result.png", "image/png", "result.jpg", "image/jpeg", jpegData},
{"no suffix", "result", "", "result.png", "image/png", pngData},
{"uppercase", "result.PNG", "image/png; charset=binary", "result.PNG", "image/png", pngData},
{"JPEG alias", "result.JPEG", "application/octet-stream", "result.JPEG", "image/jpeg", jpegData},
{"Chinese filename", "葫芦娃和爷爷.image", "", "葫芦娃和爷爷.png", "image/png", pngData},
{"multi-dot filename", "result.v1.image", "", "result.v1.png", "image/png", pngData},
{"empty filename", "", "", "image.png", "image/png", pngData},
{"video", "result.mp4", "video/mp4", "result.mp4", "video/mp4", []byte("video content")},
{"unknown does not trust MIME", "result.image", "image/png", "result.image", "image/png", []byte(`{"error":"not an image"}`)},
{"empty content", "result.image", "", "result.image", "", nil},
} {
t.Run(test.label, func(t *testing.T) {
name, mime := normalizeImageFile(test.name, test.mime, test.data)
if name != test.wantName || mime != test.wantMIME {
t.Fatalf("got %q / %q, want %q / %q", name, mime, test.wantName, test.wantMIME)
}
})
}
}
func TestImportGeneratedNormalizesImageNameStorageAndContentType(t *testing.T) {
pngData, jpegData, webpData := imageFormatFixtures(t)
for _, test := range []struct {
extension, contentType string
data []byte
}{{"png", "image/png", pngData}, {"jpg", "image/jpeg", jpegData}, {"webp", "image/webp", webpData}} {
t.Run(test.extension, func(t *testing.T) {
cat, blobs := &memoryCatalog{}, &memoryBlobs{}
remote := &memoryRemote{blob: Blob{Body: io.NopCloser(bytes.NewReader(test.data)), ContentType: "application/octet-stream", Size: int64(len(test.data))}}
svc := NewService(cat, blobs, remote, nil, func(prefix string) string { return prefix + "-format" })
url := "https://provider.test/result.image?signature=fixture"
asset, err := svc.ImportGenerated(context.Background(), PlatformScope("owner-a"), ImportGeneratedCommand{URL: url, Capability: "image.generate"})
if err != nil {
t.Fatal(err)
}
if asset.Name != "result."+test.extension || !strings.HasSuffix(asset.StoragePath, "-result."+test.extension) || asset.Kind != KindImage {
t.Fatalf("asset=%#v", asset)
}
if asset.Metadata["contentType"] != test.contentType || asset.Metadata["importedFrom"] != url || blobs.putContentType != test.contentType {
t.Fatalf("metadata=%#v stored MIME=%q", asset.Metadata, blobs.putContentType)
}
if !bytes.Equal(blobs.putBody, test.data) {
t.Fatal("renaming changed the original image bytes")
}
})
}
}
func TestUploadRecognizesReuploadedLegacyImage(t *testing.T) {
pngData, _, _ := imageFormatFixtures(t)
cat, blobs := &memoryCatalog{}, &memoryBlobs{}
svc := NewService(cat, blobs, nil, nil, nil)
asset, err := svc.Upload(context.Background(), PlatformScope("owner-a"), UploadCommand{FileName: "download.image", ContentType: "application/octet-stream", Bytes: pngData})
if err != nil {
t.Fatal(err)
}
if asset.Name != "download.png" || asset.Kind != KindImage || asset.Metadata["contentType"] != "image/png" || !strings.HasSuffix(asset.StoragePath, "-download.png") || !bytes.Equal(blobs.putBody, pngData) {
t.Fatalf("upload=%#v", asset)
}
}
func TestNormalizeImageDownloadStreamsAllBytesAndClosesOriginal(t *testing.T) {
pngData, _, _ := imageFormatFixtures(t)
for _, data := range [][]byte{pngData, append(append([]byte(nil), pngData...), bytes.Repeat([]byte{7}, 4096)...)} {
body := &imageTrackedBody{Reader: bytes.NewReader(data)}
name, blob, err := NormalizeImageDownload("legacy.image", Blob{Body: body, ContentType: "application/octet-stream", Size: int64(len(data))})
if err != nil || name != "legacy.png" || blob.ContentType != "image/png" || blob.Size != int64(len(data)) {
t.Fatalf("name=%q blob=%#v err=%v", name, blob, err)
}
if body.readBytes > 512 || body.closed {
t.Fatalf("prefix read consumed %d bytes, closed=%v", body.readBytes, body.closed)
}
got, err := io.ReadAll(blob.Body)
if err != nil || !bytes.Equal(got, data) {
t.Fatalf("stream mismatch; error=%v", err)
}
if err := blob.Body.Close(); err != nil || !body.closed {
t.Fatalf("original body not closed; error=%v", err)
}
}
}
func TestNormalizeImageDownloadUnknownDataIsNotRenamed(t *testing.T) {
for _, data := range [][]byte{[]byte("video bytes"), nil} {
name, blob, err := NormalizeImageDownload("legacy.image", Blob{Body: io.NopCloser(bytes.NewReader(data)), ContentType: "application/octet-stream", Size: int64(len(data))})
if err != nil || name != "legacy.image" || blob.ContentType != "application/octet-stream" {
t.Fatalf("name=%q blob=%#v err=%v", name, blob, err)
}
got, _ := io.ReadAll(blob.Body)
_ = blob.Body.Close()
if !bytes.Equal(got, data) {
t.Fatal("unknown bytes changed")
}
}
}
func TestNormalizeImageDownloadReadError(t *testing.T) {
failure := errors.New("read failed")
body := &imageTrackedBody{Reader: imageErrorReader{err: failure}}
_, _, err := NormalizeImageDownload("legacy.image", Blob{Body: body})
if !errors.Is(err, failure) || body.closed {
t.Fatalf("error=%v closed=%v", err, body.closed)
}
_ = body.Close()
if _, _, err := NormalizeImageDownload("legacy.image", Blob{}); !errors.Is(err, ErrBlobNotFound) {
t.Fatalf("nil body error=%v", err)
}
}
type imageTrackedBody struct {
io.Reader
readBytes int
closed bool
}
func (b *imageTrackedBody) Read(p []byte) (int, error) {
n, err := b.Reader.Read(p)
b.readBytes += n
return n, err
}
func (b *imageTrackedBody) Close() error {
b.closed = true
return nil
}
type imageErrorReader struct{ err error }
func (r imageErrorReader) Read([]byte) (int, error) { return 0, r.err }