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  }