1/*
2Copyright 2019 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package listtype
18
19import (
20	"testing"
21
22	"k8s.io/apimachinery/pkg/util/validation/field"
23
24	"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
25)
26
27func TestValidateListSetsAndMaps(t *testing.T) {
28	tests := []struct {
29		name   string
30		schema *schema.Structural
31		obj    map[string]interface{}
32		errors []validationMatch
33	}{
34		{name: "nil"},
35		{name: "no schema", obj: make(map[string]interface{})},
36		{name: "no object", schema: &schema.Structural{}},
37		{name: "list without schema",
38			obj: map[string]interface{}{
39				"array": []interface{}{"a", "b", "a"},
40			},
41		},
42		{name: "list without items",
43			obj: map[string]interface{}{
44				"array": []interface{}{"a", "b", "a"},
45			},
46			schema: &schema.Structural{
47				Generic: schema.Generic{
48					Type: "object",
49				},
50				Properties: map[string]schema.Structural{
51					"array": {
52						Generic: schema.Generic{
53							Type: "array",
54						},
55					},
56				},
57			},
58		},
59
60		{name: "set list with one item",
61			obj: map[string]interface{}{
62				"array": []interface{}{"a"},
63			},
64			schema: &schema.Structural{
65				Generic: schema.Generic{
66					Type: "object",
67				},
68				Properties: map[string]schema.Structural{
69					"array": {
70						Extensions: schema.Extensions{
71							XListType: strPtr("set"),
72						},
73						Generic: schema.Generic{
74							Type: "array",
75						},
76					},
77				},
78			},
79		},
80		{name: "set list with two equal items",
81			obj: map[string]interface{}{
82				"array": []interface{}{"a", "a"},
83			},
84			schema: &schema.Structural{
85				Generic: schema.Generic{
86					Type: "object",
87				},
88				Properties: map[string]schema.Structural{
89					"array": {
90						Extensions: schema.Extensions{
91							XListType: strPtr("set"),
92						},
93						Generic: schema.Generic{
94							Type: "array",
95						},
96					},
97				},
98			},
99			errors: []validationMatch{
100				duplicate("root", "array[1]"),
101			},
102		},
103		{name: "set list with two different items",
104			obj: map[string]interface{}{
105				"array": []interface{}{"a", "b"},
106			},
107			schema: &schema.Structural{
108				Generic: schema.Generic{
109					Type: "object",
110				},
111				Properties: map[string]schema.Structural{
112					"array": {
113						Extensions: schema.Extensions{
114							XListType: strPtr("set"),
115						},
116						Generic: schema.Generic{
117							Type: "array",
118						},
119					},
120				},
121			},
122		},
123		{name: "set list with multiple duplicated items",
124			obj: map[string]interface{}{
125				"array": []interface{}{"a", "a", "b", "c", "d", "c"},
126			},
127			schema: &schema.Structural{
128				Generic: schema.Generic{
129					Type: "object",
130				},
131				Properties: map[string]schema.Structural{
132					"array": {
133						Extensions: schema.Extensions{
134							XListType: strPtr("set"),
135						},
136						Generic: schema.Generic{
137							Type: "array",
138						},
139					},
140				},
141			},
142			errors: []validationMatch{
143				duplicate("root", "array[1]"),
144				duplicate("root", "array[5]"),
145			},
146		},
147
148		{name: "normal list with items",
149			obj: map[string]interface{}{
150				"array": []interface{}{"a", "b", "a"},
151			},
152			schema: &schema.Structural{
153				Generic: schema.Generic{
154					Type: "object",
155				},
156				Properties: map[string]schema.Structural{
157					"array": {
158						Generic: schema.Generic{
159							Type: "array",
160						},
161						Items: &schema.Structural{
162							Generic: schema.Generic{
163								Type: "string",
164							},
165						},
166					},
167				},
168			},
169		},
170		{name: "set list with items",
171			obj: map[string]interface{}{
172				"array": []interface{}{"a", "b", "a"},
173			},
174			schema: &schema.Structural{
175				Generic: schema.Generic{
176					Type: "object",
177				},
178				Properties: map[string]schema.Structural{
179					"array": {
180						Generic: schema.Generic{
181							Type: "array",
182						},
183						Extensions: schema.Extensions{
184							XListType: strPtr("set"),
185						},
186						Items: &schema.Structural{
187							Generic: schema.Generic{
188								Type: "string",
189							},
190						},
191					},
192				},
193			},
194			errors: []validationMatch{
195				duplicate("root", "array[2]"),
196			},
197		},
198		{name: "set list with items under additionalProperties",
199			obj: map[string]interface{}{
200				"array": []interface{}{"a", "b", "a"},
201			},
202			schema: &schema.Structural{
203				Generic: schema.Generic{
204					Type: "object",
205					AdditionalProperties: &schema.StructuralOrBool{
206						Structural: &schema.Structural{
207							Generic: schema.Generic{
208								Type: "array",
209							},
210							Extensions: schema.Extensions{
211								XListType: strPtr("set"),
212							},
213							Items: &schema.Structural{
214								Generic: schema.Generic{
215									Type: "string",
216								},
217							},
218						},
219					},
220				},
221			},
222			errors: []validationMatch{
223				duplicate("root[array][2]"),
224			},
225		},
226		{name: "set list with items under items",
227			obj: map[string]interface{}{
228				"array": []interface{}{
229					[]interface{}{"a", "b", "a"},
230					[]interface{}{"b", "b", "a"},
231				},
232			},
233			schema: &schema.Structural{
234				Generic: schema.Generic{
235					Type: "object",
236				},
237				Properties: map[string]schema.Structural{
238					"array": {
239						Generic: schema.Generic{
240							Type: "array",
241						},
242						Items: &schema.Structural{
243							Generic: schema.Generic{
244								Type: "array",
245							},
246							Extensions: schema.Extensions{
247								XListType: strPtr("set"),
248							},
249							Items: &schema.Structural{
250								Generic: schema.Generic{
251									Type: "string",
252								},
253							},
254						},
255					},
256				},
257			},
258			errors: []validationMatch{
259				duplicate("root", "array[0][2]"),
260				duplicate("root", "array[1][1]"),
261			},
262		},
263
264		{name: "nested set lists",
265			obj: map[string]interface{}{
266				"array": []interface{}{
267					"a", "b", "a", []interface{}{"b", "b", "a"},
268				},
269			},
270			schema: &schema.Structural{
271				Generic: schema.Generic{
272					Type: "object",
273				},
274				Properties: map[string]schema.Structural{
275					"array": {
276						Generic: schema.Generic{
277							Type: "array",
278						},
279						Extensions: schema.Extensions{
280							XListType: strPtr("set"),
281						},
282						Items: &schema.Structural{
283							Generic: schema.Generic{
284								Type: "array",
285							},
286							Extensions: schema.Extensions{
287								XListType: strPtr("set"),
288							},
289						},
290					},
291				},
292			},
293			errors: []validationMatch{
294				duplicate("root", "array[2]"),
295				duplicate("root", "array[3][1]"),
296			},
297		},
298
299		{name: "set list with compound map items",
300			obj: map[string]interface{}{
301				"strings":             []interface{}{"a", "b", "a"},
302				"integers":            []interface{}{int64(1), int64(2), int64(1)},
303				"booleans":            []interface{}{false, true, true},
304				"float64":             []interface{}{float64(1.0), float64(2.0), float64(2.0)},
305				"nil":                 []interface{}{"a", nil, nil},
306				"empty maps":          []interface{}{map[string]interface{}{"a": "b"}, map[string]interface{}{}, map[string]interface{}{}},
307				"map values":          []interface{}{map[string]interface{}{"a": "b"}, map[string]interface{}{"a": "c"}, map[string]interface{}{"a": "b"}},
308				"nil values":          []interface{}{map[string]interface{}{"a": nil}, map[string]interface{}{"b": "c", "a": nil}},
309				"array":               []interface{}{[]interface{}{}, []interface{}{"a"}, []interface{}{"b"}, []interface{}{"a"}},
310				"nil array":           []interface{}{[]interface{}{}, []interface{}{nil}, []interface{}{nil, nil}, []interface{}{nil}, []interface{}{"a"}},
311				"multiple duplicates": []interface{}{map[string]interface{}{"a": "b"}, map[string]interface{}{"a": "c"}, map[string]interface{}{"a": "b"}, map[string]interface{}{"a": "c"}, map[string]interface{}{"a": "c"}},
312			},
313			schema: &schema.Structural{
314				Generic: schema.Generic{
315					Type: "object",
316				},
317				Properties: map[string]schema.Structural{
318					"strings": {
319						Generic: schema.Generic{
320							Type: "array",
321						},
322						Items: &schema.Structural{
323							Generic: schema.Generic{
324								Type: "string",
325							},
326						},
327						Extensions: schema.Extensions{
328							XListType: strPtr("set"),
329						},
330					},
331					"integers": {
332						Generic: schema.Generic{
333							Type: "array",
334						},
335						Items: &schema.Structural{
336							Generic: schema.Generic{
337								Type: "integer",
338							},
339						},
340						Extensions: schema.Extensions{
341							XListType: strPtr("set"),
342						},
343					},
344					"booleans": {
345						Generic: schema.Generic{
346							Type: "array",
347						},
348						Items: &schema.Structural{
349							Generic: schema.Generic{
350								Type: "boolean",
351							},
352						},
353						Extensions: schema.Extensions{
354							XListType: strPtr("set"),
355						},
356					},
357					"float64": {
358						Generic: schema.Generic{
359							Type: "array",
360						},
361						Items: &schema.Structural{
362							Generic: schema.Generic{
363								Type: "number",
364							},
365						},
366						Extensions: schema.Extensions{
367							XListType: strPtr("set"),
368						},
369					},
370					"nil": {
371						Generic: schema.Generic{
372							Type: "array",
373						}, Items: &schema.Structural{
374							Generic: schema.Generic{
375								Type:     "string",
376								Nullable: true,
377							},
378						},
379						Extensions: schema.Extensions{
380							XListType: strPtr("set"),
381						},
382					},
383					"empty maps": {
384						Generic: schema.Generic{
385							Type: "array",
386						},
387						Items: &schema.Structural{
388							Generic: schema.Generic{
389								Type: "object",
390								AdditionalProperties: &schema.StructuralOrBool{
391									Structural: &schema.Structural{
392										Generic: schema.Generic{
393											Type: "string",
394										},
395									},
396								},
397							},
398						},
399						Extensions: schema.Extensions{
400							XListType: strPtr("set"),
401						},
402					},
403					"map values": {
404						Generic: schema.Generic{
405							Type: "array",
406						},
407						Items: &schema.Structural{
408							Generic: schema.Generic{
409								Type: "object",
410								AdditionalProperties: &schema.StructuralOrBool{
411									Structural: &schema.Structural{
412										Generic: schema.Generic{
413											Type: "string",
414										},
415									},
416								},
417							},
418						},
419						Extensions: schema.Extensions{
420							XListType: strPtr("set"),
421						},
422					},
423					"nil values": {
424						Generic: schema.Generic{
425							Type: "array",
426						},
427						Items: &schema.Structural{
428							Generic: schema.Generic{
429								Type: "object",
430								AdditionalProperties: &schema.StructuralOrBool{
431									Structural: &schema.Structural{
432										Generic: schema.Generic{
433											Type:     "string",
434											Nullable: true,
435										},
436									},
437								},
438							},
439						},
440						Extensions: schema.Extensions{
441							XListType: strPtr("set"),
442						},
443					},
444					"array": {
445						Generic: schema.Generic{
446							Type: "array",
447						},
448						Items: &schema.Structural{
449							Generic: schema.Generic{
450								Type: "array",
451							},
452							Items: &schema.Structural{
453								Generic: schema.Generic{
454									Type: "string",
455								},
456							},
457						},
458						Extensions: schema.Extensions{
459							XListType: strPtr("set"),
460						},
461					},
462					"nil array": {
463						Generic: schema.Generic{
464							Type: "array",
465						},
466						Items: &schema.Structural{
467							Generic: schema.Generic{
468								Type: "array",
469							},
470							Items: &schema.Structural{
471								Generic: schema.Generic{
472									Type:     "string",
473									Nullable: true,
474								},
475							},
476						},
477						Extensions: schema.Extensions{
478							XListType: strPtr("set"),
479						},
480					},
481					"multiple duplicates": {
482						Generic: schema.Generic{
483							Type: "array",
484						},
485						Items: &schema.Structural{
486							Generic: schema.Generic{
487								Type: "object",
488								AdditionalProperties: &schema.StructuralOrBool{
489									Structural: &schema.Structural{
490										Generic: schema.Generic{
491											Type: "string",
492										},
493									},
494								},
495							},
496						},
497						Extensions: schema.Extensions{
498							XListType: strPtr("set"),
499						},
500					},
501				},
502			},
503			errors: []validationMatch{
504				duplicate("root", "strings[2]"),
505				duplicate("root", "integers[2]"),
506				duplicate("root", "booleans[2]"),
507				duplicate("root", "float64[2]"),
508				duplicate("root", "nil[2]"),
509				duplicate("root", "empty maps[2]"),
510				duplicate("root", "map values[2]"),
511				duplicate("root", "array[3]"),
512				duplicate("root", "nil array[3]"),
513				duplicate("root", "multiple duplicates[2]"),
514				duplicate("root", "multiple duplicates[3]"),
515			},
516		},
517		{name: "set list with compound array items",
518			obj: map[string]interface{}{
519				"array": []interface{}{[]interface{}{}, []interface{}{"a"}, []interface{}{"a"}},
520			},
521			schema: &schema.Structural{
522				Generic: schema.Generic{
523					Type: "object",
524				},
525				Properties: map[string]schema.Structural{
526					"array": {
527						Generic: schema.Generic{
528							Type: "array",
529						},
530						Extensions: schema.Extensions{
531							XListType: strPtr("set"),
532						},
533						Items: &schema.Structural{
534							Generic: schema.Generic{
535								Type: "string",
536							},
537						},
538					},
539				},
540			},
541			errors: []validationMatch{
542				duplicate("root", "array[2]"),
543			},
544		},
545
546		{name: "map list with compound map items",
547			obj: map[string]interface{}{
548				"strings":                []interface{}{"a"},
549				"integers":               []interface{}{int64(1)},
550				"booleans":               []interface{}{false},
551				"float64":                []interface{}{float64(1.0)},
552				"nil":                    []interface{}{nil},
553				"array":                  []interface{}{[]interface{}{"a"}},
554				"one key":                []interface{}{map[string]interface{}{"a": "0", "c": "2"}, map[string]interface{}{"a": "1", "c": "1"}, map[string]interface{}{"a": "1", "c": "2"}, map[string]interface{}{}},
555				"two keys":               []interface{}{map[string]interface{}{"a": "1", "b": "1", "c": "1"}, map[string]interface{}{"a": "1", "b": "2", "c": "2"}, map[string]interface{}{"a": "1", "b": "2", "c": "3"}, map[string]interface{}{}},
556				"undefined key":          []interface{}{map[string]interface{}{"a": "1", "b": "1", "c": "1"}, map[string]interface{}{"a": "1", "c": "2"}, map[string]interface{}{"a": "1", "c": "3"}, map[string]interface{}{}},
557				"compound key":           []interface{}{map[string]interface{}{"a": []interface{}{}, "c": "1"}, map[string]interface{}{"a": nil, "c": "1"}, map[string]interface{}{"a": []interface{}{"a"}, "c": "1"}, map[string]interface{}{"a": []interface{}{"a", int64(42)}, "c": "2"}, map[string]interface{}{"a": []interface{}{"a", int64(42)}, "c": []interface{}{"3"}}},
558				"nil key":                []interface{}{map[string]interface{}{"a": []interface{}{}, "c": "1"}, map[string]interface{}{"a": nil, "c": "1"}, map[string]interface{}{"c": "1"}, map[string]interface{}{"a": nil}},
559				"nil item":               []interface{}{nil, map[string]interface{}{"a": "0", "c": "1"}, map[string]interface{}{"a": nil}, map[string]interface{}{"c": "1"}},
560				"nil item multiple keys": []interface{}{nil, map[string]interface{}{"b": "0", "c": "1"}, map[string]interface{}{"a": nil}, map[string]interface{}{"c": "1"}},
561				"multiple duplicates": []interface{}{
562					map[string]interface{}{"a": []interface{}{}, "c": "1"},
563					map[string]interface{}{"a": nil, "c": "1"},
564					map[string]interface{}{"a": []interface{}{"a"}, "c": "1"},
565					map[string]interface{}{"a": []interface{}{"a", int64(42)}, "c": "2"},
566					map[string]interface{}{"a": []interface{}{"a", int64(42)}, "c": []interface{}{"3"}},
567					map[string]interface{}{"a": []interface{}{"a"}, "c": "1", "d": "1"},
568					map[string]interface{}{"a": []interface{}{"a"}, "c": "1", "d": "2"},
569				},
570			},
571			schema: &schema.Structural{
572				Generic: schema.Generic{
573					Type: "object",
574				},
575				Properties: map[string]schema.Structural{
576					"strings": {
577						Generic: schema.Generic{
578							Type: "array",
579						},
580						Items: &schema.Structural{
581							Generic: schema.Generic{
582								Type: "string",
583							},
584						},
585						Extensions: schema.Extensions{
586							XListType:    strPtr("map"),
587							XListMapKeys: []string{"a"},
588						},
589					},
590					"integers": {
591						Generic: schema.Generic{
592							Type: "array",
593						},
594						Items: &schema.Structural{
595							Generic: schema.Generic{
596								Type: "integer",
597							},
598						},
599						Extensions: schema.Extensions{
600							XListType:    strPtr("map"),
601							XListMapKeys: []string{"a"},
602						},
603					},
604					"booleans": {
605						Generic: schema.Generic{
606							Type: "array",
607						},
608						Items: &schema.Structural{
609							Generic: schema.Generic{
610								Type: "boolean",
611							},
612						},
613						Extensions: schema.Extensions{
614							XListType:    strPtr("map"),
615							XListMapKeys: []string{"a"},
616						},
617					},
618					"float64": {
619						Generic: schema.Generic{
620							Type: "array",
621						},
622						Items: &schema.Structural{
623							Generic: schema.Generic{
624								Type: "number",
625							},
626						},
627						Extensions: schema.Extensions{
628							XListType:    strPtr("map"),
629							XListMapKeys: []string{"a"},
630						},
631					},
632					"nil": {
633						Generic: schema.Generic{
634							Type: "array",
635						},
636						Items: &schema.Structural{
637							Generic: schema.Generic{
638								Type:     "string",
639								Nullable: true,
640							},
641						},
642						Extensions: schema.Extensions{
643							XListType:    strPtr("map"),
644							XListMapKeys: []string{"a"},
645						},
646					},
647					"array": {
648						Generic: schema.Generic{
649							Type: "array",
650						},
651						Items: &schema.Structural{
652							Generic: schema.Generic{
653								Type: "array",
654							},
655							Items: &schema.Structural{
656								Generic: schema.Generic{
657									Type: "string",
658								},
659							},
660						},
661						Extensions: schema.Extensions{
662							XListType:    strPtr("map"),
663							XListMapKeys: []string{"a"},
664						},
665					},
666					"one key": {
667						Generic: schema.Generic{
668							Type: "array",
669						},
670						Items: &schema.Structural{
671							Generic: schema.Generic{
672								Type: "object",
673								AdditionalProperties: &schema.StructuralOrBool{
674									Structural: &schema.Structural{
675										Generic: schema.Generic{
676											Type: "string",
677										},
678									},
679								},
680							},
681						},
682						Extensions: schema.Extensions{
683							XListType:    strPtr("map"),
684							XListMapKeys: []string{"a"},
685						},
686					},
687					"two keys": {
688						Generic: schema.Generic{
689							Type: "array",
690						},
691						Items: &schema.Structural{
692							Generic: schema.Generic{
693								Type: "object",
694								AdditionalProperties: &schema.StructuralOrBool{
695									Structural: &schema.Structural{
696										Generic: schema.Generic{
697											Type: "string",
698										},
699									},
700								},
701							},
702						},
703						Extensions: schema.Extensions{
704							XListType:    strPtr("map"),
705							XListMapKeys: []string{"a", "b"},
706						},
707					},
708					"undefined key": {
709						Generic: schema.Generic{
710							Type: "array",
711						},
712						Items: &schema.Structural{
713							Generic: schema.Generic{
714								Type: "object",
715								AdditionalProperties: &schema.StructuralOrBool{
716									Structural: &schema.Structural{
717										Generic: schema.Generic{
718											Type: "string",
719										},
720									},
721								},
722							},
723						},
724						Extensions: schema.Extensions{
725							XListType:    strPtr("map"),
726							XListMapKeys: []string{"a", "b"},
727						},
728					},
729					"compound key": {
730						Generic: schema.Generic{
731							Type: "array",
732						},
733						Items: &schema.Structural{
734							Generic: schema.Generic{
735								Type: "object",
736								AdditionalProperties: &schema.StructuralOrBool{
737									Structural: &schema.Structural{
738										Generic: schema.Generic{
739											Type:     "string",
740											Nullable: true,
741										},
742									},
743								},
744							},
745						},
746						Extensions: schema.Extensions{
747							XListType:    strPtr("map"),
748							XListMapKeys: []string{"a"},
749						},
750					},
751					"nil key": {
752						Generic: schema.Generic{
753							Type: "array",
754						},
755						Items: &schema.Structural{
756							Generic: schema.Generic{
757								Type: "object",
758							},
759							Properties: map[string]schema.Structural{
760								"a": {
761									Generic: schema.Generic{
762										Type:     "array",
763										Nullable: true,
764									},
765									Items: &schema.Structural{
766										Generic: schema.Generic{
767											Type: "string",
768										},
769									},
770								},
771								"c": {
772									Generic: schema.Generic{
773										Type: "string",
774									},
775								},
776							},
777						},
778						Extensions: schema.Extensions{
779							XListType:    strPtr("map"),
780							XListMapKeys: []string{"a"},
781						},
782					},
783					"nil item": {
784						Generic: schema.Generic{
785							Type: "array",
786						},
787						Items: &schema.Structural{
788							Generic: schema.Generic{
789								Type: "object",
790							},
791							Properties: map[string]schema.Structural{
792								"a": {
793									Generic: schema.Generic{
794										Type:     "array",
795										Nullable: true,
796									},
797									Items: &schema.Structural{
798										Generic: schema.Generic{
799											Type: "string",
800										},
801									},
802								},
803								"c": {
804									Generic: schema.Generic{
805										Type: "string",
806									},
807								},
808							},
809						},
810						Extensions: schema.Extensions{
811							XListType:    strPtr("map"),
812							XListMapKeys: []string{"a"},
813						},
814					},
815					"nil item multiple keys": {
816						Generic: schema.Generic{
817							Type: "array",
818						},
819						Items: &schema.Structural{
820							Generic: schema.Generic{
821								Type: "object",
822							},
823							Properties: map[string]schema.Structural{
824								"a": {
825									Generic: schema.Generic{
826										Type:     "array",
827										Nullable: true,
828									},
829									Items: &schema.Structural{
830										Generic: schema.Generic{
831											Type: "string",
832										},
833									},
834								},
835								"b": {
836									Generic: schema.Generic{
837										Type: "string",
838									},
839								},
840								"c": {
841									Generic: schema.Generic{
842										Type: "string",
843									},
844								},
845							},
846						},
847						Extensions: schema.Extensions{
848							XListType:    strPtr("map"),
849							XListMapKeys: []string{"a", "b"},
850						},
851					},
852					"multiple duplicates": {
853						Generic: schema.Generic{
854							Type: "array",
855						},
856						Items: &schema.Structural{
857							Generic: schema.Generic{
858								Type: "object",
859								AdditionalProperties: &schema.StructuralOrBool{
860									Structural: &schema.Structural{
861										Generic: schema.Generic{
862											Type:     "string",
863											Nullable: true,
864										},
865									},
866								},
867							},
868						},
869						Extensions: schema.Extensions{
870							XListType:    strPtr("map"),
871							XListMapKeys: []string{"a"},
872						},
873					},
874				},
875			},
876			errors: []validationMatch{
877				invalid("root", "strings[0]"),
878				invalid("root", "integers[0]"),
879				invalid("root", "booleans[0]"),
880				invalid("root", "float64[0]"),
881				invalid("root", "array[0]"),
882				duplicate("root", "one key[2]"),
883				duplicate("root", "two keys[2]"),
884				duplicate("root", "undefined key[2]"),
885				duplicate("root", "compound key[4]"),
886				duplicate("root", "nil key[3]"),
887				duplicate("root", "nil item[3]"),
888				duplicate("root", "nil item multiple keys[3]"),
889				duplicate("root", "multiple duplicates[4]"),
890				duplicate("root", "multiple duplicates[5]"),
891			},
892		},
893	}
894	for _, tt := range tests {
895		t.Run(tt.name, func(t *testing.T) {
896			errs := ValidateListSetsAndMaps(field.NewPath("root"), tt.schema, tt.obj)
897
898			seenErrs := make([]bool, len(errs))
899
900			for _, expectedError := range tt.errors {
901				found := false
902				for i, err := range errs {
903					if expectedError.matches(err) && !seenErrs[i] {
904						found = true
905						seenErrs[i] = true
906						break
907					}
908				}
909
910				if !found {
911					t.Errorf("expected %v at %v, got %v", expectedError.errorType, expectedError.path.String(), errs)
912				}
913			}
914
915			for i, seen := range seenErrs {
916				if !seen {
917					t.Errorf("unexpected error: %v", errs[i])
918				}
919			}
920		})
921	}
922}
923
924type validationMatch struct {
925	path      *field.Path
926	errorType field.ErrorType
927}
928
929func (v validationMatch) matches(err *field.Error) bool {
930	return err.Type == v.errorType && err.Field == v.path.String()
931}
932
933func duplicate(path ...string) validationMatch {
934	return validationMatch{path: field.NewPath(path[0], path[1:]...), errorType: field.ErrorTypeDuplicate}
935}
936func invalid(path ...string) validationMatch {
937	return validationMatch{path: field.NewPath(path[0], path[1:]...), errorType: field.ErrorTypeInvalid}
938}
939
940func strPtr(s string) *string { return &s }
941