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  }