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  }