Files
code-utils/avatar_test.go
2026-08-14 07:52:01 +08:00

179 lines
4.8 KiB
Go

package main
import (
"bytes"
"encoding/base64"
"image"
"image/color"
"image/jpeg"
"image/png"
"os"
"path/filepath"
"strings"
"testing"
)
func writeTestImage(t *testing.T, name string, w, h int, asJPEG bool) string {
t.Helper()
img := image.NewRGBA(image.Rect(0, 0, w, h))
for x := 0; x < w; x += 3 {
for y := 0; y < h; y += 3 {
img.Set(x, y, color.RGBA{R: uint8(x % 255), G: uint8(y % 255), B: 128, A: 255})
}
}
var buf bytes.Buffer
var e error
if asJPEG {
e = jpeg.Encode(&buf, img, nil)
} else {
e = png.Encode(&buf, img)
}
if e != nil {
t.Fatal(e)
}
p := filepath.Join(t.TempDir(), name)
if e := os.WriteFile(p, buf.Bytes(), 0o644); e != nil {
t.Fatal(e)
}
return p
}
func decodeDataURL(t *testing.T, dataURL string) image.Image {
t.Helper()
i := strings.Index(dataURL, ";base64,")
if !strings.HasPrefix(dataURL, "data:image/") || i < 0 {
t.Fatalf("not a data URL: %.40s", dataURL)
}
raw, e := base64.StdEncoding.DecodeString(dataURL[i+8:])
if e != nil {
t.Fatal(e)
}
img, _, e := image.Decode(bytes.NewReader(raw))
if e != nil {
t.Fatal(e)
}
return img
}
func TestImageFileToDataURLDownscalesLargeImages(t *testing.T) {
p := writeTestImage(t, "big.png", 600, 400, false)
dataURL, e := imageFileToDataURL(p)
if e != nil {
t.Fatal(e)
}
if !strings.HasPrefix(dataURL, "data:image/png;base64,") {
t.Fatalf("png source should stay png: %.40s", dataURL)
}
img := decodeDataURL(t, dataURL)
if b := img.Bounds(); b.Dx() != 256 || b.Dy() != 170 {
t.Fatalf("expected 256x170, got %dx%d", b.Dx(), b.Dy())
}
}
func TestImageFileToDataURLKeepsSmallJPEG(t *testing.T) {
p := writeTestImage(t, "small.jpg", 120, 80, true)
dataURL, e := imageFileToDataURL(p)
if e != nil {
t.Fatal(e)
}
if !strings.HasPrefix(dataURL, "data:image/jpeg;base64,") {
t.Fatalf("jpeg source should stay jpeg: %.40s", dataURL)
}
img := decodeDataURL(t, dataURL)
if b := img.Bounds(); b.Dx() != 120 || b.Dy() != 80 {
t.Fatalf("small image should keep size, got %dx%d", b.Dx(), b.Dy())
}
}
func TestImageFileToDataURLErrors(t *testing.T) {
if _, e := imageFileToDataURL(filepath.Join(t.TempDir(), "missing.png")); e == nil || e.Error() != "FILE_NOT_FOUND" {
t.Fatalf("want FILE_NOT_FOUND, got %v", e)
}
bad := filepath.Join(t.TempDir(), "not-image.png")
if e := os.WriteFile(bad, []byte("hello world"), 0o644); e != nil {
t.Fatal(e)
}
if _, e := imageFileToDataURL(bad); e == nil || e.Error() != "AVATAR_DECODE_FAILED" {
t.Fatalf("want AVATAR_DECODE_FAILED, got %v", e)
}
}
func TestEnsureDefaultAvatar(t *testing.T) {
s, e := OpenStore(filepath.Join(t.TempDir(), "defavatar.db"))
if e != nil {
t.Fatal(e)
}
defer s.db.Close()
a := &App{store: s}
a.ensureDefaultAvatar()
st, e := s.Settings()
if e != nil {
t.Fatal(e)
}
if st.AvatarMode != "base64" || !strings.HasPrefix(st.AvatarValue, "data:image/") {
t.Fatalf("default avatar not set: mode=%q value=%.40s", st.AvatarMode, st.AvatarValue)
}
if s.Meta("avatar_updated_at") == "" {
t.Fatal("default avatar should bump avatar_updated_at so sync pushes it")
}
if img := decodeDataURL(t, st.AvatarValue); img.Bounds().Dx() > avatarMaxEdge || img.Bounds().Dy() > avatarMaxEdge {
t.Fatalf("default avatar should fit %dpx, got %v", avatarMaxEdge, img.Bounds())
}
// 已设置过头像时不覆盖。
st.AvatarMode, st.AvatarValue = "url", "https://cdn.example.com/me.png"
if e = s.SaveSettings(st); e != nil {
t.Fatal(e)
}
a.ensureDefaultAvatar()
got, e := s.Settings()
if e != nil {
t.Fatal(e)
}
if got.AvatarMode != "url" || got.AvatarValue != "https://cdn.example.com/me.png" {
t.Fatalf("existing avatar should not be overwritten: mode=%q", got.AvatarMode)
}
}
func TestSaveSettingsBumpsAvatarTimestampOnlyOnAvatarChange(t *testing.T) {
s, e := OpenStore(filepath.Join(t.TempDir(), "avatar.db"))
if e != nil {
t.Fatal(e)
}
defer s.db.Close()
base, e := s.Settings()
if e != nil {
t.Fatal(e)
}
base.Theme = "light"
if e = s.SaveSettings(base); e != nil {
t.Fatal(e)
}
if got := s.Meta("avatar_updated_at"); got != "" {
t.Fatalf("non-avatar save should not bump avatar timestamp, got %q", got)
}
base.AvatarMode, base.AvatarValue = "url", "https://cdn.example.com/a.png"
if e = s.SaveSettings(base); e != nil {
t.Fatal(e)
}
first := s.Meta("avatar_updated_at")
if first == "" {
t.Fatal("avatar change should bump timestamp")
}
got, e := s.Settings()
if e != nil {
t.Fatal(e)
}
if got.AvatarMode != "url" || got.AvatarValue != "https://cdn.example.com/a.png" {
t.Fatalf("avatar not persisted: %+v", got)
}
// 非法模式会被清空存储。
base.AvatarMode, base.AvatarValue = "weird", "x"
if e = s.SaveSettings(base); e != nil {
t.Fatal(e)
}
got, _ = s.Settings()
if got.AvatarMode != "" || got.AvatarValue != "" {
t.Fatalf("invalid mode should clear avatar, got %+v", got)
}
}