mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 11:16:37 +08:00
b0fe40142a
* Rebrand MMTL to MeBox across codebase and assets Rename the project display name, Go module path, environment variable prefix (MEBOX_*), Docker image references, and UI branding from MMTL/mmtl to MeBox/mebox. Replace logo assets with the new MeBox icon and keep legacy SQLite migration support for existing mmtl.db deployments. Co-authored-by: truewhile <truewhile@users.noreply.github.com> * Fix logo icons: use cube-only crop without truncated text Previous icon generation cropped too much of the source image, including partial MeBox wordmark text that was cut off in square icon containers. Regenerate logo-64/192/512, favicon, and SVG from cube-only region. Co-authored-by: truewhile <truewhile@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: truewhile <truewhile@users.noreply.github.com>
149 lines
4.8 KiB
Go
149 lines
4.8 KiB
Go
package handler
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/glebarez/sqlite"
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"go.uber.org/zap"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/truewhile/MeBox/internal/config"
|
|
"github.com/truewhile/MeBox/internal/middleware"
|
|
"github.com/truewhile/MeBox/internal/model"
|
|
"github.com/truewhile/MeBox/internal/repository"
|
|
"github.com/truewhile/MeBox/internal/service"
|
|
)
|
|
|
|
func TestMountedEmbyPlayingProgressAndResumePipeline(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatalf("open sqlite: %v", err)
|
|
}
|
|
if err := db.AutoMigrate(model.AllModels()...); err != nil {
|
|
t.Fatalf("migrate: %v", err)
|
|
}
|
|
|
|
repos := repository.New(db)
|
|
user := &model.User{
|
|
Base: model.Base{ID: "user-1"},
|
|
Username: "test_viewer",
|
|
PasswordHash: "x",
|
|
Role: "user",
|
|
Tier: "free",
|
|
IsActive: true,
|
|
}
|
|
if err := repos.User.Create(t.Context(), user); err != nil {
|
|
t.Fatalf("create user: %v", err)
|
|
}
|
|
|
|
cfg := &config.Config{}
|
|
logger := zap.NewNop()
|
|
svc := &service.Container{
|
|
Repo: repos,
|
|
Emby: service.NewEmbyService(cfg, logger, repos),
|
|
Sessions: service.NewSessionTrackerService(logger),
|
|
Playback: service.NewPlaybackService(logger, repos),
|
|
}
|
|
|
|
router := gin.New()
|
|
// 注册带认证的路由,模拟已登录用户
|
|
router.Use(func(c *gin.Context) {
|
|
c.Set(middleware.CtxUserID, user.ID)
|
|
c.Next()
|
|
})
|
|
router.POST("/Sessions/Playing/Progress", embyPlayingProgressHandler(svc))
|
|
router.GET("/Items", embyItemsHandler(svc))
|
|
router.GET("/Users/:userId/Items/Resume", embyResumeItemsHandler(svc))
|
|
router.GET("/Sessions", embySessionsHandler(svc))
|
|
|
|
remoteMediaID := service.EncodeEmbyRemoteID("mount-1", "remote-item-123")
|
|
|
|
// 1. 测试上报进度:客户端使用小写 query 参数 itemId / positionTicks
|
|
progressReq := httptest.NewRequest(
|
|
http.MethodPost,
|
|
"/Sessions/Playing/Progress?itemId="+remoteMediaID+"&positionTicks=300000000&runTimeTicks=1000000000",
|
|
nil,
|
|
)
|
|
wProgress := httptest.NewRecorder()
|
|
router.ServeHTTP(wProgress, progressReq)
|
|
if wProgress.Code != http.StatusNoContent {
|
|
t.Fatalf("progress status = %d, body = %s", wProgress.Code, wProgress.Body.String())
|
|
}
|
|
|
|
// 验证已持久化到 PlaybackHistory
|
|
var hist model.PlaybackHistory
|
|
if err := db.Where("user_id = ? AND media_id = ?", user.ID, remoteMediaID).First(&hist).Error; err != nil {
|
|
t.Fatalf("playback history not saved: %v", err)
|
|
}
|
|
if hist.PositionMs != 30000 {
|
|
t.Fatalf("expected position_ms = 30000, got %d", hist.PositionMs)
|
|
}
|
|
|
|
// 2. 测试 Filters=IsResumable 能够包含该远程条目
|
|
resumableReq := httptest.NewRequest(
|
|
http.MethodGet,
|
|
"/Items?Filters=IsResumable",
|
|
nil,
|
|
)
|
|
wResumable := httptest.NewRecorder()
|
|
router.ServeHTTP(wResumable, resumableReq)
|
|
if wResumable.Code != http.StatusOK {
|
|
t.Fatalf("items resumable status = %d, body = %s", wResumable.Code, wResumable.Body.String())
|
|
}
|
|
var resumableEnvelope map[string]any
|
|
if err := json.Unmarshal(wResumable.Body.Bytes(), &resumableEnvelope); err != nil {
|
|
t.Fatalf("decode resumable: %v", err)
|
|
}
|
|
// 因为没有配置真实的远程客户端连接,该远程条目在当前离线测试中不会 panic 崩溃,并且正常响应 Envelope
|
|
if resumableEnvelope["TotalRecordCount"] == nil {
|
|
t.Fatalf("missing TotalRecordCount in resumable envelope")
|
|
}
|
|
|
|
// 3. 测试 /Users/:userId/Items/Resume 别名路由
|
|
resumeAliasReq := httptest.NewRequest(
|
|
http.MethodGet,
|
|
"/Users/"+user.ID+"/Items/Resume",
|
|
nil,
|
|
)
|
|
wResumeAlias := httptest.NewRecorder()
|
|
router.ServeHTTP(wResumeAlias, resumeAliasReq)
|
|
if wResumeAlias.Code != http.StatusOK {
|
|
t.Fatalf("resume alias status = %d, body = %s", wResumeAlias.Code, wResumeAlias.Body.String())
|
|
}
|
|
|
|
// 4. 测试 /Sessions 返回 NowPlayingItem
|
|
sessionsReq := httptest.NewRequest(http.MethodGet, "/Sessions", nil)
|
|
wSessions := httptest.NewRecorder()
|
|
router.ServeHTTP(wSessions, sessionsReq)
|
|
if wSessions.Code != http.StatusOK {
|
|
t.Fatalf("sessions status = %d, body = %s", wSessions.Code, wSessions.Body.String())
|
|
}
|
|
var sessionsList []map[string]any
|
|
if err := json.Unmarshal(wSessions.Body.Bytes(), &sessionsList); err != nil {
|
|
t.Fatalf("decode sessions: %v", err)
|
|
}
|
|
if len(sessionsList) == 0 {
|
|
t.Fatalf("expected at least 1 session")
|
|
}
|
|
nowPlaying, ok := sessionsList[0]["NowPlayingItem"].(map[string]any)
|
|
if !ok || nowPlaying["Id"] != remoteMediaID {
|
|
t.Fatalf("expected NowPlayingItem with id %q, got %#v", remoteMediaID, sessionsList[0]["NowPlayingItem"])
|
|
}
|
|
}
|
|
|
|
func signMockToken(secret, userID string) string {
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
|
"sub": userID,
|
|
"exp": time.Now().Add(time.Hour).Unix(),
|
|
})
|
|
s, _ := token.SignedString([]byte(secret))
|
|
return s
|
|
}
|