mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-01 07:21:17 +00:00
* fix(criteria): coerce string booleans in smart playlist rules - #4826 When clients (e.g. Feishin) send boolean values as strings ("true"/"false") in smart playlist JSON rules, the SQL comparison fails because SQLite stores booleans as 0/1 integers. For example, `COALESCE(annotation.starred, false) = 'true'` never matches. This adds a `boolean` flag to mapped fields and coerces string values to native Go bools in `mapFields`, so squirrel generates correct SQL parameters. Signed-off-by: mango766 <mango766@users.noreply.github.com> Signed-off-by: easonysliu <easonysliu@tencent.com> * fix(criteria): implement boolean string coercion for smart playlist rules Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: mango766 <mango766@users.noreply.github.com> Signed-off-by: easonysliu <easonysliu@tencent.com> Signed-off-by: Deluan <deluan@navidrome.org> Co-authored-by: easonysliu <easonysliu@tencent.com>
651 lines
23 KiB
Go
651 lines
23 KiB
Go
package e2e
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/navidrome/navidrome/conf"
|
|
"github.com/navidrome/navidrome/model"
|
|
"github.com/navidrome/navidrome/model/criteria"
|
|
"github.com/navidrome/navidrome/server/subsonic/responses"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
var _ = Describe("Playlist Endpoints", Ordered, func() {
|
|
var playlistID string
|
|
var songIDs []string
|
|
|
|
BeforeAll(func() {
|
|
setupTestDB()
|
|
|
|
// Look up song IDs from scanned data for playlist operations
|
|
songs, err := ds.MediaFile(ctx).GetAll(model.QueryOptions{Sort: "title", Max: 6})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(len(songs)).To(BeNumerically(">=", 5))
|
|
for _, s := range songs {
|
|
songIDs = append(songIDs, s.ID)
|
|
}
|
|
})
|
|
|
|
It("getPlaylists returns empty list initially", func() {
|
|
resp := doReq("getPlaylists")
|
|
|
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
|
Expect(resp.Playlists).ToNot(BeNil())
|
|
Expect(resp.Playlists.Playlist).To(BeEmpty())
|
|
})
|
|
|
|
It("createPlaylist creates a new playlist with songs", func() {
|
|
resp := doReq("createPlaylist", "name", "Test Playlist",
|
|
"songId", songIDs[0], "songId", songIDs[1], "songId", songIDs[2])
|
|
|
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
|
Expect(resp.Playlist).ToNot(BeNil())
|
|
Expect(resp.Playlist.Name).To(Equal("Test Playlist"))
|
|
Expect(resp.Playlist.SongCount).To(Equal(int32(3)))
|
|
Expect(resp.Playlist.Entry).To(HaveLen(3))
|
|
Expect(resp.Playlist.Entry[0].Id).To(Equal(songIDs[0]))
|
|
Expect(resp.Playlist.Entry[1].Id).To(Equal(songIDs[1]))
|
|
Expect(resp.Playlist.Entry[2].Id).To(Equal(songIDs[2]))
|
|
playlistID = resp.Playlist.Id
|
|
})
|
|
|
|
It("getPlaylist returns playlist with tracks in order", func() {
|
|
resp := doReq("getPlaylist", "id", playlistID)
|
|
|
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
|
Expect(resp.Playlist).ToNot(BeNil())
|
|
Expect(resp.Playlist.Name).To(Equal("Test Playlist"))
|
|
Expect(resp.Playlist.Entry).To(HaveLen(3))
|
|
Expect(resp.Playlist.Entry[0].Id).To(Equal(songIDs[0]))
|
|
Expect(resp.Playlist.Entry[1].Id).To(Equal(songIDs[1]))
|
|
Expect(resp.Playlist.Entry[2].Id).To(Equal(songIDs[2]))
|
|
})
|
|
|
|
It("createPlaylist without name or playlistId returns error", func() {
|
|
resp := doReq("createPlaylist", "songId", songIDs[0])
|
|
|
|
Expect(resp.Status).To(Equal(responses.StatusFailed))
|
|
Expect(resp.Error).ToNot(BeNil())
|
|
})
|
|
|
|
It("createPlaylist with playlistId replaces tracks on existing playlist", func() {
|
|
// Replace tracks: the playlist had [song0, song1, song2], replace with [song3, song4]
|
|
resp := doReq("createPlaylist", "playlistId", playlistID,
|
|
"songId", songIDs[3], "songId", songIDs[4])
|
|
|
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
|
Expect(resp.Playlist).ToNot(BeNil())
|
|
Expect(resp.Playlist.Id).To(Equal(playlistID))
|
|
Expect(resp.Playlist.SongCount).To(Equal(int32(2)))
|
|
Expect(resp.Playlist.Entry).To(HaveLen(2))
|
|
Expect(resp.Playlist.Entry[0].Id).To(Equal(songIDs[3]))
|
|
Expect(resp.Playlist.Entry[1].Id).To(Equal(songIDs[4]))
|
|
})
|
|
|
|
It("updatePlaylist can rename the playlist", func() {
|
|
resp := doReq("updatePlaylist", "playlistId", playlistID, "name", "Renamed Playlist")
|
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
|
|
|
// Verify the rename
|
|
resp = doReq("getPlaylist", "id", playlistID)
|
|
Expect(resp.Playlist.Name).To(Equal("Renamed Playlist"))
|
|
// Tracks should be unchanged
|
|
Expect(resp.Playlist.SongCount).To(Equal(int32(2)))
|
|
})
|
|
|
|
It("updatePlaylist can set comment", func() {
|
|
resp := doReq("updatePlaylist", "playlistId", playlistID, "comment", "My favorite songs")
|
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
|
|
|
resp = doReq("getPlaylist", "id", playlistID)
|
|
Expect(resp.Playlist.Comment).To(Equal("My favorite songs"))
|
|
})
|
|
|
|
It("updatePlaylist can set public visibility", func() {
|
|
resp := doReq("updatePlaylist", "playlistId", playlistID, "public", "true")
|
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
|
|
|
resp = doReq("getPlaylist", "id", playlistID)
|
|
Expect(resp.Playlist.Public).To(BeTrue())
|
|
})
|
|
|
|
It("updatePlaylist can add songs", func() {
|
|
// Playlist currently has [song3, song4], add song0
|
|
resp := doReq("updatePlaylist", "playlistId", playlistID, "songIdToAdd", songIDs[0])
|
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
|
|
|
resp = doReq("getPlaylist", "id", playlistID)
|
|
Expect(resp.Playlist.SongCount).To(Equal(int32(3)))
|
|
Expect(resp.Playlist.Entry).To(HaveLen(3))
|
|
Expect(resp.Playlist.Entry[0].Id).To(Equal(songIDs[3]))
|
|
Expect(resp.Playlist.Entry[1].Id).To(Equal(songIDs[4]))
|
|
Expect(resp.Playlist.Entry[2].Id).To(Equal(songIDs[0]))
|
|
})
|
|
|
|
It("updatePlaylist can add multiple songs at once", func() {
|
|
// Playlist currently has [song3, song4, song0], add song1 and song2
|
|
resp := doReq("updatePlaylist", "playlistId", playlistID,
|
|
"songIdToAdd", songIDs[1], "songIdToAdd", songIDs[2])
|
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
|
|
|
resp = doReq("getPlaylist", "id", playlistID)
|
|
Expect(resp.Playlist.SongCount).To(Equal(int32(5)))
|
|
Expect(resp.Playlist.Entry).To(HaveLen(5))
|
|
})
|
|
|
|
It("updatePlaylist can remove songs by index and verifies correct songs remain", func() {
|
|
// Playlist has [song3, song4, song0, song1, song2]
|
|
// Remove index 0 (song3) and index 2 (song0)
|
|
resp := doReq("updatePlaylist", "playlistId", playlistID,
|
|
"songIndexToRemove", "0", "songIndexToRemove", "2")
|
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
|
|
|
resp = doReq("getPlaylist", "id", playlistID)
|
|
Expect(resp.Playlist.SongCount).To(Equal(int32(3)))
|
|
Expect(resp.Playlist.Entry).To(HaveLen(3))
|
|
Expect(resp.Playlist.Entry[0].Id).To(Equal(songIDs[4]))
|
|
Expect(resp.Playlist.Entry[1].Id).To(Equal(songIDs[1]))
|
|
Expect(resp.Playlist.Entry[2].Id).To(Equal(songIDs[2]))
|
|
})
|
|
|
|
It("updatePlaylist can remove and add songs in a single call", func() {
|
|
// Playlist has [song4, song1, song2]
|
|
// Remove index 1 (song1) and add song3
|
|
resp := doReq("updatePlaylist", "playlistId", playlistID,
|
|
"songIndexToRemove", "1", "songIdToAdd", songIDs[3])
|
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
|
|
|
resp = doReq("getPlaylist", "id", playlistID)
|
|
Expect(resp.Playlist.SongCount).To(Equal(int32(3)))
|
|
Expect(resp.Playlist.Entry).To(HaveLen(3))
|
|
Expect(resp.Playlist.Entry[0].Id).To(Equal(songIDs[4]))
|
|
Expect(resp.Playlist.Entry[1].Id).To(Equal(songIDs[2]))
|
|
Expect(resp.Playlist.Entry[2].Id).To(Equal(songIDs[3]))
|
|
})
|
|
|
|
It("updatePlaylist can combine metadata change with track removal", func() {
|
|
// Playlist has [song4, song2, song3]
|
|
// Rename + remove index 0 (song4)
|
|
resp := doReq("updatePlaylist", "playlistId", playlistID,
|
|
"name", "Final Playlist", "songIndexToRemove", "0")
|
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
|
|
|
resp = doReq("getPlaylist", "id", playlistID)
|
|
Expect(resp.Playlist.Name).To(Equal("Final Playlist"))
|
|
Expect(resp.Playlist.SongCount).To(Equal(int32(2)))
|
|
Expect(resp.Playlist.Entry[0].Id).To(Equal(songIDs[2]))
|
|
Expect(resp.Playlist.Entry[1].Id).To(Equal(songIDs[3]))
|
|
})
|
|
|
|
It("updatePlaylist can remove all songs from playlist", func() {
|
|
// Playlist has [song2, song3] — remove both
|
|
resp := doReq("updatePlaylist", "playlistId", playlistID,
|
|
"songIndexToRemove", "0", "songIndexToRemove", "1")
|
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
|
|
|
resp = doReq("getPlaylist", "id", playlistID)
|
|
Expect(resp.Playlist.SongCount).To(Equal(int32(0)))
|
|
Expect(resp.Playlist.Entry).To(BeEmpty())
|
|
})
|
|
|
|
It("updatePlaylist can add songs to an empty playlist", func() {
|
|
resp := doReq("updatePlaylist", "playlistId", playlistID,
|
|
"songIdToAdd", songIDs[0])
|
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
|
|
|
resp = doReq("getPlaylist", "id", playlistID)
|
|
Expect(resp.Playlist.SongCount).To(Equal(int32(1)))
|
|
Expect(resp.Playlist.Entry).To(HaveLen(1))
|
|
Expect(resp.Playlist.Entry[0].Id).To(Equal(songIDs[0]))
|
|
})
|
|
|
|
It("updatePlaylist without playlistId returns error", func() {
|
|
resp := doReq("updatePlaylist", "name", "No ID")
|
|
|
|
Expect(resp.Status).To(Equal(responses.StatusFailed))
|
|
Expect(resp.Error).ToNot(BeNil())
|
|
})
|
|
|
|
It("getPlaylists shows the playlist", func() {
|
|
resp := doReq("getPlaylists")
|
|
|
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
|
Expect(resp.Playlists.Playlist).To(HaveLen(1))
|
|
Expect(resp.Playlists.Playlist[0].Id).To(Equal(playlistID))
|
|
})
|
|
|
|
It("deletePlaylist removes the playlist", func() {
|
|
resp := doReq("deletePlaylist", "id", playlistID)
|
|
|
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
|
})
|
|
|
|
It("getPlaylist on deleted playlist returns error", func() {
|
|
resp := doReq("getPlaylist", "id", playlistID)
|
|
|
|
Expect(resp.Status).To(Equal(responses.StatusFailed))
|
|
Expect(resp.Error).ToNot(BeNil())
|
|
})
|
|
|
|
It("getPlaylists returns empty after deletion", func() {
|
|
resp := doReq("getPlaylists")
|
|
|
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
|
Expect(resp.Playlists.Playlist).To(BeEmpty())
|
|
})
|
|
|
|
Describe("Playlist Permissions", Ordered, func() {
|
|
var songIDs []string
|
|
var adminPrivateID string
|
|
var adminPublicID string
|
|
var regularPlaylistID string
|
|
|
|
BeforeAll(func() {
|
|
setupTestDB()
|
|
|
|
songs, err := ds.MediaFile(ctx).GetAll(model.QueryOptions{Sort: "title", Max: 6})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(len(songs)).To(BeNumerically(">=", 3))
|
|
for _, s := range songs {
|
|
songIDs = append(songIDs, s.ID)
|
|
}
|
|
})
|
|
|
|
It("admin creates a private playlist", func() {
|
|
resp := doReqWithUser(adminUser, "createPlaylist", "name", "Admin Private",
|
|
"songId", songIDs[0], "songId", songIDs[1])
|
|
|
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
|
adminPrivateID = resp.Playlist.Id
|
|
})
|
|
|
|
It("admin creates a public playlist", func() {
|
|
resp := doReqWithUser(adminUser, "createPlaylist", "name", "Admin Public",
|
|
"songId", songIDs[0], "songId", songIDs[1])
|
|
|
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
|
adminPublicID = resp.Playlist.Id
|
|
|
|
// Make it public
|
|
resp = doReqWithUser(adminUser, "updatePlaylist",
|
|
"playlistId", adminPublicID, "public", "true")
|
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
|
})
|
|
|
|
It("regular user creates a playlist", func() {
|
|
resp := doReqWithUser(regularUser, "createPlaylist", "name", "Regular Playlist",
|
|
"songId", songIDs[0])
|
|
|
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
|
regularPlaylistID = resp.Playlist.Id
|
|
})
|
|
|
|
// --- Private playlist: regular user gets "not found" (repo hides it entirely) ---
|
|
|
|
It("regular user cannot see admin's private playlist", func() {
|
|
resp := doReqWithUser(regularUser, "getPlaylist", "id", adminPrivateID)
|
|
|
|
Expect(resp.Status).To(Equal(responses.StatusFailed))
|
|
Expect(resp.Error).ToNot(BeNil())
|
|
})
|
|
|
|
It("regular user cannot update admin's private playlist (not found)", func() {
|
|
resp := doReqWithUser(regularUser, "updatePlaylist",
|
|
"playlistId", adminPrivateID, "name", "Hacked")
|
|
|
|
Expect(resp.Status).To(Equal(responses.StatusFailed))
|
|
Expect(resp.Error).ToNot(BeNil())
|
|
})
|
|
|
|
It("regular user cannot delete admin's private playlist (not found)", func() {
|
|
resp := doReqWithUser(regularUser, "deletePlaylist", "id", adminPrivateID)
|
|
|
|
Expect(resp.Status).To(Equal(responses.StatusFailed))
|
|
Expect(resp.Error).ToNot(BeNil())
|
|
})
|
|
|
|
// --- Public playlist: regular user can see but cannot modify (authorization fail, code 50) ---
|
|
|
|
It("regular user can see admin's public playlist", func() {
|
|
resp := doReqWithUser(regularUser, "getPlaylist", "id", adminPublicID)
|
|
|
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
|
Expect(resp.Playlist.Name).To(Equal("Admin Public"))
|
|
})
|
|
|
|
It("regular user cannot update admin's public playlist", func() {
|
|
resp := doReqWithUser(regularUser, "updatePlaylist",
|
|
"playlistId", adminPublicID, "name", "Hacked")
|
|
|
|
Expect(resp.Status).To(Equal(responses.StatusFailed))
|
|
Expect(resp.Error).ToNot(BeNil())
|
|
Expect(resp.Error.Code).To(Equal(int32(50)))
|
|
})
|
|
|
|
It("regular user cannot add songs to admin's public playlist", func() {
|
|
resp := doReqWithUser(regularUser, "updatePlaylist",
|
|
"playlistId", adminPublicID, "songIdToAdd", songIDs[2])
|
|
|
|
Expect(resp.Status).To(Equal(responses.StatusFailed))
|
|
Expect(resp.Error.Code).To(Equal(int32(50)))
|
|
})
|
|
|
|
It("regular user cannot remove songs from admin's public playlist", func() {
|
|
resp := doReqWithUser(regularUser, "updatePlaylist",
|
|
"playlistId", adminPublicID, "songIndexToRemove", "0")
|
|
|
|
Expect(resp.Status).To(Equal(responses.StatusFailed))
|
|
Expect(resp.Error.Code).To(Equal(int32(50)))
|
|
})
|
|
|
|
It("regular user cannot delete admin's public playlist", func() {
|
|
resp := doReqWithUser(regularUser, "deletePlaylist", "id", adminPublicID)
|
|
|
|
Expect(resp.Status).To(Equal(responses.StatusFailed))
|
|
Expect(resp.Error.Code).To(Equal(int32(50)))
|
|
})
|
|
|
|
It("regular user cannot replace tracks on admin's public playlist via createPlaylist", func() {
|
|
resp := doReqWithUser(regularUser, "createPlaylist",
|
|
"playlistId", adminPublicID, "songId", songIDs[2])
|
|
|
|
Expect(resp.Status).To(Equal(responses.StatusFailed))
|
|
Expect(resp.Error).ToNot(BeNil())
|
|
})
|
|
|
|
// --- Regular user can manage their own playlists ---
|
|
|
|
It("regular user can update their own playlist", func() {
|
|
resp := doReqWithUser(regularUser, "updatePlaylist",
|
|
"playlistId", regularPlaylistID, "name", "My Updated Playlist")
|
|
|
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
|
|
|
resp = doReqWithUser(regularUser, "getPlaylist", "id", regularPlaylistID)
|
|
Expect(resp.Playlist.Name).To(Equal("My Updated Playlist"))
|
|
})
|
|
|
|
It("regular user can add songs to their own playlist", func() {
|
|
resp := doReqWithUser(regularUser, "updatePlaylist",
|
|
"playlistId", regularPlaylistID, "songIdToAdd", songIDs[1])
|
|
|
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
|
|
|
resp = doReqWithUser(regularUser, "getPlaylist", "id", regularPlaylistID)
|
|
Expect(resp.Playlist.SongCount).To(Equal(int32(2)))
|
|
})
|
|
|
|
It("regular user can delete their own playlist", func() {
|
|
resp := doReqWithUser(regularUser, "deletePlaylist", "id", regularPlaylistID)
|
|
|
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
|
})
|
|
|
|
// --- Admin can manage any user's playlists ---
|
|
|
|
It("admin can update any user's playlist", func() {
|
|
resp := doReqWithUser(regularUser, "createPlaylist", "name", "To Be Admin-Edited",
|
|
"songId", songIDs[0])
|
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
|
plsID := resp.Playlist.Id
|
|
|
|
resp = doReqWithUser(adminUser, "updatePlaylist",
|
|
"playlistId", plsID, "name", "Admin Edited")
|
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
|
|
|
resp = doReqWithUser(adminUser, "getPlaylist", "id", plsID)
|
|
Expect(resp.Playlist.Name).To(Equal("Admin Edited"))
|
|
})
|
|
|
|
It("admin can delete any user's playlist", func() {
|
|
resp := doReqWithUser(regularUser, "createPlaylist", "name", "To Be Admin-Deleted",
|
|
"songId", songIDs[0])
|
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
|
plsID := resp.Playlist.Id
|
|
|
|
resp = doReqWithUser(adminUser, "deletePlaylist", "id", plsID)
|
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
|
|
|
resp = doReqWithUser(adminUser, "getPlaylist", "id", plsID)
|
|
Expect(resp.Status).To(Equal(responses.StatusFailed))
|
|
})
|
|
|
|
// --- Verify admin's playlists are unchanged ---
|
|
|
|
It("admin's private playlist is unchanged after failed regular user operations", func() {
|
|
resp := doReqWithUser(adminUser, "getPlaylist", "id", adminPrivateID)
|
|
|
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
|
Expect(resp.Playlist.Name).To(Equal("Admin Private"))
|
|
Expect(resp.Playlist.SongCount).To(Equal(int32(2)))
|
|
})
|
|
|
|
It("admin's public playlist is unchanged after failed regular user operations", func() {
|
|
resp := doReqWithUser(adminUser, "getPlaylist", "id", adminPublicID)
|
|
|
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
|
Expect(resp.Playlist.Name).To(Equal("Admin Public"))
|
|
Expect(resp.Playlist.SongCount).To(Equal(int32(2)))
|
|
})
|
|
})
|
|
|
|
Describe("Smart Playlist Protection", Ordered, func() {
|
|
var smartPlaylistID string
|
|
var songID string
|
|
|
|
BeforeAll(func() {
|
|
setupTestDB()
|
|
|
|
// Look up a song ID for mutation tests
|
|
songs, err := ds.MediaFile(ctx).GetAll(model.QueryOptions{Sort: "title", Max: 1})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(songs).ToNot(BeEmpty())
|
|
songID = songs[0].ID
|
|
|
|
// Insert a smart playlist directly into the DB
|
|
smartPls := &model.Playlist{
|
|
Name: "Smart Playlist",
|
|
OwnerID: adminUser.ID,
|
|
Public: false,
|
|
Rules: &criteria.Criteria{Expression: criteria.Contains{"title": ""}},
|
|
}
|
|
Expect(ds.Playlist(ctx).Put(smartPls)).To(Succeed())
|
|
smartPlaylistID = smartPls.ID
|
|
})
|
|
|
|
It("getPlaylist returns smart playlist with readonly flag and validUntil", func() {
|
|
resp := doReq("getPlaylist", "id", smartPlaylistID)
|
|
|
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
|
Expect(resp.Playlist.Name).To(Equal("Smart Playlist"))
|
|
Expect(resp.Playlist.OpenSubsonicPlaylist).ToNot(BeNil())
|
|
Expect(resp.Playlist.OpenSubsonicPlaylist.Readonly).To(BeTrue())
|
|
expectedValidUntil := time.Now().Add(conf.Server.SmartPlaylistRefreshDelay)
|
|
Expect(*resp.Playlist.OpenSubsonicPlaylist.ValidUntil).To(BeTemporally("~", expectedValidUntil, time.Second))
|
|
})
|
|
|
|
It("createPlaylist rejects replacing tracks on smart playlist", func() {
|
|
resp := doReq("createPlaylist", "playlistId", smartPlaylistID, "songId", songID)
|
|
|
|
Expect(resp.Status).To(Equal(responses.StatusFailed))
|
|
Expect(resp.Error).ToNot(BeNil())
|
|
Expect(resp.Error.Code).To(Equal(int32(50)))
|
|
})
|
|
|
|
It("updatePlaylist rejects adding songs to smart playlist", func() {
|
|
resp := doReq("updatePlaylist", "playlistId", smartPlaylistID,
|
|
"songIdToAdd", songID)
|
|
|
|
Expect(resp.Status).To(Equal(responses.StatusFailed))
|
|
Expect(resp.Error).ToNot(BeNil())
|
|
Expect(resp.Error.Code).To(Equal(int32(50)))
|
|
})
|
|
|
|
It("updatePlaylist rejects removing songs from smart playlist", func() {
|
|
resp := doReq("updatePlaylist", "playlistId", smartPlaylistID,
|
|
"songIndexToRemove", "0")
|
|
|
|
Expect(resp.Status).To(Equal(responses.StatusFailed))
|
|
Expect(resp.Error).ToNot(BeNil())
|
|
Expect(resp.Error.Code).To(Equal(int32(50)))
|
|
})
|
|
|
|
It("updatePlaylist allows renaming smart playlist", func() {
|
|
resp := doReq("updatePlaylist", "playlistId", smartPlaylistID,
|
|
"name", "Renamed Smart")
|
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
|
|
|
resp = doReq("getPlaylist", "id", smartPlaylistID)
|
|
Expect(resp.Playlist.Name).To(Equal("Renamed Smart"))
|
|
})
|
|
|
|
It("updatePlaylist allows setting comment on smart playlist", func() {
|
|
resp := doReq("updatePlaylist", "playlistId", smartPlaylistID,
|
|
"comment", "Auto-generated playlist")
|
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
|
|
|
resp = doReq("getPlaylist", "id", smartPlaylistID)
|
|
Expect(resp.Playlist.Comment).To(Equal("Auto-generated playlist"))
|
|
})
|
|
|
|
It("deletePlaylist can delete smart playlist", func() {
|
|
resp := doReq("deletePlaylist", "id", smartPlaylistID)
|
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
|
|
|
resp = doReq("getPlaylist", "id", smartPlaylistID)
|
|
Expect(resp.Status).To(Equal(responses.StatusFailed))
|
|
})
|
|
})
|
|
|
|
Describe("Smart Playlist Boolean String Normalization (issue #4826)", Ordered, func() {
|
|
var songID string
|
|
var boolPlaylistID, stringPlaylistID, nestedPlaylistID string
|
|
|
|
BeforeAll(func() {
|
|
setupTestDB()
|
|
|
|
songs, err := ds.MediaFile(ctx).GetAll(model.QueryOptions{Sort: "title", Max: 1})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(songs).ToNot(BeEmpty())
|
|
songID = songs[0].ID
|
|
|
|
// Star the song via the Subsonic API
|
|
resp := doReq("star", "id", songID)
|
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
|
|
|
// Force immediate refresh for all smart playlists
|
|
conf.Server.SmartPlaylistRefreshDelay = -1 * time.Second
|
|
|
|
// Create smart playlist with boolean true
|
|
boolPls := &model.Playlist{
|
|
Name: "Bool Loved",
|
|
OwnerID: adminUser.ID,
|
|
Rules: &criteria.Criteria{Expression: criteria.All{criteria.Is{"loved": true}}},
|
|
}
|
|
Expect(ds.Playlist(ctx).Put(boolPls)).To(Succeed())
|
|
boolPlaylistID = boolPls.ID
|
|
|
|
// Create smart playlist with string "true"
|
|
stringPls := &model.Playlist{
|
|
Name: "String Loved",
|
|
OwnerID: adminUser.ID,
|
|
Rules: &criteria.Criteria{Expression: criteria.All{criteria.Is{"loved": "true"}}},
|
|
}
|
|
Expect(ds.Playlist(ctx).Put(stringPls)).To(Succeed())
|
|
stringPlaylistID = stringPls.ID
|
|
|
|
// Create smart playlist with string "true" in nested any group (exact issue #4826 scenario)
|
|
nestedPls := &model.Playlist{
|
|
Name: "Nested String Loved",
|
|
OwnerID: adminUser.ID,
|
|
Rules: &criteria.Criteria{Expression: criteria.All{
|
|
criteria.Any{
|
|
criteria.Is{"loved": "true"},
|
|
},
|
|
}},
|
|
}
|
|
Expect(ds.Playlist(ctx).Put(nestedPls)).To(Succeed())
|
|
nestedPlaylistID = nestedPls.ID
|
|
})
|
|
|
|
It("smart playlist with bool loved=true returns starred song", func() {
|
|
resp := doReq("getPlaylist", "id", boolPlaylistID)
|
|
|
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
|
Expect(resp.Playlist.SongCount).To(BeNumerically(">=", int32(1)))
|
|
entryIDs := make([]string, len(resp.Playlist.Entry))
|
|
for i, e := range resp.Playlist.Entry {
|
|
entryIDs[i] = e.Id
|
|
}
|
|
Expect(entryIDs).To(ContainElement(songID))
|
|
})
|
|
|
|
It("smart playlist with string loved='true' returns same results as bool (issue #4826)", func() {
|
|
boolResp := doReq("getPlaylist", "id", boolPlaylistID)
|
|
stringResp := doReq("getPlaylist", "id", stringPlaylistID)
|
|
|
|
Expect(stringResp.Status).To(Equal(responses.StatusOK))
|
|
Expect(stringResp.Playlist.SongCount).To(Equal(boolResp.Playlist.SongCount))
|
|
})
|
|
|
|
It("nested any group with string loved='true' returns starred song (issue #4826)", func() {
|
|
resp := doReq("getPlaylist", "id", nestedPlaylistID)
|
|
|
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
|
Expect(resp.Playlist.SongCount).To(BeNumerically(">=", int32(1)))
|
|
entryIDs := make([]string, len(resp.Playlist.Entry))
|
|
for i, e := range resp.Playlist.Entry {
|
|
entryIDs[i] = e.Id
|
|
}
|
|
Expect(entryIDs).To(ContainElement(songID))
|
|
})
|
|
|
|
It("isPresent with string 'true' matches songs that have the tag", func() {
|
|
pls := &model.Playlist{
|
|
Name: "Genre Present String",
|
|
OwnerID: adminUser.ID,
|
|
Rules: &criteria.Criteria{Expression: criteria.All{criteria.IsPresent{"genre": "true"}}},
|
|
}
|
|
Expect(ds.Playlist(ctx).Put(pls)).To(Succeed())
|
|
|
|
resp := doReq("getPlaylist", "id", pls.ID)
|
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
|
Expect(resp.Playlist.SongCount).To(BeNumerically(">=", int32(1)))
|
|
})
|
|
|
|
It("isMissing with string 'true' excludes songs that have the tag", func() {
|
|
pls := &model.Playlist{
|
|
Name: "Genre Missing String",
|
|
OwnerID: adminUser.ID,
|
|
Rules: &criteria.Criteria{Expression: criteria.All{criteria.IsMissing{"genre": "true"}}},
|
|
}
|
|
Expect(ds.Playlist(ctx).Put(pls)).To(Succeed())
|
|
|
|
resp := doReq("getPlaylist", "id", pls.ID)
|
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
|
Expect(resp.Playlist.SongCount).To(Equal(int32(0)))
|
|
})
|
|
|
|
It("isMissing with string 'true' returns same results as bool true", func() {
|
|
boolPls := &model.Playlist{
|
|
Name: "Genre Missing Bool",
|
|
OwnerID: adminUser.ID,
|
|
Rules: &criteria.Criteria{Expression: criteria.All{criteria.IsMissing{"genre": true}}},
|
|
}
|
|
Expect(ds.Playlist(ctx).Put(boolPls)).To(Succeed())
|
|
|
|
stringPls := &model.Playlist{
|
|
Name: "Genre Missing String2",
|
|
OwnerID: adminUser.ID,
|
|
Rules: &criteria.Criteria{Expression: criteria.All{criteria.IsMissing{"genre": "true"}}},
|
|
}
|
|
Expect(ds.Playlist(ctx).Put(stringPls)).To(Succeed())
|
|
|
|
boolResp := doReq("getPlaylist", "id", boolPls.ID)
|
|
stringResp := doReq("getPlaylist", "id", stringPls.ID)
|
|
Expect(stringResp.Playlist.SongCount).To(Equal(boolResp.Playlist.SongCount))
|
|
})
|
|
})
|
|
})
|