package criteria_test import ( "encoding/json" "fmt" . "github.com/navidrome/navidrome/model/criteria" . "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" ) var _ = BeforeSuite(func() { AddRoles([]string{"artist", "composer"}) AddTagNames([]string{"genre"}) AddNumericTags([]string{"rate"}) }) var _ = Describe("Operators", func() { DescribeTable("JSON Marshaling", func(op Expression, jsonString string) { obj := And{op} newJs, err := json.Marshal(obj) gomega.Expect(err).ToNot(gomega.HaveOccurred()) gomega.Expect(string(newJs)).To(gomega.Equal(fmt.Sprintf(`{"all":[%s]}`, jsonString))) var unmarshalObj UnmarshalConjunctionType js := "[" + jsonString + "]" err = json.Unmarshal([]byte(js), &unmarshalObj) gomega.Expect(err).ToNot(gomega.HaveOccurred()) gomega.Expect(unmarshalObj[0]).To(gomega.Equal(op)) }, Entry("is [string]", Is{"title": "Low Rider"}, `{"is":{"title":"Low Rider"}}`), Entry("is [bool]", Is{"loved": false}, `{"is":{"loved":false}}`), Entry("is [string does not coerce non-boolean field]", Is{"title": "true"}, `{"is":{"title":"true"}}`), Entry("isNot", IsNot{"title": "Low Rider"}, `{"isNot":{"title":"Low Rider"}}`), Entry("gt", Gt{"playCount": 10.0}, `{"gt":{"playCount":10}}`), Entry("lt", Lt{"playCount": 10.0}, `{"lt":{"playCount":10}}`), Entry("contains", Contains{"title": "Low Rider"}, `{"contains":{"title":"Low Rider"}}`), Entry("notContains", NotContains{"title": "Low Rider"}, `{"notContains":{"title":"Low Rider"}}`), Entry("startsWith", StartsWith{"title": "Low Rider"}, `{"startsWith":{"title":"Low Rider"}}`), Entry("endsWith", EndsWith{"title": "Low Rider"}, `{"endsWith":{"title":"Low Rider"}}`), Entry("inTheRange [number]", InTheRange{"year": []any{1980.0, 1990.0}}, `{"inTheRange":{"year":[1980,1990]}}`), Entry("inTheRange [date]", InTheRange{"lastPlayed": []any{"2021-10-01", "2021-11-01"}}, `{"inTheRange":{"lastPlayed":["2021-10-01","2021-11-01"]}}`), Entry("before", Before{"lastPlayed": "2021-10-01"}, `{"before":{"lastPlayed":"2021-10-01"}}`), Entry("after", After{"lastPlayed": "2021-10-01"}, `{"after":{"lastPlayed":"2021-10-01"}}`), Entry("inTheLast", InTheLast{"lastPlayed": 30.0}, `{"inTheLast":{"lastPlayed":30}}`), Entry("notInTheLast", NotInTheLast{"lastPlayed": 30.0}, `{"notInTheLast":{"lastPlayed":30}}`), Entry("inPlaylist", InPlaylist{"id": "deadbeef-dead-beef"}, `{"inPlaylist":{"id":"deadbeef-dead-beef"}}`), Entry("notInPlaylist", NotInPlaylist{"id": "deadbeef-dead-beef"}, `{"notInPlaylist":{"id":"deadbeef-dead-beef"}}`), Entry("isMissing [true]", IsMissing{"genre": true}, `{"isMissing":{"genre":true}}`), Entry("isMissing [false]", IsMissing{"genre": false}, `{"isMissing":{"genre":false}}`), Entry("isPresent [true]", IsPresent{"genre": true}, `{"isPresent":{"genre":true}}`), Entry("isPresent [false]", IsPresent{"genre": false}, `{"isPresent":{"genre":false}}`), ) Describe("Boolean string coercion at unmarshal time (issue #4826)", func() { It("coerces string 'true' to bool for boolean fields", func() { var obj UnmarshalConjunctionType err := json.Unmarshal([]byte(`[{"is":{"loved":"true"}}]`), &obj) gomega.Expect(err).ToNot(gomega.HaveOccurred()) gomega.Expect(obj[0]).To(gomega.Equal(Is{"loved": true})) }) It("coerces string 'false' to bool for boolean fields", func() { var obj UnmarshalConjunctionType err := json.Unmarshal([]byte(`[{"is":{"loved":"false"}}]`), &obj) gomega.Expect(err).ToNot(gomega.HaveOccurred()) gomega.Expect(obj[0]).To(gomega.Equal(Is{"loved": false})) }) It("does not coerce string values for non-boolean fields", func() { var obj UnmarshalConjunctionType err := json.Unmarshal([]byte(`[{"is":{"title":"true"}}]`), &obj) gomega.Expect(err).ToNot(gomega.HaveOccurred()) gomega.Expect(obj[0]).To(gomega.Equal(Is{"title": "true"})) }) It("coerces numeric 1 to bool true for boolean fields", func() { var obj UnmarshalConjunctionType err := json.Unmarshal([]byte(`[{"is":{"loved":1}}]`), &obj) gomega.Expect(err).ToNot(gomega.HaveOccurred()) gomega.Expect(obj[0]).To(gomega.Equal(Is{"loved": true})) }) It("coerces numeric 0 to bool false for boolean fields", func() { var obj UnmarshalConjunctionType err := json.Unmarshal([]byte(`[{"is":{"loved":0}}]`), &obj) gomega.Expect(err).ToNot(gomega.HaveOccurred()) gomega.Expect(obj[0]).To(gomega.Equal(Is{"loved": false})) }) It("coerces in nested any/all groups", func() { var c Criteria err := json.Unmarshal([]byte(`{"all":[{"contains":{"title":"love"}},{"any":[{"is":{"loved":"true"}}]}]}`), &c) gomega.Expect(err).ToNot(gomega.HaveOccurred()) all := c.Expression.(All) nested := all[1].(Any) gomega.Expect(nested[0]).To(gomega.Equal(Is{"loved": true})) }) It("coerces isMissing string 'true' to bool", func() { var obj UnmarshalConjunctionType err := json.Unmarshal([]byte(`[{"isMissing":{"genre":"true"}}]`), &obj) gomega.Expect(err).ToNot(gomega.HaveOccurred()) gomega.Expect(obj[0]).To(gomega.Equal(IsMissing{"genre": true})) }) It("coerces isMissing numeric 0 to bool false", func() { var obj UnmarshalConjunctionType err := json.Unmarshal([]byte(`[{"isMissing":{"genre":0}}]`), &obj) gomega.Expect(err).ToNot(gomega.HaveOccurred()) gomega.Expect(obj[0]).To(gomega.Equal(IsMissing{"genre": false})) }) It("coerces isPresent string 'false' to bool", func() { var obj UnmarshalConjunctionType err := json.Unmarshal([]byte(`[{"isPresent":{"genre":"false"}}]`), &obj) gomega.Expect(err).ToNot(gomega.HaveOccurred()) gomega.Expect(obj[0]).To(gomega.Equal(IsPresent{"genre": false})) }) It("coerces isPresent numeric 1 to bool true", func() { var obj UnmarshalConjunctionType err := json.Unmarshal([]byte(`[{"isPresent":{"genre":1}}]`), &obj) gomega.Expect(err).ToNot(gomega.HaveOccurred()) gomega.Expect(obj[0]).To(gomega.Equal(IsPresent{"genre": true})) }) }) DescribeTable("ToBool", func(in any, wantVal, wantOk bool) { got, ok := ToBool(in) gomega.Expect(ok).To(gomega.Equal(wantOk)) gomega.Expect(got).To(gomega.Equal(wantVal)) }, Entry("real bool true", true, true, true), Entry("real bool false", false, false, true), Entry("string true", "true", true, true), Entry("string false", "false", false, true), Entry("string 1", "1", true, true), Entry("string t", "t", true, true), Entry("string 0", "0", false, true), Entry("string unparseable", "yes", false, false), Entry("float64 1", float64(1), true, true), Entry("float64 0", float64(0), false, true), Entry("float64 other", float64(2), false, false), Entry("slice", []any{true}, false, false), Entry("nil", nil, false, false), ) })