package persistence
import (
"context"
"slices"
"strings"
"github.com/deluan/rest"
"github.com/navidrome/navidrome/conf/configtest"
"github.com/navidrome/navidrome/log"
"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"
"github.com/pocketbase/dbx"
)
var _ = Describe("TagRepository", func() {
var repo model.TagRepository
var restRepo rest.Repository[model.Tag]
var ctx context.Context
BeforeEach(func() {
DeferCleanup(configtest.SetupConfig())
ctx = request.WithUser(log.NewContext(context.TODO()), model.User{ID: "userid", UserName: "johndoe", IsAdmin: true})
tagRepo := NewTagRepository(GetDBXBuilder())
repo = tagRepo
restRepo = tagRepo
db := GetDBXBuilder()
_, err := db.NewQuery("DELETE FROM tag").Execute()
Expect(err).ToNot(HaveOccurred())
_, err = db.NewQuery("DELETE FROM library_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 = repo.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", "sad"),
newTag("mood", "energetic"),
newTag("mood", "calm"),
newTag("instrument", "guitar"),
newTag("instrument", "piano"),
newTag("decade", "1980s"),
newTag("decade", "1990s"),
)
Expect(err).ToNot(HaveOccurred())
})
Describe("Add", func() {
It("should handle adding new tags", func() {
newTag := model.Tag{
ID: id.NewTagID("genre", "experimental"),
TagName: "genre",
TagValue: "experimental",
}
err := repo.Add(ctx, 1, newTag)
Expect(err).ToNot(HaveOccurred())
resultTag, err := restRepo.Read(ctx, newTag.ID)
Expect(err).ToNot(HaveOccurred())
Expect(resultTag.TagValue).To(Equal("experimental"))
count, err := restRepo.Count(ctx)
Expect(err).ToNot(HaveOccurred())
Expect(count).To(Equal(int64(21)))
})
It("should handle duplicate tags gracefully", func() {
duplicateTag := model.Tag{
ID: id.NewTagID("genre", "rock"),
TagName: "genre",
TagValue: "rock",
}
count, err := restRepo.Count(ctx)
Expect(err).ToNot(HaveOccurred())
Expect(count).To(Equal(int64(20)))
err = repo.Add(ctx, 1, duplicateTag)
Expect(err).ToNot(HaveOccurred())
count, err = restRepo.Count(ctx)
Expect(err).ToNot(HaveOccurred())
Expect(count).To(Equal(int64(20)))
})
})
Describe("UpdateCounts", func() {
It("should update tag counts successfully", func() {
err := repo.UpdateCounts(ctx)
Expect(err).ToNot(HaveOccurred())
})
It("should handle empty database gracefully", func() {
db := GetDBXBuilder()
_, err := db.NewQuery("DELETE FROM tag").Execute()
Expect(err).ToNot(HaveOccurred())
err = repo.UpdateCounts(ctx)
Expect(err).ToNot(HaveOccurred())
})
It("should handle albums with non-existent tag IDs in JSON gracefully", func() {
db := GetDBXBuilder()
nonExistentTagID := id.NewTagID("genre", "nonexistent-genre")
albumWithBadTags := `{"genre":[{"id":"` + nonExistentTagID + `","value":"nonexistent-genre"}]}`
_, err := db.NewQuery("INSERT INTO album (id, name, library_id, tags) VALUES ({:id}, {:name}, {:lib}, {:tags})").
Bind(dbx.Params{
"id": "test-album-bad-tags",
"name": "Album With Bad Tags",
"lib": 1,
"tags": albumWithBadTags,
}).Execute()
Expect(err).ToNot(HaveOccurred())
err = repo.UpdateCounts(ctx)
Expect(err).ToNot(HaveOccurred())
_, err = db.NewQuery("DELETE FROM album WHERE id = {:id}").
Bind(dbx.Params{"id": "test-album-bad-tags"}).Execute()
Expect(err).ToNot(HaveOccurred())
})
It("should handle media files with non-existent tag IDs in JSON gracefully", func() {
db := GetDBXBuilder()
nonExistentTagID := id.NewTagID("genre", "another-nonexistent-genre")
mediaFileWithBadTags := `{"genre":[{"id":"` + nonExistentTagID + `","value":"another-nonexistent-genre"}]}`
_, err := db.NewQuery("INSERT INTO media_file (id, title, library_id, tags) VALUES ({:id}, {:title}, {:lib}, {:tags})").
Bind(dbx.Params{
"id": "test-media-bad-tags",
"title": "Media File With Bad Tags",
"lib": 1,
"tags": mediaFileWithBadTags,
}).Execute()
Expect(err).ToNot(HaveOccurred())
err = repo.UpdateCounts(ctx)
Expect(err).ToNot(HaveOccurred())
_, err = db.NewQuery("DELETE FROM media_file WHERE id = {:id}").
Bind(dbx.Params{"id": "test-media-bad-tags"}).Execute()
Expect(err).ToNot(HaveOccurred())
})
})
Describe("Count", func() {
It("should return correct count of tags", func() {
count, err := restRepo.Count(ctx)
Expect(err).ToNot(HaveOccurred())
Expect(count).To(Equal(int64(20)))
})
})
Describe("Read", func() {
It("should return existing tag", func() {
rockID := id.NewTagID("genre", "rock")
resultTag, err := restRepo.Read(ctx, rockID)
Expect(err).ToNot(HaveOccurred())
Expect(resultTag.ID).To(Equal(rockID))
Expect(resultTag.TagName).To(Equal(model.TagName("genre")))
Expect(resultTag.TagValue).To(Equal("rock"))
})
It("should return error for non-existent tag", func() {
_, err := restRepo.Read(ctx, "non-existent-id")
Expect(err).To(HaveOccurred())
})
})
Describe("ReadAll", func() {
It("should return all tags from dataset", func() {
tags, err := restRepo.ReadAll(ctx)
Expect(err).ToNot(HaveOccurred())
Expect(tags).To(HaveLen(20))
})
It("should filter tags by partial value correctly", func() {
options := rest.QueryOptions{
Filters: map[string]any{"name": "%rock%"},
}
tags, err := restRepo.ReadAll(ctx, options)
Expect(err).ToNot(HaveOccurred())
Expect(tags).To(HaveLen(2))
for _, tag := range tags {
Expect(strings.ToLower(tag.TagValue)).To(ContainSubstring("rock"))
}
})
It("should filter tags by partial value using LIKE", func() {
options := rest.QueryOptions{
Filters: map[string]any{"name": "%e%"},
}
tags, err := restRepo.ReadAll(ctx, options)
Expect(err).ToNot(HaveOccurred())
Expect(tags).To(HaveLen(8))
for _, tag := range tags {
Expect(strings.ToLower(tag.TagValue)).To(ContainSubstring("e"))
}
})
It("should sort tags by value ascending", func() {
options := rest.QueryOptions{
Filters: map[string]any{"name": "%r%"},
Sort: "name",
Order: "asc",
}
tags, err := restRepo.ReadAll(ctx, options)
Expect(err).ToNot(HaveOccurred())
Expect(tags).To(HaveLen(7))
Expect(slices.IsSortedFunc(tags, func(a, b model.Tag) int {
return strings.Compare(strings.ToLower(a.TagValue), strings.ToLower(b.TagValue))
}))
})
It("should sort tags by value descending", func() {
options := rest.QueryOptions{
Filters: map[string]any{"name": "%r%"},
Sort: "name",
Order: "desc",
}
tags, err := restRepo.ReadAll(ctx, options)
Expect(err).ToNot(HaveOccurred())
Expect(tags).To(HaveLen(7))
Expect(slices.IsSortedFunc(tags, func(a, b model.Tag) int {
return strings.Compare(strings.ToLower(b.TagValue), strings.ToLower(a.TagValue))
}))
})
})
})