mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-30 19:46:38 +08:00
319 lines
8.7 KiB
Go
319 lines
8.7 KiB
Go
package handler
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/glebarez/sqlite"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/truewhile/MeBox/internal/model"
|
|
"github.com/truewhile/MeBox/internal/repository"
|
|
"github.com/truewhile/MeBox/internal/service"
|
|
)
|
|
|
|
func TestNormalizeEmbyPath(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
wantPath string
|
|
changed bool
|
|
}{
|
|
{
|
|
input: "/emby/System/Info",
|
|
wantPath: "/emby/system/info",
|
|
changed: true,
|
|
},
|
|
{
|
|
input: "/emby/emby/System/Info",
|
|
wantPath: "/emby/system/info",
|
|
changed: true,
|
|
},
|
|
{
|
|
input: "/emby/emby/emby/items/123/playbackInfo",
|
|
wantPath: "/emby/items/123/playbackinfo",
|
|
changed: true,
|
|
},
|
|
{
|
|
input: "//emby//System//Info//Public",
|
|
wantPath: "/emby/system/info/public",
|
|
changed: true,
|
|
},
|
|
{
|
|
input: "/Items/msgo-series-1/PlaybackInfo",
|
|
wantPath: "/items/msgo-series-1/playbackinfo",
|
|
changed: true,
|
|
},
|
|
{
|
|
input: "/Videos/m-123/Master.m3u8",
|
|
wantPath: "/videos/m-123/master.m3u8",
|
|
changed: true,
|
|
},
|
|
{
|
|
input: "/api/unknown/other",
|
|
wantPath: "/api/unknown/other",
|
|
changed: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
gotPath, changed := NormalizeEmbyPath(tt.input)
|
|
if gotPath != tt.wantPath || changed != tt.changed {
|
|
t.Errorf("NormalizeEmbyPath(%q) = (%q, %v), want (%q, %v)", tt.input, gotPath, changed, tt.wantPath, tt.changed)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEmbyDuplicatePrefixHandling(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.User{}); err != nil {
|
|
t.Fatalf("migrate: %v", err)
|
|
}
|
|
repos := repository.New(db)
|
|
svc := &service.Container{
|
|
Repo: repos,
|
|
Emby: service.NewEmbyService(nil, nil, repos),
|
|
}
|
|
|
|
router := gin.New()
|
|
registerEmbyRoutes(router, "secret", svc)
|
|
|
|
// 模拟重复拼接前缀的客户端请求: /emby/emby/System/Info/Public
|
|
req := httptest.NewRequest(http.MethodGet, "/emby/emby/System/Info/Public", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200 OK for /emby/emby/System/Info/Public, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
if !strings.Contains(w.Body.String(), "ServerName") {
|
|
t.Fatalf("expected server info body, got: %s", w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestEmbyMixedCaseHandling(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.User{}); err != nil {
|
|
t.Fatalf("migrate: %v", err)
|
|
}
|
|
repos := repository.New(db)
|
|
svc := &service.Container{
|
|
Repo: repos,
|
|
Emby: service.NewEmbyService(nil, nil, repos),
|
|
}
|
|
|
|
router := gin.New()
|
|
registerEmbyRoutes(router, "secret", svc)
|
|
|
|
// 混合大小写驼峰: /emby/system/Info/Public
|
|
req := httptest.NewRequest(http.MethodGet, "/emby/system/Info/Public", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200 OK for /emby/system/Info/Public, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestEmbyClientIdentification(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
ua string
|
|
query string
|
|
headerAuth string
|
|
wantClient string
|
|
}{
|
|
{
|
|
name: "CapyPlayer via UA",
|
|
ua: "CapyPlayer/1.2.0 (iOS)",
|
|
wantClient: "CapyPlayer",
|
|
},
|
|
{
|
|
name: "SenPlayer via UA",
|
|
ua: "SenPlayer/2.1",
|
|
wantClient: "SenPlayer",
|
|
},
|
|
{
|
|
name: "Fileball via UA",
|
|
ua: "Fileball/1.0.0",
|
|
wantClient: "Fileball",
|
|
},
|
|
{
|
|
name: "Kodi via UA",
|
|
ua: "Kodi/20.2",
|
|
wantClient: "Kodi",
|
|
},
|
|
{
|
|
name: "Client in query",
|
|
ua: "CustomApp/1.0",
|
|
query: "?X-Emby-Client=CapyPlayer",
|
|
wantClient: "CapyPlayer",
|
|
},
|
|
{
|
|
name: "Client in auth header",
|
|
ua: "Custom/1.0",
|
|
headerAuth: `MediaBrowser Client="SenPlayer", Device="AppleTV", DeviceId="abc"`,
|
|
wantClient: "SenPlayer",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
|
url := "/test"
|
|
if tt.query != "" {
|
|
url += tt.query
|
|
}
|
|
req := httptest.NewRequest(http.MethodGet, url, nil)
|
|
if tt.ua != "" {
|
|
req.Header.Set("User-Agent", tt.ua)
|
|
}
|
|
if tt.headerAuth != "" {
|
|
req.Header.Set("X-Emby-Authorization", tt.headerAuth)
|
|
}
|
|
c.Request = req
|
|
|
|
info := embyClientInfoFromRequest(c)
|
|
if info.Client != tt.wantClient {
|
|
t.Fatalf("embyClientInfoFromRequest Client = %q, want %q", info.Client, tt.wantClient)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEmbyAdaptivePrefixPlaybackInfo(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.AllModels()...); err != nil {
|
|
t.Fatalf("migrate: %v", err)
|
|
}
|
|
repos := repository.New(db)
|
|
svc := &service.Container{
|
|
Repo: repos,
|
|
Emby: service.NewEmbyService(nil, nil, repos),
|
|
}
|
|
const secret = "test-secret"
|
|
router := gin.New()
|
|
registerEmbyRoutes(router, secret, svc)
|
|
|
|
token := signedTestToken(t, secret)
|
|
if err := repos.User.Create(t.Context(), &model.User{
|
|
Base: model.Base{ID: "user-1"},
|
|
Username: "tester",
|
|
PasswordHash: "x",
|
|
Role: "admin",
|
|
Tier: "plus",
|
|
IsActive: true,
|
|
}); err != nil {
|
|
t.Fatalf("create user: %v", err)
|
|
}
|
|
if err := db.Create(&model.Media{
|
|
Base: model.Base{ID: "m-adaptive-1"},
|
|
Title: "测试媒体",
|
|
Path: "D:\\media\\test.mkv",
|
|
LibraryID: "lib-1",
|
|
}).Error; err != nil {
|
|
t.Fatalf("create media: %v", err)
|
|
}
|
|
|
|
// 1. 从 /emby 前缀请求 PlaybackInfo
|
|
req1 := httptest.NewRequest(http.MethodGet, "/emby/Items/m-adaptive-1/PlaybackInfo", nil)
|
|
req1.Header.Set("X-Emby-Token", token)
|
|
w1 := httptest.NewRecorder()
|
|
router.ServeHTTP(w1, req1)
|
|
|
|
if w1.Code != http.StatusOK {
|
|
t.Fatalf("playbackinfo /emby code = %d: %s", w1.Code, w1.Body.String())
|
|
}
|
|
var res1 struct {
|
|
MediaSources []struct {
|
|
DirectStreamURL string `json:"DirectStreamUrl"`
|
|
} `json:"MediaSources"`
|
|
}
|
|
if err := json.Unmarshal(w1.Body.Bytes(), &res1); err != nil || len(res1.MediaSources) == 0 {
|
|
t.Fatalf("unmarshal /emby response: %v, body: %s", err, w1.Body.String())
|
|
}
|
|
if !strings.Contains(res1.MediaSources[0].DirectStreamURL, "/Videos/m-adaptive-1/stream") {
|
|
t.Fatalf("DirectStreamUrl should point to video stream endpoint, got: %s", res1.MediaSources[0].DirectStreamURL)
|
|
}
|
|
if !strings.Contains(res1.MediaSources[0].DirectStreamURL, "api_key="+token) {
|
|
t.Fatalf("DirectStreamUrl should carry api_key token, got: %s", res1.MediaSources[0].DirectStreamURL)
|
|
}
|
|
|
|
// 2. 从重复前缀 /emby/emby 请求 PlaybackInfo (模拟客户端再次追加 BaseUrl 场景)
|
|
req2 := httptest.NewRequest(http.MethodGet, "/emby/emby/Items/m-adaptive-1/PlaybackInfo", nil)
|
|
req2.Header.Set("X-Emby-Token", token)
|
|
w2 := httptest.NewRecorder()
|
|
router.ServeHTTP(w2, req2)
|
|
|
|
if w2.Code != http.StatusOK {
|
|
t.Fatalf("playbackinfo /emby/emby code = %d: %s", w2.Code, w2.Body.String())
|
|
}
|
|
var res2 struct {
|
|
MediaSources []struct {
|
|
DirectStreamURL string `json:"DirectStreamUrl"`
|
|
} `json:"MediaSources"`
|
|
}
|
|
if err := json.Unmarshal(w2.Body.Bytes(), &res2); err != nil || len(res2.MediaSources) == 0 {
|
|
t.Fatalf("unmarshal /emby/emby response: %v, body: %s", err, w2.Body.String())
|
|
}
|
|
|
|
// 3. 从根路径 /Items 请求 PlaybackInfo
|
|
req3 := httptest.NewRequest(http.MethodGet, "/Items/m-adaptive-1/PlaybackInfo", nil)
|
|
req3.Header.Set("X-Emby-Token", token)
|
|
w3 := httptest.NewRecorder()
|
|
router.ServeHTTP(w3, req3)
|
|
|
|
if w3.Code != http.StatusOK {
|
|
t.Fatalf("playbackinfo root code = %d: %s", w3.Code, w3.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestEmbyImageClearNoStore(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.AllModels()...); err != nil {
|
|
t.Fatalf("migrate: %v", err)
|
|
}
|
|
repos := repository.New(db)
|
|
svc := &service.Container{
|
|
Repo: repos,
|
|
Emby: service.NewEmbyService(nil, nil, repos),
|
|
}
|
|
|
|
router := gin.New()
|
|
registerEmbyRoutes(router, "secret", svc)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/emby/Items/non-existent-item/Images/Primary", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("placeholder image should return 200, got %d", w.Code)
|
|
}
|
|
cacheControl := w.Header().Get("Cache-Control")
|
|
if strings.Contains(cacheControl, "no-store") {
|
|
t.Fatalf("image response should not have no-store, got: %s", cacheControl)
|
|
}
|
|
if !strings.Contains(cacheControl, "public") {
|
|
t.Fatalf("image response should have public cache-control, got: %s", cacheControl)
|
|
}
|
|
}
|