package handler import ( "net/http" "net/http/httptest" "os" "path/filepath" "testing" "github.com/gin-gonic/gin" "github.com/glebarez/sqlite" "go.uber.org/zap" "gorm.io/gorm" "github.com/ShukeBta/MediaStationGo/internal/config" "github.com/ShukeBta/MediaStationGo/internal/model" "github.com/ShukeBta/MediaStationGo/internal/repository" "github.com/ShukeBta/MediaStationGo/internal/service" ) func TestEmbyLowercaseVideoStreamRouteServesMedia(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) 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) } dir := t.TempDir() mediaPath := filepath.Join(dir, "sample.mp4") if err := os.WriteFile(mediaPath, []byte("fake-video-bytes"), 0o644); err != nil { t.Fatalf("write media: %v", err) } lib := model.Library{Name: "电影", Path: dir, Type: "movie", Enabled: true} if err := repos.Library.Create(t.Context(), &lib); err != nil { t.Fatalf("create library: %v", err) } if err := db.Create(&model.Media{ Base: model.Base{ID: "media-1"}, LibraryID: lib.ID, Title: "Lowercase Stream", Path: mediaPath, Container: "mp4", }).Error; err != nil { t.Fatalf("create media: %v", err) } const secret = "test-secret" router := gin.New() registerEmbyRoutes(router, secret, &service.Container{ Repo: repos, Emby: service.NewEmbyService(&config.Config{}, zap.NewNop(), repos), Stream: service.NewStreamService(&config.Config{}, zap.NewNop(), repos, nil), }) req := httptest.NewRequest(http.MethodGet, "/videos/media-1/stream?api_key="+signedTestToken(t, secret), nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("unexpected status: %d body=%s", w.Code, w.Body.String()) } if got := w.Body.String(); got != "fake-video-bytes" { t.Fatalf("unexpected stream body: %q", got) } } func TestEmbyPrefixedAPIStreamRouteServesMedia(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) 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) } dir := t.TempDir() mediaPath := filepath.Join(dir, "sample.mp4") if err := os.WriteFile(mediaPath, []byte("fake-video-bytes"), 0o644); err != nil { t.Fatalf("write media: %v", err) } lib := model.Library{Name: "电影", Path: dir, Type: "movie", Enabled: true} if err := repos.Library.Create(t.Context(), &lib); err != nil { t.Fatalf("create library: %v", err) } if err := db.Create(&model.Media{ Base: model.Base{ID: "media-1"}, LibraryID: lib.ID, Title: "Prefixed API Stream", Path: mediaPath, Container: "mp4", }).Error; err != nil { t.Fatalf("create media: %v", err) } const secret = "test-secret" router := gin.New() registerEmbyRoutes(router, secret, &service.Container{ Repo: repos, Emby: service.NewEmbyService(&config.Config{}, zap.NewNop(), repos), Stream: service.NewStreamService(&config.Config{}, zap.NewNop(), repos, nil), }) req := httptest.NewRequest(http.MethodGet, "/emby/api/stream/media-1?api_key="+signedTestToken(t, secret), nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("unexpected status: %d body=%s", w.Code, w.Body.String()) } if got := w.Body.String(); got != "fake-video-bytes" { t.Fatalf("unexpected stream body: %q", got) } } func TestEmbyLowercaseOriginalHeadRouteServesHeaders(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) 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) } dir := t.TempDir() mediaPath := filepath.Join(dir, "sample.mp4") if err := os.WriteFile(mediaPath, []byte("fake-video-bytes"), 0o644); err != nil { t.Fatalf("write media: %v", err) } lib := model.Library{Name: "电影", Path: dir, Type: "movie", Enabled: true} if err := repos.Library.Create(t.Context(), &lib); err != nil { t.Fatalf("create library: %v", err) } if err := db.Create(&model.Media{ Base: model.Base{ID: "media-1"}, LibraryID: lib.ID, Title: "Lowercase Original", Path: mediaPath, Container: "mp4", }).Error; err != nil { t.Fatalf("create media: %v", err) } const secret = "test-secret" router := gin.New() registerEmbyRoutes(router, secret, &service.Container{ Repo: repos, Emby: service.NewEmbyService(&config.Config{}, zap.NewNop(), repos), Stream: service.NewStreamService(&config.Config{}, zap.NewNop(), repos, nil), }) req := httptest.NewRequest(http.MethodHead, "/videos/media-1/original.mp4?api_key="+signedTestToken(t, secret), nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("unexpected status: %d body=%s", w.Code, w.Body.String()) } if w.Body.Len() != 0 { t.Fatalf("HEAD response should not include body, got %q", w.Body.String()) } } func TestEmbyLowercaseVideoHLSRouteDoesNot404WhenDirectOnly(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) 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) } dir := t.TempDir() mediaPath := filepath.Join(dir, "sample.mp4") if err := os.WriteFile(mediaPath, []byte("fake-video-bytes"), 0o644); err != nil { t.Fatalf("write media: %v", err) } lib := model.Library{Name: "电影", Path: dir, Type: "movie", Enabled: true} if err := repos.Library.Create(t.Context(), &lib); err != nil { t.Fatalf("create library: %v", err) } if err := db.Create(&model.Media{ Base: model.Base{ID: "media-1"}, LibraryID: lib.ID, Title: "Lowercase HLS", Path: mediaPath, Container: "mp4", }).Error; err != nil { t.Fatalf("create media: %v", err) } if err := repos.Setting.Set(t.Context(), service.PlaybackDirectOnlySettingKey, "true"); err != nil { t.Fatalf("set direct-only: %v", err) } const secret = "test-secret" router := gin.New() registerEmbyRoutes(router, secret, &service.Container{ Repo: repos, Emby: service.NewEmbyService(&config.Config{}, zap.NewNop(), repos), Stream: service.NewStreamService(&config.Config{}, zap.NewNop(), repos, nil), }) req := httptest.NewRequest(http.MethodGet, "/videos/media-1/master.m3u8?api_key="+signedTestToken(t, secret), nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code == http.StatusNotFound { t.Fatalf("lowercase HLS route should be registered, got 404") } if w.Code != http.StatusConflict { t.Fatalf("direct-only HLS should return 409, got %d body=%s", w.Code, w.Body.String()) } }