1
2
3
4
5
6
7
8
9
10
11
12
13 package mango
14
15 import (
16 "regexp"
17 "testing"
18
19 "github.com/google/go-cmp/cmp"
20 "gitlab.com/flimzy/testy"
21 )
22
23 var cmpOpts = []cmp.Option{
24 cmp.AllowUnexported(notNode{}, combinationNode{}, conditionNode{}),
25 }
26
27 func TestParse(t *testing.T) {
28 type test struct {
29 input string
30 want Node
31 wantErr string
32 }
33
34 tests := testy.NewTable()
35 tests.Add("empty", test{
36 input: "{}",
37 want: &combinationNode{
38 op: OpAnd,
39 sel: nil,
40 },
41 })
42 tests.Add("implicit equality", test{
43 input: `{"foo": "bar"}`,
44 want: &fieldNode{
45 field: "foo",
46 cond: &conditionNode{
47 op: OpEqual,
48 cond: "bar",
49 },
50 },
51 })
52 tests.Add("explicit equality", test{
53 input: `{"foo": {"$eq": "bar"}}`,
54 want: &fieldNode{
55 field: "foo",
56 cond: &conditionNode{
57 op: OpEqual,
58 cond: "bar",
59 },
60 },
61 })
62 tests.Add("explicit equality with too many object keys", test{
63 input: `{"foo": {"$eq": "bar", "$ne": "baz"}}`,
64 wantErr: "too many keys in object",
65 })
66 tests.Add("implicit equality with empty object", test{
67 input: `{"foo": {}}`,
68 want: &fieldNode{
69 field: "foo",
70 cond: &conditionNode{
71 op: OpEqual,
72 cond: map[string]interface{}{},
73 },
74 },
75 })
76 tests.Add("explicit invalid comparison operator", test{
77 input: `{"foo": {"$invalid": "bar"}}`,
78 wantErr: "invalid operator $invalid",
79 })
80 tests.Add("explicit equality against object", test{
81 input: `{"foo": {"$eq": {"bar": "baz"}}}`,
82 want: &fieldNode{
83 field: "foo",
84 cond: &conditionNode{
85 op: OpEqual,
86 cond: map[string]interface{}{"bar": "baz"},
87 },
88 },
89 })
90 tests.Add("less than", test{
91 input: `{"foo": {"$lt": 42}}`,
92 want: &fieldNode{
93 field: "foo",
94 cond: &conditionNode{
95 op: OpLessThan,
96 cond: float64(42),
97 },
98 },
99 })
100 tests.Add("less than or equal", test{
101 input: `{"foo": {"$lte": 42}}`,
102 want: &fieldNode{
103 field: "foo",
104 cond: &conditionNode{
105 op: OpLessThanOrEqual,
106 cond: float64(42),
107 },
108 },
109 })
110 tests.Add("not equal", test{
111 input: `{"foo": {"$ne": 42}}`,
112 want: &fieldNode{
113 field: "foo",
114 cond: &conditionNode{
115 op: OpNotEqual,
116 cond: float64(42),
117 },
118 },
119 })
120 tests.Add("greater than", test{
121 input: `{"foo": {"$gt": 42}}`,
122 want: &fieldNode{
123 field: "foo",
124 cond: &conditionNode{
125 op: OpGreaterThan,
126 cond: float64(42),
127 },
128 },
129 })
130 tests.Add("greater than or equal", test{
131 input: `{"foo": {"$gte": 42}}`,
132 want: &fieldNode{
133 field: "foo",
134 cond: &conditionNode{
135 op: OpGreaterThanOrEqual,
136 cond: float64(42),
137 },
138 },
139 })
140 tests.Add("exists", test{
141 input: `{"foo": {"$exists": true}}`,
142 want: &fieldNode{
143 field: "foo",
144 cond: &conditionNode{
145 op: OpExists,
146 cond: true,
147 },
148 },
149 })
150 tests.Add("exists with non-boolean", test{
151 input: `{"foo": {"$exists": 42}}`,
152 wantErr: "$exists: json: cannot unmarshal number into Go value of type bool",
153 })
154 tests.Add("type", test{
155 input: `{"foo": {"$type": "string"}}`,
156 want: &fieldNode{
157 field: "foo",
158 cond: &conditionNode{
159 op: OpType,
160 cond: "string",
161 },
162 },
163 })
164 tests.Add("type with non-string", test{
165 input: `{"foo": {"$type": 42}}`,
166 wantErr: "$type: json: cannot unmarshal number into Go value of type string",
167 })
168 tests.Add("in", test{
169 input: `{"foo": {"$in": [1, 2, 3]}}`,
170 want: &fieldNode{
171 field: "foo",
172 cond: &conditionNode{
173 op: OpIn,
174 cond: []interface{}{float64(1), float64(2), float64(3)},
175 },
176 },
177 })
178 tests.Add("in with non-array", test{
179 input: `{"foo": {"$in": 42}}`,
180 wantErr: "$in: json: cannot unmarshal number into Go value of type []interface {}",
181 })
182 tests.Add("not in", test{
183 input: `{"foo": {"$nin": [1, 2, 3]}}`,
184 want: &fieldNode{
185 field: "foo",
186 cond: &conditionNode{
187 op: OpNotIn,
188 cond: []interface{}{float64(1), float64(2), float64(3)},
189 },
190 },
191 })
192 tests.Add("not in with non-array", test{
193 input: `{"foo": {"$nin": 42}}`,
194 wantErr: "$nin: json: cannot unmarshal number into Go value of type []interface {}",
195 })
196 tests.Add("size", test{
197 input: `{"foo": {"$size": 42}}`,
198 want: &fieldNode{
199 field: "foo",
200 cond: &conditionNode{
201 op: OpSize,
202 cond: float64(42),
203 },
204 },
205 })
206 tests.Add("size with non-integer", test{
207 input: `{"foo": {"$size": 42.5}}`,
208 wantErr: "$size: json: cannot unmarshal number 42.5 into Go value of type uint",
209 })
210 tests.Add("mod", test{
211 input: `{"foo": {"$mod": [2, 1]}}`,
212 want: &fieldNode{
213 field: "foo",
214 cond: &conditionNode{
215 op: OpMod,
216 cond: [2]int64{2, 1},
217 },
218 },
219 })
220 tests.Add("mod with non-array", test{
221 input: `{"foo": {"$mod": 42}}`,
222 wantErr: "$mod: json: cannot unmarshal number into Go value of type [2]int64",
223 })
224 tests.Add("mod with zero divisor", test{
225 input: `{"foo": {"$mod": [0, 1]}}`,
226 wantErr: "$mod: divisor must be non-zero",
227 })
228 tests.Add("regex", test{
229 input: `{"foo": {"$regex": "^bar$"}}`,
230 want: &fieldNode{
231 field: "foo",
232 cond: &conditionNode{
233 op: OpRegex,
234 cond: regexp.MustCompile("^bar$"),
235 },
236 },
237 })
238 tests.Add("regexp non-string", test{
239 input: `{"foo": {"$regex": 42}}`,
240 wantErr: "$regex: json: cannot unmarshal number into Go value of type string",
241 })
242 tests.Add("regexp invalid", test{
243 input: `{"foo": {"$regex": "["}}`,
244 wantErr: "$regex: error parsing regexp: missing closing ]: `[`",
245 })
246 tests.Add("implicit $and", test{
247 input: `{"foo":"bar","baz":"qux"}`,
248 want: &combinationNode{
249 op: OpAnd,
250 sel: []Node{
251 &fieldNode{
252 field: "baz",
253 cond: &conditionNode{
254 op: OpEqual,
255 cond: "qux",
256 },
257 },
258 &fieldNode{
259 field: "foo",
260 cond: &conditionNode{
261 op: OpEqual,
262 cond: "bar",
263 },
264 },
265 },
266 },
267 })
268 tests.Add("explicit $and", test{
269 input: `{"$and":[{"foo":"bar"},{"baz":"qux"}]}`,
270 want: &combinationNode{
271 op: OpAnd,
272 sel: []Node{
273 &fieldNode{
274 field: "foo",
275 cond: &conditionNode{
276 op: OpEqual,
277 cond: "bar",
278 },
279 },
280 &fieldNode{
281 field: "baz",
282 cond: &conditionNode{
283 op: OpEqual,
284 cond: "qux",
285 },
286 },
287 },
288 },
289 })
290 tests.Add("nested implicit and explicit $and", test{
291 input: `{"$and":[{"foo":"bar"},{"baz":"qux"}, {"quux":"corge","grault":"garply"}]}`,
292 want: &combinationNode{
293 op: OpAnd,
294 sel: []Node{
295 &fieldNode{
296 field: "foo",
297 cond: &conditionNode{
298 op: OpEqual,
299 cond: "bar",
300 },
301 },
302 &fieldNode{
303 field: "baz",
304 cond: &conditionNode{
305 op: OpEqual,
306 cond: "qux",
307 },
308 },
309 &combinationNode{
310 op: OpAnd,
311 sel: []Node{
312 &fieldNode{
313 field: "grault",
314 cond: &conditionNode{
315 op: OpEqual,
316 cond: "garply",
317 },
318 },
319 &fieldNode{
320 field: "quux",
321 cond: &conditionNode{
322 op: OpEqual,
323 cond: "corge",
324 },
325 },
326 },
327 },
328 },
329 },
330 })
331 tests.Add("$or", test{
332 input: `{"$or":[{"foo":"bar"},{"baz":"qux"}]}`,
333 want: &combinationNode{
334 op: OpOr,
335 sel: []Node{
336 &fieldNode{
337 field: "foo",
338 cond: &conditionNode{
339 op: OpEqual,
340 cond: "bar",
341 },
342 },
343 &fieldNode{
344 field: "baz",
345 cond: &conditionNode{
346 op: OpEqual,
347 cond: "qux",
348 },
349 },
350 },
351 },
352 })
353 tests.Add("invalid operator", test{
354 input: `{"$invalid": "bar"}`,
355 wantErr: "unknown operator $invalid",
356 })
357 tests.Add("$not", test{
358 input: `{"$not": {"foo":"bar"}}`,
359 want: ¬Node{
360 sel: &fieldNode{
361 field: "foo",
362 cond: &conditionNode{
363 op: OpEqual,
364 cond: "bar",
365 },
366 },
367 },
368 })
369 tests.Add("$not with invalid selector", test{
370 input: `{"$not": []}`,
371 wantErr: "$not: json: cannot unmarshal array into Go value of type map[string]json.RawMessage",
372 })
373 tests.Add("$and with invalid selector array", test{
374 input: `{"$and": {}}`,
375 wantErr: "$and: json: cannot unmarshal object into Go value of type []json.RawMessage",
376 })
377 tests.Add("$and with invalid selector", test{
378 input: `{"$and": [42]}`,
379 wantErr: "$and: json: cannot unmarshal number into Go value of type map[string]json.RawMessage",
380 })
381 tests.Add("$nor", test{
382 input: `{"$nor":[{"foo":"bar"},{"baz":"qux"}]}`,
383 want: &combinationNode{
384 op: OpNor,
385 sel: []Node{
386 &fieldNode{
387 field: "foo",
388 cond: &conditionNode{
389 op: OpEqual,
390 cond: "bar",
391 },
392 },
393 &fieldNode{
394 field: "baz",
395 cond: &conditionNode{
396 op: OpEqual,
397 cond: "qux",
398 },
399 },
400 },
401 },
402 })
403 tests.Add("$all", test{
404 input: `{"foo": {"$all": ["bar", "baz"]}}`,
405 want: &fieldNode{
406 field: "foo",
407 cond: &conditionNode{
408 op: OpAll,
409 cond: []interface{}{"bar", "baz"},
410 },
411 },
412 })
413 tests.Add("$all with non-array", test{
414 input: `{"foo": {"$all": "bar"}}`,
415 wantErr: "$all: json: cannot unmarshal string into Go value of type []interface {}",
416 })
417 tests.Add("$elemMatch", test{
418 input: `{"genre": {"$elemMatch": {"$eq": "Horror"}}}`,
419 want: &fieldNode{
420 field: "genre",
421 cond: &elementNode{
422 op: OpElemMatch,
423 cond: &conditionNode{
424 op: OpEqual,
425 cond: "Horror",
426 },
427 },
428 },
429 })
430 tests.Add("$allMatch", test{
431 input: `{"genre": {"$allMatch": {"$eq": "Horror"}}}`,
432 want: &fieldNode{
433 field: "genre",
434 cond: &elementNode{
435 op: OpAllMatch,
436 cond: &conditionNode{
437 op: OpEqual,
438 cond: "Horror",
439 },
440 },
441 },
442 })
443 tests.Add("$keyMapMatch", test{
444 input: `{"cameras": {"$keyMapMatch": {"$eq": "secondary"}}}`,
445 want: &fieldNode{
446 field: "cameras",
447 cond: &elementNode{
448 op: OpKeyMapMatch,
449 cond: &conditionNode{
450 op: OpEqual,
451 cond: "secondary",
452 },
453 },
454 },
455 })
456 tests.Add("element selector with invalid selector", test{
457 input: `{"cameras": {"$keyMapMatch": 42}}`,
458 wantErr: "$keyMapMatch: json: cannot unmarshal number into Go value of type map[string]json.RawMessage",
459 })
460
461
466
467 tests.Run(t, func(t *testing.T, tt test) {
468 got, err := Parse([]byte(tt.input))
469 if !testy.ErrorMatches(tt.wantErr, err) {
470 t.Fatalf("Unexpected error: %s", err)
471 }
472 if err != nil {
473 return
474 }
475 if d := cmp.Diff(tt.want.String(), got.String(), cmpOpts...); d != "" {
476 t.Errorf("Unexpected result (-want +got):\n%s", d)
477 }
478 })
479 }
480
View as plain text