navidrome/persistence/sql_smartplaylist_test.go

178 lines
6.4 KiB
Go

package persistence
import (
"time"
"github.com/Masterminds/squirrel"
"github.com/navidrome/navidrome/model"
. "github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo/extensions/table"
. "github.com/onsi/gomega"
)
var _ = Describe("SmartPlaylist", func() {
var pls SmartPlaylist
Describe("AddFilters", func() {
BeforeEach(func() {
sp := model.SmartPlaylist{
RuleGroup: model.RuleGroup{
Combinator: "and", Rules: model.Rules{
model.Rule{Field: "title", Operator: "contains", Value: "love"},
model.Rule{Field: "year", Operator: "is in the range", Value: []int{1980, 1989}},
model.Rule{Field: "loved", Operator: "is true"},
model.Rule{Field: "lastPlayed", Operator: "in the last", Value: "30"},
model.RuleGroup{
Combinator: "or",
Rules: model.Rules{
model.Rule{Field: "artist", Operator: "is not", Value: "zé"},
model.Rule{Field: "album", Operator: "is", Value: "4"},
},
},
}},
Order: "artist asc",
Limit: 100,
}
pls = SmartPlaylist(sp)
})
It("returns a proper SQL query", func() {
sel := pls.AddFilters(squirrel.Select("media_file").Columns("*"))
sql, args, err := sel.ToSql()
Expect(err).ToNot(HaveOccurred())
Expect(sql).To(Equal("SELECT media_file, * WHERE (media_file.title ILIKE ? AND (media_file.year >= ? AND media_file.year <= ?) AND annotation.starred = ? AND annotation.play_date > ? AND (media_file.artist <> ? OR media_file.album = ?)) ORDER BY artist asc LIMIT 100"))
lastMonth := time.Now().Add(-30 * 24 * time.Hour)
Expect(args).To(ConsistOf("%love%", 1980, 1989, true, BeTemporally("~", lastMonth, time.Second), "zé", "4"))
})
It("returns an error if field is invalid", func() {
r := pls.Rules[0].(model.Rule)
r.Field = "INVALID"
pls.Rules[0] = r
sel := pls.AddFilters(squirrel.Select("media_file").Columns("*"))
_, _, err := sel.ToSql()
Expect(err).To(MatchError("invalid smart playlist field 'INVALID'"))
})
})
Describe("fieldMap", func() {
It("includes all possible fields", func() {
for _, field := range model.SmartPlaylistFields {
Expect(fieldMap).To(HaveKey(field))
}
})
It("does not have extra fields", func() {
for field := range fieldMap {
Expect(model.SmartPlaylistFields).To(ContainElement(field))
}
})
})
Describe("stringRule", func() {
DescribeTable("stringRule",
func(operator, expectedSql, expectedValue string) {
r := stringRule{Field: "title", Operator: operator, Value: "value"}
sql, args, err := r.ToSql()
Expect(err).ToNot(HaveOccurred())
Expect(sql).To(Equal(expectedSql))
Expect(args).To(ConsistOf(expectedValue))
},
Entry("is", "is", "title = ?", "value"),
Entry("is not", "is not", "title <> ?", "value"),
Entry("contains", "contains", "title ILIKE ?", "%value%"),
Entry("does not contains", "does not contains", "title NOT ILIKE ?", "%value%"),
Entry("begins with", "begins with", "title ILIKE ?", "value%"),
Entry("ends with", "ends with", "title ILIKE ?", "%value"),
)
})
Describe("numberRule", func() {
DescribeTable("operators",
func(operator, expectedSql string, expectedValue int) {
r := numberRule{Field: "year", Operator: operator, Value: 1985}
sql, args, err := r.ToSql()
Expect(err).ToNot(HaveOccurred())
Expect(sql).To(Equal(expectedSql))
Expect(args).To(ConsistOf(expectedValue))
},
Entry("is", "is", "year = ?", 1985),
Entry("is not", "is not", "year <> ?", 1985),
Entry("is greater than", "is greater than", "year > ?", 1985),
Entry("is less than", "is less than", "year < ?", 1985),
)
It("implements the 'is in the range' operator", func() {
r := numberRule{Field: "year", Operator: "is in the range", Value: []int{1981, 1990}}
sql, args, err := r.ToSql()
Expect(err).ToNot(HaveOccurred())
Expect(sql).To(Equal("(year >= ? AND year <= ?)"))
Expect(args).To(ConsistOf(1981, 1990))
})
})
Describe("dateRule", func() {
dateStr := "2021-10-14"
date, _ := time.Parse("2006-01-02", dateStr)
DescribeTable("simple operators",
func(operator, expectedSql string, expectedValue time.Time) {
r := dateRule{Field: "lastPlayed", Operator: operator, Value: dateStr}
sql, args, err := r.ToSql()
Expect(err).ToNot(HaveOccurred())
Expect(sql).To(Equal(expectedSql))
Expect(args).To(ConsistOf(expectedValue))
},
Entry("is", "is", "lastPlayed = ?", date),
Entry("is not", "is not", "lastPlayed <> ?", date),
Entry("is before", "is before", "lastPlayed < ?", date),
Entry("is after", "is after", "lastPlayed > ?", date),
)
DescribeTable("period operators",
func(operator, expectedSql string, expectedValue time.Time) {
r := dateRule{Field: "lastPlayed", Operator: operator, Value: 90}
sql, args, err := r.ToSql()
Expect(err).ToNot(HaveOccurred())
Expect(sql).To(Equal(expectedSql))
Expect(args).To(ConsistOf(BeTemporally("~", expectedValue, 25*time.Hour)))
},
Entry("in the last", "in the last", "lastPlayed > ?", date.Add(-90*24*time.Hour)),
Entry("not in the last", "not in the last", "lastPlayed < ?", date.Add(-90*24*time.Hour)),
)
It("accepts string as the 'in the last' operator value", func() {
r := dateRule{Field: "lastPlayed", Operator: "in the last", Value: "90"}
_, args, _ := r.ToSql()
Expect(args).To(ConsistOf(BeTemporally("~", date.Add(-90*24*time.Hour), 25*time.Hour)))
})
It("implements the 'is in the range' operator", func() {
date2Str := "2021-09-14"
date2, _ := time.Parse("2006-01-02", date2Str)
r := dateRule{Field: "lastPlayed", Operator: "is in the range", Value: []string{date2Str, dateStr}}
sql, args, err := r.ToSql()
Expect(err).ToNot(HaveOccurred())
Expect(sql).To(Equal("(lastPlayed >= ? AND lastPlayed <= ?)"))
Expect(args).To(ConsistOf(BeTemporally("~", date2, 25*time.Hour), BeTemporally("~", date, 25*time.Hour)))
})
It("returns error if date is invalid", func() {
r := dateRule{Field: "lastPlayed", Operator: "is", Value: "INVALID"}
_, _, err := r.ToSql()
Expect(err).To(MatchError("invalid date: INVALID"))
})
})
Describe("boolRule", func() {
DescribeTable("operators",
func(operator, expectedSql string, expectedValue ...interface{}) {
r := boolRule{Field: "loved", Operator: operator}
sql, args, err := r.ToSql()
Expect(err).ToNot(HaveOccurred())
Expect(sql).To(Equal(expectedSql))
Expect(args).To(ConsistOf(expectedValue...))
},
Entry("is true", "is true", "loved = ?", true),
Entry("is false", "is false", "loved = ?", false),
)
})
})