mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 11:16:37 +08:00
326 lines
11 KiB
Go
326 lines
11 KiB
Go
package handler
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"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/middleware"
|
|
"github.com/ShukeBta/MediaStationGo/internal/model"
|
|
"github.com/ShukeBta/MediaStationGo/internal/repository"
|
|
"github.com/ShukeBta/MediaStationGo/internal/service"
|
|
)
|
|
|
|
func TestListLibrariesHidesAdultDirectoriesUnlessAdminRequestsAll(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.AutoMigrate(&model.User{}, &model.Library{}, &model.Media{}, &model.Setting{}, &model.PlayProfile{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
repos := repository.New(db)
|
|
viewer := &model.User{Username: "viewer", PasswordHash: "hash", Role: "admin", HideAdult: true}
|
|
if err := repos.User.Create(t.Context(), viewer); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
safe := model.Library{Name: "电影", Path: "/media/movie", Type: "movie", Enabled: true}
|
|
adult := model.Library{Name: "9KG", Path: "/media/9KG", Type: "movie", Enabled: true}
|
|
if err := repos.Library.Create(t.Context(), &safe); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := repos.Library.Create(t.Context(), &adult); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := repos.Setting.Set(t.Context(), service.AdultLibraryIDsSettingKey, `["`+adult.ID+`"]`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := repos.Media.Upsert(t.Context(), &model.Media{LibraryID: safe.ID, Title: "误入普通库的成人条目", Path: "/media/movie/nsfw.mkv", NSFW: true}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
svc := &service.Container{
|
|
Repo: repos,
|
|
Media: service.NewMediaService(&config.Config{}, zap.NewNop(), repos),
|
|
}
|
|
|
|
visible := requestLibraries(t, svc, viewer.ID, "admin", "/api/libraries")
|
|
if len(visible) != 1 || visible[0].ID != safe.ID {
|
|
t.Fatalf("watching library list should hide adult directories, got %#v", visible)
|
|
}
|
|
|
|
all := requestLibraries(t, svc, viewer.ID, "admin", "/api/libraries?include_hidden=1")
|
|
if len(all) != 2 {
|
|
t.Fatalf("admin include_hidden list should keep management access, got %#v", all)
|
|
}
|
|
}
|
|
|
|
func TestListLibrariesIncludeHiddenNormalizesCloudDisplayNames(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.AutoMigrate(&model.Library{}, &model.Media{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
repos := repository.New(db)
|
|
cloud := model.Library{Name: "OpenList · 国产剧", Path: service.BuildCloudLibraryPath("openlist", "/国产剧", "/国产剧"), Type: "tv", Enabled: true}
|
|
if err := repos.Library.Create(t.Context(), &cloud); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
svc := &service.Container{
|
|
Repo: repos,
|
|
Media: service.NewMediaService(&config.Config{}, zap.NewNop(), repos),
|
|
}
|
|
|
|
all := requestLibraries(t, svc, "admin", "admin", "/api/libraries?include_hidden=1")
|
|
if len(all) != 1 {
|
|
t.Fatalf("include_hidden list = %#v, want one library", all)
|
|
}
|
|
if all[0].Name != "国产剧" {
|
|
t.Fatalf("cloud display name = %q, want stripped directory name", all[0].Name)
|
|
}
|
|
}
|
|
|
|
func TestListMediaGroupsMultipleVersionsByDefault(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.AutoMigrate(&model.User{}, &model.Library{}, &model.Media{}, &model.Setting{}, &model.PlayProfile{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
repos := repository.New(db)
|
|
lib := model.Library{Name: "Movies", Path: "/media/movies", Type: "movie", Enabled: true}
|
|
if err := repos.Library.Create(t.Context(), &lib); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := repos.DB.Create(&[]model.Media{
|
|
{
|
|
Base: model.Base{ID: "movie-1080", CreatedAt: time.Now().Add(-time.Minute)},
|
|
LibraryID: lib.ID,
|
|
Title: "流浪地球",
|
|
Path: "/media/movies/The.Wandering.Earth.2019.1080p.mkv",
|
|
Year: 2019,
|
|
Width: 1920,
|
|
Height: 1080,
|
|
SizeBytes: 100,
|
|
},
|
|
{
|
|
Base: model.Base{ID: "movie-2160", CreatedAt: time.Now()},
|
|
LibraryID: lib.ID,
|
|
Title: "流浪地球",
|
|
Path: "cloud://openlist/Movies/The.Wandering.Earth.2019.2160p.mkv",
|
|
Year: 2019,
|
|
Width: 3840,
|
|
Height: 2160,
|
|
SizeBytes: 200,
|
|
},
|
|
}).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
svc := &service.Container{
|
|
Repo: repos,
|
|
Media: service.NewMediaService(&config.Config{}, zap.NewNop(), repos),
|
|
}
|
|
|
|
grouped := requestMediaList(t, svc, "/api/libraries/"+lib.ID+"/media", lib.ID)
|
|
if grouped.Total != 1 || len(grouped.Items) != 1 {
|
|
t.Fatalf("grouped response total=%d len=%d body=%#v", grouped.Total, len(grouped.Items), grouped)
|
|
}
|
|
if grouped.Items[0].ID != "movie-1080" {
|
|
t.Fatalf("primary id = %q, want local version to remain primary", grouped.Items[0].ID)
|
|
}
|
|
if len(grouped.Items[0].Versions) != 2 {
|
|
t.Fatalf("versions = %#v, want both versions", grouped.Items[0].Versions)
|
|
}
|
|
if grouped.Items[0].Versions[0].ID != "movie-1080" || grouped.Items[0].Versions[1].ID != "movie-2160" {
|
|
t.Fatalf("versions should keep local before cloud: %#v", grouped.Items[0].Versions)
|
|
}
|
|
|
|
raw := requestMediaList(t, svc, "/api/libraries/"+lib.ID+"/media?group_versions=0", lib.ID)
|
|
if raw.Total != 2 || len(raw.Items) != 2 {
|
|
t.Fatalf("raw response total=%d len=%d body=%#v", raw.Total, len(raw.Items), raw)
|
|
}
|
|
}
|
|
|
|
func TestListLibrarySeriesDoesNotTruncateLargeEpisodeLibraries(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.AutoMigrate(&model.User{}, &model.Library{}, &model.Media{}, &model.Setting{}, &model.PlayProfile{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
repos := repository.New(db)
|
|
lib := model.Library{Name: "国漫", Path: "cloud://openlist/国漫", Type: "anime", Enabled: true}
|
|
if err := repos.Library.Create(t.Context(), &lib); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rows := make([]model.Media, 0, 2001)
|
|
for i := 1; i <= 2001; i++ {
|
|
rows = append(rows, model.Media{
|
|
Base: model.Base{ID: fmt.Sprintf("ep-%04d", i), CreatedAt: time.Now().Add(time.Duration(i) * time.Second)},
|
|
LibraryID: lib.ID,
|
|
Title: "大剧",
|
|
Path: fmt.Sprintf("cloud://openlist/国漫/大剧 (2026) {tmdb-123}/Season 1/大剧.S01E%04d.mkv", i),
|
|
SeasonNum: 1,
|
|
EpisodeNum: i,
|
|
})
|
|
}
|
|
if err := repos.DB.CreateInBatches(rows, 500).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
svc := &service.Container{
|
|
Repo: repos,
|
|
Media: service.NewMediaService(&config.Config{}, zap.NewNop(), repos),
|
|
}
|
|
|
|
series := requestLibrarySeries(t, svc, "/api/libraries/"+lib.ID+"/series", lib.ID)
|
|
if series.Total != 1 || len(series.Items) != 1 {
|
|
t.Fatalf("series response total=%d len=%d body=%#v", series.Total, len(series.Items), series)
|
|
}
|
|
if series.Items[0].Count != 2001 {
|
|
t.Fatalf("series count = %d, want 2001", series.Items[0].Count)
|
|
}
|
|
if !strings.HasPrefix(series.Items[0].Key, "series:") ||
|
|
strings.Contains(series.Items[0].Key, "lib:") ||
|
|
strings.Contains(series.Items[0].Key, "show:") {
|
|
t.Fatalf("series key = %q, want compact non-raw key", series.Items[0].Key)
|
|
}
|
|
episodes := requestLibrarySeriesEpisodes(t, svc, "/api/libraries/"+lib.ID+"/series/episodes?key="+url.QueryEscape(series.Items[0].Key), lib.ID)
|
|
if episodes.Total != 2001 || len(episodes.Items) != 2001 {
|
|
t.Fatalf("episodes total=%d len=%d, want 2001", episodes.Total, len(episodes.Items))
|
|
}
|
|
if episodes.Items[0].EpisodeNum != 1 || episodes.Items[len(episodes.Items)-1].EpisodeNum != 2001 {
|
|
t.Fatalf("episode order first=%d last=%d", episodes.Items[0].EpisodeNum, episodes.Items[len(episodes.Items)-1].EpisodeNum)
|
|
}
|
|
}
|
|
|
|
func TestScrapeOptionsFromRequestPreservesEpisodeImagesFalse(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Request = httptest.NewRequest(http.MethodPost, "/api/media/ep-1/scrape", bytes.NewBufferString(`{"episode_images":false,"refresh_matched":true}`))
|
|
c.Request.Header.Set("Content-Type", "application/json")
|
|
|
|
options, err := scrapeOptionsFromRequest(c, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if options.EpisodeArtwork == nil {
|
|
t.Fatal("EpisodeArtwork is nil, want explicit false")
|
|
}
|
|
if *options.EpisodeArtwork {
|
|
t.Fatal("EpisodeArtwork = true, want false")
|
|
}
|
|
if !options.IncludeMatched {
|
|
t.Fatal("IncludeMatched = false, want true from refresh_matched")
|
|
}
|
|
}
|
|
|
|
func requestLibraries(t *testing.T, svc *service.Container, userID, role, path string) []model.Library {
|
|
t.Helper()
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Set(middleware.CtxUserID, userID)
|
|
c.Set(middleware.CtxUserRole, role)
|
|
c.Request = httptest.NewRequest(http.MethodGet, path, nil)
|
|
listLibrariesHandler(svc)(c)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("GET %s status = %d body=%s", path, w.Code, w.Body.String())
|
|
}
|
|
var libs []model.Library
|
|
if err := json.Unmarshal(w.Body.Bytes(), &libs); err != nil {
|
|
t.Fatalf("decode libraries: %v", err)
|
|
}
|
|
return libs
|
|
}
|
|
|
|
type mediaListResponse struct {
|
|
Items []service.MediaItem `json:"items"`
|
|
Total int64 `json:"total"`
|
|
}
|
|
|
|
type seriesListResponse struct {
|
|
Items []service.SeriesCard `json:"items"`
|
|
Total int64 `json:"total"`
|
|
}
|
|
|
|
type seriesEpisodesResponse struct {
|
|
Items []model.Media `json:"items"`
|
|
Total int64 `json:"total"`
|
|
}
|
|
|
|
func requestMediaList(t *testing.T, svc *service.Container, path, libraryID string) mediaListResponse {
|
|
t.Helper()
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Set(middleware.CtxUserID, "user-1")
|
|
c.Set(middleware.CtxUserRole, "user")
|
|
c.Params = gin.Params{{Key: "id", Value: libraryID}}
|
|
c.Request = httptest.NewRequest(http.MethodGet, path, nil)
|
|
listMediaHandler(svc)(c)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("GET %s status = %d body=%s", path, w.Code, w.Body.String())
|
|
}
|
|
var payload mediaListResponse
|
|
if err := json.Unmarshal(w.Body.Bytes(), &payload); err != nil {
|
|
t.Fatalf("decode media list: %v", err)
|
|
}
|
|
return payload
|
|
}
|
|
|
|
func requestLibrarySeries(t *testing.T, svc *service.Container, path, libraryID string) seriesListResponse {
|
|
t.Helper()
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Set(middleware.CtxUserID, "user-1")
|
|
c.Set(middleware.CtxUserRole, "user")
|
|
c.Params = gin.Params{{Key: "id", Value: libraryID}}
|
|
c.Request = httptest.NewRequest(http.MethodGet, path, nil)
|
|
listLibrarySeriesHandler(svc)(c)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("GET %s status = %d body=%s", path, w.Code, w.Body.String())
|
|
}
|
|
var payload seriesListResponse
|
|
if err := json.Unmarshal(w.Body.Bytes(), &payload); err != nil {
|
|
t.Fatalf("decode series list: %v", err)
|
|
}
|
|
return payload
|
|
}
|
|
|
|
func requestLibrarySeriesEpisodes(t *testing.T, svc *service.Container, path, libraryID string) seriesEpisodesResponse {
|
|
t.Helper()
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Set(middleware.CtxUserID, "user-1")
|
|
c.Set(middleware.CtxUserRole, "user")
|
|
c.Params = gin.Params{{Key: "id", Value: libraryID}}
|
|
c.Request = httptest.NewRequest(http.MethodGet, path, nil)
|
|
listLibrarySeriesEpisodesHandler(svc)(c)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("GET %s status = %d body=%s", path, w.Code, w.Body.String())
|
|
}
|
|
var payload seriesEpisodesResponse
|
|
if err := json.Unmarshal(w.Body.Bytes(), &payload); err != nil {
|
|
t.Fatalf("decode series episodes: %v", err)
|
|
}
|
|
return payload
|
|
}
|