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  }