mirror of
https://github.com/navidrome/navidrome.git
synced 2026-03-04 06:35:52 +00:00
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.
168 lines
4.5 KiB
Go
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))
|
|
})
|
|
})
|
|
})
|