190 lines
5.5 KiB
Go
190 lines
5.5 KiB
Go
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: "ignored"})
|
||
if e := a.store.SetMeta(fileStorageKey, string(cfg)); e != nil {
|
||
t.Fatal(e)
|
||
}
|
||
_ = a.store.SetMeta("sync_access_token", "jwt-token-1")
|
||
_ = a.store.SetMeta("sync_user_id", "9")
|
||
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 jwt-token-1" {
|
||
t.Fatalf("missing bearer jwt, 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 }
|