* feat(conf): add Dir type with lazy directory creation Introduces the Dir type that wraps a directory path string and defers os.MkdirAll until the first call to Path() or MustPath(), using sync.Once to ensure the creation happens exactly once. Implements fmt.Stringer, encoding.TextMarshaler, and encoding.TextUnmarshaler for config integration. Includes Ginkgo/Gomega tests covering all methods and error paths. * refactor(conf): replace eager dir creation with lazy Dir type Change DataFolder, CacheFolder, Plugins.Folder, and Backup.Path from string to Dir. Remove all os.MkdirAll calls from Load() so directories are created lazily on first Path()/MustPath() call. Artwork folder creation was already handled at point-of-use in image_upload.go. Add SnapshotConfig() to conf package for safe test config save/restore that avoids copying sync.Once inside Dir fields. Fix copy-lock vet warning in nativeapi/config.go by marshalling pointer instead of value. * refactor(conf): migrate tests and db init to lazy Dir type Update all test files to use conf.NewDir() for Dir field assignments. Ensure DataFolder is created lazily when the database is first opened in db.Db(). Remove eager directory creation from conf.Load() tests. * fix(conf): address review findings for Dir type - Use os.ModePerm for DataFolder/CacheFolder (was 0700, should match original behavior). Add NewDirWithPerm for PluginsFolder (0700). - Use Path() instead of MustPath() in db.Prune() to avoid logFatal from background cron job. - Panic on marshal/unmarshal errors in SnapshotConfig (test helper). - Clean up redundant String()/MustPath() calls in plugin manager. - Remove dead code in dir_test.go. Signed-off-by: Deluan <deluan@navidrome.org> * fix(conf): add GoString to Dir for clean config dump output Implement fmt.GoStringer on Dir so pretty.Sprintf shows the path string instead of internal struct fields (sync.Once, perm, err). Also add TODO comment to configtest about removing the indirection. * fix(dir): improve error logging in MustPath method Signed-off-by: Deluan <deluan@navidrome.org> * refactor(tests): remove redundant tests for unwritable DataFolder and CacheFolder Signed-off-by: Deluan <deluan@navidrome.org> * fix(conf): address PR review feedback - Ensure Plugins.Folder always uses 0700, even when user-configured (previously only the derived default got restrictive permissions). - Create LogFile parent directory before opening, so LogFile paths inside a not-yet-created DataFolder work correctly. --------- Signed-off-by: Deluan <deluan@navidrome.org>
418 lines
15 KiB
Go
418 lines
15 KiB
Go
package playlists_test
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/navidrome/navidrome/conf"
|
|
"github.com/navidrome/navidrome/conf/configtest"
|
|
"github.com/navidrome/navidrome/core"
|
|
"github.com/navidrome/navidrome/core/playlists"
|
|
"github.com/navidrome/navidrome/model"
|
|
"github.com/navidrome/navidrome/model/criteria"
|
|
"github.com/navidrome/navidrome/model/request"
|
|
"github.com/navidrome/navidrome/tests"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
var _ = Describe("Playlists", func() {
|
|
var ds *tests.MockDataStore
|
|
var ps playlists.Playlists
|
|
var mockPlsRepo *tests.MockPlaylistRepo
|
|
ctx := context.Background()
|
|
|
|
BeforeEach(func() {
|
|
mockPlsRepo = tests.CreateMockPlaylistRepo()
|
|
ds = &tests.MockDataStore{
|
|
MockedPlaylist: mockPlsRepo,
|
|
MockedLibrary: &tests.MockLibraryRepo{},
|
|
}
|
|
ctx = request.WithUser(ctx, model.User{ID: "123"})
|
|
})
|
|
|
|
Describe("Delete", func() {
|
|
var mockTracks *tests.MockPlaylistTrackRepo
|
|
|
|
BeforeEach(func() {
|
|
mockTracks = &tests.MockPlaylistTrackRepo{AddCount: 3}
|
|
mockPlsRepo.Data = map[string]*model.Playlist{
|
|
"pls-1": {ID: "pls-1", Name: "My Playlist", OwnerID: "user-1"},
|
|
}
|
|
mockPlsRepo.TracksRepo = mockTracks
|
|
ps = playlists.NewPlaylists(ds, core.NewImageUploadService())
|
|
})
|
|
|
|
It("allows owner to delete their playlist", func() {
|
|
ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false})
|
|
err := ps.Delete(ctx, "pls-1")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(mockPlsRepo.Deleted).To(ContainElement("pls-1"))
|
|
})
|
|
|
|
It("allows admin to delete any playlist", func() {
|
|
ctx = request.WithUser(ctx, model.User{ID: "admin-1", IsAdmin: true})
|
|
err := ps.Delete(ctx, "pls-1")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(mockPlsRepo.Deleted).To(ContainElement("pls-1"))
|
|
})
|
|
|
|
It("denies non-owner, non-admin from deleting", func() {
|
|
ctx = request.WithUser(ctx, model.User{ID: "other-user", IsAdmin: false})
|
|
err := ps.Delete(ctx, "pls-1")
|
|
Expect(err).To(MatchError(model.ErrNotAuthorized))
|
|
Expect(mockPlsRepo.Deleted).To(BeEmpty())
|
|
})
|
|
|
|
It("returns error when playlist not found", func() {
|
|
ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false})
|
|
err := ps.Delete(ctx, "nonexistent")
|
|
Expect(err).To(Equal(model.ErrNotFound))
|
|
})
|
|
})
|
|
|
|
Describe("Create", func() {
|
|
BeforeEach(func() {
|
|
mockPlsRepo.Data = map[string]*model.Playlist{
|
|
"pls-1": {ID: "pls-1", Name: "Existing", OwnerID: "user-1"},
|
|
"pls-2": {ID: "pls-2", Name: "Other's", OwnerID: "other-user"},
|
|
"pls-smart": {ID: "pls-smart", Name: "Smart", OwnerID: "user-1",
|
|
Rules: &criteria.Criteria{Expression: criteria.Contains{"title": "test"}}},
|
|
}
|
|
ps = playlists.NewPlaylists(ds, core.NewImageUploadService())
|
|
})
|
|
|
|
It("creates a new playlist with owner set from context", func() {
|
|
ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false})
|
|
id, err := ps.Create(ctx, "", "New Playlist", []string{"song-1", "song-2"})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(id).ToNot(BeEmpty())
|
|
Expect(mockPlsRepo.Last.Name).To(Equal("New Playlist"))
|
|
Expect(mockPlsRepo.Last.OwnerID).To(Equal("user-1"))
|
|
})
|
|
|
|
It("replaces tracks on existing playlist when owner matches", func() {
|
|
ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false})
|
|
id, err := ps.Create(ctx, "pls-1", "", []string{"song-3"})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(id).To(Equal("pls-1"))
|
|
Expect(mockPlsRepo.Last.Tracks).To(HaveLen(1))
|
|
})
|
|
|
|
It("allows admin to replace tracks on any playlist", func() {
|
|
ctx = request.WithUser(ctx, model.User{ID: "admin-1", IsAdmin: true})
|
|
id, err := ps.Create(ctx, "pls-2", "", []string{"song-3"})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(id).To(Equal("pls-2"))
|
|
})
|
|
|
|
It("denies non-owner, non-admin from replacing tracks on existing playlist", func() {
|
|
ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false})
|
|
_, err := ps.Create(ctx, "pls-2", "", []string{"song-3"})
|
|
Expect(err).To(MatchError(model.ErrNotAuthorized))
|
|
})
|
|
|
|
It("returns error when existing playlistId not found", func() {
|
|
ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false})
|
|
_, err := ps.Create(ctx, "nonexistent", "", []string{"song-1"})
|
|
Expect(err).To(Equal(model.ErrNotFound))
|
|
})
|
|
|
|
It("denies replacing tracks on a smart playlist", func() {
|
|
ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false})
|
|
_, err := ps.Create(ctx, "pls-smart", "", []string{"song-1"})
|
|
Expect(err).To(MatchError(model.ErrNotAuthorized))
|
|
})
|
|
})
|
|
|
|
Describe("Update", func() {
|
|
var mockTracks *tests.MockPlaylistTrackRepo
|
|
|
|
BeforeEach(func() {
|
|
mockTracks = &tests.MockPlaylistTrackRepo{AddCount: 2}
|
|
mockPlsRepo.Data = map[string]*model.Playlist{
|
|
"pls-1": {ID: "pls-1", Name: "My Playlist", OwnerID: "user-1"},
|
|
"pls-other": {ID: "pls-other", Name: "Other's", OwnerID: "other-user"},
|
|
"pls-smart": {ID: "pls-smart", Name: "Smart", OwnerID: "user-1",
|
|
Rules: &criteria.Criteria{Expression: criteria.Contains{"title": "test"}}},
|
|
}
|
|
mockPlsRepo.TracksRepo = mockTracks
|
|
ps = playlists.NewPlaylists(ds, core.NewImageUploadService())
|
|
})
|
|
|
|
It("allows owner to update their playlist", func() {
|
|
ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false})
|
|
newName := "Updated Name"
|
|
err := ps.Update(ctx, "pls-1", &newName, nil, nil, nil, nil)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
})
|
|
|
|
It("allows admin to update any playlist", func() {
|
|
ctx = request.WithUser(ctx, model.User{ID: "admin-1", IsAdmin: true})
|
|
newName := "Updated Name"
|
|
err := ps.Update(ctx, "pls-other", &newName, nil, nil, nil, nil)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
})
|
|
|
|
It("denies non-owner, non-admin from updating", func() {
|
|
ctx = request.WithUser(ctx, model.User{ID: "other-user", IsAdmin: false})
|
|
newName := "Updated Name"
|
|
err := ps.Update(ctx, "pls-1", &newName, nil, nil, nil, nil)
|
|
Expect(err).To(MatchError(model.ErrNotAuthorized))
|
|
})
|
|
|
|
It("returns error when playlist not found", func() {
|
|
ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false})
|
|
newName := "Updated Name"
|
|
err := ps.Update(ctx, "nonexistent", &newName, nil, nil, nil, nil)
|
|
Expect(err).To(Equal(model.ErrNotFound))
|
|
})
|
|
|
|
It("denies adding tracks to a smart playlist", func() {
|
|
ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false})
|
|
err := ps.Update(ctx, "pls-smart", nil, nil, nil, []string{"song-1"}, nil)
|
|
Expect(err).To(MatchError(model.ErrNotAuthorized))
|
|
})
|
|
|
|
It("denies removing tracks from a smart playlist", func() {
|
|
ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false})
|
|
err := ps.Update(ctx, "pls-smart", nil, nil, nil, nil, []int{0})
|
|
Expect(err).To(MatchError(model.ErrNotAuthorized))
|
|
})
|
|
|
|
It("allows metadata updates on a smart playlist", func() {
|
|
ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false})
|
|
newName := "Updated Smart"
|
|
err := ps.Update(ctx, "pls-smart", &newName, nil, nil, nil, nil)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
})
|
|
})
|
|
|
|
Describe("AddTracks", func() {
|
|
var mockTracks *tests.MockPlaylistTrackRepo
|
|
|
|
BeforeEach(func() {
|
|
mockTracks = &tests.MockPlaylistTrackRepo{AddCount: 2}
|
|
mockPlsRepo.Data = map[string]*model.Playlist{
|
|
"pls-1": {ID: "pls-1", Name: "My Playlist", OwnerID: "user-1"},
|
|
"pls-smart": {ID: "pls-smart", Name: "Smart", OwnerID: "user-1",
|
|
Rules: &criteria.Criteria{Expression: criteria.Contains{"title": "test"}}},
|
|
"pls-other": {ID: "pls-other", Name: "Other's", OwnerID: "other-user"},
|
|
}
|
|
mockPlsRepo.TracksRepo = mockTracks
|
|
ps = playlists.NewPlaylists(ds, core.NewImageUploadService())
|
|
})
|
|
|
|
It("allows owner to add tracks", func() {
|
|
ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false})
|
|
count, err := ps.AddTracks(ctx, "pls-1", []string{"song-1", "song-2"})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(count).To(Equal(2))
|
|
Expect(mockTracks.AddedIds).To(ConsistOf("song-1", "song-2"))
|
|
})
|
|
|
|
It("allows admin to add tracks to any playlist", func() {
|
|
ctx = request.WithUser(ctx, model.User{ID: "admin-1", IsAdmin: true})
|
|
count, err := ps.AddTracks(ctx, "pls-other", []string{"song-1"})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(count).To(Equal(2))
|
|
})
|
|
|
|
It("denies non-owner, non-admin", func() {
|
|
ctx = request.WithUser(ctx, model.User{ID: "other-user", IsAdmin: false})
|
|
_, err := ps.AddTracks(ctx, "pls-1", []string{"song-1"})
|
|
Expect(err).To(MatchError(model.ErrNotAuthorized))
|
|
})
|
|
|
|
It("denies editing smart playlists", func() {
|
|
ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false})
|
|
_, err := ps.AddTracks(ctx, "pls-smart", []string{"song-1"})
|
|
Expect(err).To(MatchError(model.ErrNotAuthorized))
|
|
})
|
|
|
|
It("returns error when playlist not found", func() {
|
|
ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false})
|
|
_, err := ps.AddTracks(ctx, "nonexistent", []string{"song-1"})
|
|
Expect(err).To(Equal(model.ErrNotFound))
|
|
})
|
|
})
|
|
|
|
Describe("RemoveTracks", func() {
|
|
var mockTracks *tests.MockPlaylistTrackRepo
|
|
|
|
BeforeEach(func() {
|
|
mockTracks = &tests.MockPlaylistTrackRepo{}
|
|
mockPlsRepo.Data = map[string]*model.Playlist{
|
|
"pls-1": {ID: "pls-1", Name: "My Playlist", OwnerID: "user-1"},
|
|
"pls-smart": {ID: "pls-smart", Name: "Smart", OwnerID: "user-1",
|
|
Rules: &criteria.Criteria{Expression: criteria.Contains{"title": "test"}}},
|
|
}
|
|
mockPlsRepo.TracksRepo = mockTracks
|
|
ps = playlists.NewPlaylists(ds, core.NewImageUploadService())
|
|
})
|
|
|
|
It("allows owner to remove tracks", func() {
|
|
ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false})
|
|
err := ps.RemoveTracks(ctx, "pls-1", []string{"track-1", "track-2"})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(mockTracks.DeletedIds).To(ConsistOf("track-1", "track-2"))
|
|
})
|
|
|
|
It("denies on smart playlist", func() {
|
|
ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false})
|
|
err := ps.RemoveTracks(ctx, "pls-smart", []string{"track-1"})
|
|
Expect(err).To(MatchError(model.ErrNotAuthorized))
|
|
})
|
|
|
|
It("denies non-owner", func() {
|
|
ctx = request.WithUser(ctx, model.User{ID: "other-user", IsAdmin: false})
|
|
err := ps.RemoveTracks(ctx, "pls-1", []string{"track-1"})
|
|
Expect(err).To(MatchError(model.ErrNotAuthorized))
|
|
})
|
|
})
|
|
|
|
Describe("ReorderTrack", func() {
|
|
var mockTracks *tests.MockPlaylistTrackRepo
|
|
|
|
BeforeEach(func() {
|
|
mockTracks = &tests.MockPlaylistTrackRepo{}
|
|
mockPlsRepo.Data = map[string]*model.Playlist{
|
|
"pls-1": {ID: "pls-1", Name: "My Playlist", OwnerID: "user-1"},
|
|
"pls-smart": {ID: "pls-smart", Name: "Smart", OwnerID: "user-1",
|
|
Rules: &criteria.Criteria{Expression: criteria.Contains{"title": "test"}}},
|
|
}
|
|
mockPlsRepo.TracksRepo = mockTracks
|
|
ps = playlists.NewPlaylists(ds, core.NewImageUploadService())
|
|
})
|
|
|
|
It("allows owner to reorder", func() {
|
|
ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false})
|
|
err := ps.ReorderTrack(ctx, "pls-1", 1, 3)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(mockTracks.Reordered).To(BeTrue())
|
|
})
|
|
|
|
It("denies on smart playlist", func() {
|
|
ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false})
|
|
err := ps.ReorderTrack(ctx, "pls-smart", 1, 3)
|
|
Expect(err).To(MatchError(model.ErrNotAuthorized))
|
|
})
|
|
})
|
|
|
|
Describe("SetImage", func() {
|
|
var tmpDir string
|
|
|
|
BeforeEach(func() {
|
|
DeferCleanup(configtest.SetupConfig())
|
|
tmpDir = GinkgoT().TempDir()
|
|
conf.Server.DataFolder = conf.NewDir(tmpDir)
|
|
|
|
mockPlsRepo.Data = map[string]*model.Playlist{
|
|
"pls-1": {ID: "pls-1", Name: "My Playlist", OwnerID: "user-1"},
|
|
"pls-other": {ID: "pls-other", Name: "Other's", OwnerID: "other-user"},
|
|
}
|
|
ps = playlists.NewPlaylists(ds, core.NewImageUploadService())
|
|
})
|
|
|
|
It("saves image file and updates UploadedImage", func() {
|
|
ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false})
|
|
reader := strings.NewReader("fake image data")
|
|
err := ps.SetImage(ctx, "pls-1", reader, ".jpg")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
Expect(mockPlsRepo.Last.UploadedImage).To(Equal("pls-1_my_playlist.jpg"))
|
|
absPath := filepath.Join(tmpDir, "artwork", "playlist", "pls-1_my_playlist.jpg")
|
|
data, err := os.ReadFile(absPath)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(string(data)).To(Equal("fake image data"))
|
|
})
|
|
|
|
It("removes old image when replacing", func() {
|
|
ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false})
|
|
|
|
// Upload first image
|
|
err := ps.SetImage(ctx, "pls-1", strings.NewReader("first"), ".png")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
oldPath := filepath.Join(tmpDir, "artwork", "playlist", "pls-1_my_playlist.png")
|
|
Expect(oldPath).To(BeAnExistingFile())
|
|
|
|
// Upload replacement image
|
|
err = ps.SetImage(ctx, "pls-1", strings.NewReader("second"), ".jpg")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(oldPath).ToNot(BeAnExistingFile())
|
|
newPath := filepath.Join(tmpDir, "artwork", "playlist", "pls-1_my_playlist.jpg")
|
|
Expect(newPath).To(BeAnExistingFile())
|
|
})
|
|
|
|
It("allows admin to set image on any playlist", func() {
|
|
ctx = request.WithUser(ctx, model.User{ID: "admin-1", IsAdmin: true})
|
|
err := ps.SetImage(ctx, "pls-other", strings.NewReader("data"), ".jpg")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
})
|
|
|
|
It("denies non-owner", func() {
|
|
ctx = request.WithUser(ctx, model.User{ID: "other-user", IsAdmin: false})
|
|
err := ps.SetImage(ctx, "pls-1", strings.NewReader("data"), ".jpg")
|
|
Expect(err).To(MatchError(model.ErrNotAuthorized))
|
|
})
|
|
|
|
It("returns error when playlist not found", func() {
|
|
ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false})
|
|
err := ps.SetImage(ctx, "nonexistent", strings.NewReader("data"), ".jpg")
|
|
Expect(err).To(Equal(model.ErrNotFound))
|
|
})
|
|
})
|
|
|
|
Describe("RemoveImage", func() {
|
|
var tmpDir string
|
|
|
|
BeforeEach(func() {
|
|
DeferCleanup(configtest.SetupConfig())
|
|
tmpDir = GinkgoT().TempDir()
|
|
conf.Server.DataFolder = conf.NewDir(tmpDir)
|
|
|
|
// Create a real image file on disk
|
|
imgDir := filepath.Join(tmpDir, "artwork", "playlist")
|
|
Expect(os.MkdirAll(imgDir, 0755)).To(Succeed())
|
|
Expect(os.WriteFile(filepath.Join(imgDir, "pls-1.jpg"), []byte("img data"), 0600)).To(Succeed())
|
|
|
|
mockPlsRepo.Data = map[string]*model.Playlist{
|
|
"pls-1": {ID: "pls-1", Name: "My Playlist", OwnerID: "user-1", UploadedImage: "pls-1.jpg"},
|
|
"pls-empty": {ID: "pls-empty", Name: "No Cover", OwnerID: "user-1"},
|
|
"pls-other": {ID: "pls-other", Name: "Other's", OwnerID: "other-user"},
|
|
}
|
|
ps = playlists.NewPlaylists(ds, core.NewImageUploadService())
|
|
})
|
|
|
|
It("removes file and clears UploadedImage", func() {
|
|
ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false})
|
|
err := ps.RemoveImage(ctx, "pls-1")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
Expect(mockPlsRepo.Last.UploadedImage).To(BeEmpty())
|
|
absPath := filepath.Join(tmpDir, "artwork", "playlist", "pls-1.jpg")
|
|
Expect(absPath).ToNot(BeAnExistingFile())
|
|
})
|
|
|
|
It("succeeds even if playlist has no image", func() {
|
|
ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false})
|
|
err := ps.RemoveImage(ctx, "pls-empty")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(mockPlsRepo.Last.UploadedImage).To(BeEmpty())
|
|
})
|
|
|
|
It("denies non-owner", func() {
|
|
ctx = request.WithUser(ctx, model.User{ID: "other-user", IsAdmin: false})
|
|
err := ps.RemoveImage(ctx, "pls-1")
|
|
Expect(err).To(MatchError(model.ErrNotAuthorized))
|
|
})
|
|
|
|
It("returns error when playlist not found", func() {
|
|
ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false})
|
|
err := ps.RemoveImage(ctx, "nonexistent")
|
|
Expect(err).To(Equal(model.ErrNotFound))
|
|
})
|
|
})
|
|
})
|