179 lines
4.8 KiB
Go
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)
|
|
}
|
|
}
|