Files
iu 04617d1cdb feat(profile): add round avatar with upload and cropping
- backend: user_avatar_objects binding (PG+SQLite), GC preserves avatar blobs,
  avatar service (EXIF-stripping WebP 128/512), public avatar read route
- web: avatar upload/crop on profile, round avatar in header/members/comments/author
- android: avatar upload/crop, round avatar on profile and albums header
- openspec: add-user-avatar proposal/design/specs/tasks
2026-10-12 16:16:47 +03:00

90 lines
2.6 KiB
Go

package avatar
import (
"bytes"
"errors"
"image"
"image/color"
"image/jpeg"
"testing"
"github.com/gen2brain/webp"
)
// jpegImage builds a non-square JPEG so the square centre crop is observable.
func jpegImage(t *testing.T, w, h int) []byte {
t.Helper()
img := image.NewRGBA(image.Rect(0, 0, w, h))
for x := 0; x < w; x++ {
for y := 0; y < h; y++ {
img.Set(x, y, color.RGBA{R: uint8(x % 256), G: uint8(y % 256), B: 128, A: 255})
}
}
var buf bytes.Buffer
if err := jpeg.Encode(&buf, img, nil); err != nil {
t.Fatalf("encode jpeg: %v", err)
}
return buf.Bytes()
}
// withEXIFSegment inserts an APP1 segment carrying an "Exif\x00\x00" marker.
// The payload stands in for real EXIF metadata (including GPS): the JPEG
// decoder ignores it, and re-encoding must drop it.
func withEXIFSegment(t *testing.T, jpegData []byte) []byte {
t.Helper()
payload := append([]byte("Exif\x00\x00"), 0x00, 0x00) // bogus TIFF header
segLen := len(payload) + 2
seg := []byte{0xFF, 0xE1, byte(segLen >> 8), byte(segLen & 0xFF)}
seg = append(seg, payload...)
if len(jpegData) < 2 {
t.Fatalf("not a jpeg")
}
out := append([]byte{}, jpegData[:2]...)
out = append(out, seg...)
out = append(out, jpegData[2:]...)
return out
}
func TestNormalizeSquaresAndStripsMetadata(t *testing.T) {
source := withEXIFSegment(t, jpegImage(t, 800, 400))
if !bytes.Contains(source, []byte("Exif")) {
t.Fatalf("test setup: source has no EXIF marker")
}
large, small, err := normalize(source)
if err != nil {
t.Fatalf("normalize: %v", err)
}
cfg, err := webp.DecodeConfig(bytes.NewReader(large))
if err != nil {
t.Fatalf("decode large: %v", err)
}
if cfg.Width != SizeLarge || cfg.Height != SizeLarge {
t.Fatalf("large = %dx%d, want %dx%d", cfg.Width, cfg.Height, SizeLarge, SizeLarge)
}
cfg, err = webp.DecodeConfig(bytes.NewReader(small))
if err != nil {
t.Fatalf("decode small: %v", err)
}
if cfg.Width != SizeSmall || cfg.Height != SizeSmall {
t.Fatalf("small = %dx%d, want %dx%d", cfg.Width, cfg.Height, SizeSmall, SizeSmall)
}
// Re-encoding must discard the source metadata.
for name, data := range map[string][]byte{"large": large, "small": small} {
if bytes.Contains(data, []byte("Exif")) {
t.Fatalf("%s WebP still contains an EXIF marker", name)
}
if _, err := webp.DecodeExif(bytes.NewReader(data)); !errors.Is(err, webp.ErrNoExif) {
t.Fatalf("%s metadata not stripped: %v", name, err)
}
}
}
func TestNormalizeRejectsNonImage(t *testing.T) {
if _, _, err := normalize([]byte("definitely not an image")); !errors.Is(err, errInvalidImage) {
t.Fatalf("err = %v, want errInvalidImage", err)
}
}