navidrome/server/subsonic/bookmarks_test.go
Deluan Quintão 8e784b6af7
fix: apply the per-user library filter to bookmarks, playlists and now-playing (#6179)
On a multi-library instance, a few reads and writes built their own queries
without the per-user library filter that every other media read applies. A
user granted only some libraries could see, and store, tracks from libraries
they had no access to.

- getBookmarks now filters the query. It has to be the query and not the
  result: the loop below it pre-sizes the response from the bookmark count,
  so a row dropped afterwards would emit an empty bookmark entry.
- createBookmark rejects an id the caller cannot read, returning error 70 to
  match getSong. Stored rows are left alone rather than purged, so a
  temporary revoke does not lose saved playback positions.
- playlistTrackRepository Read, Count and GetAlbumIDs get the filter their
  siblings CountAll and GetMediaFileIDs already had. Read is the one that
  mattered most: its id is the integer playlist position, so it needed no
  track id at all.
- Playlist track writes are filtered in playlistRepository.addTracks, the
  only writer of playlist_tracks rows apart from smart playlists, so Add,
  Insert, AddAlbums/AddArtists/AddDiscs and a full replace through Put all
  go through it. Insert reserves a slot per requested id, so when the filter
  drops one it renumbers to close the hole.
- playTracker.GetNowPlaying honours its context instead of discarding it.
  The cache is process-global, so the filter belongs in the tracker rather
  than in the Subsonic handler, and any future caller inherits it.

Admins and single-library installs are unaffected: applyLibraryFilter and
HasLibraryAccess both short-circuit for them. Scanner playlist sync runs as
admin, and M3U and CLI imports already resolve tracks through FindByPaths as
the same user, so neither changes.
2026-09-20 12:37:18 -04:00

48 lines
1.4 KiB
Go

package subsonic
import (
"context"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request"
"github.com/navidrome/navidrome/server/subsonic/responses"
"github.com/navidrome/navidrome/tests"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("Bookmarks", func() {
var router *Router
var ds *tests.MockDataStore
var mfRepo *tests.MockMediaFileRepo
var ctx context.Context
BeforeEach(func() {
ds = &tests.MockDataStore{}
router = New(ds, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
ctx = request.WithUser(context.Background(), model.User{ID: "u1", UserName: "u1"})
mfRepo = ds.MediaFile(ctx).(*tests.MockMediaFileRepo)
mfRepo.SetData(model.MediaFiles{{ID: "visible"}})
})
Describe("CreateBookmark", func() {
It("rejects an id the user cannot read", func() {
r := newGetRequest("id=hidden", "position=1").WithContext(ctx)
_, err := router.CreateBookmark(r)
Expect(err).To(HaveOccurred())
Expect(mapToSubsonicError(err).code).To(Equal(responses.ErrorDataNotFound))
Expect(mfRepo.BookmarksAdded).To(BeEmpty())
})
It("accepts an id the user can read", func() {
r := newGetRequest("id=visible", "position=1").WithContext(ctx)
_, err := router.CreateBookmark(r)
Expect(err).ToNot(HaveOccurred())
Expect(mfRepo.BookmarksAdded).To(ConsistOf("visible"))
})
})
})