mirror of
https://github.com/navidrome/navidrome.git
synced 2026-02-05 20:41:07 -05:00
* fix(db): Include items with no annotation for starred=false, handle has_rating=false * hardcode starred instead * test: ensure albums and artists without annotations are included in starred and has_rating filters Signed-off-by: Deluan <deluan@navidrome.org> * refactor: replace starred and has_rating filters with annotationBoolFilter for consistency Signed-off-by: Deluan <deluan@navidrome.org> * fix: update annotationBoolFilter to handle boolean values correctly in SQL expressions Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org> Co-authored-by: Deluan <deluan@navidrome.org>
154 lines
4.8 KiB
Go
154 lines
4.8 KiB
Go
package persistence
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/Masterminds/squirrel"
|
|
"github.com/deluan/rest"
|
|
"github.com/navidrome/navidrome/model"
|
|
"github.com/navidrome/navidrome/model/request"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
var _ = Describe("Annotation Filters", func() {
|
|
var (
|
|
albumRepo *albumRepository
|
|
albumWithoutAnnotation model.Album
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
ctx := request.WithUser(context.Background(), model.User{ID: "userid", UserName: "johndoe"})
|
|
albumRepo = NewAlbumRepository(ctx, GetDBXBuilder()).(*albumRepository)
|
|
|
|
// Create album without any annotation (no star, no rating)
|
|
albumWithoutAnnotation = model.Album{ID: "no-annotation-album", Name: "No Annotation", LibraryID: 1}
|
|
Expect(albumRepo.Put(&albumWithoutAnnotation)).To(Succeed())
|
|
})
|
|
|
|
AfterEach(func() {
|
|
_, _ = albumRepo.executeSQL(squirrel.Delete("album").Where(squirrel.Eq{"id": albumWithoutAnnotation.ID}))
|
|
})
|
|
|
|
Describe("annotationBoolFilter", func() {
|
|
DescribeTable("creates correct SQL expressions",
|
|
func(field, value string, expectedSQL string, expectedArgs []interface{}) {
|
|
sqlizer := annotationBoolFilter(field)(field, value)
|
|
sql, args, err := sqlizer.ToSql()
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(sql).To(Equal(expectedSQL))
|
|
Expect(args).To(Equal(expectedArgs))
|
|
},
|
|
Entry("starred=true", "starred", "true", "COALESCE(starred, 0) > 0", []interface{}(nil)),
|
|
Entry("starred=false", "starred", "false", "COALESCE(starred, 0) = 0", []interface{}(nil)),
|
|
Entry("starred=True (case insensitive)", "starred", "True", "COALESCE(starred, 0) > 0", []interface{}(nil)),
|
|
Entry("rating=true", "rating", "true", "COALESCE(rating, 0) > 0", []interface{}(nil)),
|
|
)
|
|
|
|
It("returns nil if value is not a string", func() {
|
|
sqlizer := annotationBoolFilter("starred")("starred", 123)
|
|
Expect(sqlizer).To(BeNil())
|
|
})
|
|
})
|
|
|
|
Describe("starredFilter", func() {
|
|
It("false includes items without annotations", func() {
|
|
albums, err := albumRepo.GetAll(model.QueryOptions{
|
|
Filters: annotationBoolFilter("starred")("starred", "false"),
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
var found bool
|
|
for _, a := range albums {
|
|
if a.ID == albumWithoutAnnotation.ID {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
Expect(found).To(BeTrue(), "Item without annotation should be included in starred=false filter")
|
|
})
|
|
|
|
It("true excludes items without annotations", func() {
|
|
albums, err := albumRepo.GetAll(model.QueryOptions{
|
|
Filters: annotationBoolFilter("starred")("starred", "true"),
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
for _, a := range albums {
|
|
Expect(a.ID).ToNot(Equal(albumWithoutAnnotation.ID))
|
|
}
|
|
})
|
|
})
|
|
|
|
Describe("hasRatingFilter", func() {
|
|
It("false includes items without annotations", func() {
|
|
albums, err := albumRepo.GetAll(model.QueryOptions{
|
|
Filters: annotationBoolFilter("rating")("rating", "false"),
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
var found bool
|
|
for _, a := range albums {
|
|
if a.ID == albumWithoutAnnotation.ID {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
Expect(found).To(BeTrue(), "Item without annotation should be included in has_rating=false filter")
|
|
})
|
|
|
|
It("true excludes items without annotations", func() {
|
|
albums, err := albumRepo.GetAll(model.QueryOptions{
|
|
Filters: annotationBoolFilter("rating")("rating", "true"),
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
for _, a := range albums {
|
|
Expect(a.ID).ToNot(Equal(albumWithoutAnnotation.ID))
|
|
}
|
|
})
|
|
|
|
It("true includes items with rating > 0", func() {
|
|
// Create album with rating 1
|
|
ratedAlbum := model.Album{ID: "rated-album", Name: "Rated Album", LibraryID: 1}
|
|
Expect(albumRepo.Put(&ratedAlbum)).To(Succeed())
|
|
Expect(albumRepo.SetRating(1, ratedAlbum.ID)).To(Succeed())
|
|
defer func() {
|
|
_, _ = albumRepo.executeSQL(squirrel.Delete("annotation").Where(squirrel.Eq{"item_id": ratedAlbum.ID}))
|
|
_, _ = albumRepo.executeSQL(squirrel.Delete("album").Where(squirrel.Eq{"id": ratedAlbum.ID}))
|
|
}()
|
|
|
|
albums, err := albumRepo.GetAll(model.QueryOptions{
|
|
Filters: annotationBoolFilter("rating")("rating", "true"),
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
var found bool
|
|
for _, a := range albums {
|
|
if a.ID == ratedAlbum.ID {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
Expect(found).To(BeTrue(), "Album with rating 5 should be included in has_rating=true filter")
|
|
})
|
|
})
|
|
|
|
It("ignores invalid filter values (not strings)", func() {
|
|
res, err := albumRepo.ReadAll(rest.QueryOptions{
|
|
Filters: map[string]any{"starred": 123},
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
albums := res.(model.Albums)
|
|
|
|
var found bool
|
|
for _, a := range albums {
|
|
if a.ID == albumWithoutAnnotation.ID {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
Expect(found).To(BeTrue(), "Item without annotation should be included when filter is ignored")
|
|
})
|
|
})
|