mirror of
https://github.com/truewhile/MeBox.git
synced 2026-11-12 19:34:24 +08:00
229 lines
7.7 KiB
Go
229 lines
7.7 KiB
Go
package handler
|
|
|
|
import (
|
|
"bytes"
|
|
"image"
|
|
"image/color"
|
|
"image/jpeg"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/glebarez/sqlite"
|
|
"go.uber.org/zap"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/truewhile/MeBox/internal/config"
|
|
"github.com/truewhile/MeBox/internal/model"
|
|
"github.com/truewhile/MeBox/internal/repository"
|
|
"github.com/truewhile/MeBox/internal/service"
|
|
)
|
|
|
|
func coverTestJPEG(t *testing.T, c color.RGBA) []byte {
|
|
t.Helper()
|
|
img := image.NewRGBA(image.Rect(0, 0, 160, 240))
|
|
for y := 0; y < 240; y++ {
|
|
for x := 0; x < 160; x++ {
|
|
img.Set(x, y, c)
|
|
}
|
|
}
|
|
var buf bytes.Buffer
|
|
if err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: 90}); err != nil {
|
|
t.Fatalf("encode jpeg: %v", err)
|
|
}
|
|
return buf.Bytes()
|
|
}
|
|
|
|
// newLibraryCoverTestRouter 构造一个带真实图片代理的路由:成员海报来自
|
|
// 本地 httptest 服务,走的是与生产一致的 ImageProxy 抓取路径。
|
|
func newLibraryCoverTestRouter(t *testing.T) (*gin.Engine, *repository.Container, *httptest.Server) {
|
|
t.Helper()
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
poster := coverTestJPEG(t, color.RGBA{R: 200, G: 120, B: 60, A: 255})
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "image/jpeg")
|
|
_, _ = w.Write(poster)
|
|
}))
|
|
t.Cleanup(upstream.Close)
|
|
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatalf("open db: %v", err)
|
|
}
|
|
if err := db.AutoMigrate(&model.Library{}, &model.Media{}); err != nil {
|
|
t.Fatalf("migrate: %v", err)
|
|
}
|
|
repos := repository.New(db)
|
|
|
|
parsed, err := url.Parse(upstream.URL)
|
|
if err != nil {
|
|
t.Fatalf("parse upstream: %v", err)
|
|
}
|
|
cfg := &config.Config{Cache: config.CacheConfig{CacheDir: filepath.Join(t.TempDir(), "cache")}}
|
|
proxy := service.NewImageProxy(cfg, zap.NewNop())
|
|
proxy.SetAllowedRemoteHostsProvider(func() []string { return []string{parsed.Host} })
|
|
|
|
svc := &service.Container{Repo: repos, ImageProxy: proxy}
|
|
router := gin.New()
|
|
router.GET("/libraries/:id/cover", libraryCoverHandler(svc))
|
|
router.HEAD("/libraries/:id/cover", libraryCoverHandler(svc))
|
|
return router, repos, upstream
|
|
}
|
|
|
|
func seedCoverLibrary(t *testing.T, repos *repository.Container, posterBase string, coverURL string) model.Library {
|
|
t.Helper()
|
|
lib := model.Library{Name: "电影", Path: "/media/movies", Type: "movie", Enabled: true, CoverURL: coverURL}
|
|
if err := repos.Library.Create(t.Context(), &lib); err != nil {
|
|
t.Fatalf("create library: %v", err)
|
|
}
|
|
for i := 0; i < 4; i++ {
|
|
row := model.Media{
|
|
Base: model.Base{ID: "m" + string(rune('a'+i))},
|
|
LibraryID: lib.ID,
|
|
Title: "Movie " + string(rune('A'+i)),
|
|
Path: "/media/movies/m" + string(rune('a'+i)) + ".mkv",
|
|
PosterURL: posterBase + "/p" + string(rune('1'+i)) + ".jpg",
|
|
}
|
|
if err := repos.DB.Create(&row).Error; err != nil {
|
|
t.Fatalf("create media: %v", err)
|
|
}
|
|
}
|
|
return lib
|
|
}
|
|
|
|
// TestLibraryCoverHandlerServesGeneratedCollage 覆盖 Web/App 的封面接口:
|
|
// 返回的是服务端拼好的 16:9 图片,而不是单张成员海报。
|
|
func TestLibraryCoverHandlerServesGeneratedCollage(t *testing.T) {
|
|
router, repos, upstream := newLibraryCoverTestRouter(t)
|
|
lib := seedCoverLibrary(t, repos, upstream.URL, "")
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/libraries/"+lib.ID+"/cover?w=480&h=270", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status = %d body=%s", w.Code, w.Body.String())
|
|
}
|
|
if ct := w.Header().Get("Content-Type"); ct != "image/jpeg" {
|
|
t.Fatalf("content type = %q, want image/jpeg", ct)
|
|
}
|
|
img, err := jpeg.Decode(bytes.NewReader(w.Body.Bytes()))
|
|
if err != nil {
|
|
t.Fatalf("decode cover: %v", err)
|
|
}
|
|
if img.Bounds().Dx() != 480 || img.Bounds().Dy() != 270 {
|
|
t.Fatalf("cover size = %v, want 480x270", img.Bounds())
|
|
}
|
|
if etag := w.Header().Get("ETag"); etag == "" {
|
|
t.Fatal("expected an ETag so clients can revalidate")
|
|
}
|
|
|
|
// 条件请求应拿到 304。
|
|
req = httptest.NewRequest(http.MethodGet, "/libraries/"+lib.ID+"/cover?w=480&h=270", nil)
|
|
req.Header.Set("If-None-Match", w.Header().Get("ETag"))
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusNotModified {
|
|
t.Fatalf("conditional request status = %d, want 304", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestLibraryCoverHandlerUnknownLibraryReturns404(t *testing.T) {
|
|
router, _, _ := newLibraryCoverTestRouter(t)
|
|
req := httptest.NewRequest(http.MethodGet, "/libraries/missing/cover", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
if w.Code != http.StatusNotFound {
|
|
t.Fatalf("status = %d, want 404", w.Code)
|
|
}
|
|
}
|
|
|
|
// TestLibraryCoverHandlerServesCustomCover 管理员填了 cover_url 时必须继续
|
|
// 用自定义封面,不能在不知不觉中换成拼图。
|
|
func TestLibraryCoverHandlerServesCustomCover(t *testing.T) {
|
|
router, repos, upstream := newLibraryCoverTestRouter(t)
|
|
lib := seedCoverLibrary(t, repos, upstream.URL, upstream.URL+"/custom.jpg")
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/libraries/"+lib.ID+"/cover", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status = %d body=%s", w.Code, w.Body.String())
|
|
}
|
|
if _, err := jpeg.Decode(bytes.NewReader(w.Body.Bytes())); err != nil {
|
|
t.Fatalf("custom cover should be an image: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestEmbyLibraryPrimaryUsesGeneratedCollage 保证 Emby 客户端请求库主图时
|
|
// 拿到的也是拼图(与 Web/App 同一份生成逻辑)。
|
|
func TestEmbyLibraryPrimaryUsesGeneratedCollage(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatalf("open db: %v", err)
|
|
}
|
|
if err := db.AutoMigrate(&model.Library{}, &model.Media{}); err != nil {
|
|
t.Fatalf("migrate: %v", err)
|
|
}
|
|
|
|
poster := coverTestJPEG(t, color.RGBA{G: 180, R: 90, B: 60, A: 255})
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "image/jpeg")
|
|
_, _ = w.Write(poster)
|
|
}))
|
|
defer upstream.Close()
|
|
parsed, _ := url.Parse(upstream.URL)
|
|
|
|
repos := repository.New(db)
|
|
lib := model.Library{Name: "剧集", Path: "/media/tv", Type: "tv", Enabled: true}
|
|
if err := repos.Library.Create(t.Context(), &lib); err != nil {
|
|
t.Fatalf("create library: %v", err)
|
|
}
|
|
for i := 0; i < 4; i++ {
|
|
row := model.Media{
|
|
Base: model.Base{ID: "e" + string(rune('a'+i))},
|
|
LibraryID: lib.ID,
|
|
Title: "Show " + string(rune('A'+i)),
|
|
Path: "/media/tv/show" + string(rune('a'+i)) + "/S01E01.mkv",
|
|
SeasonNum: 1, EpisodeNum: 1,
|
|
PosterURL: upstream.URL + "/p" + string(rune('1'+i)) + ".jpg",
|
|
}
|
|
if err := repos.DB.Create(&row).Error; err != nil {
|
|
t.Fatalf("create media: %v", err)
|
|
}
|
|
}
|
|
|
|
cfg := &config.Config{Cache: config.CacheConfig{CacheDir: filepath.Join(t.TempDir(), "cache")}}
|
|
proxy := service.NewImageProxy(cfg, zap.NewNop())
|
|
proxy.SetAllowedRemoteHostsProvider(func() []string { return []string{parsed.Host} })
|
|
svc := &service.Container{
|
|
Repo: repos,
|
|
Emby: service.NewEmbyService(cfg, zap.NewNop(), repos),
|
|
ImageProxy: proxy,
|
|
}
|
|
router := gin.New()
|
|
registerEmbyRoutes(router, "test-secret", svc)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/Items/"+lib.ID+"/Images/Primary?maxWidth=400&maxHeight=300", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status = %d body=%s", w.Code, w.Body.String())
|
|
}
|
|
img, err := jpeg.Decode(bytes.NewReader(w.Body.Bytes()))
|
|
if err != nil {
|
|
t.Fatalf("decode emby library cover: %v", err)
|
|
}
|
|
// 请求的是 400×300 的盒子,拼图按 16:9 装进去 → 400×225。
|
|
if img.Bounds().Dx() != 400 || img.Bounds().Dy() != 225 {
|
|
t.Fatalf("cover size = %v, want 400x225 (16:9 inside the requested box)", img.Bounds())
|
|
}
|
|
}
|