k8s.io/apiserver@v0.31.1/pkg/cel/openapi/values_test.go (about) 1 /* 2 Copyright 2021 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package openapi 18 19 import ( 20 "reflect" 21 "testing" 22 23 "github.com/google/cel-go/common/types" 24 "github.com/google/cel-go/common/types/ref" 25 "github.com/google/cel-go/common/types/traits" 26 27 "k8s.io/kube-openapi/pkg/validation/spec" 28 ) 29 30 var ( 31 listTypeSet = "set" 32 listTypeMap = "map" 33 stringSchema = spec.StringProperty() 34 intSchema = spec.Int64Property() 35 36 mapListElementSchema = &spec.Schema{ 37 SchemaProps: spec.SchemaProps{ 38 Type: []string{"object"}, 39 Properties: map[string]spec.Schema{ 40 "key": *stringSchema, 41 "val": *intSchema, 42 }, 43 }} 44 mapListSchema = &spec.Schema{ 45 SchemaProps: spec.SchemaProps{ 46 Type: []string{"array"}, 47 Items: &spec.SchemaOrArray{Schema: mapListElementSchema}, 48 }, 49 VendorExtensible: spec.VendorExtensible{ 50 Extensions: map[string]interface{}{ 51 extListType: listTypeMap, 52 extListMapKeys: []any{"key"}, 53 }}, 54 } 55 multiKeyMapListSchema = &spec.Schema{ 56 SchemaProps: spec.SchemaProps{ 57 Type: []string{"array"}, 58 Items: &spec.SchemaOrArray{Schema: &spec.Schema{ 59 SchemaProps: spec.SchemaProps{ 60 Type: []string{"object"}, 61 Properties: map[string]spec.Schema{ 62 "key1": *stringSchema, 63 "key2": *stringSchema, 64 "val": *intSchema, 65 }, 66 }}}}, 67 VendorExtensible: spec.VendorExtensible{ 68 Extensions: map[string]interface{}{ 69 extListType: listTypeMap, 70 extListMapKeys: []any{"key1", "key2"}, 71 }}, 72 } 73 setListSchema = &spec.Schema{ 74 SchemaProps: spec.SchemaProps{ 75 Type: []string{"array"}, 76 Items: &spec.SchemaOrArray{Schema: stringSchema}}, 77 VendorExtensible: spec.VendorExtensible{ 78 Extensions: map[string]interface{}{ 79 extListType: listTypeSet, 80 }}, 81 } 82 atomicListSchema = &spec.Schema{ 83 SchemaProps: spec.SchemaProps{ 84 Type: []string{"array"}, 85 Items: &spec.SchemaOrArray{Schema: stringSchema}, 86 }} 87 objectSchema = &spec.Schema{ 88 SchemaProps: spec.SchemaProps{ 89 Type: []string{"object"}, 90 Properties: map[string]spec.Schema{ 91 "field1": *stringSchema, 92 "field2": *stringSchema, 93 }, 94 }} 95 mapSchema = &spec.Schema{ 96 SchemaProps: spec.SchemaProps{ 97 Type: []string{"object"}, 98 AdditionalProperties: &spec.SchemaOrBool{Schema: stringSchema}, 99 }} 100 emptyObjectSchema = &spec.Schema{ 101 SchemaProps: spec.SchemaProps{Type: []string{"object"}}, 102 } 103 ) 104 105 func TestEquality(t *testing.T) { 106 cases := []struct { 107 name string 108 lhs ref.Val 109 rhs ref.Val 110 equal bool 111 }{ 112 { 113 name: "map lists are equal regardless of order", 114 lhs: UnstructuredToVal([]interface{}{ 115 map[string]interface{}{ 116 "key": "a", 117 "val": 1, 118 }, 119 map[string]interface{}{ 120 "key": "b", 121 "val": 2, 122 }, 123 }, mapListSchema), 124 rhs: UnstructuredToVal([]interface{}{ 125 map[string]interface{}{ 126 "key": "b", 127 "val": 2, 128 }, 129 map[string]interface{}{ 130 "key": "a", 131 "val": 1, 132 }, 133 }, mapListSchema), 134 equal: true, 135 }, 136 { 137 name: "map lists are not equal if contents differs", 138 lhs: UnstructuredToVal([]interface{}{ 139 map[string]interface{}{ 140 "key": "a", 141 "val": 1, 142 }, 143 map[string]interface{}{ 144 "key": "b", 145 "val": 2, 146 }, 147 }, mapListSchema), 148 rhs: UnstructuredToVal([]interface{}{ 149 map[string]interface{}{ 150 "key": "a", 151 "val": 1, 152 }, 153 map[string]interface{}{ 154 "key": "b", 155 "val": 3, 156 }, 157 }, mapListSchema), 158 equal: false, 159 }, 160 { 161 name: "map lists are not equal if length differs", 162 lhs: UnstructuredToVal([]interface{}{ 163 map[string]interface{}{ 164 "key": "a", 165 "val": 1, 166 }, 167 map[string]interface{}{ 168 "key": "b", 169 "val": 2, 170 }, 171 }, mapListSchema), 172 rhs: UnstructuredToVal([]interface{}{ 173 map[string]interface{}{ 174 "key": "a", 175 "val": 1, 176 }, 177 map[string]interface{}{ 178 "key": "b", 179 "val": 2, 180 }, 181 map[string]interface{}{ 182 "key": "c", 183 "val": 3, 184 }, 185 }, mapListSchema), 186 equal: false, 187 }, 188 { 189 name: "multi-key map lists are equal regardless of order", 190 lhs: UnstructuredToVal([]interface{}{ 191 map[string]interface{}{ 192 "key1": "a1", 193 "key2": "a2", 194 "val": 1, 195 }, 196 map[string]interface{}{ 197 "key1": "b1", 198 "key2": "b2", 199 "val": 2, 200 }, 201 }, multiKeyMapListSchema), 202 rhs: UnstructuredToVal([]interface{}{ 203 map[string]interface{}{ 204 "key1": "b1", 205 "key2": "b2", 206 "val": 2, 207 }, 208 map[string]interface{}{ 209 "key1": "a1", 210 "key2": "a2", 211 "val": 1, 212 }, 213 }, multiKeyMapListSchema), 214 equal: true, 215 }, 216 { 217 name: "multi-key map lists with different contents are not equal", 218 lhs: UnstructuredToVal([]interface{}{ 219 map[string]interface{}{ 220 "key1": "a1", 221 "key2": "a2", 222 "val": 1, 223 }, 224 map[string]interface{}{ 225 "key1": "b1", 226 "key2": "b2", 227 "val": 2, 228 }, 229 }, multiKeyMapListSchema), 230 rhs: UnstructuredToVal([]interface{}{ 231 map[string]interface{}{ 232 "key1": "a1", 233 "key2": "a2", 234 "val": 1, 235 }, 236 map[string]interface{}{ 237 "key1": "b1", 238 "key2": "b2", 239 "val": 3, 240 }, 241 }, multiKeyMapListSchema), 242 equal: false, 243 }, 244 { 245 name: "multi-key map lists with different keys are not equal", 246 lhs: UnstructuredToVal([]interface{}{ 247 map[string]interface{}{ 248 "key1": "a1", 249 "key2": "a2", 250 "val": 1, 251 }, 252 map[string]interface{}{ 253 "key1": "b1", 254 "key2": "b2", 255 "val": 2, 256 }, 257 }, multiKeyMapListSchema), 258 rhs: UnstructuredToVal([]interface{}{ 259 map[string]interface{}{ 260 "key1": "a1", 261 "key2": "a2", 262 "val": 1, 263 }, 264 map[string]interface{}{ 265 "key1": "c1", 266 "key2": "c2", 267 "val": 3, 268 }, 269 }, multiKeyMapListSchema), 270 equal: false, 271 }, 272 { 273 name: "multi-key map lists with different lengths are not equal", 274 lhs: UnstructuredToVal([]interface{}{ 275 map[string]interface{}{ 276 "key1": "a1", 277 "key2": "a2", 278 "val": 1, 279 }, 280 }, multiKeyMapListSchema), 281 rhs: UnstructuredToVal([]interface{}{ 282 map[string]interface{}{ 283 "key1": "a1", 284 "key2": "a2", 285 "val": 1, 286 }, 287 map[string]interface{}{ 288 "key1": "b1", 289 "key2": "b2", 290 "val": 3, 291 }, 292 }, multiKeyMapListSchema), 293 equal: false, 294 }, 295 { 296 name: "set lists are equal regardless of order", 297 lhs: UnstructuredToVal([]interface{}{"a", "b"}, setListSchema), 298 rhs: UnstructuredToVal([]interface{}{"b", "a"}, setListSchema), 299 equal: true, 300 }, 301 { 302 name: "set lists are not equal if contents differ", 303 lhs: UnstructuredToVal([]interface{}{"a", "b"}, setListSchema), 304 rhs: UnstructuredToVal([]interface{}{"a", "c"}, setListSchema), 305 equal: false, 306 }, 307 { 308 name: "set lists are not equal if lengths differ", 309 lhs: UnstructuredToVal([]interface{}{"a", "b"}, setListSchema), 310 rhs: UnstructuredToVal([]interface{}{"a", "b", "c"}, setListSchema), 311 equal: false, 312 }, 313 { 314 name: "identical atomic lists are equal", 315 lhs: UnstructuredToVal([]interface{}{"a", "b"}, atomicListSchema), 316 rhs: UnstructuredToVal([]interface{}{"a", "b"}, atomicListSchema), 317 equal: true, 318 }, 319 { 320 name: "atomic lists are not equal if order differs", 321 lhs: UnstructuredToVal([]interface{}{"a", "b"}, atomicListSchema), 322 rhs: UnstructuredToVal([]interface{}{"b", "a"}, atomicListSchema), 323 equal: false, 324 }, 325 { 326 name: "atomic lists are not equal if contents differ", 327 lhs: UnstructuredToVal([]interface{}{"a", "b"}, atomicListSchema), 328 rhs: UnstructuredToVal([]interface{}{"a", "c"}, atomicListSchema), 329 equal: false, 330 }, 331 { 332 name: "atomic lists are not equal if lengths differ", 333 lhs: UnstructuredToVal([]interface{}{"a", "b"}, atomicListSchema), 334 rhs: UnstructuredToVal([]interface{}{"a", "b", "c"}, atomicListSchema), 335 equal: false, 336 }, 337 { 338 name: "empty objects are equal", 339 lhs: UnstructuredToVal(map[string]interface{}{}, emptyObjectSchema), 340 rhs: UnstructuredToVal(map[string]interface{}{}, emptyObjectSchema), 341 equal: true, 342 }, 343 { 344 name: "identical objects are equal", 345 lhs: UnstructuredToVal(map[string]interface{}{"field1": "a", "field2": "b"}, objectSchema), 346 rhs: UnstructuredToVal(map[string]interface{}{"field1": "a", "field2": "b"}, objectSchema), 347 equal: true, 348 }, 349 { 350 name: "objects are equal regardless of field order", 351 lhs: UnstructuredToVal(map[string]interface{}{"field1": "a", "field2": "b"}, objectSchema), 352 rhs: UnstructuredToVal(map[string]interface{}{"field2": "b", "field1": "a"}, objectSchema), 353 equal: true, 354 }, 355 { 356 name: "objects are not equal if contents differs", 357 lhs: UnstructuredToVal(map[string]interface{}{"field1": "a", "field2": "b"}, objectSchema), 358 rhs: UnstructuredToVal(map[string]interface{}{"field1": "a", "field2": "c"}, objectSchema), 359 equal: false, 360 }, 361 { 362 name: "objects are not equal if length differs", 363 lhs: UnstructuredToVal(map[string]interface{}{"field1": "a", "field2": "b"}, objectSchema), 364 rhs: UnstructuredToVal(map[string]interface{}{"field1": "a"}, objectSchema), 365 equal: false, 366 }, 367 { 368 name: "identical maps are equal", 369 lhs: UnstructuredToVal(map[string]interface{}{"key1": "a", "key2": "b"}, mapSchema), 370 rhs: UnstructuredToVal(map[string]interface{}{"key1": "a", "key2": "b"}, mapSchema), 371 equal: true, 372 }, 373 { 374 name: "maps are equal regardless of field order", 375 lhs: UnstructuredToVal(map[string]interface{}{"key1": "a", "key2": "b"}, mapSchema), 376 rhs: UnstructuredToVal(map[string]interface{}{"key2": "b", "key1": "a"}, mapSchema), 377 equal: true, 378 }, 379 { 380 name: "maps are not equal if contents differs", 381 lhs: UnstructuredToVal(map[string]interface{}{"key1": "a", "key2": "b"}, mapSchema), 382 rhs: UnstructuredToVal(map[string]interface{}{"key1": "a", "key2": "c"}, mapSchema), 383 equal: false, 384 }, 385 { 386 name: "maps are not equal if length differs", 387 lhs: UnstructuredToVal(map[string]interface{}{"key1": "a", "key2": "b"}, mapSchema), 388 rhs: UnstructuredToVal(map[string]interface{}{"key1": "a", "key2": "b", "key3": "c"}, mapSchema), 389 equal: false, 390 }, 391 } 392 393 for _, tc := range cases { 394 t.Run(tc.name, func(t *testing.T) { 395 // Compare types with schema against themselves 396 if tc.lhs.Equal(tc.rhs) != types.Bool(tc.equal) { 397 t.Errorf("expected Equals to return %v", tc.equal) 398 } 399 if tc.rhs.Equal(tc.lhs) != types.Bool(tc.equal) { 400 t.Errorf("expected Equals to return %v", tc.equal) 401 } 402 403 // Compare types with schema against native types. This is slightly different than how 404 // CEL performs equality against data literals, but is a good sanity check. 405 if tc.lhs.Equal(types.DefaultTypeAdapter.NativeToValue(tc.rhs.Value())) != types.Bool(tc.equal) { 406 t.Errorf("expected unstructuredVal.Equals(<native type>) to return %v", tc.equal) 407 } 408 if tc.rhs.Equal(types.DefaultTypeAdapter.NativeToValue(tc.lhs.Value())) != types.Bool(tc.equal) { 409 t.Errorf("expected unstructuredVal.Equals(<native type>) to return %v", tc.equal) 410 } 411 }) 412 } 413 } 414 415 func TestLister(t *testing.T) { 416 cases := []struct { 417 name string 418 unstructured []interface{} 419 schema *spec.Schema 420 itemSchema *spec.Schema 421 size int64 422 notContains []ref.Val 423 addition []interface{} 424 expectAdded []interface{} 425 }{ 426 { 427 name: "map list", 428 unstructured: []interface{}{ 429 map[string]interface{}{ 430 "key": "a", 431 "val": 1, 432 }, 433 map[string]interface{}{ 434 "key": "b", 435 "val": 2, 436 }, 437 }, 438 schema: mapListSchema, 439 itemSchema: mapListElementSchema, 440 size: 2, 441 notContains: []ref.Val{ 442 UnstructuredToVal(map[string]interface{}{ 443 "key": "a", 444 "val": 2, 445 }, mapListElementSchema), 446 UnstructuredToVal(map[string]interface{}{ 447 "key": "c", 448 "val": 1, 449 }, mapListElementSchema), 450 }, 451 addition: []interface{}{ 452 map[string]interface{}{ 453 "key": "b", 454 "val": 3, 455 }, 456 map[string]interface{}{ 457 "key": "c", 458 "val": 4, 459 }, 460 }, 461 expectAdded: []interface{}{ 462 map[string]interface{}{ 463 "key": "a", 464 "val": 1, 465 }, 466 map[string]interface{}{ 467 "key": "b", 468 "val": 3, 469 }, 470 map[string]interface{}{ 471 "key": "c", 472 "val": 4, 473 }, 474 }, 475 }, 476 { 477 name: "set list", 478 unstructured: []interface{}{"a", "b"}, 479 schema: setListSchema, 480 itemSchema: stringSchema, 481 size: 2, 482 notContains: []ref.Val{UnstructuredToVal("c", stringSchema)}, 483 addition: []interface{}{"b", "c"}, 484 expectAdded: []interface{}{"a", "b", "c"}, 485 }, 486 { 487 name: "atomic list", 488 unstructured: []interface{}{"a", "b"}, 489 schema: atomicListSchema, 490 itemSchema: stringSchema, 491 size: 2, 492 notContains: []ref.Val{UnstructuredToVal("c", stringSchema)}, 493 addition: []interface{}{"b", "c"}, 494 expectAdded: []interface{}{"a", "b", "b", "c"}, 495 }, 496 } 497 498 for _, tc := range cases { 499 t.Run(tc.name, func(t *testing.T) { 500 lister := UnstructuredToVal(tc.unstructured, tc.schema).(traits.Lister) 501 if lister.Size().Value() != tc.size { 502 t.Errorf("Expected Size to return %d but got %d", tc.size, lister.Size().Value()) 503 } 504 iter := lister.Iterator() 505 for i := 0; i < int(tc.size); i++ { 506 get := lister.Get(types.Int(i)).Value() 507 if !reflect.DeepEqual(get, tc.unstructured[i]) { 508 t.Errorf("Expected Get to return %v for index %d but got %v", tc.unstructured[i], i, get) 509 } 510 if iter.HasNext() != types.True { 511 t.Error("Expected HasNext to return true") 512 } 513 next := iter.Next().Value() 514 if !reflect.DeepEqual(next, tc.unstructured[i]) { 515 t.Errorf("Expected Next to return %v for index %d but got %v", tc.unstructured[i], i, next) 516 } 517 } 518 if iter.HasNext() != types.False { 519 t.Error("Expected HasNext to return false") 520 } 521 for _, contains := range tc.unstructured { 522 if lister.Contains(UnstructuredToVal(contains, tc.itemSchema)) != types.True { 523 t.Errorf("Expected Contains to return true for %v", contains) 524 } 525 } 526 for _, notContains := range tc.notContains { 527 if lister.Contains(notContains) != types.False { 528 t.Errorf("Expected Contains to return false for %v", notContains) 529 } 530 } 531 532 addition := UnstructuredToVal(tc.addition, tc.schema).(traits.Lister) 533 added := lister.Add(addition).Value() 534 if !reflect.DeepEqual(added, tc.expectAdded) { 535 t.Errorf("Expected Add to return %v but got %v", tc.expectAdded, added) 536 } 537 }) 538 } 539 } 540 541 func TestMapper(t *testing.T) { 542 cases := []struct { 543 name string 544 unstructured map[string]interface{} 545 schema *spec.Schema 546 propertySchema func(key string) (*spec.Schema, bool) 547 size int64 548 notContains []ref.Val 549 }{ 550 { 551 name: "object", 552 unstructured: map[string]interface{}{ 553 "field1": "a", 554 "field2": "b", 555 }, 556 schema: objectSchema, 557 propertySchema: func(key string) (*spec.Schema, bool) { 558 if s, ok := objectSchema.Properties[key]; ok { 559 return &s, true 560 } 561 return nil, false 562 }, 563 size: 2, 564 notContains: []ref.Val{ 565 UnstructuredToVal("field3", stringSchema), 566 }, 567 }, 568 { 569 name: "map", 570 unstructured: map[string]interface{}{ 571 "key1": "a", 572 "key2": "b", 573 }, 574 schema: mapSchema, 575 propertySchema: func(key string) (*spec.Schema, bool) { return mapSchema.AdditionalProperties.Schema, true }, 576 size: 2, 577 notContains: []ref.Val{ 578 UnstructuredToVal("key3", stringSchema), 579 }, 580 }, 581 } 582 583 for _, tc := range cases { 584 t.Run(tc.name, func(t *testing.T) { 585 mapper := UnstructuredToVal(tc.unstructured, tc.schema).(traits.Mapper) 586 if mapper.Size().Value() != tc.size { 587 t.Errorf("Expected Size to return %d but got %d", tc.size, mapper.Size().Value()) 588 } 589 iter := mapper.Iterator() 590 iterResults := map[interface{}]struct{}{} 591 keys := map[interface{}]struct{}{} 592 for k := range tc.unstructured { 593 keys[k] = struct{}{} 594 get := mapper.Get(types.String(k)).Value() 595 if !reflect.DeepEqual(get, tc.unstructured[k]) { 596 t.Errorf("Expected Get to return %v for key %s but got %v", tc.unstructured[k], k, get) 597 } 598 if iter.HasNext() != types.True { 599 t.Error("Expected HasNext to return true") 600 } 601 iterResults[iter.Next().Value()] = struct{}{} 602 } 603 if !reflect.DeepEqual(iterResults, keys) { 604 t.Errorf("Expected accumulation of iterator.Next calls to be %v but got %v", keys, iterResults) 605 } 606 if iter.HasNext() != types.False { 607 t.Error("Expected HasNext to return false") 608 } 609 for contains := range tc.unstructured { 610 if mapper.Contains(UnstructuredToVal(contains, stringSchema)) != types.True { 611 t.Errorf("Expected Contains to return true for %v", contains) 612 } 613 } 614 for _, notContains := range tc.notContains { 615 if mapper.Contains(notContains) != types.False { 616 t.Errorf("Expected Contains to return false for %v", notContains) 617 } 618 } 619 }) 620 } 621 } 622 623 func BenchmarkUnstructuredToVal(b *testing.B) { 624 u := []interface{}{ 625 map[string]interface{}{ 626 "key": "a", 627 "val": 1, 628 }, 629 map[string]interface{}{ 630 "key": "b", 631 "val": 2, 632 }, 633 map[string]interface{}{ 634 "key": "@b", 635 "val": 2, 636 }, 637 } 638 639 b.ReportAllocs() 640 b.ResetTimer() 641 642 for n := 0; n < b.N; n++ { 643 if val := UnstructuredToVal(u, mapListSchema); val == nil { 644 b.Fatal(val) 645 } 646 } 647 } 648 649 func BenchmarkUnstructuredToValWithEscape(b *testing.B) { 650 u := []interface{}{ 651 map[string]interface{}{ 652 "key": "a.1", 653 "val": "__i.1", 654 }, 655 map[string]interface{}{ 656 "key": "b.1", 657 "val": 2, 658 }, 659 } 660 661 b.ReportAllocs() 662 b.ResetTimer() 663 664 for n := 0; n < b.N; n++ { 665 if val := UnstructuredToVal(u, mapListSchema); val == nil { 666 b.Fatal(val) 667 } 668 } 669 }