256 lines
6.3 KiB
Go
256 lines
6.3 KiB
Go
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
|
|
}
|