k8s.io/kube-openapi@v0.0.0-20240228011516-70dd3763d340/pkg/validation/spec/gnostic_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 spec_test 18 19 import ( 20 "encoding/json" 21 "io" 22 "os" 23 "reflect" 24 "testing" 25 "time" 26 27 "github.com/google/gnostic-models/compiler" 28 openapi_v2 "github.com/google/gnostic-models/openapiv2" 29 "github.com/google/go-cmp/cmp" 30 fuzz "github.com/google/gofuzz" 31 "github.com/stretchr/testify/require" 32 "google.golang.org/protobuf/proto" 33 "gopkg.in/yaml.v3" 34 jsontesting "k8s.io/kube-openapi/pkg/util/jsontesting" 35 . "k8s.io/kube-openapi/pkg/validation/spec" 36 ) 37 38 func gnosticCommonTest(t testing.TB, fuzzer *fuzz.Fuzzer) { 39 fuzzer.Funcs( 40 SwaggerFuzzFuncs..., 41 ) 42 43 expected := Swagger{} 44 fuzzer.Fuzz(&expected) 45 46 // Convert to gnostic via JSON to compare 47 jsonBytes, err := expected.MarshalJSON() 48 require.NoError(t, err) 49 50 t.Log("Specimen", string(jsonBytes)) 51 52 gnosticSpec, err := openapi_v2.ParseDocument(jsonBytes) 53 require.NoError(t, err) 54 55 actual := Swagger{} 56 ok, err := actual.FromGnostic(gnosticSpec) 57 require.NoError(t, err) 58 require.True(t, ok) 59 if !cmp.Equal(expected, actual, SwaggerDiffOptions...) { 60 t.Fatal(cmp.Diff(expected, actual, SwaggerDiffOptions...)) 61 } 62 63 newJsonBytes, err := actual.MarshalJSON() 64 require.NoError(t, err) 65 if err := jsontesting.JsonCompare(jsonBytes, newJsonBytes); err != nil { 66 t.Fatal(err) 67 } 68 } 69 70 func TestGnosticConversionSmallDeterministic(t *testing.T) { 71 gnosticCommonTest( 72 t, 73 fuzz. 74 NewWithSeed(15). 75 NilChance(0.8). 76 MaxDepth(10). 77 NumElements(1, 2), 78 ) 79 } 80 81 func TestGnosticConversionSmallDeterministic2(t *testing.T) { 82 // A failed case of TestGnosticConversionSmallRandom 83 // which failed during development/testing loop 84 gnosticCommonTest( 85 t, 86 fuzz. 87 NewWithSeed(1646770841). 88 NilChance(0.8). 89 MaxDepth(10). 90 NumElements(1, 2), 91 ) 92 } 93 94 func TestGnosticConversionSmallDeterministic3(t *testing.T) { 95 // A failed case of TestGnosticConversionSmallRandom 96 // which failed during development/testing loop 97 gnosticCommonTest( 98 t, 99 fuzz. 100 NewWithSeed(1646772024). 101 NilChance(0.8). 102 MaxDepth(10). 103 NumElements(1, 2), 104 ) 105 } 106 107 func TestGnosticConversionSmallDeterministic4(t *testing.T) { 108 // A failed case of TestGnosticConversionSmallRandom 109 // which failed during development/testing loop 110 gnosticCommonTest( 111 t, 112 fuzz. 113 NewWithSeed(1646791953). 114 NilChance(0.8). 115 MaxDepth(10). 116 NumElements(1, 2), 117 ) 118 } 119 120 func TestGnosticConversionSmallDeterministic5(t *testing.T) { 121 // A failed case of TestGnosticConversionSmallRandom 122 // which failed during development/testing loop 123 gnosticCommonTest( 124 t, 125 fuzz. 126 NewWithSeed(1646940131). 127 NilChance(0.8). 128 MaxDepth(10). 129 NumElements(1, 2), 130 ) 131 } 132 133 func TestGnosticConversionSmallDeterministic6(t *testing.T) { 134 // A failed case of TestGnosticConversionSmallRandom 135 // which failed during development/testing loop 136 gnosticCommonTest( 137 t, 138 fuzz. 139 NewWithSeed(1646941926). 140 NilChance(0.8). 141 MaxDepth(10). 142 NumElements(1, 2), 143 ) 144 } 145 146 func TestGnosticConversionSmallDeterministic7(t *testing.T) { 147 // A failed case of TestGnosticConversionSmallRandom 148 // which failed during development/testing loop 149 // This case did not convert nil/empty array within OperationProps.Security 150 // correctly 151 gnosticCommonTest( 152 t, 153 fuzz. 154 NewWithSeed(1647297721085690000). 155 NilChance(0.8). 156 MaxDepth(10). 157 NumElements(1, 2), 158 ) 159 } 160 161 func TestGnosticConversionSmallRandom(t *testing.T) { 162 seed := time.Now().UnixNano() 163 t.Log("Using seed: ", seed) 164 fuzzer := fuzz. 165 NewWithSeed(seed). 166 NilChance(0.8). 167 MaxDepth(10). 168 NumElements(1, 2) 169 170 for i := 0; i <= 50; i++ { 171 gnosticCommonTest( 172 t, 173 fuzzer, 174 ) 175 } 176 } 177 178 func TestGnosticConversionMediumDeterministic(t *testing.T) { 179 gnosticCommonTest( 180 t, 181 fuzz. 182 NewWithSeed(15). 183 NilChance(0.4). 184 MaxDepth(12). 185 NumElements(3, 5), 186 ) 187 } 188 189 func TestGnosticConversionLargeDeterministic(t *testing.T) { 190 gnosticCommonTest( 191 t, 192 fuzz. 193 NewWithSeed(15). 194 NilChance(0.1). 195 MaxDepth(15). 196 NumElements(3, 5), 197 ) 198 } 199 200 func TestGnosticConversionLargeRandom(t *testing.T) { 201 var seed int64 = time.Now().UnixNano() 202 t.Log("Using seed: ", seed) 203 fuzzer := fuzz. 204 NewWithSeed(seed). 205 NilChance(0). 206 MaxDepth(15). 207 NumElements(3, 5) 208 209 for i := 0; i < 5; i++ { 210 gnosticCommonTest( 211 t, 212 fuzzer, 213 ) 214 } 215 } 216 217 func BenchmarkGnosticConversion(b *testing.B) { 218 // Download kube-openapi swagger json 219 swagFile, err := os.Open("../../schemaconv/testdata/swagger.json") 220 if err != nil { 221 b.Fatal(err) 222 } 223 defer swagFile.Close() 224 225 originalJSON, err := io.ReadAll(swagFile) 226 if err != nil { 227 b.Fatal(err) 228 } 229 230 // Parse into kube-openapi types 231 var result *Swagger 232 b.Run("json->swagger", func(b2 *testing.B) { 233 for i := 0; i < b2.N; i++ { 234 if err := json.Unmarshal(originalJSON, &result); err != nil { 235 b2.Fatal(err) 236 } 237 } 238 }) 239 240 // Convert to JSON 241 var encodedJSON []byte 242 b.Run("swagger->json", func(b2 *testing.B) { 243 for i := 0; i < b2.N; i++ { 244 encodedJSON, err = json.Marshal(result) 245 if err != nil { 246 b2.Fatal(err) 247 } 248 } 249 }) 250 251 // Convert to gnostic 252 var originalGnostic *openapi_v2.Document 253 b.Run("json->gnostic", func(b2 *testing.B) { 254 for i := 0; i < b2.N; i++ { 255 originalGnostic, err = openapi_v2.ParseDocument(encodedJSON) 256 if err != nil { 257 b2.Fatal(err) 258 } 259 } 260 }) 261 262 // Convert to PB 263 var encodedProto []byte 264 b.Run("gnostic->pb", func(b2 *testing.B) { 265 for i := 0; i < b2.N; i++ { 266 encodedProto, err = proto.Marshal(originalGnostic) 267 if err != nil { 268 b2.Fatal(err) 269 } 270 } 271 }) 272 273 // Convert to gnostic 274 var backToGnostic openapi_v2.Document 275 b.Run("pb->gnostic", func(b2 *testing.B) { 276 for i := 0; i < b2.N; i++ { 277 if err := proto.Unmarshal(encodedProto, &backToGnostic); err != nil { 278 b2.Fatal(err) 279 } 280 } 281 }) 282 283 for i := 0; i < b.N; i++ { 284 b.Run("gnostic->kube", func(b2 *testing.B) { 285 for i := 0; i < b2.N; i++ { 286 decodedSwagger := &Swagger{} 287 if ok, err := decodedSwagger.FromGnostic(&backToGnostic); err != nil { 288 b2.Fatal(err) 289 } else if !ok { 290 b2.Fatal("conversion lost data") 291 } 292 } 293 }) 294 } 295 } 296 297 // Ensure all variants of SecurityDefinition are being exercised by tests 298 func TestSecurityDefinitionVariants(t *testing.T) { 299 type TestPattern struct { 300 Name string 301 Pattern string 302 } 303 304 patterns := []TestPattern{ 305 { 306 Name: "Basic Authentication", 307 Pattern: `{"type": "basic", "description": "cool basic auth"}`, 308 }, 309 { 310 Name: "API Key Query", 311 Pattern: `{"type": "apiKey", "description": "cool api key auth", "in": "query", "name": "coolAuth"}`, 312 }, 313 { 314 Name: "API Key Header", 315 Pattern: `{"type": "apiKey", "description": "cool api key auth", "in": "header", "name": "coolAuth"}`, 316 }, 317 { 318 Name: "OAuth2 Implicit", 319 Pattern: `{"type": "oauth2", "flow": "implicit", "authorizationUrl": "https://google.com", "scopes": {"scope1": "a scope", "scope2": "a scope"}, "description": "cool oauth2 auth"}`, 320 }, 321 { 322 Name: "OAuth2 Password", 323 Pattern: `{"type": "oauth2", "flow": "password", "tokenUrl": "https://google.com", "scopes": {"scope1": "a scope", "scope2": "a scope"}, "description": "cool oauth2 auth"}`, 324 }, 325 { 326 Name: "OAuth2 Application", 327 Pattern: `{"type": "oauth2", "flow": "application", "tokenUrl": "https://google.com", "scopes": {"scope1": "a scope", "scope2": "a scope"}, "description": "cool oauth2 auth"}`, 328 }, 329 { 330 Name: "OAuth2 Access Code", 331 Pattern: `{"type": "oauth2", "flow": "accessCode", "authorizationUrl": "https://google.com", "tokenUrl": "https://google.com", "scopes": {"scope1": "a scope", "scope2": "a scope"}, "description": "cool oauth2 auth"}`, 332 }, 333 } 334 335 for _, p := range patterns { 336 t.Run(p.Name, func(t *testing.T) { 337 // Parse JSON into yaml 338 var nodes yaml.Node 339 if err := yaml.Unmarshal([]byte(p.Pattern), &nodes); err != nil { 340 t.Error(err) 341 return 342 } else if len(nodes.Content) != 1 { 343 t.Errorf("unexpected yaml parse result") 344 return 345 } 346 347 root := nodes.Content[0] 348 349 parsed, err := openapi_v2.NewSecurityDefinitionsItem(root, compiler.NewContextWithExtensions("$root", root, nil, nil)) 350 if err != nil { 351 t.Error(err) 352 return 353 } 354 355 converted := SecurityScheme{} 356 if err := converted.FromGnostic(parsed); err != nil { 357 t.Error(err) 358 return 359 } 360 361 // Ensure that the same JSON parsed via kube-openapi gives the same 362 // result 363 var expected SecurityScheme 364 if err := json.Unmarshal([]byte(p.Pattern), &expected); err != nil { 365 t.Error(err) 366 return 367 } else if !reflect.DeepEqual(expected, converted) { 368 t.Errorf("expected equal values: %v", cmp.Diff(expected, converted, SwaggerDiffOptions...)) 369 return 370 } 371 }) 372 } 373 } 374 375 // Ensure all variants of Parameter are being exercised by tests 376 func TestParamVariants(t *testing.T) { 377 type TestPattern struct { 378 Name string 379 Pattern string 380 } 381 382 patterns := []TestPattern{ 383 { 384 Name: "Body Parameter", 385 Pattern: `{"in": "body", "name": "myBodyParam", "schema": {}}`, 386 }, 387 { 388 Name: "NonBody Header Parameter", 389 Pattern: `{"in": "header", "name": "myHeaderParam", "description": "a cool parameter", "type": "string", "collectionFormat": "pipes"}`, 390 }, 391 { 392 Name: "NonBody FormData Parameter", 393 Pattern: `{"in": "formData", "name": "myFormDataParam", "description": "a cool parameter", "type": "string", "collectionFormat": "pipes"}`, 394 }, 395 { 396 Name: "NonBody Query Parameter", 397 Pattern: `{"in": "query", "name": "myQueryParam", "description": "a cool parameter", "type": "string", "collectionFormat": "pipes"}`, 398 }, 399 { 400 Name: "NonBody Path Parameter", 401 Pattern: `{"required": true, "in": "path", "name": "myPathParam", "description": "a cool parameter", "type": "string", "collectionFormat": "pipes"}`, 402 }, 403 } 404 405 for _, p := range patterns { 406 t.Run(p.Name, func(t *testing.T) { 407 // Parse JSON into yaml 408 var nodes yaml.Node 409 if err := yaml.Unmarshal([]byte(p.Pattern), &nodes); err != nil { 410 t.Error(err) 411 return 412 } else if len(nodes.Content) != 1 { 413 t.Errorf("unexpected yaml parse result") 414 return 415 } 416 417 root := nodes.Content[0] 418 419 ctx := compiler.NewContextWithExtensions("$root", root, nil, nil) 420 parsed, err := openapi_v2.NewParameter(root, ctx) 421 if err != nil { 422 t.Error(err) 423 return 424 } 425 426 converted := Parameter{} 427 if ok, err := converted.FromGnostic(parsed); err != nil { 428 t.Error(err) 429 return 430 } else if !ok { 431 t.Errorf("expected no data loss while converting parameter: %v", p.Pattern) 432 return 433 } 434 435 // Ensure that the same JSON parsed via kube-openapi gives the same 436 // result 437 var expected Parameter 438 if err := json.Unmarshal([]byte(p.Pattern), &expected); err != nil { 439 t.Error(err) 440 return 441 } else if !reflect.DeepEqual(expected, converted) { 442 t.Errorf("expected equal values: %v", cmp.Diff(expected, converted, SwaggerDiffOptions...)) 443 return 444 } 445 }) 446 } 447 } 448 449 // Test that a few patterns of obvious data loss are detected 450 func TestCommonDataLoss(t *testing.T) { 451 type TestPattern struct { 452 Name string 453 BadInstance string 454 FixedInstance string 455 } 456 457 patterns := []TestPattern{ 458 { 459 Name: "License with Vendor Extension", 460 BadInstance: `{"swagger": "2.0", "info": {"title": "test", "version": "1.0", "license": {"name": "MIT", "x-hello": "ignored"}}, "paths": {}}`, 461 FixedInstance: `{"swagger": "2.0", "info": {"title": "test", "version": "1.0", "license": {"name": "MIT"}}, "paths": {}}`, 462 }, 463 { 464 Name: "Contact with Vendor Extension", 465 BadInstance: `{"swagger": "2.0", "info": {"title": "test", "version": "1.0", "contact": {"name": "bill", "x-hello": "ignored"}}, "paths": {}}`, 466 FixedInstance: `{"swagger": "2.0", "info": {"title": "test", "version": "1.0", "contact": {"name": "bill"}}, "paths": {}}`, 467 }, 468 { 469 Name: "External Documentation with Vendor Extension", 470 BadInstance: `{"swagger": "2.0", "info": {"title": "test", "version": "1.0", "contact": {"name": "bill", "x-hello": "ignored"}}, "paths": {}}`, 471 FixedInstance: `{"swagger": "2.0", "info": {"title": "test", "version": "1.0", "contact": {"name": "bill"}}, "paths": {}}`, 472 }, 473 } 474 475 for _, v := range patterns { 476 t.Run(v.Name, func(t *testing.T) { 477 bad, err := openapi_v2.ParseDocument([]byte(v.BadInstance)) 478 if err != nil { 479 t.Error(err) 480 return 481 } 482 483 fixed, err := openapi_v2.ParseDocument([]byte(v.FixedInstance)) 484 if err != nil { 485 t.Error(err) 486 return 487 } 488 489 badConverted := Swagger{} 490 if ok, err := badConverted.FromGnostic(bad); err != nil { 491 t.Error(err) 492 return 493 } else if ok { 494 t.Errorf("expected test to have data loss") 495 return 496 } 497 498 fixedConverted := Swagger{} 499 if ok, err := fixedConverted.FromGnostic(fixed); err != nil { 500 t.Error(err) 501 return 502 } else if !ok { 503 t.Errorf("expected fixed test to not have data loss") 504 return 505 } 506 507 // Convert JSON directly into our kube-openapi type and check that 508 // it is exactly equal to the converted instance 509 fixedDirect := Swagger{} 510 if err := json.Unmarshal([]byte(v.FixedInstance), &fixedDirect); err != nil { 511 t.Error(err) 512 return 513 } 514 515 if !reflect.DeepEqual(fixedConverted, badConverted) { 516 t.Errorf("expected equal documents: %v", cmp.Diff(fixedConverted, badConverted, SwaggerDiffOptions...)) 517 return 518 } 519 520 // Make sure that they were exactly the same, except for the data loss 521 // by checking JSON encodes the some 522 badConvertedJSON, err := badConverted.MarshalJSON() 523 if err != nil { 524 t.Error(err) 525 return 526 } 527 528 fixedConvertedJSON, err := fixedConverted.MarshalJSON() 529 if err != nil { 530 t.Error(err) 531 return 532 } 533 534 fixedDirectJSON, err := fixedDirect.MarshalJSON() 535 if err != nil { 536 t.Error(err) 537 return 538 } 539 540 if !reflect.DeepEqual(badConvertedJSON, fixedConvertedJSON) { 541 t.Errorf("encoded json values for bad and fixed tests are not identical: %v", cmp.Diff(string(badConvertedJSON), string(fixedConvertedJSON))) 542 } 543 544 if !reflect.DeepEqual(fixedDirectJSON, fixedConvertedJSON) { 545 t.Errorf("encoded json values for fixed direct and fixed-from-gnostic tests are not identical: %v", cmp.Diff(string(fixedDirectJSON), string(fixedConvertedJSON))) 546 } 547 }) 548 } 549 } 550 551 func TestBadStatusCode(t *testing.T) { 552 const testCase = `{"swagger": "2.0", "info": {"title": "test", "version": "1.0"}, "paths": {"/": {"get": {"responses" : { "default": { "$ref": "#/definitions/a" }, "200": { "$ref": "#/definitions/b" }}}}}}` 553 const dropped = `{"swagger": "2.0", "info": {"title": "test", "version": "1.0"}, "paths": {"/": {"get": {"responses" : { "200": { "$ref": "#/definitions/b" }}}}}}` 554 gnosticInstance, err := openapi_v2.ParseDocument([]byte(testCase)) 555 if err != nil { 556 t.Fatal(err) 557 } 558 559 droppedGnosticInstance, err := openapi_v2.ParseDocument([]byte(dropped)) 560 if err != nil { 561 t.Fatal(err) 562 } 563 564 // Manually poke an response code name which gnostic's json parser would not allow 565 gnosticInstance.Paths.Path[0].Value.Get.Responses.ResponseCode[0].Name = "bad" 566 567 badConverted := Swagger{} 568 droppedConverted := Swagger{} 569 570 if ok, err := badConverted.FromGnostic(gnosticInstance); err != nil { 571 t.Fatal(err) 572 } else if ok { 573 t.Fatalf("expected data loss converting an operation with a response code 'bad'") 574 } 575 576 if ok, err := droppedConverted.FromGnostic(droppedGnosticInstance); err != nil { 577 t.Fatal(err) 578 } else if !ok { 579 t.Fatalf("expected no data loss converting a known good operation") 580 } 581 582 // Make sure that they were exactly the same, except for the data loss 583 // by checking JSON encodes the some 584 badConvertedJSON, err := badConverted.MarshalJSON() 585 if err != nil { 586 t.Error(err) 587 return 588 } 589 590 droppedConvertedJSON, err := droppedConverted.MarshalJSON() 591 if err != nil { 592 t.Error(err) 593 return 594 } 595 596 if !reflect.DeepEqual(badConvertedJSON, droppedConvertedJSON) { 597 t.Errorf("encoded json values for bad and fixed tests are not identical: %v", cmp.Diff(string(badConvertedJSON), string(droppedConvertedJSON))) 598 } 599 }