mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 11:16:37 +08:00
245 lines
7.7 KiB
Go
245 lines
7.7 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"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"
|
|
)
|
|
|
|
// newEmbyDiscoveryEnv 搭一个跑在内存库上的 Emby 路由环境。
|
|
func newEmbyDiscoveryEnv(t *testing.T) (*gin.Engine, *service.Container, string) {
|
|
t.Helper()
|
|
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.User{}, &model.Library{}, &model.Media{}, &model.PlaybackHistory{},
|
|
&model.Setting{}, &model.Favorite{}, &model.UserDevice{},
|
|
); err != nil {
|
|
t.Fatalf("migrate: %v", err)
|
|
}
|
|
repos := repository.New(db)
|
|
cfg := &config.Config{}
|
|
cfg.Secrets.JWTSecret = "test-secret"
|
|
|
|
svc := &service.Container{Repo: repos, Log: zap.NewNop()}
|
|
svc.Emby = service.NewEmbyService(cfg, zap.NewNop(), repos).
|
|
SetDiscovery(service.NewMediaDiscoveryService(zap.NewNop(), repos))
|
|
|
|
const userID = "user-1"
|
|
if err := repos.User.Create(context.Background(), &model.User{
|
|
Base: model.Base{ID: userID}, Username: "tester", PasswordHash: "x",
|
|
Role: "user", IsActive: true,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
router := gin.New()
|
|
registerEmbyRoutes(router, cfg.Secrets.JWTSecret, svc)
|
|
return router, svc, userID
|
|
}
|
|
|
|
func seedEmbyLibrary(t *testing.T, svc *service.Container, typ string) string {
|
|
t.Helper()
|
|
lib := &model.Library{Name: "库-" + typ, Path: "/media/" + typ, Type: typ, Enabled: true}
|
|
if err := svc.Repo.Library.Create(context.Background(), lib); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return lib.ID
|
|
}
|
|
|
|
func embyGet(t *testing.T, router *gin.Engine, path, token string) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
req := httptest.NewRequest(http.MethodGet, path, nil)
|
|
if token != "" {
|
|
req.Header.Set("X-Emby-Token", token)
|
|
}
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
return w
|
|
}
|
|
|
|
func decodeItemsEnvelope(t *testing.T, body []byte) []map[string]any {
|
|
t.Helper()
|
|
var payload struct {
|
|
Items []map[string]any `json:"Items"`
|
|
TotalRecordCount int64 `json:"TotalRecordCount"`
|
|
}
|
|
if err := json.Unmarshal(body, &payload); err != nil {
|
|
t.Fatalf("decode %s: %v", string(body), err)
|
|
}
|
|
return payload.Items
|
|
}
|
|
|
|
// NextUp 必须真的返回下一集,而不是空数组。
|
|
func TestEmbyNextUpReturnsNextEpisode(t *testing.T) {
|
|
router, svc, userID := newEmbyDiscoveryEnv(t)
|
|
libID := seedEmbyLibrary(t, svc, "tv")
|
|
watchedAt := time.Now().Add(-time.Hour)
|
|
|
|
for episode, watched := range map[int]bool{1: true, 2: false, 3: false} {
|
|
m := &model.Media{
|
|
LibraryID: libID, SeriesID: "series-1", Title: "剧一",
|
|
SeasonNum: 1, EpisodeNum: episode,
|
|
Path: "/media/tv/S1E" + string(rune('0'+episode)) + ".mkv",
|
|
}
|
|
if err := svc.Repo.DB.Create(m).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if watched {
|
|
h := &model.PlaybackHistory{
|
|
UserID: userID, MediaID: m.ID, PositionMs: 1000, DurationMs: 2000,
|
|
WatchedAt: watchedAt, Completed: false,
|
|
}
|
|
if err := svc.Repo.DB.Create(h).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
w := embyGet(t, router, "/emby/Shows/NextUp", signedTestToken(t, "test-secret"))
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status = %d body=%s", w.Code, w.Body.String())
|
|
}
|
|
items := decodeItemsEnvelope(t, w.Body.Bytes())
|
|
if len(items) != 1 {
|
|
t.Fatalf("items = %d, want 1 (body=%s)", len(items), w.Body.String())
|
|
}
|
|
if index, ok := items[0]["IndexNumber"].(float64); !ok || int(index) != 2 {
|
|
t.Fatalf("IndexNumber = %v, want 2 (body=%s)", items[0]["IndexNumber"], w.Body.String())
|
|
}
|
|
}
|
|
|
|
// 没有历史时必须返回合法空信封,不能 404/500。
|
|
func TestEmbyNextUpEmptyWithoutHistory(t *testing.T) {
|
|
router, _, _ := newEmbyDiscoveryEnv(t)
|
|
|
|
w := embyGet(t, router, "/emby/Shows/NextUp", signedTestToken(t, "test-secret"))
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status = %d body=%s", w.Code, w.Body.String())
|
|
}
|
|
if items := decodeItemsEnvelope(t, w.Body.Bytes()); len(items) != 0 {
|
|
t.Fatalf("items = %d, want 0", len(items))
|
|
}
|
|
}
|
|
|
|
// 小写别名路由同样要走到真实实现(客户端路径大小写并不统一)。
|
|
func TestEmbyNextUpLowercaseAlias(t *testing.T) {
|
|
router, svc, _ := newEmbyDiscoveryEnv(t)
|
|
_ = seedEmbyLibrary(t, svc, "tv")
|
|
|
|
w := embyGet(t, router, "/emby/shows/nextup", signedTestToken(t, "test-secret"))
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status = %d body=%s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// Similar 对不存在的条目返回空列表(客户端详情页会无条件请求)。
|
|
func TestEmbySimilarUnknownItemReturnsEmpty(t *testing.T) {
|
|
router, _, _ := newEmbyDiscoveryEnv(t)
|
|
|
|
w := embyGet(t, router, "/emby/Items/does-not-exist/Similar", signedTestToken(t, "test-secret"))
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status = %d body=%s", w.Code, w.Body.String())
|
|
}
|
|
if items := decodeItemsEnvelope(t, w.Body.Bytes()); len(items) != 0 {
|
|
t.Fatalf("items = %d, want 0", len(items))
|
|
}
|
|
}
|
|
|
|
func TestEmbySimilarReturnsCandidates(t *testing.T) {
|
|
router, svc, _ := newEmbyDiscoveryEnv(t)
|
|
libID := seedEmbyLibrary(t, svc, "movie")
|
|
|
|
source := &model.Media{
|
|
LibraryID: libID, Title: "源片", Genres: "Action", Year: 2010, Rating: 8,
|
|
Path: "/media/movie/source.mkv",
|
|
}
|
|
if err := svc.Repo.DB.Create(source).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
other := &model.Media{
|
|
LibraryID: libID, Title: "同类片", Genres: "Action", Year: 2011, Rating: 8,
|
|
Path: "/media/movie/other.mkv",
|
|
}
|
|
if err := svc.Repo.DB.Create(other).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
w := embyGet(t, router, "/emby/Items/"+source.ID+"/Similar", signedTestToken(t, "test-secret"))
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status = %d body=%s", w.Code, w.Body.String())
|
|
}
|
|
items := decodeItemsEnvelope(t, w.Body.Bytes())
|
|
if len(items) != 1 {
|
|
t.Fatalf("items = %d, want 1 (body=%s)", len(items), w.Body.String())
|
|
}
|
|
if name, _ := items[0]["Name"].(string); name != "同类片" {
|
|
t.Fatalf("Name = %q, want 同类片", name)
|
|
}
|
|
}
|
|
|
|
// Genres 必须返回真实类型与计数。
|
|
func TestEmbyGenresReturnsCounts(t *testing.T) {
|
|
router, svc, _ := newEmbyDiscoveryEnv(t)
|
|
libID := seedEmbyLibrary(t, svc, "movie")
|
|
|
|
for i, genres := range []string{"Action,Drama", "Action"} {
|
|
m := &model.Media{
|
|
LibraryID: libID, Title: "片" + string(rune('A'+i)), Genres: genres,
|
|
Path: "/media/movie/m" + string(rune('0'+i)) + ".mkv",
|
|
}
|
|
if err := svc.Repo.DB.Create(m).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
w := embyGet(t, router, "/emby/Genres", signedTestToken(t, "test-secret"))
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status = %d body=%s", w.Code, w.Body.String())
|
|
}
|
|
items := decodeItemsEnvelope(t, w.Body.Bytes())
|
|
if len(items) != 2 {
|
|
t.Fatalf("items = %d, want 2 (body=%s)", len(items), w.Body.String())
|
|
}
|
|
// 排序按计数降序:Action(2) 在前。
|
|
if name, _ := items[0]["Name"].(string); name != "Action" {
|
|
t.Fatalf("first Name = %q, want Action", name)
|
|
}
|
|
if count, ok := items[0]["ItemCount"].(float64); !ok || int(count) != 2 {
|
|
t.Fatalf("ItemCount = %v, want 2", items[0]["ItemCount"])
|
|
}
|
|
if id, _ := items[0]["Id"].(string); len(id) == 0 {
|
|
t.Fatal("genre item must carry a stable Id")
|
|
}
|
|
}
|
|
|
|
// 按别人的 userId 请求 NextUp 不允许泄露他人历史。
|
|
func TestEmbyNextUpRejectsForeignUserID(t *testing.T) {
|
|
router, _, _ := newEmbyDiscoveryEnv(t)
|
|
|
|
w := embyGet(t, router, "/emby/Users/someone-else/Shows/NextUp", signedTestToken(t, "test-secret"))
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status = %d body=%s", w.Code, w.Body.String())
|
|
}
|
|
if items := decodeItemsEnvelope(t, w.Body.Bytes()); len(items) != 0 {
|
|
t.Fatalf("items = %d, want 0", len(items))
|
|
}
|
|
}
|