1
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
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
45 type TestInput struct {
46
47 Name string `json:"name,omitempty"`
48
49 ID int `json:"id,omitempty"`
50 Tags []string `json:"tags,omitempty"`
51 }
52
53
54 type TestOutput struct {
55
56 Name string `json:"name,omitempty"`
57
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
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",
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 }
481
View as plain text