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

188 lines
5.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 }