1
16
17 package validation
18
19 import (
20 "context"
21 "math/rand"
22 "os"
23 "strconv"
24 "testing"
25 "time"
26
27 "github.com/google/go-cmp/cmp"
28
29 utilpointer "k8s.io/utils/pointer"
30 kjson "sigs.k8s.io/json"
31
32 kubeopenapispec "k8s.io/kube-openapi/pkg/validation/spec"
33
34 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
35 apiextensionsfuzzer "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/fuzzer"
36 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
37 structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
38 "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel"
39 "k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
40 apiequality "k8s.io/apimachinery/pkg/api/equality"
41 "k8s.io/apimachinery/pkg/runtime"
42 "k8s.io/apimachinery/pkg/runtime/serializer"
43 "k8s.io/apimachinery/pkg/util/json"
44 "k8s.io/apimachinery/pkg/util/sets"
45 celconfig "k8s.io/apiserver/pkg/apis/cel"
46 )
47
48
49
50 func TestRoundTrip(t *testing.T) {
51 scheme := runtime.NewScheme()
52 codecs := serializer.NewCodecFactory(scheme)
53
54
55 if err := apiextensions.AddToScheme(scheme); err != nil {
56 t.Fatal(err)
57 }
58 if err := apiextensionsv1.AddToScheme(scheme); err != nil {
59 t.Fatal(err)
60 }
61
62 seed := int64(time.Now().Nanosecond())
63 if override := os.Getenv("TEST_RAND_SEED"); len(override) > 0 {
64 overrideSeed, err := strconv.Atoi(override)
65 if err != nil {
66 t.Fatal(err)
67 }
68 seed = int64(overrideSeed)
69 t.Logf("using overridden seed: %d", seed)
70 } else {
71 t.Logf("seed (override with TEST_RAND_SEED if desired): %d", seed)
72 }
73 fuzzerFuncs := fuzzer.MergeFuzzerFuncs(apiextensionsfuzzer.Funcs)
74 f := fuzzer.FuzzerFor(fuzzerFuncs, rand.NewSource(seed), codecs)
75
76 for i := 0; i < 50; i++ {
77
78 internal := &apiextensions.JSONSchemaProps{}
79 f.Fuzz(internal)
80
81
82 openAPITypes := &kubeopenapispec.Schema{}
83 if err := ConvertJSONSchemaProps(internal, openAPITypes); err != nil {
84 t.Fatal(err)
85 }
86
87
88 openAPIJSON, err := json.Marshal(openAPITypes)
89 if err != nil {
90 t.Fatal(err)
91 }
92
93
94 var j interface{}
95 if strictErrs, err := kjson.UnmarshalStrict(openAPIJSON, &j); err != nil {
96 t.Fatal(err)
97 } else if len(strictErrs) > 0 {
98 t.Fatal(strictErrs)
99 }
100 j = stripIntOrStringType(j)
101 openAPIJSON, err = json.Marshal(j)
102 if err != nil {
103 t.Fatal(err)
104 }
105
106
107 external := &apiextensionsv1.JSONSchemaProps{}
108 if strictErrs, err := kjson.UnmarshalStrict(openAPIJSON, external); err != nil {
109 t.Fatal(err)
110 } else if len(strictErrs) > 0 {
111 t.Fatal(strictErrs)
112 }
113
114
115 internalRoundTripped := &apiextensions.JSONSchemaProps{}
116 if err := scheme.Convert(external, internalRoundTripped, nil); err != nil {
117 t.Fatal(err)
118 }
119
120 if !apiequality.Semantic.DeepEqual(internal, internalRoundTripped) {
121 t.Log(string(openAPIJSON))
122 t.Fatalf("%d: unexpected diff\n\t%s", i, cmp.Diff(internal, internalRoundTripped))
123 }
124 }
125 }
126
127 func stripIntOrStringType(x interface{}) interface{} {
128 switch x := x.(type) {
129 case map[string]interface{}:
130 if t, found := x["type"]; found {
131 switch t := t.(type) {
132 case []interface{}:
133 if len(t) == 2 && t[0] == "integer" && t[1] == "string" && x["x-kubernetes-int-or-string"] == true {
134 delete(x, "type")
135 }
136 }
137 }
138 for k := range x {
139 x[k] = stripIntOrStringType(x[k])
140 }
141 return x
142 case []interface{}:
143 for i := range x {
144 x[i] = stripIntOrStringType(x[i])
145 }
146 return x
147 default:
148 return x
149 }
150 }
151
152 type failingObject struct {
153 object interface{}
154 oldObject interface{}
155 expectErrs []string
156 }
157
158 func TestValidateCustomResource(t *testing.T) {
159 tests := []struct {
160 name string
161 schema apiextensions.JSONSchemaProps
162 objects []interface{}
163 oldObjects []interface{}
164 failingObjects []failingObject
165 }{
166 {name: "!nullable",
167 schema: apiextensions.JSONSchemaProps{
168 Type: "object",
169 Properties: map[string]apiextensions.JSONSchemaProps{
170 "field": {
171 Type: "object",
172 Nullable: false,
173 },
174 },
175 },
176 objects: []interface{}{
177 map[string]interface{}{},
178 map[string]interface{}{"field": map[string]interface{}{}},
179 },
180 failingObjects: []failingObject{
181 {object: map[string]interface{}{"field": "foo"}, expectErrs: []string{`field: Invalid value: "string": field in body must be of type object: "string"`}},
182 {object: map[string]interface{}{"field": 42}, expectErrs: []string{`field: Invalid value: "integer": field in body must be of type object: "integer"`}},
183 {object: map[string]interface{}{"field": true}, expectErrs: []string{`field: Invalid value: "boolean": field in body must be of type object: "boolean"`}},
184 {object: map[string]interface{}{"field": 1.2}, expectErrs: []string{`field: Invalid value: "number": field in body must be of type object: "number"`}},
185 {object: map[string]interface{}{"field": []interface{}{}}, expectErrs: []string{`field: Invalid value: "array": field in body must be of type object: "array"`}},
186 {object: map[string]interface{}{"field": nil}, expectErrs: []string{`field: Invalid value: "null": field in body must be of type object: "null"`}},
187 },
188 },
189 {name: "nullable",
190 schema: apiextensions.JSONSchemaProps{
191 Type: "object",
192 Properties: map[string]apiextensions.JSONSchemaProps{
193 "field": {
194 Type: "object",
195 Nullable: true,
196 },
197 },
198 },
199 objects: []interface{}{
200 map[string]interface{}{},
201 map[string]interface{}{"field": map[string]interface{}{}},
202 map[string]interface{}{"field": nil},
203 },
204 failingObjects: []failingObject{
205 {object: map[string]interface{}{"field": "foo"}, expectErrs: []string{`field: Invalid value: "string": field in body must be of type object: "string"`}},
206 {object: map[string]interface{}{"field": 42}, expectErrs: []string{`field: Invalid value: "integer": field in body must be of type object: "integer"`}},
207 {object: map[string]interface{}{"field": true}, expectErrs: []string{`field: Invalid value: "boolean": field in body must be of type object: "boolean"`}},
208 {object: map[string]interface{}{"field": 1.2}, expectErrs: []string{`field: Invalid value: "number": field in body must be of type object: "number"`}},
209 {object: map[string]interface{}{"field": []interface{}{}}, expectErrs: []string{`field: Invalid value: "array": field in body must be of type object: "array"`}},
210 },
211 },
212 {name: "nullable and no type",
213 schema: apiextensions.JSONSchemaProps{
214 Type: "object",
215 Properties: map[string]apiextensions.JSONSchemaProps{
216 "field": {
217 Nullable: true,
218 },
219 },
220 },
221 objects: []interface{}{
222 map[string]interface{}{},
223 map[string]interface{}{"field": map[string]interface{}{}},
224 map[string]interface{}{"field": nil},
225 map[string]interface{}{"field": "foo"},
226 map[string]interface{}{"field": 42},
227 map[string]interface{}{"field": true},
228 map[string]interface{}{"field": 1.2},
229 map[string]interface{}{"field": []interface{}{}},
230 },
231 },
232 {name: "x-kubernetes-int-or-string",
233 schema: apiextensions.JSONSchemaProps{
234 Type: "object",
235 Properties: map[string]apiextensions.JSONSchemaProps{
236 "field": {
237 XIntOrString: true,
238 },
239 },
240 },
241 objects: []interface{}{
242 map[string]interface{}{},
243 map[string]interface{}{"field": 42},
244 map[string]interface{}{"field": "foo"},
245 },
246 failingObjects: []failingObject{
247 {object: map[string]interface{}{"field": nil}, expectErrs: []string{`field: Invalid value: "null": field in body must be of type integer,string: "null"`}},
248 {object: map[string]interface{}{"field": true}, expectErrs: []string{`field: Invalid value: "boolean": field in body must be of type integer,string: "boolean"`}},
249 {object: map[string]interface{}{"field": 1.2}, expectErrs: []string{`field: Invalid value: "number": field in body must be of type integer,string: "number"`}},
250 {object: map[string]interface{}{"field": map[string]interface{}{}}, expectErrs: []string{`field: Invalid value: "object": field in body must be of type integer,string: "object"`}},
251 {object: map[string]interface{}{"field": []interface{}{}}, expectErrs: []string{`field: Invalid value: "array": field in body must be of type integer,string: "array"`}},
252 },
253 },
254 {name: "nullable and x-kubernetes-int-or-string",
255 schema: apiextensions.JSONSchemaProps{
256 Type: "object",
257 Properties: map[string]apiextensions.JSONSchemaProps{
258 "field": {
259 Nullable: true,
260 XIntOrString: true,
261 },
262 },
263 },
264 objects: []interface{}{
265 map[string]interface{}{},
266 map[string]interface{}{"field": 42},
267 map[string]interface{}{"field": "foo"},
268 map[string]interface{}{"field": nil},
269 },
270 failingObjects: []failingObject{
271 {object: map[string]interface{}{"field": true}, expectErrs: []string{`field: Invalid value: "boolean": field in body must be of type integer,string: "boolean"`}},
272 {object: map[string]interface{}{"field": 1.2}, expectErrs: []string{`field: Invalid value: "number": field in body must be of type integer,string: "number"`}},
273 {object: map[string]interface{}{"field": map[string]interface{}{}}, expectErrs: []string{`field: Invalid value: "object": field in body must be of type integer,string: "object"`}},
274 {object: map[string]interface{}{"field": []interface{}{}}, expectErrs: []string{`field: Invalid value: "array": field in body must be of type integer,string: "array"`}},
275 },
276 },
277 {name: "nullable, x-kubernetes-int-or-string and user-provided anyOf",
278 schema: apiextensions.JSONSchemaProps{
279 Type: "object",
280 Properties: map[string]apiextensions.JSONSchemaProps{
281 "field": {
282 Nullable: true,
283 XIntOrString: true,
284 AnyOf: []apiextensions.JSONSchemaProps{
285 {Type: "integer"},
286 {Type: "string"},
287 },
288 },
289 },
290 },
291 objects: []interface{}{
292 map[string]interface{}{},
293 map[string]interface{}{"field": nil},
294 map[string]interface{}{"field": 42},
295 map[string]interface{}{"field": "foo"},
296 },
297 failingObjects: []failingObject{
298 {object: map[string]interface{}{"field": true}, expectErrs: []string{
299 `<nil>: Invalid value: "": "field" must validate at least one schema (anyOf)`,
300 `field: Invalid value: "boolean": field in body must be of type integer,string: "boolean"`,
301 `field: Invalid value: "boolean": field in body must be of type integer: "boolean"`,
302 }},
303 {object: map[string]interface{}{"field": 1.2}, expectErrs: []string{
304 `<nil>: Invalid value: "": "field" must validate at least one schema (anyOf)`,
305 `field: Invalid value: "number": field in body must be of type integer,string: "number"`,
306 `field: Invalid value: "number": field in body must be of type integer: "number"`,
307 }},
308 {object: map[string]interface{}{"field": map[string]interface{}{}}, expectErrs: []string{
309 `<nil>: Invalid value: "": "field" must validate at least one schema (anyOf)`,
310 `field: Invalid value: "object": field in body must be of type integer,string: "object"`,
311 `field: Invalid value: "object": field in body must be of type integer: "object"`,
312 }},
313 {object: map[string]interface{}{"field": []interface{}{}}, expectErrs: []string{
314 `<nil>: Invalid value: "": "field" must validate at least one schema (anyOf)`,
315 `field: Invalid value: "array": field in body must be of type integer,string: "array"`,
316 `field: Invalid value: "array": field in body must be of type integer: "array"`,
317 }},
318 },
319 },
320 {name: "nullable, x-kubernetes-int-or-string and user-provider allOf",
321 schema: apiextensions.JSONSchemaProps{
322 Type: "object",
323 Properties: map[string]apiextensions.JSONSchemaProps{
324 "field": {
325 Nullable: true,
326 XIntOrString: true,
327 AllOf: []apiextensions.JSONSchemaProps{
328 {
329 AnyOf: []apiextensions.JSONSchemaProps{
330 {Type: "integer"},
331 {Type: "string"},
332 },
333 },
334 },
335 },
336 },
337 },
338 objects: []interface{}{
339 map[string]interface{}{},
340 map[string]interface{}{"field": nil},
341 map[string]interface{}{"field": 42},
342 map[string]interface{}{"field": "foo"},
343 },
344 failingObjects: []failingObject{
345 {object: map[string]interface{}{"field": true}, expectErrs: []string{
346 `<nil>: Invalid value: "": "field" must validate all the schemas (allOf). None validated`,
347 `<nil>: Invalid value: "": "field" must validate at least one schema (anyOf)`,
348 `field: Invalid value: "boolean": field in body must be of type integer,string: "boolean"`,
349 `field: Invalid value: "boolean": field in body must be of type integer: "boolean"`,
350 }},
351 {object: map[string]interface{}{"field": 1.2}, expectErrs: []string{
352 `<nil>: Invalid value: "": "field" must validate all the schemas (allOf). None validated`,
353 `<nil>: Invalid value: "": "field" must validate at least one schema (anyOf)`,
354 `field: Invalid value: "number": field in body must be of type integer,string: "number"`,
355 `field: Invalid value: "number": field in body must be of type integer: "number"`,
356 }},
357 {object: map[string]interface{}{"field": map[string]interface{}{}}, expectErrs: []string{
358 `<nil>: Invalid value: "": "field" must validate all the schemas (allOf). None validated`,
359 `<nil>: Invalid value: "": "field" must validate at least one schema (anyOf)`,
360 `field: Invalid value: "object": field in body must be of type integer,string: "object"`,
361 `field: Invalid value: "object": field in body must be of type integer: "object"`,
362 }},
363 {object: map[string]interface{}{"field": []interface{}{}}, expectErrs: []string{
364 `<nil>: Invalid value: "": "field" must validate all the schemas (allOf). None validated`,
365 `<nil>: Invalid value: "": "field" must validate at least one schema (anyOf)`,
366 `field: Invalid value: "array": field in body must be of type integer,string: "array"`,
367 `field: Invalid value: "array": field in body must be of type integer: "array"`,
368 }},
369 },
370 },
371 {name: "invalid regex",
372 schema: apiextensions.JSONSchemaProps{
373 Type: "object",
374 Properties: map[string]apiextensions.JSONSchemaProps{
375 "field": {
376 Type: "string",
377 Pattern: "+",
378 },
379 },
380 },
381 failingObjects: []failingObject{
382 {object: map[string]interface{}{"field": "foo"}, expectErrs: []string{"field: Invalid value: \"foo\": field in body should match '+, but pattern is invalid: error parsing regexp: missing argument to repetition operator: `+`'"}},
383 },
384 },
385 {name: "required field",
386 schema: apiextensions.JSONSchemaProps{
387 Type: "object",
388 Required: []string{"field"},
389 Properties: map[string]apiextensions.JSONSchemaProps{
390 "field": {
391 Type: "object",
392 Required: []string{"nested"},
393 Properties: map[string]apiextensions.JSONSchemaProps{
394 "nested": {},
395 },
396 },
397 },
398 },
399 failingObjects: []failingObject{
400 {object: map[string]interface{}{"test": "a"}, expectErrs: []string{`field: Required value`}},
401 {object: map[string]interface{}{"field": map[string]interface{}{}}, expectErrs: []string{`field.nested: Required value`}},
402 },
403 },
404 {name: "enum",
405 schema: apiextensions.JSONSchemaProps{
406 Type: "object",
407 Properties: map[string]apiextensions.JSONSchemaProps{
408 "field": {
409 Type: "object",
410 Required: []string{"nestedint", "nestedstring"},
411 Properties: map[string]apiextensions.JSONSchemaProps{
412 "nestedint": {
413 Type: "integer",
414 Enum: []apiextensions.JSON{1, 2},
415 },
416 "nestedstring": {
417 Type: "string",
418 Enum: []apiextensions.JSON{"a", "b"},
419 },
420 },
421 },
422 },
423 },
424 failingObjects: []failingObject{
425 {object: map[string]interface{}{"field": map[string]interface{}{}}, expectErrs: []string{
426 `field.nestedint: Required value`,
427 `field.nestedstring: Required value`,
428 }},
429 {object: map[string]interface{}{"field": map[string]interface{}{"nestedint": "x", "nestedstring": true}}, expectErrs: []string{
430 `field.nestedint: Invalid value: "string": field.nestedint in body must be of type integer: "string"`,
431 `field.nestedint: Unsupported value: "x": supported values: "1", "2"`,
432 `field.nestedstring: Invalid value: "boolean": field.nestedstring in body must be of type string: "boolean"`,
433 `field.nestedstring: Unsupported value: true: supported values: "a", "b"`,
434 }},
435 },
436 },
437 {name: "immutability transition rule",
438 schema: apiextensions.JSONSchemaProps{
439 Type: "object",
440 Properties: map[string]apiextensions.JSONSchemaProps{
441 "field": {
442 Type: "string",
443 XValidations: []apiextensions.ValidationRule{
444 {
445 Rule: "self == oldSelf",
446 },
447 },
448 },
449 },
450 },
451 objects: []interface{}{
452 map[string]interface{}{"field": "x"},
453 },
454 oldObjects: []interface{}{
455 map[string]interface{}{"field": "x"},
456 },
457 failingObjects: []failingObject{
458 {
459 object: map[string]interface{}{"field": "y"},
460 oldObject: map[string]interface{}{"field": "x"},
461 expectErrs: []string{
462 `field: Invalid value: "string": failed rule: self == oldSelf`,
463 }},
464 },
465 },
466 {name: "correlatable transition rule",
467
468 schema: apiextensions.JSONSchemaProps{
469 Type: "object",
470 Properties: map[string]apiextensions.JSONSchemaProps{
471 "field": {
472 Type: "array",
473 XListType: &listMapType,
474 XListMapKeys: []string{"k1", "k2"},
475 Items: &apiextensions.JSONSchemaPropsOrArray{
476 Schema: &apiextensions.JSONSchemaProps{
477 Type: "object",
478 Properties: map[string]apiextensions.JSONSchemaProps{
479 "k1": {
480 Type: "string",
481 },
482 "k2": {
483 Type: "string",
484 },
485 "v1": {
486 Type: "number",
487 XValidations: []apiextensions.ValidationRule{
488 {
489 Rule: "self >= oldSelf",
490 },
491 },
492 },
493 },
494 },
495 },
496 },
497 },
498 },
499 objects: []interface{}{
500 map[string]interface{}{"field": []interface{}{map[string]interface{}{"k1": "a", "k2": "b", "v1": 1.2}}},
501 },
502 oldObjects: []interface{}{
503 map[string]interface{}{"field": []interface{}{map[string]interface{}{"k1": "a", "k2": "b", "v1": 1.0}}},
504 },
505 failingObjects: []failingObject{
506 {
507 object: map[string]interface{}{"field": []interface{}{map[string]interface{}{"k1": "a", "k2": "b", "v1": 0.9}}},
508 oldObject: map[string]interface{}{"field": []interface{}{map[string]interface{}{"k1": "a", "k2": "b", "v1": 1.0}}},
509 expectErrs: []string{
510 `field[0].v1: Invalid value: "number": failed rule: self >= oldSelf`,
511 }},
512 },
513 },
514 {name: "validation rule under non-correlatable field",
515
516
517
518
519 schema: apiextensions.JSONSchemaProps{
520 Type: "object",
521 Properties: map[string]apiextensions.JSONSchemaProps{
522 "field": {
523 Type: "array",
524 Items: &apiextensions.JSONSchemaPropsOrArray{
525 Schema: &apiextensions.JSONSchemaProps{
526 Type: "object",
527 Properties: map[string]apiextensions.JSONSchemaProps{
528 "x": {
529 Type: "string",
530 XValidations: []apiextensions.ValidationRule{
531 {
532 Rule: "self == 'x'",
533 },
534 },
535 },
536 },
537 },
538 },
539 },
540 },
541 },
542 objects: []interface{}{
543 map[string]interface{}{"field": []interface{}{map[string]interface{}{"x": "x"}}},
544 },
545 failingObjects: []failingObject{
546 {
547 object: map[string]interface{}{"field": []interface{}{map[string]interface{}{"x": "y"}}},
548 expectErrs: []string{
549 `field[0].x: Invalid value: "string": failed rule: self == 'x'`,
550 }},
551 },
552 },
553 {name: "maxProperties",
554 schema: apiextensions.JSONSchemaProps{
555 Type: "object",
556 Properties: map[string]apiextensions.JSONSchemaProps{
557 "fieldX": {
558 Type: "object",
559 MaxProperties: utilpointer.Int64(2),
560 },
561 },
562 },
563 failingObjects: []failingObject{
564 {object: map[string]interface{}{"fieldX": map[string]interface{}{"a": true, "b": true, "c": true}}, expectErrs: []string{
565 `fieldX: Too many: 3: must have at most 2 items`,
566 }},
567 },
568 },
569 {name: "maxItems",
570 schema: apiextensions.JSONSchemaProps{
571 Type: "object",
572 Properties: map[string]apiextensions.JSONSchemaProps{
573 "fieldX": {
574 Type: "array",
575 MaxItems: utilpointer.Int64(2),
576 },
577 },
578 },
579 failingObjects: []failingObject{
580 {object: map[string]interface{}{"fieldX": []interface{}{"a", "b", "c"}}, expectErrs: []string{
581 `fieldX: Too many: 3: must have at most 2 items`,
582 }},
583 },
584 },
585 {name: "maxLength",
586 schema: apiextensions.JSONSchemaProps{
587 Type: "object",
588 Properties: map[string]apiextensions.JSONSchemaProps{
589 "fieldX": {
590 Type: "string",
591 MaxLength: utilpointer.Int64(2),
592 },
593 },
594 },
595 failingObjects: []failingObject{
596 {object: map[string]interface{}{"fieldX": "abc"}, expectErrs: []string{
597 `fieldX: Too long: may not be longer than 2`,
598 }},
599 },
600 },
601 }
602 for _, tt := range tests {
603 t.Run(tt.name, func(t *testing.T) {
604 validator, _, err := NewSchemaValidator(&tt.schema)
605 if err != nil {
606 t.Fatal(err)
607 }
608 structural, err := structuralschema.NewStructural(&tt.schema)
609 if err != nil {
610 t.Fatal(err)
611 }
612 celValidator := cel.NewValidator(structural, false, celconfig.PerCallLimit)
613 for i, obj := range tt.objects {
614 var oldObject interface{}
615 if len(tt.oldObjects) == len(tt.objects) {
616 oldObject = tt.oldObjects[i]
617 }
618 if errs := ValidateCustomResource(nil, obj, validator); len(errs) > 0 {
619 t.Errorf("unexpected validation error for %v: %v", obj, errs)
620 }
621 errs, _ := celValidator.Validate(context.TODO(), nil, structural, obj, oldObject, celconfig.RuntimeCELCostBudget)
622 if len(errs) > 0 {
623 t.Errorf(errs.ToAggregate().Error())
624 }
625 }
626 for i, failingObject := range tt.failingObjects {
627 errs := ValidateCustomResource(nil, failingObject.object, validator)
628 celErrs, _ := celValidator.Validate(context.TODO(), nil, structural, failingObject.object, failingObject.oldObject, celconfig.RuntimeCELCostBudget)
629 errs = append(errs, celErrs...)
630 if len(errs) == 0 {
631 t.Errorf("missing error for %v", failingObject.object)
632 } else {
633 sawErrors := sets.NewString()
634 for _, err := range errs {
635 sawErrors.Insert(err.Error())
636 }
637 expectErrs := sets.NewString(failingObject.expectErrs...)
638 for _, unexpectedError := range sawErrors.Difference(expectErrs).List() {
639 t.Errorf("%d: unexpected error: %s", i, unexpectedError)
640 }
641 for _, missingError := range expectErrs.Difference(sawErrors).List() {
642 t.Errorf("%d: missing error: %s", i, missingError)
643 }
644 }
645 }
646 })
647 }
648 }
649
650 func TestItemsProperty(t *testing.T) {
651 type args struct {
652 schema apiextensions.JSONSchemaProps
653 object interface{}
654 }
655 tests := []struct {
656 name string
657 args args
658 wantErr bool
659 }{
660 {"items in object", args{
661 apiextensions.JSONSchemaProps{
662 Properties: map[string]apiextensions.JSONSchemaProps{
663 "spec": {
664 Properties: map[string]apiextensions.JSONSchemaProps{
665 "replicas": {
666 Type: "integer",
667 },
668 },
669 },
670 },
671 },
672 map[string]interface{}{"spec": map[string]interface{}{"replicas": 1, "items": []string{"1", "2"}}},
673 }, false},
674 {"items in array", args{
675 apiextensions.JSONSchemaProps{
676 Properties: map[string]apiextensions.JSONSchemaProps{
677 "secrets": {
678 Type: "array",
679 Items: &apiextensions.JSONSchemaPropsOrArray{
680 Schema: &apiextensions.JSONSchemaProps{
681 Type: "string",
682 },
683 },
684 },
685 },
686 },
687 map[string]interface{}{"secrets": []string{"1", "2"}},
688 }, false},
689 }
690 for _, tt := range tests {
691 t.Run(tt.name, func(t *testing.T) {
692 validator, _, err := NewSchemaValidator(&tt.args.schema)
693 if err != nil {
694 t.Fatal(err)
695 }
696 if errs := ValidateCustomResource(nil, tt.args.object, validator); (len(errs) > 0) != tt.wantErr {
697 if len(errs) == 0 {
698 t.Error("expected error, but didn't get one")
699 } else {
700 t.Errorf("unexpected validation error: %v", errs)
701 }
702 }
703 })
704 }
705 }
706
707 var listMapType = "map"
708
View as plain text