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  }