summaryrefslogtreecommitdiff
path: root/player-server/internal/api/handlers_playback_test.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-18 13:47:49 +0300
committerPaul Buetow <paul@buetow.org>2026-05-18 13:47:49 +0300
commitca0200985504b480f9a664e929f64dec53a6e36b (patch)
treeff1ae93fef5dc6561cfb48b53e54693a6883302b /player-server/internal/api/handlers_playback_test.go
parent60df3397fbd14a6ad200e1fdd2530f708403ee99 (diff)
Add playback hints endpoint for client codec/container decisions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'player-server/internal/api/handlers_playback_test.go')
-rw-r--r--player-server/internal/api/handlers_playback_test.go249
1 files changed, 249 insertions, 0 deletions
diff --git a/player-server/internal/api/handlers_playback_test.go b/player-server/internal/api/handlers_playback_test.go
new file mode 100644
index 0000000..5b14b39
--- /dev/null
+++ b/player-server/internal/api/handlers_playback_test.go
@@ -0,0 +1,249 @@
+package api
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+
+ "codeberg.org/snonux/player/internal"
+ "codeberg.org/snonux/player/internal/auth"
+ "codeberg.org/snonux/player/internal/clock"
+ "codeberg.org/snonux/player/internal/model"
+ "codeberg.org/snonux/player/internal/repository"
+ "codeberg.org/snonux/player/internal/service"
+)
+
+// newPlaybackTestServer creates a Server wired with a PlaybackHintsService for testing.
+func newPlaybackTestServer(t *testing.T, store repository.Store, sm auth.SessionManager, hintSvc service.PlaybackHintsService) *Server {
+ t.Helper()
+ fs := newTestFS(map[string]string{
+ "index.html": "index", "login.html": "login",
+ "bootstrap.html": "bootstrap", "share.html": "share",
+ })
+ authSvc := &service.MockAuthService{
+ CountUsersFunc: func(context.Context) (int, error) { return 1, nil },
+ GetUserByIDFunc: func(context.Context, int64) (*model.User, error) { return &model.User{ID: 1, IsAdmin: true}, nil },
+ }
+ return NewServer(ServerDeps{
+ Store: store,
+ SessionManager: sm,
+ Config: &internal.Config{},
+ Services: ServerServices{
+ Auth: authSvc,
+ PlaybackHints: hintSvc,
+ },
+ StaticFS: fs,
+ })
+}
+
+// sessionForPlaybackTest creates a session cookie that resolves to userID 1.
+func sessionForPlaybackTest(t *testing.T, store repository.Store, sm auth.SessionManager) *http.Cookie {
+ t.Helper()
+ return addSessionCookie(t, store, sm, 1)
+}
+
+// ------------------------------------------------------------------
+// GET /api/v1/media/{id}/playback
+// ------------------------------------------------------------------
+
+func TestHandlePlaybackHints_Success(t *testing.T) {
+ store := buildSessionStore(1)
+ sm := auth.NewSessionManager(store, &clock.MockClock{T: time.Now()}, time.Hour)
+
+ hint := &service.PlaybackHint{
+ StreamURL: "/api/v1/media/5/stream",
+ Container: "mp4",
+ VideoCodec: "h264",
+ AudioCodec: "aac",
+ DurationSeconds: 300.0,
+ FileSizeBytes: 1234567,
+ Width: 1920,
+ Height: 1080,
+ Bitrate: 4000000,
+ NeedsTranscode: false,
+ }
+ hintSvc := &service.MockPlaybackHintsService{
+ GetPlaybackHintFunc: func(_ context.Context, mediaID, userID int64) (*service.PlaybackHint, error) {
+ if mediaID != 5 || userID != 1 {
+ return nil, errors.New("unexpected args")
+ }
+ return hint, nil
+ },
+ }
+
+ srv := newPlaybackTestServer(t, store, sm, hintSvc)
+ req := httptest.NewRequest(http.MethodGet, "/api/v1/media/5/playback", nil)
+ req.AddCookie(sessionForPlaybackTest(t, store, sm))
+ rr := httptest.NewRecorder()
+ srv.ServeHTTP(rr, req)
+
+ if rr.Code != http.StatusOK {
+ t.Fatalf("expected %d, got %d: %s", http.StatusOK, rr.Code, rr.Body.String())
+ }
+
+ var got service.PlaybackHint
+ if err := json.NewDecoder(rr.Body).Decode(&got); err != nil {
+ t.Fatalf("decode response: %v", err)
+ }
+ if got.Container != "mp4" {
+ t.Errorf("Container: want mp4, got %s", got.Container)
+ }
+ if got.VideoCodec != "h264" {
+ t.Errorf("VideoCodec: want h264, got %s", got.VideoCodec)
+ }
+ if got.AudioCodec != "aac" {
+ t.Errorf("AudioCodec: want aac, got %s", got.AudioCodec)
+ }
+ if got.NeedsTranscode {
+ t.Error("NeedsTranscode: want false, got true")
+ }
+ if got.DurationSeconds != 300.0 {
+ t.Errorf("DurationSeconds: want 300, got %f", got.DurationSeconds)
+ }
+}
+
+func TestHandlePlaybackHints_InvalidID(t *testing.T) {
+ store := buildSessionStore(1)
+ sm := auth.NewSessionManager(store, &clock.MockClock{T: time.Now()}, time.Hour)
+ hintSvc := &service.MockPlaybackHintsService{}
+
+ srv := newPlaybackTestServer(t, store, sm, hintSvc)
+ req := httptest.NewRequest(http.MethodGet, "/api/v1/media/abc/playback", nil)
+ req.AddCookie(sessionForPlaybackTest(t, store, sm))
+ rr := httptest.NewRecorder()
+ srv.ServeHTTP(rr, req)
+
+ if rr.Code != http.StatusBadRequest {
+ t.Fatalf("expected 400 for invalid id, got %d", rr.Code)
+ }
+}
+
+func TestHandlePlaybackHints_NotFound(t *testing.T) {
+ store := buildSessionStore(1)
+ sm := auth.NewSessionManager(store, &clock.MockClock{T: time.Now()}, time.Hour)
+ hintSvc := &service.MockPlaybackHintsService{
+ GetPlaybackHintFunc: func(_ context.Context, _, _ int64) (*service.PlaybackHint, error) {
+ return nil, service.ErrNotFound
+ },
+ }
+
+ srv := newPlaybackTestServer(t, store, sm, hintSvc)
+ req := httptest.NewRequest(http.MethodGet, "/api/v1/media/99/playback", nil)
+ req.AddCookie(sessionForPlaybackTest(t, store, sm))
+ rr := httptest.NewRecorder()
+ srv.ServeHTTP(rr, req)
+
+ if rr.Code != http.StatusNotFound {
+ t.Fatalf("expected 404, got %d", rr.Code)
+ }
+}
+
+func TestHandlePlaybackHints_Forbidden(t *testing.T) {
+ store := buildSessionStore(1)
+ sm := auth.NewSessionManager(store, &clock.MockClock{T: time.Now()}, time.Hour)
+ hintSvc := &service.MockPlaybackHintsService{
+ GetPlaybackHintFunc: func(_ context.Context, _, _ int64) (*service.PlaybackHint, error) {
+ return nil, service.ErrForbidden
+ },
+ }
+
+ srv := newPlaybackTestServer(t, store, sm, hintSvc)
+ req := httptest.NewRequest(http.MethodGet, "/api/v1/media/3/playback", nil)
+ req.AddCookie(sessionForPlaybackTest(t, store, sm))
+ rr := httptest.NewRecorder()
+ srv.ServeHTTP(rr, req)
+
+ if rr.Code != http.StatusForbidden {
+ t.Fatalf("expected 403, got %d", rr.Code)
+ }
+}
+
+func TestHandlePlaybackHints_NoService(t *testing.T) {
+ store := buildSessionStore(1)
+ sm := auth.NewSessionManager(store, &clock.MockClock{T: time.Now()}, time.Hour)
+
+ // Passing nil PlaybackHintsService should yield 501.
+ srv := newPlaybackTestServer(t, store, sm, nil)
+ req := httptest.NewRequest(http.MethodGet, "/api/v1/media/1/playback", nil)
+ req.AddCookie(sessionForPlaybackTest(t, store, sm))
+ rr := httptest.NewRecorder()
+ srv.ServeHTTP(rr, req)
+
+ if rr.Code != http.StatusNotImplemented {
+ t.Fatalf("expected 501, got %d", rr.Code)
+ }
+}
+
+func TestHandlePlaybackHints_LegacyPath(t *testing.T) {
+ // The route is also available under /api/media/{id}/playback (handleBoth).
+ store := buildSessionStore(1)
+ sm := auth.NewSessionManager(store, &clock.MockClock{T: time.Now()}, time.Hour)
+
+ hint := &service.PlaybackHint{Container: "mkv", NeedsTranscode: true}
+ hintSvc := &service.MockPlaybackHintsService{
+ GetPlaybackHintFunc: func(_ context.Context, _, _ int64) (*service.PlaybackHint, error) {
+ return hint, nil
+ },
+ }
+
+ srv := newPlaybackTestServer(t, store, sm, hintSvc)
+ req := httptest.NewRequest(http.MethodGet, "/api/media/2/playback", nil)
+ req.AddCookie(sessionForPlaybackTest(t, store, sm))
+ rr := httptest.NewRecorder()
+ srv.ServeHTTP(rr, req)
+
+ if rr.Code != http.StatusOK {
+ t.Fatalf("expected 200 on legacy path, got %d: %s", rr.Code, rr.Body.String())
+ }
+
+ var got service.PlaybackHint
+ if err := json.NewDecoder(rr.Body).Decode(&got); err != nil {
+ t.Fatalf("decode: %v", err)
+ }
+ if !got.NeedsTranscode {
+ t.Error("expected NeedsTranscode=true for mkv")
+ }
+}
+
+func TestHandlePlaybackHints_MKVNeedsTranscode(t *testing.T) {
+ // End-to-end: hintSvc returns a real hint for an mkv file with exotic codec.
+ store := buildSessionStore(1)
+ sm := auth.NewSessionManager(store, &clock.MockClock{T: time.Now()}, time.Hour)
+
+ hint := &service.PlaybackHint{
+ StreamURL: "/api/v1/media/10/stream",
+ Container: "mkv",
+ VideoCodec: "h264",
+ AudioCodec: "ac3",
+ NeedsTranscode: true,
+ }
+ hintSvc := &service.MockPlaybackHintsService{
+ GetPlaybackHintFunc: func(_ context.Context, _, _ int64) (*service.PlaybackHint, error) {
+ return hint, nil
+ },
+ }
+
+ srv := newPlaybackTestServer(t, store, sm, hintSvc)
+ req := httptest.NewRequest(http.MethodGet, "/api/v1/media/10/playback", nil)
+ req.AddCookie(sessionForPlaybackTest(t, store, sm))
+ rr := httptest.NewRecorder()
+ srv.ServeHTTP(rr, req)
+
+ if rr.Code != http.StatusOK {
+ t.Fatalf("expected 200, got %d", rr.Code)
+ }
+ var got service.PlaybackHint
+ if err := json.NewDecoder(rr.Body).Decode(&got); err != nil {
+ t.Fatalf("decode: %v", err)
+ }
+ if !got.NeedsTranscode {
+ t.Error("expected NeedsTranscode=true for mkv/ac3")
+ }
+ if got.Container != "mkv" {
+ t.Errorf("container: want mkv, got %s", got.Container)
+ }
+}