package persistence
import (
"context"
"slices"
"strings"
"github.com/Masterminds/squirrel"
"github.com/deluan/rest"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/id"
"github.com/navidrome/navidrome/model/request"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("GenreRepository", func() {
var repo model.GenreRepository
var restRepo rest.Repository[model.Genre]
var tagRepo model.TagRepository
var ctx context.Context
BeforeEach(func() {
ctx = request.WithUser(GinkgoT().Context(), model.User{ID: "userid", UserName: "johndoe", IsAdmin: true})
genreRepo := NewGenreRepository(GetDBXBuilder())
repo = genreRepo
restRepo = genreRepo
tagRepo = NewTagRepository(GetDBXBuilder())
db := GetDBXBuilder()
_, err := db.NewQuery("DELETE FROM tag").Execute()
Expect(err).ToNot(HaveOccurred())
_, err = db.NewQuery("INSERT OR IGNORE INTO library (id, name, path, default_new_users) VALUES (1, 'Test Library', '/test', true)").Execute()
Expect(err).ToNot(HaveOccurred())
_, err = db.NewQuery("INSERT OR IGNORE INTO user_library (user_id, library_id) VALUES ('userid', 1)").Execute()
Expect(err).ToNot(HaveOccurred())
newTag := func(name, value string) model.Tag {
return model.Tag{ID: id.NewTagID(name, value), TagName: model.TagName(name), TagValue: value}
}
err = tagRepo.Add(ctx, 1,
newTag("genre", "rock"),
newTag("genre", "pop"),
newTag("genre", "jazz"),
newTag("genre", "electronic"),
newTag("genre", "classical"),
newTag("genre", "ambient"),
newTag("genre", "techno"),
newTag("genre", "house"),
newTag("genre", "trance"),
newTag("genre", "Alternative Rock"),
newTag("genre", "Blues"),
newTag("genre", "Country"),
newTag("mood", "happy"),
newTag("mood", "ambient"),
)
Expect(err).ToNot(HaveOccurred())
})
Describe("GetAll", func() {
It("should return all genres", func() {
genres, err := repo.GetAll(ctx)
Expect(err).ToNot(HaveOccurred())
Expect(genres).To(HaveLen(12))
genreNames := make([]string, len(genres))
for i, genre := range genres {
genreNames[i] = genre.Name
}
Expect(genreNames).To(ContainElement("rock"))
Expect(genreNames).To(ContainElement("pop"))
Expect(genreNames).To(ContainElement("jazz"))
Expect(genreNames).ToNot(ContainElement("happy"))
})
It("should support query options", func() {
genres, err := repo.GetAll(ctx, model.QueryOptions{Max: 1})
Expect(err).ToNot(HaveOccurred())
Expect(genres).To(HaveLen(1))
})
It("should handle empty results gracefully", func() {
_, err := GetDBXBuilder().NewQuery("DELETE FROM tag WHERE tag_name = 'genre'").Execute()
Expect(err).ToNot(HaveOccurred())
genres, err := repo.GetAll(ctx)
Expect(err).ToNot(HaveOccurred())
Expect(genres).To(BeEmpty())
})
Describe("filtering and sorting", func() {
It("should filter by name using like match", func() {
options := model.QueryOptions{
Filters: squirrel.Like{"tag_value": "%rock%"},
}
genres, err := repo.GetAll(ctx, options)
Expect(err).ToNot(HaveOccurred())
Expect(genres).To(HaveLen(2))
for _, genre := range genres {
Expect(strings.ToLower(genre.Name)).To(ContainSubstring("rock"))
}
})
It("should sort by name in ascending order", func() {
options := model.QueryOptions{
Filters: squirrel.Like{"tag_value": "%e%"},
Sort: "name",
}
genres, err := repo.GetAll(ctx, options)
Expect(err).ToNot(HaveOccurred())
Expect(genres).To(HaveLen(7))
Expect(slices.IsSortedFunc(genres, func(a, b model.Genre) int {
return strings.Compare(b.Name, a.Name)
}))
})
It("should sort by name in descending order", func() {
options := model.QueryOptions{
Filters: squirrel.Like{"tag_value": "%e%"},
Sort: "name",
Order: "desc",
}
genres, err := repo.GetAll(ctx, options)
Expect(err).ToNot(HaveOccurred())
Expect(genres).To(HaveLen(7))
Expect(slices.IsSortedFunc(genres, func(a, b model.Genre) int {
return strings.Compare(a.Name, b.Name)
}))
})
})
})
Describe("Count", func() {
It("should return correct count of genres", func() {
count, err := restRepo.Count(ctx)
Expect(err).ToNot(HaveOccurred())
Expect(count).To(Equal(int64(12)))
})
It("should handle zero count", func() {
_, err := GetDBXBuilder().NewQuery("DELETE FROM tag WHERE tag_name = 'genre'").Execute()
Expect(err).ToNot(HaveOccurred())
count, err := restRepo.Count(ctx)
Expect(err).ToNot(HaveOccurred())
Expect(count).To(BeZero())
})
It("should only count genre tags", func() {
nonGenreTag := model.Tag{
ID: id.NewTagID("mood", "energetic"),
TagName: "mood",
TagValue: "energetic",
}
err := tagRepo.Add(ctx, 1, nonGenreTag)
Expect(err).ToNot(HaveOccurred())
count, err := restRepo.Count(ctx)
Expect(err).ToNot(HaveOccurred())
Expect(count).To(Equal(int64(12)))
})
It("should filter by name using like match", func() {
options := rest.QueryOptions{
Filters: map[string]any{"name": "%rock%"},
}
count, err := restRepo.Count(ctx, options)
Expect(err).ToNot(HaveOccurred())
Expect(count).To(BeNumerically("==", 2))
})
})
Describe("Read", func() {
It("should return existing genre", func() {
genreID := id.NewTagID("genre", "rock")
genre, err := restRepo.Read(ctx, genreID)
Expect(err).ToNot(HaveOccurred())
Expect(genre.ID).To(Equal(genreID))
Expect(genre.Name).To(Equal("rock"))
})
It("should return error for non-existent genre", func() {
_, err := restRepo.Read(ctx, "non-existent-id")
Expect(err).To(HaveOccurred())
})
It("should not return non-genre tags", func() {
moodID := id.NewTagID("mood", "happy")
_, err := restRepo.Read(ctx, moodID)
Expect(err).To(HaveOccurred())
})
})
Describe("ReadAll", func() {
It("should return all genres through ReadAll", func() {
genres, err := restRepo.ReadAll(ctx)
Expect(err).ToNot(HaveOccurred())
Expect(genres).To(HaveLen(12))
genreNames := make([]string, len(genres))
for i, genre := range genres {
genreNames[i] = genre.Name
}
Expect(genreNames).To(ContainElement("rock"))
Expect(genreNames).To(ContainElement("pop"))
Expect(genreNames).To(ContainElement("jazz"))
})
It("should support rest query options", func() {
result, err := restRepo.ReadAll(ctx)
Expect(err).ToNot(HaveOccurred())
Expect(result).ToNot(BeNil())
})
})
Describe("Library Filtering", func() {
Context("Headless Processes (No User Context)", func() {
var headlessRepo model.GenreRepository
var headlessRestRepo rest.Repository[model.Genre]
var headlessCtx context.Context
BeforeEach(func() {
headlessCtx = GinkgoT().Context()
headlessGenreRepo := NewGenreRepository(GetDBXBuilder())
headlessRepo = headlessGenreRepo
headlessRestRepo = headlessGenreRepo
db := GetDBXBuilder()
_, err := db.NewQuery("INSERT OR IGNORE INTO library (id, name, path) VALUES (2, 'Test Library 2', '/test2')").Execute()
Expect(err).ToNot(HaveOccurred())
newTag := func(name, value string) model.Tag {
return model.Tag{ID: id.NewTagID(name, value), TagName: model.TagName(name), TagValue: value}
}
err = tagRepo.Add(ctx, 2, newTag("genre", "jazz"))
Expect(err).ToNot(HaveOccurred())
})
It("should see all genres from all libraries when no user is in context", func() {
genres, err := headlessRepo.GetAll(headlessCtx)
Expect(err).ToNot(HaveOccurred())
var genreNames []string
for _, genre := range genres {
genreNames = append(genreNames, genre.Name)
}
Expect(genreNames).To(ContainElement("rock"))
Expect(genreNames).To(ContainElement("jazz"))
})
It("should count all genres from all libraries when no user is in context", func() {
count, err := headlessRestRepo.Count(headlessCtx)
Expect(err).ToNot(HaveOccurred())
Expect(count).To(BeNumerically(">=", 2))
})
It("should allow headless processes to apply explicit library_id filters", func() {
genreList, err := headlessRestRepo.ReadAll(headlessCtx, rest.QueryOptions{
Filters: map[string]any{"library_id": 2},
})
Expect(err).ToNot(HaveOccurred())
Expect(genreList).To(HaveLen(1))
Expect(genreList[0].Name).To(Equal("jazz"))
})
It("should get individual genres when no user is in context", func() {
genres, err := headlessRepo.GetAll(headlessCtx)
Expect(err).ToNot(HaveOccurred())
Expect(genres).ToNot(BeEmpty())
genre, err := headlessRestRepo.Read(headlessCtx, genres[0].ID)
Expect(err).ToNot(HaveOccurred())
Expect(genre).ToNot(BeNil())
})
})
})
})