k8s.io/kube-openapi@v0.0.0-20240826222958-65a50c78dec5/pkg/generators/markers_test.go (about) 1 /* 2 Copyright 2023 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 package generators_test 17 18 import ( 19 "testing" 20 21 "github.com/stretchr/testify/require" 22 "k8s.io/gengo/v2/types" 23 "k8s.io/kube-openapi/pkg/generators" 24 "k8s.io/kube-openapi/pkg/validation/spec" 25 "k8s.io/utils/ptr" 26 ) 27 28 var structKind *types.Type = &types.Type{Kind: types.Struct, Name: types.Name{Name: "struct"}} 29 var mapType *types.Type = &types.Type{Kind: types.Map, Name: types.Name{Name: "map[string]int"}} 30 var arrayType *types.Type = &types.Type{Kind: types.Slice, Name: types.Name{Name: "[]int"}} 31 32 func TestParseCommentTags(t *testing.T) { 33 34 cases := []struct { 35 t *types.Type 36 name string 37 comments []string 38 expected *spec.Schema 39 40 // regex pattern matching the error, or empty string/unset if no error 41 // is expected 42 expectedError string 43 }{ 44 { 45 t: structKind, 46 name: "basic example", 47 comments: []string{ 48 "comment", 49 "another + comment", 50 "+k8s:validation:minimum=10.0", 51 "+k8s:validation:maximum=20.0", 52 "+k8s:validation:minLength=20", 53 "+k8s:validation:maxLength=30", 54 `+k8s:validation:pattern="asdf"`, 55 "+k8s:validation:multipleOf=1.0", 56 "+k8s:validation:minItems=1", 57 "+k8s:validation:maxItems=2", 58 "+k8s:validation:uniqueItems=true", 59 "exclusiveMaximum=true", 60 "not+k8s:validation:Minimum=0.0", 61 }, 62 expectedError: `invalid marker comments: maxItems can only be used on array types 63 minItems can only be used on array types 64 uniqueItems can only be used on array types 65 minLength can only be used on string types 66 maxLength can only be used on string types 67 pattern can only be used on string types 68 minimum can only be used on numeric types 69 maximum can only be used on numeric types 70 multipleOf can only be used on numeric types`, 71 }, 72 { 73 t: arrayType, 74 name: "basic array example", 75 comments: []string{ 76 "comment", 77 "another + comment", 78 "+k8s:validation:minItems=1", 79 "+k8s:validation:maxItems=2", 80 "+k8s:validation:uniqueItems=true", 81 }, 82 expected: &spec.Schema{ 83 SchemaProps: spec.SchemaProps{ 84 MinItems: ptr.To[int64](1), 85 MaxItems: ptr.To[int64](2), 86 UniqueItems: true, 87 }, 88 }, 89 }, 90 { 91 t: mapType, 92 name: "basic map example", 93 comments: []string{ 94 "comment", 95 "another + comment", 96 "+k8s:validation:minProperties=1", 97 "+k8s:validation:maxProperties=2", 98 }, 99 expected: &spec.Schema{ 100 SchemaProps: spec.SchemaProps{ 101 MinProperties: ptr.To[int64](1), 102 MaxProperties: ptr.To[int64](2), 103 }, 104 }, 105 }, 106 { 107 t: types.String, 108 name: "basic string example", 109 comments: []string{ 110 "comment", 111 "another + comment", 112 "+k8s:validation:minLength=20", 113 "+k8s:validation:maxLength=30", 114 `+k8s:validation:pattern="asdf"`, 115 }, 116 expected: &spec.Schema{ 117 SchemaProps: spec.SchemaProps{ 118 MinLength: ptr.To[int64](20), 119 MaxLength: ptr.To[int64](30), 120 Pattern: "asdf", 121 }, 122 }, 123 }, 124 { 125 t: types.Int, 126 name: "basic int example", 127 comments: []string{ 128 "comment", 129 "another + comment", 130 "+k8s:validation:minimum=10.0", 131 "+k8s:validation:maximum=20.0", 132 "+k8s:validation:multipleOf=1.0", 133 "exclusiveMaximum=true", 134 "not+k8s:validation:Minimum=0.0", 135 }, 136 expected: &spec.Schema{ 137 SchemaProps: spec.SchemaProps{ 138 Maximum: ptr.To(20.0), 139 Minimum: ptr.To(10.0), 140 MultipleOf: ptr.To(1.0), 141 }, 142 }, 143 }, 144 { 145 t: structKind, 146 name: "empty", 147 expected: &spec.Schema{}, 148 }, 149 { 150 t: types.Float64, 151 name: "single", 152 comments: []string{ 153 "+k8s:validation:minimum=10.0", 154 }, 155 expected: &spec.Schema{ 156 SchemaProps: spec.SchemaProps{ 157 Minimum: ptr.To(10.0), 158 }, 159 }, 160 }, 161 { 162 t: types.Float64, 163 name: "multiple", 164 comments: []string{ 165 "+k8s:validation:minimum=10.0", 166 "+k8s:validation:maximum=20.0", 167 }, 168 expected: &spec.Schema{ 169 SchemaProps: spec.SchemaProps{ 170 Maximum: ptr.To(20.0), 171 Minimum: ptr.To(10.0), 172 }, 173 }, 174 }, 175 { 176 t: types.Float64, 177 name: "invalid duplicate key", 178 comments: []string{ 179 "+k8s:validation:minimum=10.0", 180 "+k8s:validation:maximum=20.0", 181 "+k8s:validation:minimum=30.0", 182 }, 183 expectedError: `failed to parse marker comments: cannot have multiple values for key 'minimum'`, 184 }, 185 { 186 t: structKind, 187 name: "unrecognized key is ignored", 188 comments: []string{ 189 "+ignored=30.0", 190 }, 191 expected: &spec.Schema{}, 192 }, 193 { 194 t: types.Float64, 195 name: "invalid: non-JSON value", 196 comments: []string{ 197 `+k8s:validation:minimum=asdf`, 198 }, 199 expectedError: `failed to parse marker comments: failed to parse value for key minimum as JSON: invalid character 'a' looking for beginning of value`, 200 }, 201 { 202 t: types.Float64, 203 name: "invalid: invalid value type", 204 comments: []string{ 205 `+k8s:validation:minimum="asdf"`, 206 }, 207 expectedError: `failed to unmarshal marker comments: json: cannot unmarshal string into Go struct field commentTags.minimum of type float64`, 208 }, 209 { 210 211 t: structKind, 212 name: "invalid: empty key", 213 comments: []string{ 214 "+k8s:validation:", 215 }, 216 expectedError: `failed to parse marker comments: cannot have empty key for marker comment`, 217 }, 218 { 219 t: types.Float64, 220 // temporary test. ref support may be added in the future 221 name: "ignore refs", 222 comments: []string{ 223 "+k8s:validation:pattern=ref(asdf)", 224 }, 225 expected: &spec.Schema{}, 226 }, 227 { 228 t: types.Float64, 229 name: "cel rule", 230 comments: []string{ 231 `+k8s:validation:cel[0]:rule="oldSelf == self"`, 232 `+k8s:validation:cel[0]:message="immutable field"`, 233 }, 234 expected: &spec.Schema{ 235 VendorExtensible: spec.VendorExtensible{ 236 Extensions: map[string]interface{}{ 237 "x-kubernetes-validations": []interface{}{ 238 map[string]interface{}{ 239 "rule": "oldSelf == self", 240 "message": "immutable field", 241 }, 242 }, 243 }, 244 }, 245 }, 246 }, 247 { 248 t: types.Float64, 249 name: "skipped CEL rule", 250 comments: []string{ 251 // This should parse, but return an error in validation since 252 // index 1 is missing 253 `+k8s:validation:cel[0]:rule="oldSelf == self"`, 254 `+k8s:validation:cel[0]:message="immutable field"`, 255 `+k8s:validation:cel[2]:rule="self > 5"`, 256 `+k8s:validation:cel[2]:message="must be greater than 5"`, 257 }, 258 expectedError: `failed to parse marker comments: error parsing cel[2]:rule="self > 5": non-consecutive index 2 for key 'cel'`, 259 }, 260 { 261 t: types.Float64, 262 name: "multiple CEL params", 263 comments: []string{ 264 `+k8s:validation:cel[0]:rule="oldSelf == self"`, 265 `+k8s:validation:cel[0]:message="immutable field"`, 266 `+k8s:validation:cel[1]:rule="self > 5"`, 267 `+k8s:validation:cel[1]:optionalOldSelf=true`, 268 `+k8s:validation:cel[1]:message="must be greater than 5"`, 269 }, 270 expected: &spec.Schema{ 271 VendorExtensible: spec.VendorExtensible{ 272 Extensions: map[string]interface{}{ 273 "x-kubernetes-validations": []interface{}{ 274 map[string]interface{}{ 275 "rule": "oldSelf == self", 276 "message": "immutable field", 277 }, 278 map[string]interface{}{ 279 "rule": "self > 5", 280 "optionalOldSelf": true, 281 "message": "must be greater than 5", 282 }, 283 }, 284 }, 285 }, 286 }, 287 }, 288 { 289 t: types.Float64, 290 name: "multiple rules with multiple params", 291 comments: []string{ 292 `+k8s:validation:cel[0]:rule="oldSelf == self"`, 293 `+k8s:validation:cel[0]:optionalOldSelf`, 294 `+k8s:validation:cel[0]:messageExpression="self + ' must be equal to old value'"`, 295 `+k8s:validation:cel[1]:rule="self > 5"`, 296 `+k8s:validation:cel[1]:optionalOldSelf=true`, 297 `+k8s:validation:cel[1]:message="must be greater than 5"`, 298 }, 299 expected: &spec.Schema{ 300 VendorExtensible: spec.VendorExtensible{ 301 Extensions: map[string]interface{}{ 302 "x-kubernetes-validations": []interface{}{ 303 map[string]interface{}{ 304 "rule": "oldSelf == self", 305 "optionalOldSelf": true, 306 "messageExpression": "self + ' must be equal to old value'", 307 }, 308 map[string]interface{}{ 309 "rule": "self > 5", 310 "optionalOldSelf": true, 311 "message": "must be greater than 5", 312 }, 313 }, 314 }, 315 }, 316 }, 317 }, 318 { 319 t: types.Float64, 320 name: "skipped array index", 321 comments: []string{ 322 `+k8s:validation:cel[0]:rule="oldSelf == self"`, 323 `+k8s:validation:cel[0]:optionalOldSelf`, 324 `+k8s:validation:cel[0]:messageExpression="self + ' must be equal to old value'"`, 325 `+k8s:validation:cel[2]:rule="self > 5"`, 326 `+k8s:validation:cel[2]:optionalOldSelf=true`, 327 `+k8s:validation:cel[2]:message="must be greater than 5"`, 328 }, 329 expectedError: `failed to parse marker comments: error parsing cel[2]:rule="self > 5": non-consecutive index 2 for key 'cel'`, 330 }, 331 { 332 t: types.Float64, 333 name: "non-consecutive array index", 334 comments: []string{ 335 `+k8s:validation:cel[0]:rule="oldSelf == self"`, 336 `+k8s:validation:cel[1]:rule="self > 5"`, 337 `+k8s:validation:cel[1]:message="self > 5"`, 338 `+k8s:validation:cel[0]:optionalOldSelf=true`, 339 `+k8s:validation:cel[0]:message="must be greater than 5"`, 340 }, 341 expectedError: "failed to parse marker comments: error parsing cel[0]:optionalOldSelf=true: non-consecutive index 0 for key 'cel'", 342 }, 343 { 344 t: types.Float64, 345 name: "interjected array index", 346 comments: []string{ 347 `+k8s:validation:cel[0]:rule="oldSelf == self"`, 348 `+k8s:validation:cel[0]:message="cant change"`, 349 `+k8s:validation:cel[1]:rule="self > 5"`, 350 `+k8s:validation:cel[1]:message="must be greater than 5"`, 351 `+k8s:validation:minimum=5`, 352 `+k8s:validation:cel[2]:rule="a rule"`, 353 `+k8s:validation:cel[2]:message="message 2"`, 354 }, 355 expectedError: "failed to parse marker comments: error parsing cel[2]:rule=\"a rule\": non-consecutive index 2 for key 'cel'", 356 }, 357 { 358 t: types.Float64, 359 name: "interjected array index with non-prefixed comment", 360 comments: []string{ 361 `+k8s:validation:cel[0]:rule="oldSelf == self"`, 362 `+k8s:validation:cel[0]:message="cant change"`, 363 `+k8s:validation:cel[1]:rule="self > 5"`, 364 `+k8s:validation:cel[1]:message="must be greater than 5"`, 365 `+minimum=5`, 366 `+k8s:validation:cel[2]:rule="a rule"`, 367 `+k8s:validation:cel[2]:message="message 2"`, 368 }, 369 expectedError: "failed to parse marker comments: error parsing cel[2]:rule=\"a rule\": non-consecutive index 2 for key 'cel'", 370 }, 371 { 372 t: types.Float64, 373 name: "non-consecutive raw string indexing", 374 comments: []string{ 375 `+k8s:validation:cel[0]:rule> raw string rule`, 376 `+k8s:validation:cel[1]:rule="self > 5"`, 377 `+k8s:validation:cel[1]:message="must be greater than 5"`, 378 `+k8s:validation:cel[0]:message>another raw string message`, 379 }, 380 expectedError: "failed to parse marker comments: error parsing cel[0]:message>another raw string message: non-consecutive index 0 for key 'cel'", 381 }, 382 { 383 t: types.String, 384 name: "non-consecutive string indexing false positive", 385 comments: []string{ 386 `+k8s:validation:cel[0]:message>[3]string rule [1]`, 387 `+k8s:validation:cel[0]:rule="string rule [1]"`, 388 `+k8s:validation:pattern="self[3] == 'hi'"`, 389 }, 390 expected: &spec.Schema{ 391 SchemaProps: spec.SchemaProps{ 392 Pattern: `self[3] == 'hi'`, 393 }, 394 VendorExtensible: spec.VendorExtensible{ 395 Extensions: map[string]interface{}{ 396 "x-kubernetes-validations": []interface{}{ 397 map[string]interface{}{ 398 "rule": "string rule [1]", 399 "message": "[3]string rule [1]", 400 }, 401 }, 402 }, 403 }, 404 }, 405 }, 406 { 407 t: types.String, 408 name: "non-consecutive raw string indexing false positive", 409 comments: []string{ 410 `+k8s:validation:cel[0]:message>[3]raw string message with subscirpt [3]"`, 411 `+k8s:validation:cel[0]:rule> raw string rule [1]`, 412 `+k8s:validation:pattern>"self[3] == 'hi'"`, 413 }, 414 expected: &spec.Schema{ 415 SchemaProps: spec.SchemaProps{ 416 Pattern: `"self[3] == 'hi'"`, 417 }, 418 VendorExtensible: spec.VendorExtensible{ 419 Extensions: map[string]interface{}{ 420 "x-kubernetes-validations": []interface{}{ 421 map[string]interface{}{ 422 "rule": "raw string rule [1]", 423 "message": "[3]raw string message with subscirpt [3]\"", 424 }, 425 }, 426 }, 427 }, 428 }, 429 }, 430 { 431 t: types.Float64, 432 name: "boolean key at invalid index", 433 comments: []string{ 434 `+k8s:validation:cel[0]:rule="oldSelf == self"`, 435 `+k8s:validation:cel[0]:message="cant change"`, 436 `+k8s:validation:cel[2]:optionalOldSelf`, 437 }, 438 expectedError: `failed to parse marker comments: error parsing cel[2]:optionalOldSelf: non-consecutive index 2 for key 'cel'`, 439 }, 440 { 441 t: types.Float64, 442 name: "boolean key after non-prefixed comment", 443 comments: []string{ 444 `+k8s:validation:cel[0]:rule="oldSelf == self"`, 445 `+k8s:validation:cel[0]:message="cant change"`, 446 `+k8s:validation:cel[1]:rule="self > 5"`, 447 `+k8s:validation:cel[1]:message="must be greater than 5"`, 448 `+minimum=5`, 449 `+k8s:validation:cel[1]:optionalOldSelf`, 450 }, 451 expectedError: `failed to parse marker comments: error parsing cel[1]:optionalOldSelf: non-consecutive index 1 for key 'cel'`, 452 }, 453 { 454 t: types.Float64, 455 name: "boolean key at index allowed", 456 comments: []string{ 457 `+k8s:validation:cel[0]:rule="oldSelf == self"`, 458 `+k8s:validation:cel[0]:message="cant change"`, 459 `+k8s:validation:cel[1]:rule="self > 5"`, 460 `+k8s:validation:cel[1]:message="must be greater than 5"`, 461 `+k8s:validation:cel[1]:optionalOldSelf`, 462 }, 463 expected: &spec.Schema{ 464 VendorExtensible: spec.VendorExtensible{ 465 Extensions: map[string]interface{}{ 466 "x-kubernetes-validations": []interface{}{ 467 map[string]interface{}{ 468 "rule": "oldSelf == self", 469 "message": "cant change", 470 }, 471 map[string]interface{}{ 472 "rule": "self > 5", 473 "message": "must be greater than 5", 474 "optionalOldSelf": true, 475 }, 476 }, 477 }, 478 }, 479 }, 480 }, 481 { 482 t: types.Float64, 483 name: "raw string rule", 484 comments: []string{ 485 `+k8s:validation:cel[0]:rule> raw string rule`, 486 `+k8s:validation:cel[0]:message="raw string message"`, 487 }, 488 expected: &spec.Schema{ 489 VendorExtensible: spec.VendorExtensible{ 490 Extensions: map[string]interface{}{ 491 "x-kubernetes-validations": []interface{}{ 492 map[string]interface{}{ 493 "rule": "raw string rule", 494 "message": "raw string message", 495 }, 496 }, 497 }, 498 }, 499 }, 500 }, 501 { 502 t: types.Float64, 503 name: "multiline string rule", 504 comments: []string{ 505 `+k8s:validation:cel[0]:rule> self.length() % 2 == 0`, 506 `+k8s:validation:cel[0]:rule> ? self.field == self.name + ' is even'`, 507 `+k8s:validation:cel[0]:rule> : self.field == self.name + ' is odd'`, 508 `+k8s:validation:cel[0]:message>raw string message`, 509 }, 510 expected: &spec.Schema{ 511 VendorExtensible: spec.VendorExtensible{ 512 Extensions: map[string]interface{}{ 513 "x-kubernetes-validations": []interface{}{ 514 map[string]interface{}{ 515 "rule": "self.length() % 2 == 0\n? self.field == self.name + ' is even'\n: self.field == self.name + ' is odd'", 516 "message": "raw string message", 517 }, 518 }, 519 }, 520 }, 521 }, 522 }, 523 { 524 t: types.Float64, 525 name: "mix raw and non-raw string marker", 526 comments: []string{ 527 `+k8s:validation:cel[0]:message>raw string message`, 528 `+k8s:validation:cel[0]:rule="self.length() % 2 == 0"`, 529 `+k8s:validation:cel[0]:rule> ? self.field == self.name + ' is even'`, 530 `+k8s:validation:cel[0]:rule> : self.field == self.name + ' is odd'`, 531 }, 532 expected: &spec.Schema{ 533 VendorExtensible: spec.VendorExtensible{ 534 Extensions: map[string]interface{}{ 535 "x-kubernetes-validations": []interface{}{ 536 map[string]interface{}{ 537 "rule": "self.length() % 2 == 0\n? self.field == self.name + ' is even'\n: self.field == self.name + ' is odd'", 538 "message": "raw string message", 539 }, 540 }, 541 }, 542 }, 543 }, 544 }, 545 { 546 name: "raw string with different key in between", 547 t: types.Float64, 548 comments: []string{ 549 `+k8s:validation:cel[0]:message>raw string message`, 550 `+k8s:validation:cel[0]:rule="self.length() % 2 == 0"`, 551 `+k8s:validation:cel[0]:message>raw string message 2`, 552 }, 553 expectedError: `failed to parse marker comments: concatenations to key 'cel[0]:message' must be consecutive with its assignment`, 554 }, 555 { 556 name: "raw string with different raw string key in between", 557 t: types.Float64, 558 comments: []string{ 559 `+k8s:validation:cel[0]:message>raw string message`, 560 `+k8s:validation:cel[0]:rule>self.length() % 2 == 0`, 561 `+k8s:validation:cel[0]:message>raw string message 2`, 562 }, 563 expectedError: `failed to parse marker comments: concatenations to key 'cel[0]:message' must be consecutive with its assignment`, 564 }, 565 { 566 name: "nested cel", 567 comments: []string{ 568 `+k8s:validation:items:cel[0]:rule="self.length() % 2 == 0"`, 569 `+k8s:validation:items:cel[0]:message="must be even"`, 570 }, 571 t: &types.Type{ 572 Kind: types.Alias, 573 Underlying: &types.Type{ 574 Kind: types.Slice, 575 Elem: types.String, 576 }, 577 }, 578 expected: &spec.Schema{ 579 SchemaProps: spec.SchemaProps{ 580 AllOf: []spec.Schema{ 581 { 582 SchemaProps: spec.SchemaProps{ 583 Items: &spec.SchemaOrArray{ 584 Schema: &spec.Schema{ 585 VendorExtensible: spec.VendorExtensible{ 586 Extensions: map[string]interface{}{ 587 "x-kubernetes-validations": []interface{}{ 588 map[string]interface{}{ 589 "rule": "self.length() % 2 == 0", 590 "message": "must be even", 591 }, 592 }, 593 }, 594 }, 595 }, 596 }, 597 }, 598 }, 599 }, 600 }, 601 }, 602 }, 603 } 604 605 for _, tc := range cases { 606 t.Run(tc.name, func(t *testing.T) { 607 actual, err := generators.ParseCommentTags(tc.t, tc.comments, "+k8s:validation:") 608 if tc.expectedError != "" { 609 require.Error(t, err) 610 require.EqualError(t, err, tc.expectedError) 611 return 612 } else { 613 require.NoError(t, err) 614 } 615 616 require.Equal(t, tc.expected, actual) 617 }) 618 } 619 } 620 621 // Test comment tag validation function 622 func TestCommentTags_Validate(t *testing.T) { 623 624 testCases := []struct { 625 name string 626 comments []string 627 t *types.Type 628 errorMessage string 629 }{ 630 { 631 name: "invalid minimum type", 632 comments: []string{ 633 `+k8s:validation:minimum=10.5`, 634 }, 635 t: types.String, 636 errorMessage: "minimum can only be used on numeric types", 637 }, 638 { 639 name: "invalid minLength type", 640 comments: []string{ 641 `+k8s:validation:minLength=10`, 642 }, 643 t: types.Bool, 644 errorMessage: "minLength can only be used on string types", 645 }, 646 { 647 name: "invalid minItems type", 648 comments: []string{ 649 `+k8s:validation:minItems=10`, 650 }, 651 t: types.String, 652 errorMessage: "minItems can only be used on array types", 653 }, 654 { 655 name: "invalid minProperties type", 656 comments: []string{ 657 `+k8s:validation:minProperties=10`, 658 }, 659 t: types.String, 660 errorMessage: "minProperties can only be used on map types", 661 }, 662 { 663 name: "invalid exclusiveMinimum type", 664 comments: []string{ 665 `+k8s:validation:exclusiveMinimum=true`, 666 }, 667 t: arrayType, 668 errorMessage: "exclusiveMinimum can only be used on numeric types", 669 }, 670 { 671 name: "invalid maximum type", 672 comments: []string{ 673 `+k8s:validation:maximum=10.5`, 674 }, 675 t: arrayType, 676 errorMessage: "maximum can only be used on numeric types", 677 }, 678 { 679 name: "invalid maxLength type", 680 comments: []string{ 681 `+k8s:validation:maxLength=10`, 682 }, 683 t: mapType, 684 errorMessage: "maxLength can only be used on string types", 685 }, 686 { 687 name: "invalid maxItems type", 688 comments: []string{ 689 `+k8s:validation:maxItems=10`, 690 }, 691 t: types.Bool, 692 errorMessage: "maxItems can only be used on array types", 693 }, 694 { 695 name: "invalid maxProperties type", 696 comments: []string{ 697 `+k8s:validation:maxProperties=10`, 698 }, 699 t: types.Bool, 700 errorMessage: "maxProperties can only be used on map types", 701 }, 702 { 703 name: "invalid exclusiveMaximum type", 704 comments: []string{ 705 `+k8s:validation:exclusiveMaximum=true`, 706 }, 707 t: mapType, 708 errorMessage: "exclusiveMaximum can only be used on numeric types", 709 }, 710 { 711 name: "invalid pattern type", 712 comments: []string{ 713 `+k8s:validation:pattern=".*"`, 714 }, 715 t: types.Int, 716 errorMessage: "pattern can only be used on string types", 717 }, 718 { 719 name: "invalid multipleOf type", 720 comments: []string{ 721 `+k8s:validation:multipleOf=10.5`, 722 }, 723 t: types.String, 724 errorMessage: "multipleOf can only be used on numeric types", 725 }, 726 { 727 name: "invalid uniqueItems type", 728 comments: []string{ 729 `+k8s:validation:uniqueItems=true`, 730 }, 731 t: types.Int, 732 errorMessage: "uniqueItems can only be used on array types", 733 }, 734 { 735 name: "negative minLength", 736 comments: []string{ 737 `+k8s:validation:minLength=-10`, 738 }, 739 t: types.String, 740 errorMessage: "minLength cannot be negative", 741 }, 742 { 743 name: "negative minItems", 744 comments: []string{ 745 `+k8s:validation:minItems=-10`, 746 }, 747 t: arrayType, 748 errorMessage: "minItems cannot be negative", 749 }, 750 { 751 name: "negative minProperties", 752 comments: []string{ 753 `+k8s:validation:minProperties=-10`, 754 }, 755 t: mapType, 756 errorMessage: "minProperties cannot be negative", 757 }, 758 { 759 name: "negative maxLength", 760 comments: []string{ 761 `+k8s:validation:maxLength=-10`, 762 }, 763 t: types.String, 764 errorMessage: "maxLength cannot be negative", 765 }, 766 { 767 name: "negative maxItems", 768 comments: []string{ 769 `+k8s:validation:maxItems=-10`, 770 }, 771 t: arrayType, 772 errorMessage: "maxItems cannot be negative", 773 }, 774 { 775 name: "negative maxProperties", 776 comments: []string{ 777 `+k8s:validation:maxProperties=-10`, 778 }, 779 t: mapType, 780 errorMessage: "maxProperties cannot be negative", 781 }, 782 { 783 name: "minimum > maximum", 784 comments: []string{ 785 `+k8s:validation:minimum=10.5`, 786 `+k8s:validation:maximum=5.5`, 787 }, 788 t: types.Float64, 789 errorMessage: "minimum 10.500000 is greater than maximum 5.500000", 790 }, 791 { 792 name: "exclusiveMinimum when minimum == maximum", 793 comments: []string{ 794 `+k8s:validation:minimum=10.5`, 795 `+k8s:validation:maximum=10.5`, 796 `+k8s:validation:exclusiveMinimum=true`, 797 }, 798 t: types.Float64, 799 errorMessage: "exclusiveMinimum/Maximum cannot be set when minimum == maximum", 800 }, 801 { 802 name: "exclusiveMaximum when minimum == maximum", 803 comments: []string{ 804 `+k8s:validation:minimum=10.5`, 805 `+k8s:validation:maximum=10.5`, 806 `+k8s:validation:exclusiveMaximum=true`, 807 }, 808 t: types.Float64, 809 errorMessage: "exclusiveMinimum/Maximum cannot be set when minimum == maximum", 810 }, 811 { 812 name: "minLength > maxLength", 813 comments: []string{ 814 `+k8s:validation:minLength=10`, 815 `+k8s:validation:maxLength=5`, 816 }, 817 t: types.String, 818 errorMessage: "minLength 10 is greater than maxLength 5", 819 }, 820 { 821 name: "minItems > maxItems", 822 comments: []string{ 823 `+k8s:validation:minItems=10`, 824 `+k8s:validation:maxItems=5`, 825 }, 826 t: arrayType, 827 errorMessage: "minItems 10 is greater than maxItems 5", 828 }, 829 { 830 name: "minProperties > maxProperties", 831 comments: []string{ 832 `+k8s:validation:minProperties=10`, 833 `+k8s:validation:maxProperties=5`, 834 }, 835 t: mapType, 836 errorMessage: "minProperties 10 is greater than maxProperties 5", 837 }, 838 { 839 name: "invalid pattern", 840 comments: []string{ 841 `+k8s:validation:pattern="([a-z]+"`, 842 }, 843 t: types.String, 844 errorMessage: "invalid pattern \"([a-z]+\": error parsing regexp: missing closing ): `([a-z]+`", 845 }, 846 { 847 name: "multipleOf = 0", 848 comments: []string{ 849 `+k8s:validation:multipleOf=0.0`, 850 }, 851 t: types.Int, 852 errorMessage: "multipleOf cannot be 0", 853 }, 854 { 855 name: "valid comment tags with no invalid validations", 856 comments: []string{ 857 `+k8s:validation:pattern=".*"`, 858 }, 859 t: types.String, 860 errorMessage: "", 861 }, 862 { 863 name: "additionalProperties on non-map", 864 comments: []string{ 865 `+k8s:validation:additionalProperties:pattern=".*"`, 866 }, 867 t: types.String, 868 errorMessage: "additionalProperties can only be used on map types", 869 }, 870 { 871 name: "properties on non-struct", 872 comments: []string{ 873 `+k8s:validation:properties:name:pattern=".*"`, 874 }, 875 t: types.String, 876 errorMessage: "properties can only be used on struct types", 877 }, 878 { 879 name: "items on non-array", 880 comments: []string{ 881 `+k8s:validation:items:pattern=".*"`, 882 }, 883 t: types.String, 884 errorMessage: "items can only be used on array types", 885 }, 886 { 887 name: "property missing from struct", 888 comments: []string{ 889 `+k8s:validation:properties:name:pattern=".*"`, 890 }, 891 t: &types.Type{ 892 Kind: types.Struct, 893 Name: types.Name{Name: "struct"}, 894 Members: []types.Member{ 895 { 896 Name: "notname", 897 Type: types.String, 898 Tags: `json:"notname"`, 899 }, 900 }, 901 }, 902 errorMessage: `property used in comment tag "name" not found in struct struct`, 903 }, 904 { 905 name: "nested comments also type checked", 906 comments: []string{ 907 `+k8s:validation:properties:name:items:pattern=".*"`, 908 }, 909 t: &types.Type{ 910 Kind: types.Struct, 911 Name: types.Name{Name: "struct"}, 912 Members: []types.Member{ 913 { 914 Name: "name", 915 Type: types.String, 916 Tags: `json:"name"`, 917 }, 918 }, 919 }, 920 errorMessage: `failed to validate property "name": items can only be used on array types`, 921 }, 922 { 923 name: "nested comments also type checked - passing", 924 comments: []string{ 925 `+k8s:validation:properties:name:pattern=".*"`, 926 }, 927 t: &types.Type{ 928 Kind: types.Struct, 929 Name: types.Name{Name: "struct"}, 930 Members: []types.Member{ 931 { 932 Name: "name", 933 Type: types.String, 934 Tags: `json:"name"`, 935 }, 936 }, 937 }, 938 }, 939 { 940 name: "nested marker type checking through alias", 941 comments: []string{ 942 `+k8s:validation:properties:name:pattern=".*"`, 943 }, 944 t: &types.Type{ 945 Kind: types.Struct, 946 Name: types.Name{Name: "struct"}, 947 Members: []types.Member{ 948 { 949 Name: "name", 950 Tags: `json:"name"`, 951 Type: &types.Type{ 952 Kind: types.Alias, 953 Underlying: &types.Type{ 954 Kind: types.Slice, 955 Elem: types.String, 956 }, 957 }, 958 }, 959 }, 960 }, 961 errorMessage: `failed to validate property "name": pattern can only be used on string types`, 962 }, 963 } 964 965 for _, tc := range testCases { 966 t.Run(tc.name, func(t *testing.T) { 967 _, err := generators.ParseCommentTags(tc.t, tc.comments, "+k8s:validation:") 968 if tc.errorMessage != "" { 969 require.Error(t, err) 970 require.Equal(t, "invalid marker comments: "+tc.errorMessage, err.Error()) 971 } else { 972 require.NoError(t, err) 973 } 974 }) 975 } 976 }