diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-18 13:47:49 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-18 13:47:49 +0300 |
| commit | ca0200985504b480f9a664e929f64dec53a6e36b (patch) | |
| tree | ff1ae93fef5dc6561cfb48b53e54693a6883302b /player-server/internal/api/handlers_playback_test.go | |
| parent | 60df3397fbd14a6ad200e1fdd2530f708403ee99 (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.go | 249 |
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) + } +} |
