k8s.io/kube-openapi@v0.0.0-20240228011516-70dd3763d340/pkg/builder/openapi_test.go (about) 1 /* 2 Copyright 2016 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 builder 18 19 import ( 20 "encoding/json" 21 "fmt" 22 "net/http" 23 "strings" 24 "testing" 25 26 "github.com/emicklei/go-restful/v3" 27 "github.com/stretchr/testify/assert" 28 29 openapi "k8s.io/kube-openapi/pkg/common" 30 "k8s.io/kube-openapi/pkg/util/jsontesting" 31 "k8s.io/kube-openapi/pkg/validation/spec" 32 ) 33 34 // setUp is a convenience function for setting up for (most) tests. 35 func setUp(t *testing.T, fullMethods bool) (*openapi.Config, *restful.Container, *assert.Assertions) { 36 assert := assert.New(t) 37 config, container := getConfig(fullMethods) 38 return config, container, assert 39 } 40 41 func noOp(request *restful.Request, response *restful.Response) {} 42 43 // Test input 44 type TestInput struct { 45 // Name of the input 46 Name string `json:"name,omitempty"` 47 // ID of the input 48 ID int `json:"id,omitempty"` 49 Tags []string `json:"tags,omitempty"` 50 } 51 52 // Test output 53 type TestOutput struct { 54 // Name of the output 55 Name string `json:"name,omitempty"` 56 // Number of outputs 57 Count int `json:"count,omitempty"` 58 } 59 60 type TestExtensionV2Schema struct{} 61 62 func (_ TestExtensionV2Schema) OpenAPIDefinition() *openapi.OpenAPIDefinition { 63 schema := spec.Schema{ 64 VendorExtensible: spec.VendorExtensible{ 65 Extensions: map[string]interface{}{ 66 openapi.ExtensionV2Schema: spec.Schema{ 67 SchemaProps: spec.SchemaProps{ 68 Type: []string{"integer"}, 69 }, 70 }, 71 }, 72 }, 73 } 74 schema.Description = "Test extension V2 spec conversion" 75 schema.Properties = map[string]spec.Schema{ 76 "apple": { 77 SchemaProps: spec.SchemaProps{ 78 Description: "Name of the output", 79 Type: []string{"string"}, 80 Format: "", 81 }, 82 }, 83 } 84 return &openapi.OpenAPIDefinition{ 85 Schema: schema, 86 Dependencies: []string{}, 87 } 88 } 89 90 func (_ TestInput) OpenAPIDefinition() *openapi.OpenAPIDefinition { 91 schema := spec.Schema{} 92 schema.Description = "Test input" 93 schema.Properties = map[string]spec.Schema{ 94 "name": { 95 SchemaProps: spec.SchemaProps{ 96 Description: "Name of the input", 97 Type: []string{"string"}, 98 Format: "", 99 }, 100 }, 101 "id": { 102 SchemaProps: spec.SchemaProps{ 103 Description: "ID of the input", 104 Type: []string{"integer"}, 105 Format: "int32", 106 }, 107 }, 108 "tags": { 109 SchemaProps: spec.SchemaProps{ 110 Description: "", 111 Type: []string{"array"}, 112 Items: &spec.SchemaOrArray{ 113 Schema: &spec.Schema{ 114 SchemaProps: spec.SchemaProps{ 115 Type: []string{"string"}, 116 Format: "", 117 }, 118 }, 119 }, 120 }, 121 }, 122 } 123 schema.Extensions = spec.Extensions{"x-test": "test"} 124 return &openapi.OpenAPIDefinition{ 125 Schema: schema, 126 Dependencies: []string{}, 127 } 128 } 129 130 func (_ TestOutput) OpenAPIDefinition() *openapi.OpenAPIDefinition { 131 schema := spec.Schema{} 132 schema.Description = "Test output" 133 schema.Properties = map[string]spec.Schema{ 134 "name": { 135 SchemaProps: spec.SchemaProps{ 136 Description: "Name of the output", 137 Type: []string{"string"}, 138 Format: "", 139 }, 140 }, 141 "count": { 142 SchemaProps: spec.SchemaProps{ 143 Description: "Number of outputs", 144 Type: []string{"integer"}, 145 Format: "int32", 146 }, 147 }, 148 } 149 return &openapi.OpenAPIDefinition{ 150 Schema: schema, 151 Dependencies: []string{}, 152 } 153 } 154 155 var _ openapi.OpenAPIDefinitionGetter = TestInput{} 156 var _ openapi.OpenAPIDefinitionGetter = TestOutput{} 157 158 func getTestRoute(ws *restful.WebService, method string, additionalParams bool, opPrefix string) *restful.RouteBuilder { 159 ret := ws.Method(method). 160 Path("/test/{path:*}"). 161 Doc(fmt.Sprintf("%s test input", method)). 162 Operation(fmt.Sprintf("%s%sTestInput", method, opPrefix)). 163 Produces(restful.MIME_JSON). 164 Consumes(restful.MIME_JSON). 165 Param(ws.PathParameter("path", "path to the resource").DataType("string")). 166 Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")). 167 Reads(TestInput{}). 168 Returns(200, "OK", TestOutput{}). 169 Writes(TestOutput{}). 170 To(noOp) 171 if additionalParams { 172 ret.Param(ws.HeaderParameter("hparam", "a test head parameter").DataType("integer")) 173 ret.Param(ws.FormParameter("fparam", "a test form parameter").DataType("number")) 174 } 175 return ret 176 } 177 178 func getConfig(fullMethods bool) (*openapi.Config, *restful.Container) { 179 mux := http.NewServeMux() 180 container := restful.NewContainer() 181 container.ServeMux = mux 182 ws := new(restful.WebService) 183 ws.Path("/foo") 184 ws.Route(getTestRoute(ws, "get", true, "foo")) 185 if fullMethods { 186 ws.Route(getTestRoute(ws, "post", false, "foo")). 187 Route(getTestRoute(ws, "put", false, "foo")). 188 Route(getTestRoute(ws, "head", false, "foo")). 189 Route(getTestRoute(ws, "patch", false, "foo")). 190 Route(getTestRoute(ws, "options", false, "foo")). 191 Route(getTestRoute(ws, "delete", false, "foo")) 192 193 } 194 ws.Path("/bar") 195 ws.Route(getTestRoute(ws, "get", true, "bar")) 196 if fullMethods { 197 ws.Route(getTestRoute(ws, "post", false, "bar")). 198 Route(getTestRoute(ws, "put", false, "bar")). 199 Route(getTestRoute(ws, "head", false, "bar")). 200 Route(getTestRoute(ws, "patch", false, "bar")). 201 Route(getTestRoute(ws, "options", false, "bar")). 202 Route(getTestRoute(ws, "delete", false, "bar")) 203 204 } 205 container.Add(ws) 206 return &openapi.Config{ 207 ProtocolList: []string{"https"}, 208 Info: &spec.Info{ 209 InfoProps: spec.InfoProps{ 210 Title: "TestAPI", 211 Description: "Test API", 212 Version: "unversioned", 213 }, 214 }, 215 GetDefinitions: func(_ openapi.ReferenceCallback) map[string]openapi.OpenAPIDefinition { 216 return map[string]openapi.OpenAPIDefinition{ 217 "k8s.io/kube-openapi/pkg/builder.TestInput": *TestInput{}.OpenAPIDefinition(), 218 "k8s.io/kube-openapi/pkg/builder.TestOutput": *TestOutput{}.OpenAPIDefinition(), 219 "k8s.io/kube-openapi/pkg/builder.TestExtensionV2Schema": *TestExtensionV2Schema{}.OpenAPIDefinition(), 220 } 221 }, 222 GetDefinitionName: func(name string) (string, spec.Extensions) { 223 friendlyName := name[strings.LastIndex(name, "/")+1:] 224 return friendlyName, spec.Extensions{"x-test2": "test2"} 225 }, 226 }, container 227 } 228 229 func getTestOperation(method string, opPrefix string) *spec.Operation { 230 return &spec.Operation{ 231 OperationProps: spec.OperationProps{ 232 Description: fmt.Sprintf("%s test input", method), 233 Consumes: []string{"application/json"}, 234 Produces: []string{"application/json"}, 235 Schemes: []string{"https"}, 236 Parameters: []spec.Parameter{}, 237 Responses: getTestResponses(), 238 ID: fmt.Sprintf("%s%sTestInput", method, opPrefix), 239 }, 240 } 241 } 242 243 func getTestPathItem(allMethods bool, opPrefix string) spec.PathItem { 244 ret := spec.PathItem{ 245 PathItemProps: spec.PathItemProps{ 246 Get: getTestOperation("get", opPrefix), 247 Parameters: getTestCommonParameters(), 248 }, 249 } 250 ret.Get.Parameters = getAdditionalTestParameters() 251 if allMethods { 252 ret.Put = getTestOperation("put", opPrefix) 253 ret.Put.Parameters = getTestParameters() 254 ret.Post = getTestOperation("post", opPrefix) 255 ret.Post.Parameters = getTestParameters() 256 ret.Head = getTestOperation("head", opPrefix) 257 ret.Head.Parameters = getTestParameters() 258 ret.Patch = getTestOperation("patch", opPrefix) 259 ret.Patch.Parameters = getTestParameters() 260 ret.Delete = getTestOperation("delete", opPrefix) 261 ret.Delete.Parameters = getTestParameters() 262 ret.Options = getTestOperation("options", opPrefix) 263 ret.Options.Parameters = getTestParameters() 264 } 265 return ret 266 } 267 268 func getRefSchema(ref string) *spec.Schema { 269 return &spec.Schema{ 270 SchemaProps: spec.SchemaProps{ 271 Ref: spec.MustCreateRef(ref), 272 }, 273 } 274 } 275 276 func getTestResponses() *spec.Responses { 277 ret := spec.Responses{ 278 ResponsesProps: spec.ResponsesProps{ 279 StatusCodeResponses: map[int]spec.Response{}, 280 }, 281 } 282 ret.StatusCodeResponses[200] = spec.Response{ 283 ResponseProps: spec.ResponseProps{ 284 Description: "OK", 285 Schema: getRefSchema("#/definitions/builder.TestOutput"), 286 }, 287 } 288 return &ret 289 } 290 291 func getTestCommonParameters() []spec.Parameter { 292 ret := make([]spec.Parameter, 2) 293 ret[0] = spec.Parameter{ 294 Refable: spec.Refable{ 295 Ref: spec.MustCreateRef("#/parameters/path-z6Ciiujn"), 296 }, 297 } 298 ret[1] = spec.Parameter{ 299 Refable: spec.Refable{ 300 Ref: spec.MustCreateRef("#/parameters/pretty-nN7o5FEq"), 301 }, 302 } 303 return ret 304 } 305 306 func getTestParameters() []spec.Parameter { 307 ret := make([]spec.Parameter, 1) 308 ret[0] = spec.Parameter{ 309 ParamProps: spec.ParamProps{ 310 In: "body", 311 Name: "body", 312 Required: true, 313 Schema: getRefSchema("#/definitions/builder.TestInput"), 314 }, 315 } 316 return ret 317 } 318 319 func getAdditionalTestParameters() []spec.Parameter { 320 ret := make([]spec.Parameter, 3) 321 ret[0] = spec.Parameter{ 322 ParamProps: spec.ParamProps{ 323 In: "body", 324 Name: "body", 325 Required: true, 326 Schema: getRefSchema("#/definitions/builder.TestInput"), 327 }, 328 } 329 ret[1] = spec.Parameter{ 330 Refable: spec.Refable{ 331 Ref: spec.MustCreateRef("#/parameters/fparam-xCJg5kHS"), 332 }, 333 } 334 ret[2] = spec.Parameter{ 335 Refable: spec.Refable{ 336 Ref: spec.MustCreateRef("#/parameters/hparam-tx-jfxM1"), 337 }, 338 } 339 return ret 340 } 341 342 func getTestInputDefinition() spec.Schema { 343 return spec.Schema{ 344 SchemaProps: spec.SchemaProps{ 345 Description: "Test input", 346 Properties: map[string]spec.Schema{ 347 "id": { 348 SchemaProps: spec.SchemaProps{ 349 Description: "ID of the input", 350 Type: spec.StringOrArray{"integer"}, 351 Format: "int32", 352 }, 353 }, 354 "name": { 355 SchemaProps: spec.SchemaProps{ 356 Description: "Name of the input", 357 Type: spec.StringOrArray{"string"}, 358 }, 359 }, 360 "tags": { 361 SchemaProps: spec.SchemaProps{ 362 Type: spec.StringOrArray{"array"}, 363 Items: &spec.SchemaOrArray{ 364 Schema: &spec.Schema{ 365 SchemaProps: spec.SchemaProps{ 366 Type: spec.StringOrArray{"string"}, 367 }, 368 }, 369 }, 370 }, 371 }, 372 }, 373 }, 374 VendorExtensible: spec.VendorExtensible{ 375 Extensions: spec.Extensions{ 376 "x-test": "test", 377 "x-test2": "test2", 378 }, 379 }, 380 } 381 } 382 383 func getTestOutputDefinition() spec.Schema { 384 return spec.Schema{ 385 SchemaProps: spec.SchemaProps{ 386 Description: "Test output", 387 Properties: map[string]spec.Schema{ 388 "count": { 389 SchemaProps: spec.SchemaProps{ 390 Description: "Number of outputs", 391 Type: spec.StringOrArray{"integer"}, 392 Format: "int32", 393 }, 394 }, 395 "name": { 396 SchemaProps: spec.SchemaProps{ 397 Description: "Name of the output", 398 Type: spec.StringOrArray{"string"}, 399 }, 400 }, 401 }, 402 }, 403 VendorExtensible: spec.VendorExtensible{ 404 Extensions: spec.Extensions{ 405 "x-test2": "test2", 406 }, 407 }, 408 } 409 } 410 411 func TestBuildOpenAPISpec(t *testing.T) { 412 config, container, assert := setUp(t, true) 413 expected := &spec.Swagger{ 414 SwaggerProps: spec.SwaggerProps{ 415 Info: &spec.Info{ 416 InfoProps: spec.InfoProps{ 417 Title: "TestAPI", 418 Description: "Test API", 419 Version: "unversioned", 420 }, 421 }, 422 Swagger: "2.0", 423 Paths: &spec.Paths{ 424 Paths: map[string]spec.PathItem{ 425 "/foo/test/{path}": getTestPathItem(true, "foo"), 426 "/bar/test/{path}": getTestPathItem(true, "bar"), 427 }, 428 }, 429 Definitions: spec.Definitions{ 430 "builder.TestInput": getTestInputDefinition(), 431 "builder.TestOutput": getTestOutputDefinition(), 432 }, 433 Parameters: map[string]spec.Parameter{ 434 "fparam-xCJg5kHS": { 435 CommonValidations: spec.CommonValidations{ 436 UniqueItems: true, 437 }, 438 SimpleSchema: spec.SimpleSchema{ 439 Type: "number", 440 }, 441 ParamProps: spec.ParamProps{ 442 In: "formData", 443 Name: "fparam", 444 Description: "a test form parameter", 445 }, 446 }, 447 "hparam-tx-jfxM1": { 448 CommonValidations: spec.CommonValidations{ 449 UniqueItems: true, 450 }, 451 SimpleSchema: spec.SimpleSchema{ 452 Type: "integer", 453 }, 454 ParamProps: spec.ParamProps{ 455 In: "header", 456 Name: "hparam", 457 Description: "a test head parameter", 458 }, 459 }, 460 "path-z6Ciiujn": { 461 CommonValidations: spec.CommonValidations{ 462 UniqueItems: true, 463 }, 464 SimpleSchema: spec.SimpleSchema{ 465 Type: "string", 466 }, 467 ParamProps: spec.ParamProps{ 468 In: "path", 469 Name: "path", 470 Description: "path to the resource", 471 Required: true, 472 }, 473 }, 474 "pretty-nN7o5FEq": { 475 CommonValidations: spec.CommonValidations{ 476 UniqueItems: true, 477 }, 478 SimpleSchema: spec.SimpleSchema{ 479 Type: "string", 480 }, 481 ParamProps: spec.ParamProps{ 482 In: "query", 483 Name: "pretty", 484 Description: "If 'true', then the output is pretty printed.", 485 }, 486 }, 487 }, 488 }, 489 } 490 swagger, err := BuildOpenAPISpec(container.RegisteredWebServices(), config) 491 if !assert.NoError(err) { 492 return 493 } 494 expected_json, err := expected.MarshalJSON() 495 if !assert.NoError(err) { 496 return 497 } 498 actual_json, err := swagger.MarshalJSON() 499 if !assert.NoError(err) { 500 return 501 } 502 if err := jsontesting.JsonCompare(expected_json, actual_json); err != nil { 503 t.Error(err) 504 } 505 } 506 507 func TestBuildOpenAPIDefinitionsForResource(t *testing.T) { 508 config, _, assert := setUp(t, true) 509 expected := &spec.Definitions{ 510 "builder.TestInput": getTestInputDefinition(), 511 } 512 swagger, err := BuildOpenAPIDefinitionsForResource(TestInput{}, config) 513 if !assert.NoError(err) { 514 return 515 } 516 expected_json, err := json.Marshal(expected) 517 if !assert.NoError(err) { 518 return 519 } 520 actual_json, err := json.Marshal(swagger) 521 if !assert.NoError(err) { 522 return 523 } 524 if err := jsontesting.JsonCompare(expected_json, actual_json); err != nil { 525 t.Error(err) 526 } 527 } 528 529 func TestBuildOpenAPIDefinitionsForResourceWithExtensionV2Schema(t *testing.T) { 530 config, _, assert := setUp(t, true) 531 expected := &spec.Definitions{ 532 "builder.TestExtensionV2Schema": spec.Schema{ 533 SchemaProps: spec.SchemaProps{ 534 Type: []string{"integer"}, 535 }, 536 }, 537 } 538 swagger, err := BuildOpenAPIDefinitionsForResource(TestExtensionV2Schema{}, config) 539 if !assert.NoError(err) { 540 return 541 } 542 expected_json, err := json.Marshal(expected) 543 if !assert.NoError(err) { 544 return 545 } 546 actual_json, err := json.Marshal(swagger) 547 if !assert.NoError(err) { 548 return 549 } 550 assert.Equal(string(expected_json), string(actual_json)) 551 }