navidrome/server/nativeapi/playlists_test.go
Deluan b64d8ad334 fix(server): return 404 instead of 500 for non-existent playlists
The native API endpoints GET /playlist/{id}/tracks and
GET /playlist/{id}/tracks/{id} were panicking with a nil pointer
dereference (resulting in a 500) when the playlist did not exist.
This happened because Tracks() returns nil for missing playlists,
and the nil repository was passed directly to the rest handler.
Extracted a shared playlistTracksHandler that checks for nil and
returns 404 early. Added tests covering both the error and happy paths.
2026-02-15 22:39:27 -05:00

168 lines
4.5 KiB
Go

package nativeapi
import (
"encoding/json"
"net/http"
"net/http/httptest"
"time"
"github.com/deluan/rest"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/conf/configtest"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/core/auth"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/server"
"github.com/navidrome/navidrome/tests"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
type mockPlaylistTrackRepo struct {
model.PlaylistTrackRepository
tracks model.PlaylistTracks
}
func (m *mockPlaylistTrackRepo) Count(...rest.QueryOptions) (int64, error) {
return int64(len(m.tracks)), nil
}
func (m *mockPlaylistTrackRepo) ReadAll(...rest.QueryOptions) (any, error) {
return m.tracks, nil
}
func (m *mockPlaylistTrackRepo) EntityName() string {
return "playlist_track"
}
func (m *mockPlaylistTrackRepo) NewInstance() any {
return &model.PlaylistTrack{}
}
func (m *mockPlaylistTrackRepo) Read(id string) (any, error) {
for _, t := range m.tracks {
if t.ID == id {
return &t, nil
}
}
return nil, rest.ErrNotFound
}
var _ = Describe("Playlist Tracks Endpoint", func() {
var (
router http.Handler
ds *tests.MockDataStore
plsRepo *tests.MockPlaylistRepo
userRepo *tests.MockedUserRepo
w *httptest.ResponseRecorder
)
BeforeEach(func() {
DeferCleanup(configtest.SetupConfig())
conf.Server.SessionTimeout = time.Minute
plsRepo = &tests.MockPlaylistRepo{}
userRepo = tests.CreateMockUserRepo()
ds = &tests.MockDataStore{
MockedPlaylist: plsRepo,
MockedUser: userRepo,
MockedProperty: &tests.MockedPropertyRepo{},
}
auth.Init(ds)
testUser := model.User{
ID: "user-1",
UserName: "testuser",
Name: "Test User",
IsAdmin: false,
NewPassword: "testpass",
}
err := userRepo.Put(&testUser)
Expect(err).ToNot(HaveOccurred())
nativeRouter := New(ds, nil, nil, nil, tests.NewMockLibraryService(), tests.NewMockUserService(), nil, nil)
router = server.JWTVerifier(nativeRouter)
w = httptest.NewRecorder()
})
createAuthenticatedRequest := func(method, path string) *http.Request {
req := httptest.NewRequest(method, path, nil)
testUser := model.User{ID: "user-1", UserName: "testuser"}
token, err := auth.CreateToken(&testUser)
Expect(err).ToNot(HaveOccurred())
req.Header.Set(consts.UIAuthorizationHeader, "Bearer "+token)
return req
}
Describe("GET /playlist/{playlistId}/tracks", func() {
It("returns 404 when playlist does not exist", func() {
req := createAuthenticatedRequest("GET", "/playlist/non-existent/tracks")
router.ServeHTTP(w, req)
Expect(w.Code).To(Equal(http.StatusNotFound))
})
It("returns tracks when playlist exists", func() {
plsRepo.TracksReturn = &mockPlaylistTrackRepo{
tracks: model.PlaylistTracks{
{ID: "1", MediaFileID: "mf-1", PlaylistID: "pls-1"},
{ID: "2", MediaFileID: "mf-2", PlaylistID: "pls-1"},
},
}
req := createAuthenticatedRequest("GET", "/playlist/pls-1/tracks")
router.ServeHTTP(w, req)
Expect(w.Code).To(Equal(http.StatusOK))
var response []model.PlaylistTrack
err := json.Unmarshal(w.Body.Bytes(), &response)
Expect(err).ToNot(HaveOccurred())
Expect(response).To(HaveLen(2))
Expect(response[0].ID).To(Equal("1"))
Expect(response[1].ID).To(Equal("2"))
})
})
Describe("GET /playlist/{playlistId}/tracks/{id}", func() {
It("returns 404 when playlist does not exist", func() {
req := createAuthenticatedRequest("GET", "/playlist/non-existent/tracks/1")
router.ServeHTTP(w, req)
Expect(w.Code).To(Equal(http.StatusNotFound))
})
It("returns the track when playlist exists", func() {
plsRepo.TracksReturn = &mockPlaylistTrackRepo{
tracks: model.PlaylistTracks{
{ID: "1", MediaFileID: "mf-1", PlaylistID: "pls-1"},
},
}
req := createAuthenticatedRequest("GET", "/playlist/pls-1/tracks/1")
router.ServeHTTP(w, req)
Expect(w.Code).To(Equal(http.StatusOK))
var response model.PlaylistTrack
err := json.Unmarshal(w.Body.Bytes(), &response)
Expect(err).ToNot(HaveOccurred())
Expect(response.ID).To(Equal("1"))
Expect(response.MediaFileID).To(Equal("mf-1"))
})
It("returns 404 when track does not exist in playlist", func() {
plsRepo.TracksReturn = &mockPlaylistTrackRepo{
tracks: model.PlaylistTracks{},
}
req := createAuthenticatedRequest("GET", "/playlist/pls-1/tracks/999")
router.ServeHTTP(w, req)
Expect(w.Code).To(Equal(http.StatusNotFound))
})
})
})