Files
code-utils/images_test.go

188 lines
5.4 KiB
Go
Raw Normal View History

2026-08-14 07:52:01 +08:00
package main
import (
"bytes"
"encoding/base64"
"encoding/json"
"image"
"image/color"
"image/png"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
)
// pngDataURL builds a w-by-h PNG dataURL; opaque=false adds transparency
// so tests can hit both the JPEG (opaque) and PNG (alpha) encode branches.
func pngDataURL(t *testing.T, w, h int, opaque bool) string {
t.Helper()
img := image.NewNRGBA(image.Rect(0, 0, w, h))
a := uint8(255)
if !opaque {
a = 128
}
for x := 0; x < w; x++ {
for y := 0; y < h; y++ {
img.Set(x, y, color.NRGBA{R: 200, G: 90, B: 60, A: a})
}
}
var buf bytes.Buffer
if e := png.Encode(&buf, img); e != nil {
t.Fatal(e)
}
return "data:image/png;base64," + base64.StdEncoding.EncodeToString(buf.Bytes())
}
func TestSaveContentImageBase64Mode(t *testing.T) {
a := newSyncTestApp(t)
// Opaque large image: downscaled to contentImgMaxEdge and re-encoded as JPEG.
got, e := a.SaveContentImage(pngDataURL(t, 2200, 1100, true))
if e != nil {
t.Fatal(e)
}
if !strings.HasPrefix(got, "data:image/jpeg;base64,") {
t.Fatalf("opaque image should become jpeg dataURL: %.40s", got)
}
img := decodeDataURL(t, got)
if b := img.Bounds(); b.Dx() != contentImgMaxEdge {
t.Fatalf("expected width %d, got %d", contentImgMaxEdge, b.Dx())
}
// Semi-transparent image must stay PNG to keep the alpha channel.
got, e = a.SaveContentImage(pngDataURL(t, 40, 40, false))
if e != nil {
t.Fatal(e)
}
if !strings.HasPrefix(got, "data:image/png;base64,") {
t.Fatalf("transparent image should stay png: %.40s", got)
}
}
func TestSaveContentImagePathMode(t *testing.T) {
a := newSyncTestApp(t)
st, _ := a.store.Settings()
st.ImageMode = "path"
if e := a.store.SaveSettings(st); e != nil {
t.Fatal(e)
}
got, e := a.SaveContentImage(pngDataURL(t, 64, 64, true))
if e != nil {
t.Fatal(e)
}
if strings.Contains(got, "\\") || !strings.Contains(got, "/images/img-") {
t.Fatalf("path mode should return forward-slash path in images dir: %s", got)
}
if _, e := os.Stat(filepath.FromSlash(got)); e != nil {
t.Fatalf("saved image file missing: %v", e)
}
// Files written in path mode must be readable back as dataURL for rendering.
dataURL, e := a.ReadContentImageAsDataURL(filepath.FromSlash(got))
if e != nil {
t.Fatal(e)
}
if !strings.HasPrefix(dataURL, "data:image/") {
t.Fatalf("unexpected dataURL: %.40s", dataURL)
}
}
func TestSaveContentImageRejectsBadInput(t *testing.T) {
a := newSyncTestApp(t)
for _, bad := range []string{"", "hello", "data:text/plain;base64,aGk=", "data:image/png;base64,!!!"} {
if _, e := a.SaveContentImage(bad); e == nil {
t.Fatalf("input %q should fail", bad)
}
}
}
func TestSettingsImageModeRoundTrip(t *testing.T) {
s, e := OpenStore(filepath.Join(t.TempDir(), "im.db"))
if e != nil {
t.Fatal(e)
}
defer s.db.Close()
st, _ := s.Settings()
if st.ImageMode != "base64" {
t.Fatalf("default should be base64, got %q", st.ImageMode)
}
st.ImageMode = "path"
if e = s.SaveSettings(st); e != nil {
t.Fatal(e)
}
st, _ = s.Settings()
if st.ImageMode != "path" {
t.Fatalf("imageMode not persisted, got %q", st.ImageMode)
}
// server 是合法模式(上传到 nl-pms-api
st.ImageMode = "server"
if e = s.SaveSettings(st); e != nil {
t.Fatal(e)
}
st, _ = s.Settings()
if st.ImageMode != "server" {
t.Fatalf("server mode should persist, got %q", st.ImageMode)
}
// Invalid value falls back to the default.
st.ImageMode = "oss"
if e = s.SaveSettings(st); e != nil {
t.Fatal(e)
}
st, _ = s.Settings()
if st.ImageMode != "base64" {
t.Fatalf("invalid mode should fall back to base64, got %q", st.ImageMode)
}
}
// TestSaveContentImageServerMode 用本地 httptest 假扮 nl-pms-api
// server 模式下内容图上传成功返回 http URL服务器不可达时报错而非静默降级。
func TestSaveContentImageServerMode(t *testing.T) {
a := newSyncTestApp(t)
var gotAuth, gotKind string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != "/api/v1/files" {
http.NotFound(w, r)
return
}
gotAuth = r.Header.Get("Authorization")
if e := r.ParseMultipartForm(32 << 20); e != nil {
http.Error(w, "bad form", 400)
return
}
gotKind = r.FormValue("kind")
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id":1,"name":"2026/08/13/abc.jpg","url":"` + serverBase(r) + `/files/2026/08/13/abc.jpg"}`))
}))
defer srv.Close()
cfg, _ := json.Marshal(FileStorageConfig{Mode: "server", BaseURL: srv.URL, APIKey: "k1"})
if e := a.store.SetMeta(fileStorageKey, string(cfg)); e != nil {
t.Fatal(e)
}
st, _ := a.store.Settings()
st.ImageMode = "server"
if e := a.store.SaveSettings(st); e != nil {
t.Fatal(e)
}
got, e := a.SaveContentImage(pngDataURL(t, 64, 64, true))
if e != nil {
t.Fatal(e)
}
if !strings.HasPrefix(got, "http") || !strings.Contains(got, "/files/") {
t.Fatalf("server mode should return http url, got %q", got)
}
if gotAuth != "Bearer k1" {
t.Fatalf("missing bearer key, got %q", gotAuth)
}
if gotKind != "content" {
t.Fatalf("kind should be content, got %q", gotKind)
}
// 服务器关闭后上传必须报错(不能静默转 base64
srv.Close()
if _, e := a.SaveContentImage(pngDataURL(t, 32, 32, true)); e == nil {
t.Fatal("upload to dead server should fail")
}
}
func serverBase(r *http.Request) string { return "http://" + r.Host }