Files
MeBox/internal/handler/emby_authed_loop_test.go
T
truewhile b0fe40142a Rebrand MMTL to MeBox (name, logo, Docker image) (#17)
* 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>
2026-09-02 16:26:28 +08:00

148 lines
5.0 KiB
Go

package handler
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"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"
)
// TestEmbyLoginThenAuthedRequest simulates the exact flow an Emby-compatible
// client performs: POST /Users/AuthenticateByName to obtain an AccessToken,
// then immediately reuses that token to fetch /Users/Me and /Items.
//
// This guards against regressions where login succeeds but the returned token
// fails downstream auth (the "每次登录后立刻 401 Invalid token" report).
func TestEmbyLoginThenAuthedRequest(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 sqlDB, err := db.DB(); err == nil {
sqlDB.SetMaxOpenConns(1)
}
if err := db.AutoMigrate(model.AllModels()...); err != nil {
t.Fatalf("migrate: %v", err)
}
repos := repository.New(db)
cfg := &config.Config{}
cfg.Secrets.JWTSecret = "test-secret"
log := zap.NewNop()
permissions := service.NewPermissionService(log, repos)
tokenSvc := service.NewTokenService(cfg, log, repos)
auth := service.NewAuthService(cfg, log, repos, tokenSvc, permissions)
if _, _, err := auth.Register(context.Background(), "viewer", "secret-pass"); err != nil {
t.Fatalf("register: %v", err)
}
router := gin.New()
registerEmbyRoutes(router, cfg.Secrets.JWTSecret, &service.Container{
Repo: repos,
Auth: auth,
Emby: service.NewEmbyService(cfg, log, repos),
Device: service.NewDeviceService(log, repos),
Audit: service.NewAuditService(log, repos),
Permissions: permissions,
})
loginBody := `{"Username":"viewer","Pw":"secret-pass"}`
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/emby/Users/AuthenticateByName", strings.NewReader(loginBody))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("login status = %d body=%s", w.Code, w.Body.String())
}
var login struct {
AccessToken string `json:"AccessToken"`
}
if err := json.Unmarshal(w.Body.Bytes(), &login); err != nil {
t.Fatalf("decode login: %v", err)
}
if login.AccessToken == "" {
t.Fatalf("empty AccessToken from login")
}
// Reuse the returned token against authenticated endpoints.
for _, path := range []string{"/emby/Users/Me", "/emby/Items", "/emby/Library/VirtualFolders"} {
w = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodGet, path, nil)
req.Header.Set("X-Emby-Token", login.AccessToken)
router.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("GET %s with returned token = %d body=%s", path, w.Code, w.Body.String())
}
}
}
// TestEmbyLoginWithAuthorizationHeaderToken covers clients that place the
// bearer token in Authorization instead of X-Emby-Token.
func TestEmbyLoginWithAuthorizationHeaderToken(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 sqlDB, err := db.DB(); err == nil {
// :memory: SQLite 每个连接是独立库,必须锁单连接否则表在不同连接上互相不可见。
sqlDB.SetMaxOpenConns(1)
}
if err := db.AutoMigrate(model.AllModels()...); err != nil {
t.Fatalf("migrate: %v", err)
}
repos := repository.New(db)
cfg := &config.Config{}
cfg.Secrets.JWTSecret = "test-secret"
log := zap.NewNop()
permissions := service.NewPermissionService(log, repos)
auth := service.NewAuthService(cfg, log, repos, service.NewTokenService(cfg, log, repos), permissions)
if _, _, err := auth.Register(context.Background(), "viewer", "secret-pass"); err != nil {
t.Fatalf("register: %v", err)
}
router := gin.New()
registerEmbyRoutes(router, cfg.Secrets.JWTSecret, &service.Container{
Repo: repos,
Auth: auth,
Emby: service.NewEmbyService(cfg, log, repos),
Device: service.NewDeviceService(log, repos),
Audit: service.NewAuditService(log, repos),
Permissions: permissions,
})
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/emby/Users/AuthenticateByName", strings.NewReader(`{"Username":"viewer","Pw":"secret-pass"}`))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("login status = %d body=%s", w.Code, w.Body.String())
}
var login struct {
AccessToken string `json:"AccessToken"`
}
if err := json.Unmarshal(w.Body.Bytes(), &login); err != nil {
t.Fatalf("decode login: %v", err)
}
w = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodGet, "/emby/Users/Me", nil)
req.Header.Set("Authorization", "MediaBrowser Token=\""+login.AccessToken+"\"")
router.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("Authorization-token request = %d body=%s", w.Code, w.Body.String())
}
}