mirror of
https://github.com/navidrome/navidrome.git
synced 2026-02-15 09:21:22 -05:00
* feat(subsonic): add OS readonly and validUntil properties
* remove duplicated test
* test: fix and enable disabled child smart playlist tests
Fixed the XContext("child smart playlists") tests that were disabled with
a TODO comment. The tests had several issues: nested playlists were missing
Public: true (required by InPlaylist criteria), the criteria matched no
test fixtures, the "not expired" test set EvaluatedAt on the parent too
(preventing it from refreshing at all), and the "expired" test dereferenced
a nil EvaluatedAt. Added proper cleanup with DeferCleanup and config
restoration via configtest.
* fix(subsonic): always include readonly field in JSON playlist responses
Removed omitempty from the JSON tag of the Readonly field in
OpenSubsonicPlaylist so that readonly: false is always serialized in
JSON responses, per the OpenSubsonic spec requirement that supported
fields must be returned with default values. Added a test case with an
empty OpenSubsonicPlaylist to verify the behavior.
---------
Co-authored-by: Deluan Quintão <deluan@navidrome.org>
293 lines
8.9 KiB
Go
293 lines
8.9 KiB
Go
package subsonic
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/navidrome/navidrome/conf"
|
|
"github.com/navidrome/navidrome/core"
|
|
"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 _ core.Playlists = (*fakePlaylists)(nil)
|
|
|
|
var _ = Describe("buildPlaylist", func() {
|
|
var router *Router
|
|
var ds model.DataStore
|
|
var ctx context.Context
|
|
var playlist model.Playlist
|
|
|
|
BeforeEach(func() {
|
|
ds = &tests.MockDataStore{}
|
|
router = New(ds, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
|
ctx = context.Background()
|
|
})
|
|
|
|
Describe("normal playlist", func() {
|
|
BeforeEach(func() {
|
|
createdAt := time.Date(2023, 1, 15, 10, 30, 0, 0, time.UTC)
|
|
updatedAt := time.Date(2023, 2, 20, 14, 45, 0, 0, time.UTC)
|
|
|
|
playlist = model.Playlist{
|
|
ID: "pls-1",
|
|
Name: "My Playlist",
|
|
Comment: "Test comment",
|
|
OwnerName: "admin",
|
|
OwnerID: "1234",
|
|
Public: true,
|
|
SongCount: 10,
|
|
Duration: 600,
|
|
CreatedAt: createdAt,
|
|
UpdatedAt: updatedAt,
|
|
}
|
|
})
|
|
|
|
Context("with minimal client", func() {
|
|
BeforeEach(func() {
|
|
conf.Server.Subsonic.MinimalClients = "minimal-client"
|
|
player := model.Player{Client: "minimal-client"}
|
|
ctx = request.WithPlayer(ctx, player)
|
|
})
|
|
|
|
It("returns only basic fields", func() {
|
|
result := router.buildPlaylist(ctx, playlist)
|
|
|
|
Expect(result.Id).To(Equal("pls-1"))
|
|
Expect(result.Name).To(Equal("My Playlist"))
|
|
Expect(result.SongCount).To(Equal(int32(10)))
|
|
Expect(result.Duration).To(Equal(int32(600)))
|
|
Expect(result.Created).To(Equal(playlist.CreatedAt))
|
|
Expect(result.Changed).To(Equal(playlist.UpdatedAt))
|
|
|
|
// These should not be set
|
|
Expect(result.Comment).To(BeEmpty())
|
|
Expect(result.Owner).To(BeEmpty())
|
|
Expect(result.Public).To(BeFalse())
|
|
Expect(result.CoverArt).To(BeEmpty())
|
|
})
|
|
})
|
|
|
|
Context("with non-minimal client", func() {
|
|
BeforeEach(func() {
|
|
conf.Server.Subsonic.MinimalClients = "minimal-client"
|
|
player := model.Player{Client: "regular-client"}
|
|
ctx = request.WithPlayer(ctx, player)
|
|
})
|
|
|
|
It("returns all fields", func() {
|
|
result := router.buildPlaylist(ctx, playlist)
|
|
|
|
Expect(result.Id).To(Equal("pls-1"))
|
|
Expect(result.Name).To(Equal("My Playlist"))
|
|
Expect(result.SongCount).To(Equal(int32(10)))
|
|
Expect(result.Duration).To(Equal(int32(600)))
|
|
Expect(result.Created).To(Equal(playlist.CreatedAt))
|
|
Expect(result.Changed).To(Equal(playlist.UpdatedAt))
|
|
Expect(result.Comment).To(Equal("Test comment"))
|
|
Expect(result.Owner).To(Equal("admin"))
|
|
Expect(result.Public).To(BeTrue())
|
|
Expect(result.Readonly).To(BeTrue())
|
|
})
|
|
|
|
It("returns all fields when as owner", func() {
|
|
ctx = request.WithUser(ctx, model.User{ID: "1234", UserName: "admin"})
|
|
|
|
result := router.buildPlaylist(ctx, playlist)
|
|
|
|
Expect(result.Id).To(Equal("pls-1"))
|
|
Expect(result.Name).To(Equal("My Playlist"))
|
|
Expect(result.SongCount).To(Equal(int32(10)))
|
|
Expect(result.Duration).To(Equal(int32(600)))
|
|
Expect(result.Created).To(Equal(playlist.CreatedAt))
|
|
Expect(result.Changed).To(Equal(playlist.UpdatedAt))
|
|
Expect(result.Comment).To(Equal("Test comment"))
|
|
Expect(result.Owner).To(Equal("admin"))
|
|
Expect(result.Public).To(BeTrue())
|
|
Expect(result.Readonly).To(BeFalse())
|
|
})
|
|
})
|
|
|
|
Context("when minimal clients list is empty", func() {
|
|
BeforeEach(func() {
|
|
conf.Server.Subsonic.MinimalClients = ""
|
|
player := model.Player{Client: "any-client"}
|
|
ctx = request.WithPlayer(ctx, player)
|
|
})
|
|
|
|
It("returns all fields", func() {
|
|
result := router.buildPlaylist(ctx, playlist)
|
|
|
|
Expect(result.Comment).To(Equal("Test comment"))
|
|
Expect(result.Owner).To(Equal("admin"))
|
|
Expect(result.Public).To(BeTrue())
|
|
})
|
|
})
|
|
|
|
Context("when no player in context", func() {
|
|
It("returns all fields", func() {
|
|
result := router.buildPlaylist(ctx, playlist)
|
|
|
|
Expect(result.Comment).To(Equal("Test comment"))
|
|
Expect(result.Owner).To(Equal("admin"))
|
|
Expect(result.Public).To(BeTrue())
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("smart playlist", func() {
|
|
evaluatedAt := time.Date(2023, 2, 20, 15, 45, 0, 0, time.UTC)
|
|
validUntil := evaluatedAt.Add(5 * time.Second)
|
|
|
|
BeforeEach(func() {
|
|
createdAt := time.Date(2023, 1, 15, 10, 30, 0, 0, time.UTC)
|
|
updatedAt := time.Date(2023, 2, 20, 14, 45, 0, 0, time.UTC)
|
|
|
|
playlist = model.Playlist{
|
|
ID: "pls-1",
|
|
Name: "My Playlist",
|
|
Comment: "Test comment",
|
|
OwnerName: "admin",
|
|
OwnerID: "1234",
|
|
Public: true,
|
|
SongCount: 10,
|
|
Duration: 600,
|
|
CreatedAt: createdAt,
|
|
UpdatedAt: updatedAt,
|
|
EvaluatedAt: &evaluatedAt,
|
|
Rules: &criteria.Criteria{
|
|
Expression: criteria.All{criteria.Contains{"title": "title"}},
|
|
},
|
|
}
|
|
})
|
|
|
|
Context("with minimal client", func() {
|
|
BeforeEach(func() {
|
|
conf.Server.Subsonic.MinimalClients = "minimal-client"
|
|
player := model.Player{Client: "minimal-client"}
|
|
ctx = request.WithPlayer(ctx, player)
|
|
})
|
|
|
|
It("returns only basic fields", func() {
|
|
result := router.buildPlaylist(ctx, playlist)
|
|
|
|
Expect(result.Id).To(Equal("pls-1"))
|
|
Expect(result.Name).To(Equal("My Playlist"))
|
|
Expect(result.SongCount).To(Equal(int32(10)))
|
|
Expect(result.Duration).To(Equal(int32(600)))
|
|
Expect(result.Created).To(Equal(playlist.CreatedAt))
|
|
Expect(result.Changed).To(Equal(evaluatedAt))
|
|
|
|
// These should not be set
|
|
Expect(result.Comment).To(BeEmpty())
|
|
Expect(result.Owner).To(BeEmpty())
|
|
Expect(result.Public).To(BeFalse())
|
|
Expect(result.CoverArt).To(BeEmpty())
|
|
Expect(result.OpenSubsonicPlaylist).To(BeNil())
|
|
})
|
|
})
|
|
|
|
Context("with non-minimal client", func() {
|
|
BeforeEach(func() {
|
|
conf.Server.Subsonic.MinimalClients = "minimal-client"
|
|
player := model.Player{Client: "regular-client"}
|
|
ctx = request.WithPlayer(ctx, player)
|
|
})
|
|
|
|
It("returns all fields", func() {
|
|
result := router.buildPlaylist(ctx, playlist)
|
|
Expect(result.Id).To(Equal("pls-1"))
|
|
Expect(result.Name).To(Equal("My Playlist"))
|
|
Expect(result.SongCount).To(Equal(int32(10)))
|
|
Expect(result.Duration).To(Equal(int32(600)))
|
|
Expect(result.Created).To(Equal(playlist.CreatedAt))
|
|
Expect(result.Changed).To(Equal(*playlist.EvaluatedAt))
|
|
Expect(result.Comment).To(Equal("Test comment"))
|
|
Expect(result.Owner).To(Equal("admin"))
|
|
Expect(result.Public).To(BeTrue())
|
|
Expect(result.Readonly).To(BeTrue())
|
|
Expect(result.ValidUntil).To(Equal(&validUntil))
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
var _ = Describe("UpdatePlaylist", func() {
|
|
var router *Router
|
|
var ds model.DataStore
|
|
var playlists *fakePlaylists
|
|
|
|
BeforeEach(func() {
|
|
ds = &tests.MockDataStore{}
|
|
playlists = &fakePlaylists{}
|
|
router = New(ds, nil, nil, nil, nil, nil, nil, nil, playlists, nil, nil, nil, nil)
|
|
})
|
|
|
|
It("clears the comment when parameter is empty", func() {
|
|
r := newGetRequest("playlistId=123", "comment=")
|
|
_, err := router.UpdatePlaylist(r)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(playlists.lastPlaylistID).To(Equal("123"))
|
|
Expect(playlists.lastComment).ToNot(BeNil())
|
|
Expect(*playlists.lastComment).To(Equal(""))
|
|
})
|
|
|
|
It("leaves comment unchanged when parameter is missing", func() {
|
|
r := newGetRequest("playlistId=123")
|
|
_, err := router.UpdatePlaylist(r)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(playlists.lastPlaylistID).To(Equal("123"))
|
|
Expect(playlists.lastComment).To(BeNil())
|
|
})
|
|
|
|
It("sets public to true when parameter is 'true'", func() {
|
|
r := newGetRequest("playlistId=123", "public=true")
|
|
_, err := router.UpdatePlaylist(r)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(playlists.lastPlaylistID).To(Equal("123"))
|
|
Expect(playlists.lastPublic).ToNot(BeNil())
|
|
Expect(*playlists.lastPublic).To(BeTrue())
|
|
})
|
|
|
|
It("sets public to false when parameter is 'false'", func() {
|
|
r := newGetRequest("playlistId=123", "public=false")
|
|
_, err := router.UpdatePlaylist(r)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(playlists.lastPlaylistID).To(Equal("123"))
|
|
Expect(playlists.lastPublic).ToNot(BeNil())
|
|
Expect(*playlists.lastPublic).To(BeFalse())
|
|
})
|
|
|
|
It("leaves public unchanged when parameter is missing", func() {
|
|
r := newGetRequest("playlistId=123")
|
|
_, err := router.UpdatePlaylist(r)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(playlists.lastPlaylistID).To(Equal("123"))
|
|
Expect(playlists.lastPublic).To(BeNil())
|
|
})
|
|
})
|
|
|
|
type fakePlaylists struct {
|
|
core.Playlists
|
|
lastPlaylistID string
|
|
lastName *string
|
|
lastComment *string
|
|
lastPublic *bool
|
|
lastAdd []string
|
|
lastRemove []int
|
|
}
|
|
|
|
func (f *fakePlaylists) Update(ctx context.Context, playlistID string, name *string, comment *string, public *bool, idsToAdd []string, idxToRemove []int) error {
|
|
f.lastPlaylistID = playlistID
|
|
f.lastName = name
|
|
f.lastComment = comment
|
|
f.lastPublic = public
|
|
f.lastAdd = idsToAdd
|
|
f.lastRemove = idxToRemove
|
|
return nil
|
|
}
|