mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-01 07:21:17 +00:00
test(smartplaylists): add smart playlist e2e test suite (#5409)
* test: add smart playlist e2e suite infrastructure * test: add string field smart playlist e2e tests * test: add numeric, boolean, tag, participant, annotation, logic, sorting smart playlist e2e tests * test: add playlist operator smart playlist e2e tests * test: add isNot and endsWith string field e2e tests * test: add date/time field smart playlist e2e tests * fix: add gosec nolint directives for safe SQL concatenation in e2e restore * refactor: address code review feedback for smart playlist e2e tests - Deduplicate evaluateRule by delegating to evaluateRuleOrdered - Cache table list in BeforeSuite instead of querying sqlite_master per test - Wrap restoreDB in a transaction with defer cleanup for DETACH/foreign_keys - Use JSON numbers for numeric criteria values to match canonical JSON shape * refactor: simplify e2e test infrastructure - Remove unused return value from buildTestFS - Add deferred ROLLBACK as safety net in restoreDB transaction - Cache Come Together ID to avoid repeated lookups in BeforeSuite - Use range-over-int for play count loop * test: add missing operator coverage to smart playlist e2e tests Add 4 tests for operators/paths that had no e2e coverage: - notContains on string fields (LIKE negation path) - before on date fields (Lt for dates, only after was tested) - startsWith on tag fields (json_tree + LIKE subquery) - endsWith on participant/role fields (json_tree + LIKE subquery)
This commit is contained in:
parent
f784224aef
commit
cfc3854a8b
326
core/playlists/e2e/e2e_suite_test.go
Normal file
326
core/playlists/e2e/e2e_suite_test.go
Normal file
@ -0,0 +1,326 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/conf/configtest"
|
||||
"github.com/navidrome/navidrome/core"
|
||||
"github.com/navidrome/navidrome/core/artwork"
|
||||
"github.com/navidrome/navidrome/core/metrics"
|
||||
"github.com/navidrome/navidrome/core/playlists"
|
||||
"github.com/navidrome/navidrome/core/storage/storagetest"
|
||||
"github.com/navidrome/navidrome/db"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/criteria"
|
||||
"github.com/navidrome/navidrome/model/request"
|
||||
"github.com/navidrome/navidrome/persistence"
|
||||
"github.com/navidrome/navidrome/scanner"
|
||||
"github.com/navidrome/navidrome/server/events"
|
||||
"github.com/navidrome/navidrome/tests"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestSmartPlaylistE2E(t *testing.T) {
|
||||
tests.Init(t, false)
|
||||
defer db.Close(t.Context())
|
||||
log.SetLevel(log.LevelFatal)
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Smart Playlist E2E Suite")
|
||||
}
|
||||
|
||||
type _t = map[string]any
|
||||
|
||||
var template = storagetest.Template
|
||||
var track = storagetest.Track
|
||||
|
||||
var (
|
||||
ctx context.Context
|
||||
ds *tests.MockDataStore
|
||||
lib model.Library
|
||||
|
||||
dbFilePath string
|
||||
snapshotPath string
|
||||
snapshotTables []string
|
||||
|
||||
testUser = model.User{
|
||||
ID: "sp-test-user-1",
|
||||
UserName: "sptestuser",
|
||||
Name: "SP Test User",
|
||||
IsAdmin: true,
|
||||
}
|
||||
|
||||
otherUser = model.User{
|
||||
ID: "sp-test-user-2",
|
||||
UserName: "spotheruser",
|
||||
Name: "SP Other User",
|
||||
IsAdmin: false,
|
||||
}
|
||||
)
|
||||
|
||||
func buildTestFS() {
|
||||
abbeyRoad := template(_t{
|
||||
"albumartist": "The Beatles",
|
||||
"artist": "The Beatles",
|
||||
"album": "Abbey Road",
|
||||
"year": 1969,
|
||||
"genre": "Rock;Blues",
|
||||
})
|
||||
ledZepIV := template(_t{
|
||||
"albumartist": "Led Zeppelin",
|
||||
"artist": "Led Zeppelin",
|
||||
"album": "IV",
|
||||
"year": 1971,
|
||||
})
|
||||
kindOfBlue := template(_t{
|
||||
"albumartist": "Miles Davis",
|
||||
"artist": "Miles Davis",
|
||||
"album": "Kind of Blue",
|
||||
"year": 1959,
|
||||
"genre": "Jazz",
|
||||
"composer": "Miles Davis",
|
||||
})
|
||||
nightAtOpera := template(_t{
|
||||
"albumartist": "Queen",
|
||||
"artist": "Queen",
|
||||
"album": "A Night at the Opera",
|
||||
"year": 1975,
|
||||
"genre": "Rock",
|
||||
})
|
||||
electricLadyland := template(_t{
|
||||
"albumartist": "Jimi Hendrix",
|
||||
"artist": "Jimi Hendrix",
|
||||
"album": "Electric Ladyland",
|
||||
"year": 1968,
|
||||
"genre": "Rock;Blues",
|
||||
})
|
||||
newsOfWorld := template(_t{
|
||||
"albumartist": "Queen",
|
||||
"artist": "Queen",
|
||||
"album": "News of the World",
|
||||
"year": 1977,
|
||||
"genre": "Rock;Pop",
|
||||
"compilation": "1",
|
||||
})
|
||||
|
||||
fs := storagetest.FakeFS{}
|
||||
fs.SetFiles(fstest.MapFS{
|
||||
"Rock/The Beatles/Abbey Road/01 - Come Together.mp3": abbeyRoad(track(1, "Come Together",
|
||||
_t{"genre": "Rock;Blues", "composer": "Lennon/McCartney", "bpm": 120})),
|
||||
"Rock/The Beatles/Abbey Road/02 - Something.mp3": abbeyRoad(track(2, "Something",
|
||||
_t{"genre": "Rock", "composer": "Harrison", "bpm": 100})),
|
||||
"Rock/Led Zeppelin/IV/01 - Stairway To Heaven.flac": ledZepIV(track(1, "Stairway To Heaven",
|
||||
_t{"genre": "Rock;Folk", "composer": "Page/Plant", "bpm": 82, "suffix": "flac",
|
||||
"bitrate": 900, "samplerate": 44100, "bitdepth": 16})),
|
||||
"Rock/Led Zeppelin/IV/02 - Black Dog.flac": ledZepIV(track(2, "Black Dog",
|
||||
_t{"genre": "Rock;Blues", "composer": "Page/Plant/Jones", "bpm": 150, "suffix": "flac",
|
||||
"bitrate": 900, "samplerate": 44100, "bitdepth": 16})),
|
||||
"Jazz/Miles Davis/Kind of Blue/01 - So What.mp3": kindOfBlue(track(1, "So What",
|
||||
_t{"bpm": 136})),
|
||||
"Rock/Queen/A Night at the Opera/01 - Bohemian Rhapsody.mp3": nightAtOpera(track(1, "Bohemian Rhapsody",
|
||||
_t{"composer": "Freddie Mercury", "bpm": 72})),
|
||||
"Rock/Jimi Hendrix/Electric Ladyland/01 - All Along the Watchtower.mp3": electricLadyland(track(1, "All Along the Watchtower",
|
||||
_t{"composer": "Bob Dylan", "bpm": 112})),
|
||||
"Rock/Queen/News of the World/01 - We Are the Champions.mp3": newsOfWorld(track(1, "We Are the Champions",
|
||||
_t{"composer": "Freddie Mercury", "bpm": 64})),
|
||||
})
|
||||
storagetest.Register("fake", &fs)
|
||||
}
|
||||
|
||||
func findMediaFileByTitle(title string) string {
|
||||
mfs, err := ds.MediaFile(ctx).GetAll(model.QueryOptions{
|
||||
Filters: squirrel.Eq{"media_file.title": title},
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(mfs).To(HaveLen(1), "expected exactly one media file with title %q", title)
|
||||
return mfs[0].ID
|
||||
}
|
||||
|
||||
func evaluateRule(jsonRule string) []string {
|
||||
titles := evaluateRuleOrdered(jsonRule)
|
||||
sort.Strings(titles)
|
||||
return titles
|
||||
}
|
||||
|
||||
func evaluateRuleOrdered(jsonRule string) []string {
|
||||
var rules criteria.Criteria
|
||||
err := json.Unmarshal([]byte(jsonRule), &rules)
|
||||
Expect(err).ToNot(HaveOccurred(), "invalid criteria JSON: %s", jsonRule)
|
||||
|
||||
pls := &model.Playlist{
|
||||
Name: "test-smart-playlist",
|
||||
OwnerID: testUser.ID,
|
||||
Rules: &rules,
|
||||
}
|
||||
err = ds.Playlist(ctx).Put(pls)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
loaded, err := ds.Playlist(ctx).GetWithTracks(pls.ID, true, false)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
titles := make([]string, len(loaded.Tracks))
|
||||
for i, t := range loaded.Tracks {
|
||||
titles[i] = t.Title
|
||||
}
|
||||
return titles
|
||||
}
|
||||
|
||||
func createPlaylist(owner model.User, public bool, titles ...string) string {
|
||||
pls := &model.Playlist{
|
||||
Name: "ref-playlist",
|
||||
OwnerID: owner.ID,
|
||||
Public: public,
|
||||
}
|
||||
for _, title := range titles {
|
||||
mfID := findMediaFileByTitle(title)
|
||||
pls.AddMediaFilesByID([]string{mfID})
|
||||
}
|
||||
Expect(ds.Playlist(ctx).Put(pls)).To(Succeed())
|
||||
return pls.ID
|
||||
}
|
||||
|
||||
func createPublicPlaylist(owner model.User, titles ...string) string {
|
||||
return createPlaylist(owner, true, titles...)
|
||||
}
|
||||
|
||||
func createPrivatePlaylist(owner model.User, titles ...string) string {
|
||||
return createPlaylist(owner, false, titles...)
|
||||
}
|
||||
|
||||
func createPublicSmartPlaylist(owner model.User, jsonRule string) string {
|
||||
var rules criteria.Criteria
|
||||
Expect(json.Unmarshal([]byte(jsonRule), &rules)).To(Succeed())
|
||||
pls := &model.Playlist{
|
||||
Name: "ref-smart-playlist",
|
||||
OwnerID: owner.ID,
|
||||
Public: true,
|
||||
Rules: &rules,
|
||||
}
|
||||
Expect(ds.Playlist(ctx).Put(pls)).To(Succeed())
|
||||
return pls.ID
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
ctx = request.WithUser(GinkgoT().Context(), testUser)
|
||||
tmpDir := GinkgoT().TempDir()
|
||||
dbFilePath = filepath.Join(tmpDir, "smartplaylist-e2e.db")
|
||||
snapshotPath = filepath.Join(tmpDir, "smartplaylist-e2e.db.snapshot")
|
||||
conf.Server.DbPath = dbFilePath + "?_journal_mode=WAL"
|
||||
db.Db().SetMaxOpenConns(1)
|
||||
|
||||
conf.Server.MusicFolder = "fake:///music"
|
||||
conf.Server.DevExternalScanner = false
|
||||
conf.Server.SmartPlaylistRefreshDelay = 0
|
||||
|
||||
db.Init(ctx)
|
||||
|
||||
initDS := &tests.MockDataStore{RealDS: persistence.New(db.Db())}
|
||||
|
||||
userWithPass := testUser
|
||||
userWithPass.NewPassword = "password"
|
||||
Expect(initDS.User(ctx).Put(&userWithPass)).To(Succeed())
|
||||
|
||||
otherUserWithPass := otherUser
|
||||
otherUserWithPass.NewPassword = "password"
|
||||
Expect(initDS.User(ctx).Put(&otherUserWithPass)).To(Succeed())
|
||||
|
||||
lib = model.Library{ID: 1, Name: "Music Library", Path: "fake:///music"}
|
||||
Expect(initDS.Library(ctx).Put(&lib)).To(Succeed())
|
||||
Expect(initDS.User(ctx).SetUserLibraries(testUser.ID, []int{lib.ID})).To(Succeed())
|
||||
Expect(initDS.User(ctx).SetUserLibraries(otherUser.ID, []int{lib.ID})).To(Succeed())
|
||||
|
||||
loadedUser, err := initDS.User(ctx).FindByUsername(testUser.UserName)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
testUser.Libraries = loadedUser.Libraries
|
||||
|
||||
loadedOther, err := initDS.User(ctx).FindByUsername(otherUser.UserName)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
otherUser.Libraries = loadedOther.Libraries
|
||||
|
||||
ctx = request.WithUser(GinkgoT().Context(), testUser)
|
||||
|
||||
buildTestFS()
|
||||
s := scanner.New(ctx, initDS, artwork.NoopCacheWarmer(), events.NoopBroker(),
|
||||
playlists.NewPlaylists(initDS, core.NewImageUploadService()), metrics.NewNoopInstance())
|
||||
_, err = s.ScanAll(ctx, true)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
ds = &tests.MockDataStore{RealDS: persistence.New(db.Db())}
|
||||
|
||||
comeTogetherID := findMediaFileByTitle("Come Together")
|
||||
Expect(ds.MediaFile(ctx).SetStar(true, comeTogetherID)).To(Succeed())
|
||||
Expect(ds.MediaFile(ctx).SetStar(true, findMediaFileByTitle("So What"))).To(Succeed())
|
||||
Expect(ds.MediaFile(ctx).SetRating(3, findMediaFileByTitle("Stairway To Heaven"))).To(Succeed())
|
||||
Expect(ds.MediaFile(ctx).SetRating(5, findMediaFileByTitle("Bohemian Rhapsody"))).To(Succeed())
|
||||
for range 10 {
|
||||
Expect(ds.MediaFile(ctx).IncPlayCount(comeTogetherID, time.Now())).To(Succeed())
|
||||
}
|
||||
Expect(ds.MediaFile(ctx).IncPlayCount(findMediaFileByTitle("Black Dog"), time.Now())).To(Succeed())
|
||||
|
||||
rows, err := db.Db().Query("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '%_fts' AND name NOT LIKE '%_fts_%'")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var name string
|
||||
Expect(rows.Scan(&name)).To(Succeed())
|
||||
snapshotTables = append(snapshotTables, name)
|
||||
}
|
||||
Expect(rows.Err()).ToNot(HaveOccurred())
|
||||
|
||||
_, err = db.Db().Exec("PRAGMA wal_checkpoint(TRUNCATE)")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
data, err := os.ReadFile(dbFilePath)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(os.WriteFile(snapshotPath, data, 0600)).To(Succeed())
|
||||
})
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
db.Close(ctx)
|
||||
})
|
||||
|
||||
func restoreDB() {
|
||||
sqlDB := db.Db()
|
||||
|
||||
_, err := sqlDB.Exec("PRAGMA foreign_keys = OFF")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
defer func() { _, _ = sqlDB.Exec("PRAGMA foreign_keys = ON") }()
|
||||
|
||||
_, err = sqlDB.Exec("ATTACH DATABASE ? AS snapshot", snapshotPath)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
defer func() { _, _ = sqlDB.Exec("DETACH DATABASE snapshot") }()
|
||||
|
||||
_, err = sqlDB.Exec("BEGIN TRANSACTION")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
defer func() { _, _ = sqlDB.Exec("ROLLBACK") }()
|
||||
|
||||
for _, table := range snapshotTables {
|
||||
_, err = sqlDB.Exec(`DELETE FROM main."` + table + `"`) //nolint:gosec
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
_, err = sqlDB.Exec(`INSERT INTO main."` + table + `" SELECT * FROM snapshot."` + table + `"`) //nolint:gosec
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
}
|
||||
|
||||
_, err = sqlDB.Exec("COMMIT")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
}
|
||||
|
||||
func setupTestDB() {
|
||||
ctx = request.WithUser(GinkgoT().Context(), testUser)
|
||||
DeferCleanup(configtest.SetupConfig())
|
||||
conf.Server.MusicFolder = "fake:///music"
|
||||
conf.Server.DevExternalScanner = false
|
||||
conf.Server.SmartPlaylistRefreshDelay = 0
|
||||
|
||||
restoreDB()
|
||||
ds = &tests.MockDataStore{RealDS: persistence.New(db.Db())}
|
||||
}
|
||||
290
core/playlists/e2e/smartplaylist_test.go
Normal file
290
core/playlists/e2e/smartplaylist_test.go
Normal file
@ -0,0 +1,290 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Smart Playlists", func() {
|
||||
BeforeEach(func() {
|
||||
setupTestDB()
|
||||
})
|
||||
|
||||
Describe("String fields", func() {
|
||||
It("matches by exact title", func() {
|
||||
results := evaluateRule(`{"all":[{"is":{"title":"Something"}}]}`)
|
||||
Expect(results).To(ConsistOf("Something"))
|
||||
})
|
||||
|
||||
It("matches by title contains", func() {
|
||||
results := evaluateRule(`{"all":[{"contains":{"title":"the"}}]}`)
|
||||
Expect(results).To(ConsistOf("Come Together", "All Along the Watchtower", "We Are the Champions"))
|
||||
})
|
||||
|
||||
It("matches by artist startsWith", func() {
|
||||
results := evaluateRule(`{"all":[{"startsWith":{"artist":"Led"}}]}`)
|
||||
Expect(results).To(ConsistOf("Stairway To Heaven", "Black Dog"))
|
||||
})
|
||||
|
||||
It("matches by title isNot", func() {
|
||||
results := evaluateRule(`{"all":[{"isNot":{"title":"Something"}},{"is":{"artist":"The Beatles"}}]}`)
|
||||
Expect(results).To(ConsistOf("Come Together"))
|
||||
})
|
||||
|
||||
It("matches by artist endsWith", func() {
|
||||
results := evaluateRule(`{"all":[{"endsWith":{"artist":"Davis"}}]}`)
|
||||
Expect(results).To(ConsistOf("So What"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Numeric fields", func() {
|
||||
It("matches by year greater than", func() {
|
||||
results := evaluateRule(`{"all":[{"gt":{"year":1970}}]}`)
|
||||
Expect(results).To(ConsistOf("Stairway To Heaven", "Black Dog", "Bohemian Rhapsody", "We Are the Champions"))
|
||||
})
|
||||
|
||||
It("matches by year less than", func() {
|
||||
results := evaluateRule(`{"all":[{"lt":{"year":1969}}]}`)
|
||||
Expect(results).To(ConsistOf("So What", "All Along the Watchtower"))
|
||||
})
|
||||
|
||||
It("matches by BPM in range", func() {
|
||||
results := evaluateRule(`{"all":[{"inTheRange":{"bpm":[100,130]}}]}`)
|
||||
Expect(results).To(ConsistOf("Come Together", "Something", "All Along the Watchtower"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Boolean fields", func() {
|
||||
It("matches compilations", func() {
|
||||
results := evaluateRule(`{"all":[{"is":{"compilation":true}}]}`)
|
||||
Expect(results).To(ConsistOf("We Are the Champions"))
|
||||
})
|
||||
|
||||
It("matches non-compilations", func() {
|
||||
results := evaluateRule(`{"all":[{"is":{"compilation":false}}]}`)
|
||||
Expect(results).To(ConsistOf("Come Together", "Something", "Stairway To Heaven", "Black Dog", "So What", "Bohemian Rhapsody", "All Along the Watchtower"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("File type fields", func() {
|
||||
It("matches by filetype", func() {
|
||||
results := evaluateRule(`{"all":[{"is":{"filetype":"flac"}}]}`)
|
||||
Expect(results).To(ConsistOf("Stairway To Heaven", "Black Dog"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Multi-valued tags", func() {
|
||||
It("matches tracks with Blues genre", func() {
|
||||
results := evaluateRule(`{"all":[{"is":{"genre":"Blues"}}]}`)
|
||||
Expect(results).To(ConsistOf("Come Together", "Black Dog", "All Along the Watchtower"))
|
||||
})
|
||||
|
||||
It("excludes tracks with Rock genre", func() {
|
||||
results := evaluateRule(`{"all":[{"isNot":{"genre":"Rock"}}]}`)
|
||||
Expect(results).To(ConsistOf("So What"))
|
||||
})
|
||||
|
||||
It("matches genre contains", func() {
|
||||
results := evaluateRule(`{"all":[{"contains":{"genre":"ol"}}]}`)
|
||||
Expect(results).To(ConsistOf("Stairway To Heaven"))
|
||||
})
|
||||
|
||||
It("matches tracks with Pop genre", func() {
|
||||
results := evaluateRule(`{"all":[{"is":{"genre":"Pop"}}]}`)
|
||||
Expect(results).To(ConsistOf("We Are the Champions"))
|
||||
})
|
||||
|
||||
It("matches genre startsWith", func() {
|
||||
results := evaluateRule(`{"all":[{"startsWith":{"genre":"Ro"}}]}`)
|
||||
Expect(results).To(ConsistOf("Come Together", "Something", "Stairway To Heaven", "Black Dog",
|
||||
"Bohemian Rhapsody", "All Along the Watchtower", "We Are the Champions"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Participants", func() {
|
||||
It("matches by exact composer", func() {
|
||||
results := evaluateRule(`{"all":[{"is":{"composer":"Harrison"}}]}`)
|
||||
Expect(results).To(ConsistOf("Something"))
|
||||
})
|
||||
|
||||
It("matches by composer contains", func() {
|
||||
results := evaluateRule(`{"all":[{"contains":{"composer":"Plant"}}]}`)
|
||||
Expect(results).To(ConsistOf("Stairway To Heaven", "Black Dog"))
|
||||
})
|
||||
|
||||
It("matches by composer isNot", func() {
|
||||
results := evaluateRule(`{"all":[{"isNot":{"composer":"Freddie Mercury"}}]}`)
|
||||
Expect(results).To(ConsistOf("Come Together", "Something", "Stairway To Heaven", "Black Dog", "So What", "All Along the Watchtower"))
|
||||
})
|
||||
|
||||
It("matches by composer endsWith", func() {
|
||||
results := evaluateRule(`{"all":[{"endsWith":{"composer":"Mercury"}}]}`)
|
||||
Expect(results).To(ConsistOf("Bohemian Rhapsody", "We Are the Champions"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Annotations", func() {
|
||||
It("matches starred tracks", func() {
|
||||
results := evaluateRule(`{"all":[{"is":{"loved":true}}]}`)
|
||||
Expect(results).To(ConsistOf("Come Together", "So What"))
|
||||
})
|
||||
|
||||
It("matches unstarred tracks", func() {
|
||||
results := evaluateRule(`{"all":[{"is":{"loved":false}}]}`)
|
||||
Expect(results).To(ConsistOf("Something", "Stairway To Heaven", "Black Dog", "Bohemian Rhapsody", "All Along the Watchtower", "We Are the Champions"))
|
||||
})
|
||||
|
||||
It("matches by rating greater than", func() {
|
||||
results := evaluateRule(`{"all":[{"gt":{"rating":3}}]}`)
|
||||
Expect(results).To(ConsistOf("Bohemian Rhapsody"))
|
||||
})
|
||||
|
||||
It("matches by rating greater than or equal via inTheRange", func() {
|
||||
results := evaluateRule(`{"all":[{"inTheRange":{"rating":[3,5]}}]}`)
|
||||
Expect(results).To(ConsistOf("Stairway To Heaven", "Bohemian Rhapsody"))
|
||||
})
|
||||
|
||||
It("matches by play count greater than", func() {
|
||||
results := evaluateRule(`{"all":[{"gt":{"playcount":5}}]}`)
|
||||
Expect(results).To(ConsistOf("Come Together"))
|
||||
})
|
||||
|
||||
It("matches by play count greater than zero", func() {
|
||||
results := evaluateRule(`{"all":[{"gt":{"playcount":0}}]}`)
|
||||
Expect(results).To(ConsistOf("Come Together", "Black Dog"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Negated string operators", func() {
|
||||
It("matches by title notContains", func() {
|
||||
results := evaluateRule(`{"all":[{"notContains":{"title":"the"}}]}`)
|
||||
Expect(results).To(ConsistOf("Something", "Stairway To Heaven", "Black Dog", "So What", "Bohemian Rhapsody"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Date/time fields", func() {
|
||||
It("matches dateAdded before a far-future date", func() {
|
||||
results := evaluateRule(`{"all":[{"before":{"dateadded":"2099-01-01"}}]}`)
|
||||
Expect(results).To(ConsistOf("Come Together", "Something", "Stairway To Heaven", "Black Dog",
|
||||
"So What", "Bohemian Rhapsody", "All Along the Watchtower", "We Are the Champions"))
|
||||
})
|
||||
|
||||
It("matches lastPlayed inTheLast 1 day", func() {
|
||||
results := evaluateRule(`{"all":[{"inTheLast":{"lastplayed":1}}]}`)
|
||||
Expect(results).To(ConsistOf("Come Together", "Black Dog"))
|
||||
})
|
||||
|
||||
It("matches lastPlayed notInTheLast (far future)", func() {
|
||||
results := evaluateRule(`{"all":[{"notInTheLast":{"lastplayed":99999}}]}`)
|
||||
Expect(results).To(ConsistOf("Something", "Stairway To Heaven", "So What",
|
||||
"Bohemian Rhapsody", "All Along the Watchtower", "We Are the Champions"))
|
||||
})
|
||||
|
||||
It("matches dateLoved after a past date", func() {
|
||||
results := evaluateRule(`{"all":[{"after":{"dateloved":"2020-01-01"}}]}`)
|
||||
Expect(results).To(ConsistOf("Come Together", "So What"))
|
||||
})
|
||||
|
||||
It("matches dateRated after a past date", func() {
|
||||
results := evaluateRule(`{"all":[{"after":{"daterated":"2020-01-01"}}]}`)
|
||||
Expect(results).To(ConsistOf("Stairway To Heaven", "Bohemian Rhapsody"))
|
||||
})
|
||||
|
||||
It("matches dateAdded inTheLast 1 day", func() {
|
||||
results := evaluateRule(`{"all":[{"inTheLast":{"dateadded":1}}]}`)
|
||||
Expect(results).To(ConsistOf("Come Together", "Something", "Stairway To Heaven", "Black Dog",
|
||||
"So What", "Bohemian Rhapsody", "All Along the Watchtower", "We Are the Champions"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Logic operators", func() {
|
||||
It("matches with ALL (AND)", func() {
|
||||
results := evaluateRule(`{"all":[{"is":{"genre":"Blues"}},{"gt":{"bpm":130}}]}`)
|
||||
Expect(results).To(ConsistOf("Black Dog"))
|
||||
})
|
||||
|
||||
It("matches with ANY (OR)", func() {
|
||||
results := evaluateRule(`{"any":[{"is":{"genre":"Jazz"}},{"is":{"compilation":true}}]}`)
|
||||
Expect(results).To(ConsistOf("So What", "We Are the Champions"))
|
||||
})
|
||||
|
||||
It("matches nested all/any", func() {
|
||||
results := evaluateRule(`{"all":[{"any":[{"is":{"genre":"Blues"}},{"is":{"genre":"Jazz"}}]},{"gt":{"year":1960}}]}`)
|
||||
Expect(results).To(ConsistOf("Come Together", "Black Dog", "All Along the Watchtower"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Sorting and limits", func() {
|
||||
It("returns tracks sorted by year descending with limit", func() {
|
||||
results := evaluateRuleOrdered(`{"all":[{"gt":{"year":0}}],"sort":"year","order":"desc","limit":2}`)
|
||||
Expect(results).To(Equal([]string{"We Are the Champions", "Bohemian Rhapsody"}))
|
||||
})
|
||||
|
||||
It("returns tracks sorted by title ascending", func() {
|
||||
results := evaluateRuleOrdered(`{"all":[{"is":{"genre":"Blues"}}],"sort":"title","order":"asc"}`)
|
||||
Expect(results).To(Equal([]string{"All Along the Watchtower", "Black Dog", "Come Together"}))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Combined real-world patterns", func() {
|
||||
It("matches genre filter with exclusion and year range", func() {
|
||||
results := evaluateRuleOrdered(`{
|
||||
"all":[
|
||||
{"any":[
|
||||
{"is":{"genre":"Blues"}},
|
||||
{"is":{"genre":"Folk"}}
|
||||
]},
|
||||
{"isNot":{"genre":"Jazz"}},
|
||||
{"gt":{"year":1965}}
|
||||
],
|
||||
"sort":"-year,title"
|
||||
}`)
|
||||
Expect(results).To(Equal([]string{"Black Dog", "Stairway To Heaven", "Come Together", "All Along the Watchtower"}))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Playlist operators", func() {
|
||||
It("matches tracks in a public regular playlist", func() {
|
||||
refID := createPublicPlaylist(testUser, "Come Together", "So What")
|
||||
results := evaluateRule(`{"all":[{"inPlaylist":{"id":"` + refID + `"}}]}`)
|
||||
Expect(results).To(ConsistOf("Come Together", "So What"))
|
||||
})
|
||||
|
||||
It("matches tracks not in a public regular playlist", func() {
|
||||
refID := createPublicPlaylist(testUser, "Come Together", "So What")
|
||||
results := evaluateRule(`{"all":[{"notInPlaylist":{"id":"` + refID + `"}}]}`)
|
||||
Expect(results).To(ConsistOf("Something", "Stairway To Heaven", "Black Dog",
|
||||
"Bohemian Rhapsody", "All Along the Watchtower", "We Are the Champions"))
|
||||
})
|
||||
|
||||
It("recursively refreshes a referenced smart playlist owned by the same user", func() {
|
||||
smartBID := createPublicSmartPlaylist(testUser, `{"all":[{"is":{"genre":"Jazz"}}]}`)
|
||||
results := evaluateRule(`{"all":[{"inPlaylist":{"id":"` + smartBID + `"}}]}`)
|
||||
Expect(results).To(ConsistOf("So What"))
|
||||
})
|
||||
|
||||
It("does not refresh a referenced smart playlist owned by another user", func() {
|
||||
smartBID := createPublicSmartPlaylist(otherUser, `{"all":[{"is":{"genre":"Jazz"}}]}`)
|
||||
results := evaluateRule(`{"all":[{"inPlaylist":{"id":"` + smartBID + `"}}]}`)
|
||||
Expect(results).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("does not match tracks from a private playlist", func() {
|
||||
refID := createPrivatePlaylist(testUser, "Come Together", "So What")
|
||||
results := evaluateRule(`{"all":[{"inPlaylist":{"id":"` + refID + `"}}]}`)
|
||||
Expect(results).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("matches tracks in a public playlist owned by another user", func() {
|
||||
refID := createPublicPlaylist(otherUser, "Bohemian Rhapsody")
|
||||
results := evaluateRule(`{"all":[{"inPlaylist":{"id":"` + refID + `"}}]}`)
|
||||
Expect(results).To(ConsistOf("Bohemian Rhapsody"))
|
||||
})
|
||||
|
||||
It("does not match tracks from a private playlist owned by another user", func() {
|
||||
refID := createPrivatePlaylist(otherUser, "Bohemian Rhapsody")
|
||||
results := evaluateRule(`{"all":[{"inPlaylist":{"id":"` + refID + `"}}]}`)
|
||||
Expect(results).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user