k8s.io/kube-openapi@v0.0.0-20240228011516-70dd3763d340/pkg/validation/spec/swagger_test.go (about) 1 // Copyright 2015 go-swagger maintainers 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package spec 16 17 import ( 18 "encoding/json" 19 "fmt" 20 "io" 21 "os" 22 "reflect" 23 "testing" 24 25 "github.com/google/go-cmp/cmp" 26 fuzz "github.com/google/gofuzz" 27 "github.com/stretchr/testify/assert" 28 "github.com/stretchr/testify/require" 29 "k8s.io/kube-openapi/pkg/internal" 30 jsontesting "k8s.io/kube-openapi/pkg/util/jsontesting" 31 ) 32 33 var spec = Swagger{ 34 SwaggerProps: SwaggerProps{ 35 ID: "http://localhost:3849/api-docs", 36 Swagger: "2.0", 37 Consumes: []string{"application/json", "application/x-yaml"}, 38 Produces: []string{"application/json"}, 39 Schemes: []string{"http", "https"}, 40 Info: &info, 41 Host: "some.api.out.there", 42 BasePath: "/", 43 Paths: &paths, 44 Definitions: map[string]Schema{"Category": {SchemaProps: SchemaProps{Type: []string{"string"}}}}, 45 Parameters: map[string]Parameter{ 46 "categoryParam": {ParamProps: ParamProps{Name: "category", In: "query"}, SimpleSchema: SimpleSchema{Type: "string"}}, 47 }, 48 Responses: map[string]Response{ 49 "EmptyAnswer": { 50 ResponseProps: ResponseProps{ 51 Description: "no data to return for this operation", 52 }, 53 }, 54 }, 55 SecurityDefinitions: map[string]*SecurityScheme{ 56 "internalApiKey": &(SecurityScheme{SecuritySchemeProps: SecuritySchemeProps{Type: "apiKey", Name: "api_key", In: "header"}}), 57 }, 58 Security: []map[string][]string{ 59 {"internalApiKey": {}}, 60 }, 61 Tags: []Tag{{TagProps: TagProps{Description: "", Name: "pets", ExternalDocs: nil}}}, 62 ExternalDocs: &ExternalDocumentation{Description: "the name", URL: "the url"}, 63 }, 64 VendorExtensible: VendorExtensible{Extensions: map[string]interface{}{ 65 "x-some-extension": "vendor", 66 "x-schemes": []interface{}{"unix", "amqp"}, 67 }}, 68 } 69 70 const specJSON = `{ 71 "id": "http://localhost:3849/api-docs", 72 "consumes": ["application/json", "application/x-yaml"], 73 "produces": ["application/json"], 74 "schemes": ["http", "https"], 75 "swagger": "2.0", 76 "info": { 77 "contact": { 78 "name": "wordnik api team", 79 "url": "http://developer.wordnik.com" 80 }, 81 "description": "A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0` + 82 ` specification", 83 "license": { 84 "name": "Creative Commons 4.0 International", 85 "url": "http://creativecommons.org/licenses/by/4.0/" 86 }, 87 "termsOfService": "http://helloreverb.com/terms/", 88 "title": "Swagger Sample API", 89 "version": "1.0.9-abcd", 90 "x-framework": "go-swagger" 91 }, 92 "host": "some.api.out.there", 93 "basePath": "/", 94 "paths": {"x-framework":"go-swagger","/":{"$ref":"cats"}}, 95 "definitions": { "Category": { "type": "string"} }, 96 "parameters": { 97 "categoryParam": { 98 "name": "category", 99 "in": "query", 100 "type": "string" 101 } 102 }, 103 "responses": { "EmptyAnswer": { "description": "no data to return for this operation" } }, 104 "securityDefinitions": { 105 "internalApiKey": { 106 "type": "apiKey", 107 "in": "header", 108 "name": "api_key" 109 } 110 }, 111 "security": [{"internalApiKey":[]}], 112 "tags": [{"name":"pets"}], 113 "externalDocs": {"description":"the name","url":"the url"}, 114 "x-some-extension": "vendor", 115 "x-schemes": ["unix","amqp"] 116 }` 117 118 func TestSwaggerSpec_Serialize(t *testing.T) { 119 expected := make(map[string]interface{}) 120 _ = json.Unmarshal([]byte(specJSON), &expected) 121 b, err := spec.MarshalJSON() 122 if assert.NoError(t, err) { 123 var actual map[string]interface{} 124 err := json.Unmarshal(b, &actual) 125 if assert.NoError(t, err) { 126 assert.EqualValues(t, actual, expected) 127 } 128 } 129 } 130 131 func TestSwaggerSpec_Deserialize(t *testing.T) { 132 var actual Swagger 133 err := json.Unmarshal([]byte(specJSON), &actual) 134 if assert.NoError(t, err) { 135 assert.EqualValues(t, actual, spec) 136 } 137 } 138 139 func TestSwaggerRoundtrip(t *testing.T) { 140 cases := []jsontesting.RoundTripTestCase{ 141 { 142 // Show at least one field from each embededd struct sitll allows 143 // roundtrips successfully 144 Name: "UnmarshalEmbedded", 145 Object: &Swagger{ 146 VendorExtensible{Extensions{ 147 "x-framework": "go-swagger", 148 }}, 149 SwaggerProps{ 150 Swagger: "2.0.0", 151 }, 152 }, 153 }, { 154 Name: "BasicCase", 155 JSON: specJSON, 156 Object: &spec, 157 }, 158 } 159 160 for _, tcase := range cases { 161 t.Run(tcase.Name, func(t *testing.T) { 162 require.NoError(t, tcase.RoundTripTest(&Swagger{})) 163 }) 164 } 165 } 166 167 func TestSwaggerSpec_Marshalv2Fuzzed(t *testing.T) { 168 fuzzer := fuzz. 169 NewWithSeed(1646791953). 170 NilChance(0.075). 171 MaxDepth(13). 172 NumElements(1, 2) 173 174 fuzzer.Funcs( 175 SwaggerFuzzFuncs..., 176 ) 177 178 for i := 0; i < 100; i++ { 179 t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { 180 swagger := Swagger{} 181 fuzzer.Fuzz(&swagger) 182 183 internal.UseOptimizedJSONMarshaling = false 184 want, err := json.Marshal(swagger) 185 if err != nil { 186 t.Errorf("failed to marshal swagger: %v", err) 187 } 188 internal.UseOptimizedJSONMarshaling = true 189 got, err := swagger.MarshalJSON() 190 if err != nil { 191 t.Errorf("failed to marshal next swagger: %v", err) 192 } 193 if err := jsontesting.JsonCompare(want, got); err != nil { 194 t.Errorf("fuzzed marshal doesn't match: %v", err) 195 } 196 }) 197 } 198 } 199 200 func TestSwaggerSpec_Marshalv2FuzzedIsStable(t *testing.T) { 201 swagFile, err := os.Open("../../schemaconv/testdata/swagger.json") 202 if err != nil { 203 t.Fatal(err) 204 } 205 defer swagFile.Close() 206 207 js, err := io.ReadAll(swagFile) 208 if err != nil { 209 t.Fatal(err) 210 } 211 212 swagger := Swagger{} 213 assert.NoError(t, json.Unmarshal(js, &swagger)) 214 215 for i := 0; i < 5; i++ { 216 t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { 217 want, err := swagger.MarshalJSON() 218 if err != nil { 219 t.Errorf("failed to marshal swagger: %v", err) 220 } 221 got, err := swagger.MarshalJSON() 222 if err != nil { 223 t.Errorf("failed to marshal swagger again: %v", err) 224 } 225 if err := cmp.Diff(want, got); err != "" { 226 t.Fatalf("expected both marshal to be identical/stable: %v", err) 227 } 228 }) 229 } 230 } 231 232 func TestUnmarshalAdditionalProperties(t *testing.T) { 233 cases := []string{ 234 `{}`, 235 `{"description": "the description of this schema"}`, 236 `false`, 237 `true`, 238 } 239 240 for _, tc := range cases { 241 t.Run(tc, func(t *testing.T) { 242 var v1, v2 SchemaOrBool 243 internal.UseOptimizedJSONUnmarshaling = true 244 require.NoError(t, json.Unmarshal([]byte(tc), &v2)) 245 internal.UseOptimizedJSONUnmarshaling = false 246 require.NoError(t, json.Unmarshal([]byte(tc), &v1)) 247 if !cmp.Equal(v1, v2, SwaggerDiffOptions...) { 248 t.Fatal(cmp.Diff(v1, v2, SwaggerDiffOptions...)) 249 } 250 }) 251 } 252 } 253 254 func TestSwaggerSpec_ExperimentalUnmarshal(t *testing.T) { 255 fuzzer := fuzz. 256 NewWithSeed(1646791953). 257 NilChance(0.01). 258 MaxDepth(10). 259 NumElements(1, 2) 260 261 fuzzer.Funcs( 262 SwaggerFuzzFuncs..., 263 ) 264 265 expected := Swagger{} 266 fuzzer.Fuzz(&expected) 267 268 // Serialize into JSON 269 jsonBytes, err := json.Marshal(expected) 270 require.NoError(t, err) 271 272 t.Log("Specimen", string(jsonBytes)) 273 274 actual := Swagger{} 275 internal.UseOptimizedJSONUnmarshaling = true 276 277 err = json.Unmarshal(jsonBytes, &actual) 278 require.NoError(t, err) 279 280 if !cmp.Equal(expected, actual, SwaggerDiffOptions...) { 281 t.Fatal(cmp.Diff(expected, actual, SwaggerDiffOptions...)) 282 } 283 284 control := Swagger{} 285 internal.UseOptimizedJSONUnmarshaling = false 286 err = json.Unmarshal(jsonBytes, &control) 287 require.NoError(t, err) 288 289 if !reflect.DeepEqual(control, actual) { 290 t.Fatal(cmp.Diff(control, actual, SwaggerDiffOptions...)) 291 } 292 } 293 294 func BenchmarkSwaggerSpec_ExperimentalUnmarshal(b *testing.B) { 295 // Download kube-openapi swagger json 296 swagFile, err := os.Open("../../schemaconv/testdata/swagger.json") 297 if err != nil { 298 b.Fatal(err) 299 } 300 defer swagFile.Close() 301 302 originalJSON, err := io.ReadAll(swagFile) 303 if err != nil { 304 b.Fatal(err) 305 } 306 307 b.ResetTimer() 308 309 // Parse into kube-openapi types 310 b.Run("jsonv1", func(b2 *testing.B) { 311 internal.UseOptimizedJSONUnmarshaling = false 312 for i := 0; i < b2.N; i++ { 313 var result *Swagger 314 if err := json.Unmarshal(originalJSON, &result); err != nil { 315 b2.Fatal(err) 316 } 317 } 318 }) 319 320 b.Run("jsonv2 via jsonv1", func(b2 *testing.B) { 321 internal.UseOptimizedJSONUnmarshaling = true 322 for i := 0; i < b2.N; i++ { 323 var result *Swagger 324 if err := json.Unmarshal(originalJSON, &result); err != nil { 325 b2.Fatal(err) 326 } 327 } 328 }) 329 330 // Our UnmarshalJSON implementation which defers to jsonv2 causes the 331 // text to be parsed/validated twice. This costs a significant amount of time. 332 b.Run("jsonv2", func(b2 *testing.B) { 333 internal.UseOptimizedJSONUnmarshaling = true 334 for i := 0; i < b2.N; i++ { 335 var result Swagger 336 if err := result.UnmarshalJSON(originalJSON); err != nil { 337 b2.Fatal(err) 338 } 339 } 340 }) 341 } 342 343 func BenchmarkSwaggerSpec_ExperimentalMarshal(b *testing.B) { 344 // Load kube-openapi swagger json 345 swagFile, err := os.Open("../../schemaconv/testdata/swagger.json") 346 if err != nil { 347 b.Fatal(err) 348 } 349 defer swagFile.Close() 350 351 originalJSON, err := io.ReadAll(swagFile) 352 if err != nil { 353 b.Fatal(err) 354 } 355 356 var swagger *Swagger 357 if err := json.Unmarshal(originalJSON, &swagger); err != nil { 358 b.Fatal(err) 359 } 360 361 b.ResetTimer() 362 363 // Serialize kube-openapi types 364 b.Run("jsonv1", func(b2 *testing.B) { 365 b2.ReportAllocs() 366 internal.UseOptimizedJSONMarshaling = false 367 for i := 0; i < b2.N; i++ { 368 if _, err = json.Marshal(swagger); err != nil { 369 b2.Fatal(err) 370 } 371 } 372 }) 373 374 b.Run("jsonv2 via jsonv1", func(b2 *testing.B) { 375 b2.ReportAllocs() 376 internal.UseOptimizedJSONMarshaling = true 377 for i := 0; i < b2.N; i++ { 378 if _, err := json.Marshal(swagger); err != nil { 379 b2.Fatal(err) 380 } 381 } 382 }) 383 384 b.Run("jsonv2", func(b2 *testing.B) { 385 b2.ReportAllocs() 386 internal.UseOptimizedJSONUnmarshaling = true 387 for i := 0; i < b2.N; i++ { 388 if _, err = swagger.MarshalJSON(); err != nil { 389 b2.Fatal(err) 390 } 391 } 392 }) 393 }