k8s.io/kube-openapi@v0.0.0-20240228011516-70dd3763d340/pkg/builder3/openapi_test.go (about) 1 /* 2 Copyright 2022 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 builder3 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/spec3" 31 "k8s.io/kube-openapi/pkg/util/jsontesting" 32 "k8s.io/kube-openapi/pkg/validation/spec" 33 ) 34 35 // setUp is a convenience function for setting up for (most) tests. 36 func setUp(t *testing.T, fullMethods bool) (*openapi.OpenAPIV3Config, *restful.Container, *assert.Assertions) { 37 assert := assert.New(t) 38 config, container := getConfig(fullMethods) 39 return config, container, assert 40 } 41 42 func noOp(request *restful.Request, response *restful.Response) {} 43 44 // Test input 45 type TestInput struct { 46 // Name of the input 47 Name string `json:"name,omitempty"` 48 // ID of the input 49 ID int `json:"id,omitempty"` 50 Tags []string `json:"tags,omitempty"` 51 } 52 53 // Test output 54 type TestOutput struct { 55 // Name of the output 56 Name string `json:"name,omitempty"` 57 // Number of outputs 58 Count int `json:"count,omitempty"` 59 } 60 61 func (_ TestInput) OpenAPIDefinition() *openapi.OpenAPIDefinition { 62 schema := spec.Schema{} 63 schema.Description = "Test input" 64 schema.Properties = map[string]spec.Schema{ 65 "name": { 66 SchemaProps: spec.SchemaProps{ 67 Description: "Name of the input", 68 Type: []string{"string"}, 69 Format: "", 70 }, 71 }, 72 "id": { 73 SchemaProps: spec.SchemaProps{ 74 Description: "ID of the input", 75 Type: []string{"integer"}, 76 Format: "int32", 77 }, 78 }, 79 "tags": { 80 SchemaProps: spec.SchemaProps{ 81 Description: "", 82 Type: []string{"array"}, 83 Items: &spec.SchemaOrArray{ 84 Schema: &spec.Schema{ 85 SchemaProps: spec.SchemaProps{ 86 Type: []string{"string"}, 87 Format: "", 88 }, 89 }, 90 }, 91 }, 92 }, 93 "reference-extension": { 94 VendorExtensible: spec.VendorExtensible{ 95 Extensions: map[string]interface{}{"extension": "value"}, 96 }, 97 SchemaProps: spec.SchemaProps{ 98 Ref: spec.MustCreateRef("/components/schemas/builder3.TestOutput"), 99 }, 100 }, 101 "reference-nullable": { 102 SchemaProps: spec.SchemaProps{ 103 Ref: spec.MustCreateRef("/components/schemas/builder3.TestOutput"), 104 Nullable: true, 105 }, 106 }, 107 "reference-default": { 108 SchemaProps: spec.SchemaProps{ 109 Ref: spec.MustCreateRef("/components/schemas/builder3.TestOutput"), 110 Default: map[string]interface{}{}, 111 }, 112 }, 113 } 114 schema.Extensions = spec.Extensions{"x-test": "test"} 115 def := openapi.EmbedOpenAPIDefinitionIntoV2Extension(openapi.OpenAPIDefinition{ 116 Schema: schema, 117 Dependencies: []string{}, 118 }, openapi.OpenAPIDefinition{ 119 // this empty embedded v2 definition should not appear in the result 120 }) 121 return &def 122 } 123 124 func (_ TestOutput) OpenAPIDefinition() *openapi.OpenAPIDefinition { 125 schema := spec.Schema{} 126 schema.Description = "Test output" 127 schema.Properties = map[string]spec.Schema{ 128 "name": { 129 SchemaProps: spec.SchemaProps{ 130 Description: "Name of the output", 131 Type: []string{"string"}, 132 Format: "", 133 }, 134 }, 135 "count": { 136 SchemaProps: spec.SchemaProps{ 137 Description: "Number of outputs", 138 Type: []string{"integer"}, 139 Format: "int32", 140 }, 141 }, 142 } 143 return &openapi.OpenAPIDefinition{ 144 Schema: schema, 145 Dependencies: []string{}, 146 } 147 } 148 149 var _ openapi.OpenAPIDefinitionGetter = TestInput{} 150 var _ openapi.OpenAPIDefinitionGetter = TestOutput{} 151 152 func getTestRoute(ws *restful.WebService, method string, opPrefix string) *restful.RouteBuilder { 153 ret := ws.Method(method). 154 Path("/test/{path:*}"). 155 Doc(fmt.Sprintf("%s test input", method)). 156 Operation(fmt.Sprintf("%s%sTestInput", method, opPrefix)). 157 Produces(restful.MIME_JSON). 158 Consumes(restful.MIME_JSON). 159 Param(ws.PathParameter("path", "path to the resource").DataType("string")). 160 Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")). 161 Reads(TestInput{}). 162 Returns(200, "OK", TestOutput{}). 163 Writes(TestOutput{}). 164 To(noOp) 165 return ret 166 } 167 168 func getConfig(fullMethods bool) (*openapi.OpenAPIV3Config, *restful.Container) { 169 mux := http.NewServeMux() 170 container := restful.NewContainer() 171 container.ServeMux = mux 172 ws := new(restful.WebService) 173 ws.Path("/foo") 174 ws.Route(getTestRoute(ws, "get", "foo")) 175 if fullMethods { 176 ws.Route(getTestRoute(ws, "post", "foo")). 177 Route(getTestRoute(ws, "put", "foo")). 178 Route(getTestRoute(ws, "head", "foo")). 179 Route(getTestRoute(ws, "patch", "foo")). 180 Route(getTestRoute(ws, "options", "foo")). 181 Route(getTestRoute(ws, "delete", "foo")) 182 183 } 184 ws.Path("/bar") 185 ws.Route(getTestRoute(ws, "get", "bar")) 186 if fullMethods { 187 ws.Route(getTestRoute(ws, "post", "bar")). 188 Route(getTestRoute(ws, "put", "bar")). 189 Route(getTestRoute(ws, "head", "bar")). 190 Route(getTestRoute(ws, "patch", "bar")). 191 Route(getTestRoute(ws, "options", "bar")). 192 Route(getTestRoute(ws, "delete", "bar")) 193 194 } 195 container.Add(ws) 196 return &openapi.OpenAPIV3Config{ 197 Info: &spec.Info{ 198 InfoProps: spec.InfoProps{ 199 Title: "TestAPI", 200 Description: "Test API", 201 Version: "unversioned", 202 }, 203 }, 204 GetDefinitions: func(_ openapi.ReferenceCallback) map[string]openapi.OpenAPIDefinition { 205 return map[string]openapi.OpenAPIDefinition{ 206 "k8s.io/kube-openapi/pkg/builder3.TestInput": *TestInput{}.OpenAPIDefinition(), 207 "k8s.io/kube-openapi/pkg/builder3.TestOutput": *TestOutput{}.OpenAPIDefinition(), 208 } 209 }, 210 GetDefinitionName: func(name string) (string, spec.Extensions) { 211 friendlyName := name[strings.LastIndex(name, "/")+1:] 212 return friendlyName, spec.Extensions{"x-test2": "test2"} 213 }, 214 }, container 215 } 216 217 func getTestOperation(method string, opPrefix string) *spec3.Operation { 218 return &spec3.Operation{ 219 OperationProps: spec3.OperationProps{ 220 Description: fmt.Sprintf("%s test input", method), 221 Parameters: []*spec3.Parameter{}, 222 Responses: getTestResponses(), 223 OperationId: fmt.Sprintf("%s%sTestInput", method, opPrefix), 224 }, 225 } 226 } 227 228 func getTestPathItem(opPrefix string) *spec3.Path { 229 ret := &spec3.Path{ 230 PathProps: spec3.PathProps{ 231 Get: getTestOperation("get", opPrefix), 232 Parameters: getTestCommonParameters(), 233 }, 234 } 235 ret.Get.RequestBody = getTestRequestBody() 236 ret.Put = getTestOperation("put", opPrefix) 237 ret.Put.RequestBody = getTestRequestBody() 238 ret.Post = getTestOperation("post", opPrefix) 239 ret.Post.RequestBody = getTestRequestBody() 240 ret.Head = getTestOperation("head", opPrefix) 241 ret.Head.RequestBody = getTestRequestBody() 242 ret.Patch = getTestOperation("patch", opPrefix) 243 ret.Patch.RequestBody = getTestRequestBody() 244 ret.Delete = getTestOperation("delete", opPrefix) 245 ret.Delete.RequestBody = getTestRequestBody() 246 ret.Options = getTestOperation("options", opPrefix) 247 ret.Options.RequestBody = getTestRequestBody() 248 return ret 249 } 250 251 func getRefSchema(ref string) *spec.Schema { 252 return &spec.Schema{ 253 SchemaProps: spec.SchemaProps{ 254 Ref: spec.MustCreateRef(ref), 255 }, 256 } 257 } 258 259 func getTestResponses() *spec3.Responses { 260 ret := &spec3.Responses{ 261 ResponsesProps: spec3.ResponsesProps{ 262 StatusCodeResponses: map[int]*spec3.Response{}, 263 }, 264 } 265 ret.StatusCodeResponses[200] = &spec3.Response{ 266 ResponseProps: spec3.ResponseProps{ 267 Description: "OK", 268 Content: map[string]*spec3.MediaType{}, 269 }, 270 } 271 272 ret.StatusCodeResponses[200].Content[restful.MIME_JSON] = &spec3.MediaType{ 273 MediaTypeProps: spec3.MediaTypeProps{ 274 Schema: getRefSchema("#/components/schemas/builder3.TestOutput"), 275 }, 276 } 277 278 return ret 279 } 280 281 func getTestCommonParameters() []*spec3.Parameter { 282 ret := make([]*spec3.Parameter, 2) 283 ret[0] = &spec3.Parameter{ 284 ParameterProps: spec3.ParameterProps{ 285 Description: "path to the resource", 286 Name: "path", 287 In: "path", 288 Required: true, 289 Schema: &spec.Schema{ 290 SchemaProps: spec.SchemaProps{ 291 Type: []string{"string"}, 292 UniqueItems: true, 293 }, 294 }, 295 }, 296 } 297 ret[1] = &spec3.Parameter{ 298 ParameterProps: spec3.ParameterProps{ 299 Description: "If 'true', then the output is pretty printed.", 300 Name: "pretty", 301 In: "query", 302 Schema: &spec.Schema{ 303 SchemaProps: spec.SchemaProps{ 304 Type: []string{"string"}, 305 UniqueItems: true, 306 }, 307 }, 308 }, 309 } 310 return ret 311 } 312 313 func getTestRequestBody() *spec3.RequestBody { 314 ret := &spec3.RequestBody{ 315 RequestBodyProps: spec3.RequestBodyProps{ 316 Content: map[string]*spec3.MediaType{ 317 restful.MIME_JSON: { 318 MediaTypeProps: spec3.MediaTypeProps{ 319 Schema: getRefSchema("#/components/schemas/builder3.TestInput"), 320 }, 321 }, 322 }, 323 Required: true, 324 }, 325 } 326 return ret 327 } 328 329 func getTestInputDefinition() *spec.Schema { 330 return &spec.Schema{ 331 SchemaProps: spec.SchemaProps{ 332 Description: "Test input", 333 Properties: map[string]spec.Schema{ 334 "id": { 335 SchemaProps: spec.SchemaProps{ 336 Description: "ID of the input", 337 Type: spec.StringOrArray{"integer"}, 338 Format: "int32", 339 }, 340 }, 341 "name": { 342 SchemaProps: spec.SchemaProps{ 343 Description: "Name of the input", 344 Type: spec.StringOrArray{"string"}, 345 }, 346 }, 347 "tags": { 348 SchemaProps: spec.SchemaProps{ 349 Type: spec.StringOrArray{"array"}, 350 Items: &spec.SchemaOrArray{ 351 Schema: &spec.Schema{ 352 SchemaProps: spec.SchemaProps{ 353 Type: spec.StringOrArray{"string"}, 354 }, 355 }, 356 }, 357 }, 358 }, 359 "reference-extension": { 360 VendorExtensible: spec.VendorExtensible{ 361 Extensions: map[string]interface{}{"extension": "value"}, 362 }, 363 SchemaProps: spec.SchemaProps{ 364 AllOf: []spec.Schema{{ 365 SchemaProps: spec.SchemaProps{ 366 Ref: spec.MustCreateRef("/components/schemas/builder3.TestOutput"), 367 }, 368 }}, 369 }, 370 }, 371 "reference-nullable": { 372 SchemaProps: spec.SchemaProps{ 373 Nullable: true, 374 AllOf: []spec.Schema{{ 375 SchemaProps: spec.SchemaProps{ 376 Ref: spec.MustCreateRef("/components/schemas/builder3.TestOutput"), 377 }, 378 }}, 379 }, 380 }, 381 "reference-default": { 382 SchemaProps: spec.SchemaProps{ 383 AllOf: []spec.Schema{{ 384 SchemaProps: spec.SchemaProps{ 385 Ref: spec.MustCreateRef("/components/schemas/builder3.TestOutput"), 386 }, 387 }}, 388 Default: map[string]interface{}{}, 389 }, 390 }, 391 }, 392 }, 393 VendorExtensible: spec.VendorExtensible{ 394 Extensions: spec.Extensions{ 395 "x-test": "test", 396 "x-test2": "test2", 397 }, 398 }, 399 } 400 } 401 402 func getTestOutputDefinition() *spec.Schema { 403 return &spec.Schema{ 404 SchemaProps: spec.SchemaProps{ 405 Description: "Test output", 406 Properties: map[string]spec.Schema{ 407 "count": { 408 SchemaProps: spec.SchemaProps{ 409 Description: "Number of outputs", 410 Type: spec.StringOrArray{"integer"}, 411 Format: "int32", 412 }, 413 }, 414 "name": { 415 SchemaProps: spec.SchemaProps{ 416 Description: "Name of the output", 417 Type: spec.StringOrArray{"string"}, 418 }, 419 }, 420 }, 421 }, 422 VendorExtensible: spec.VendorExtensible{ 423 Extensions: spec.Extensions{ 424 "x-test2": "test2", 425 }, 426 }, 427 } 428 } 429 430 func TestBuildOpenAPISpec(t *testing.T) { 431 config, container, assert := setUp(t, true) 432 expected := &spec3.OpenAPI{ 433 Info: &spec.Info{ 434 InfoProps: spec.InfoProps{ 435 Title: "TestAPI", 436 Description: "Test API", 437 Version: "unversioned", 438 }, 439 VendorExtensible: spec.VendorExtensible{ 440 Extensions: map[string]any{ 441 "hello": "world", // set from PostProcessSpec callback 442 }, 443 }, 444 }, 445 Version: "3.0.0", 446 Paths: &spec3.Paths{ 447 Paths: map[string]*spec3.Path{ 448 "/foo/test/{path}": getTestPathItem("foo"), 449 "/bar/test/{path}": getTestPathItem("bar"), 450 }, 451 }, 452 Components: &spec3.Components{ 453 Schemas: map[string]*spec.Schema{ 454 "builder3.TestInput": getTestInputDefinition(), 455 "builder3.TestOutput": getTestOutputDefinition(), 456 }, 457 }, 458 } 459 config.PostProcessSpec = func(s *spec3.OpenAPI) (*spec3.OpenAPI, error) { 460 s.Info.Extensions = map[string]any{ 461 "hello": "world", 462 } 463 return s, nil 464 } 465 swagger, err := BuildOpenAPISpec(container.RegisteredWebServices(), config) 466 if !assert.NoError(err) { 467 return 468 } 469 expected_json, err := json.Marshal(expected) 470 if !assert.NoError(err) { 471 return 472 } 473 actual_json, err := json.Marshal(swagger) 474 if !assert.NoError(err) { 475 return 476 } 477 if err := jsontesting.JsonCompare(expected_json, actual_json); err != nil { 478 t.Error(err) 479 } 480 }