From bb1833cfed214c170213bf0183b218321789bb9a Mon Sep 17 00:00:00 2001 From: Sean Sullivan Date: Wed, 1 Feb 2023 18:39:18 -0800 Subject: [PATCH] Refactor fake versions of openapi client into testing subdir Kubernetes-commit: 869da89eab37ce670759cea2c86325f36625e7fc --- openapi/openapitest/fake.go | 131 ++++++++++++++++++++++++ openapi/openapitest/fake_test.go | 168 +++++++++++++++++++++++++++++++ 2 files changed, 299 insertions(+) create mode 100644 openapi/openapitest/fake.go create mode 100644 openapi/openapitest/fake_test.go diff --git a/openapi/openapitest/fake.go b/openapi/openapitest/fake.go new file mode 100644 index 00000000..30d11727 --- /dev/null +++ b/openapi/openapitest/fake.go @@ -0,0 +1,131 @@ +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package openapitest + +import ( + "errors" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "k8s.io/client-go/openapi" +) + +// NewFileClient returns a pointer to a testing +// FileOpenAPIClient, which parses the OpenAPI GroupVersion +// files in the passed directory path. Example GroupVersion +// OpenAPI filename for apps/v1 would look like: +// +// apis__apps__v1_openapi.json +// +// The current directory housing these hard-coded GroupVersion +// OpenAPI V3 specification files is: +// +// /api/openapi-spec/v3 +// +// An example to invoke this function for the test files: +// +// NewFileClient("../../../api/openapi-spec/v3") +// +// This function will search passed directory for files +// with the suffix "_openapi.json". IMPORTANT: If any file in +// the directory does NOT parse correctly, this function will +// panic. +func NewFileClient(fullDirPath string) openapi.Client { + _, err := os.Stat(fullDirPath) + if err != nil { + panic(fmt.Sprintf("Unable to find test file directory: %s\n", fullDirPath)) + } + files, err := ioutil.ReadDir(fullDirPath) + if err != nil { + panic(fmt.Sprintf("Error reading test file directory: %s (%s)\n", err, fullDirPath)) + } + values := map[string]openapi.GroupVersion{} + for _, fileInfo := range files { + filename := fileInfo.Name() + apiFilename, err := apiFilepath(filename) + if err != nil { + panic(fmt.Sprintf("Error translating file to apipath: %s (%s)\n", err, filename)) + } + fullFilename := filepath.Join(fullDirPath, filename) + gvFile := fileOpenAPIGroupVersion{filepath: fullFilename} + values[apiFilename] = gvFile + } + return &fileOpenAPIClient{values: values} +} + +// fileOpenAPIClient is a testing version implementing the +// openapi.Client interface. This struct stores the hard-coded +// values returned by this file client. +type fileOpenAPIClient struct { + values map[string]openapi.GroupVersion +} + +// fileOpenAPIClient implements the openapi.Client interface. +var _ openapi.Client = &fileOpenAPIClient{} + +// Paths returns the hard-coded map of the api server relative URL +// path string to the GroupVersion swagger bytes. An example Path +// string for apps/v1 GroupVersion is: +// +// apis/apps/v1 +func (f fileOpenAPIClient) Paths() (map[string]openapi.GroupVersion, error) { + return f.values, nil +} + +// fileOpenAPIGroupVersion is a testing version implementing the +// openapi.GroupVersion interface. This struct stores the full +// filepath to the file storing the hard-coded GroupVersion bytes. +type fileOpenAPIGroupVersion struct { + filepath string +} + +// FileOpenAPIGroupVersion implements the openapi.GroupVersion interface. +var _ openapi.GroupVersion = &fileOpenAPIGroupVersion{} + +// Schemas returns the GroupVersion bytes at the stored filepath, or +// an error if one is returned from reading the file. Panics if the +// passed contentType string is not "application/json". +func (f fileOpenAPIGroupVersion) Schema(contentType string) ([]byte, error) { + if contentType != "application/json" { + panic("FileOpenAPI only supports 'application/json' contentType") + } + return ioutil.ReadFile(f.filepath) +} + +// apiFilepath is a helper function to parse a openapi filename +// and transform it to the corresponding api relative url. This function +// is the inverse of the filenaming for OpenAPI V3 specs in the +// hack/update-openapi-spec.sh +// +// Examples: +// +// apis__apps__v1_openapi.json -> apis/apps/v1 +// apis__networking.k8s.io__v1alpha1_openapi.json -> apis/networking.k8s.io/v1alpha1 +// api__v1_openapi.json -> api/v1 +// logs_openapi.json -> logs +func apiFilepath(filename string) (string, error) { + if !strings.HasSuffix(filename, "_openapi.json") { + errStr := fmt.Sprintf("Unable to parse openapi v3 spec filename: %s", filename) + return "", errors.New(errStr) + } + filename = strings.TrimSuffix(filename, "_openapi.json") + filepath := strings.ReplaceAll(filename, "__", "/") + return filepath, nil +} diff --git a/openapi/openapitest/fake_test.go b/openapi/openapitest/fake_test.go new file mode 100644 index 00000000..bda6fdd1 --- /dev/null +++ b/openapi/openapitest/fake_test.go @@ -0,0 +1,168 @@ +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package openapitest + +import ( + "io/ioutil" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var openAPIV3SpecDir = "../../../../../../api/openapi-spec/v3" + +func TestFileOpenAPIClient_Paths(t *testing.T) { + // Directory with OpenAPI V3 spec files + tests := map[string]struct { + path string + found bool + }{ + "apps/v1 path exists": { + path: "apis/apps/v1", + found: true, + }, + "core/v1 path exists": { + path: "api/v1", + found: true, + }, + "batch/v1 path exists": { + path: "apis/batch/v1", + found: true, + }, + "networking/v1alpha1 path exists": { + path: "apis/networking.k8s.io/v1alpha1", + found: true, + }, + "discovery/v1 path exists": { + path: "apis/discovery.k8s.io/v1", + found: true, + }, + "fake path does not exists": { + path: "does/not/exist", + found: false, + }, + } + + fileClient := NewFileClient(openAPIV3SpecDir) + paths, err := fileClient.Paths() + require.NoError(t, err) + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + _, found := paths[tc.path] + if tc.found { + require.True(t, found) + } else { + require.False(t, found) + } + }) + } +} + +func TestFileOpenAPIClient_GroupVersions(t *testing.T) { + tests := map[string]struct { + path string + filename string + }{ + "apps/v1 groupversion spec validation": { + path: "apis/apps/v1", + filename: "apis__apps__v1_openapi.json", + }, + "core/v1 groupversion spec validation": { + path: "api/v1", + filename: "api__v1_openapi.json", + }, + "networking/v1alpha1 groupversion spec validation": { + path: "apis/networking.k8s.io/v1alpha1", + filename: "apis__networking.k8s.io__v1alpha1_openapi.json", + }, + } + + fileClient := NewFileClient(openAPIV3SpecDir) + paths, err := fileClient.Paths() + require.NoError(t, err) + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + gv, found := paths[tc.path] + require.True(t, found) + actualBytes, err := gv.Schema("application/json") + require.NoError(t, err) + expectedBytes, err := ioutil.ReadFile( + filepath.Join(openAPIV3SpecDir, tc.filename)) + require.NoError(t, err) + assert.Equal(t, expectedBytes, actualBytes) + }) + } +} + +func TestFileOpenAPIClient_apiFilePath(t *testing.T) { + tests := map[string]struct { + filename string + expected string + isError bool + }{ + "apps/v1 filename": { + filename: "apis__apps__v1_openapi.json", + expected: "apis/apps/v1", + }, + "core/v1 filename": { + filename: "api__v1_openapi.json", + expected: "api/v1", + }, + "logs filename": { + filename: "logs_openapi.json", + expected: "logs", + }, + "api filename": { + filename: "api_openapi.json", + expected: "api", + }, + "unversioned autoscaling filename": { + filename: "apis__autoscaling_openapi.json", + expected: "apis/autoscaling", + }, + "networking/v1alpha1 filename": { + filename: "apis__networking.k8s.io__v1alpha1_openapi.json", + expected: "apis/networking.k8s.io/v1alpha1", + }, + "batch/v1beta1 filename": { + filename: "apis__batch__v1beta1_openapi.json", + expected: "apis/batch/v1beta1", + }, + "non-JSON suffix is invalid": { + filename: "apis__networking.k8s.io__v1alpha1_openapi.yaml", + isError: true, + }, + "missing final openapi before suffix is invalid": { + filename: "apis__apps__v1_something.json", + isError: true, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + actual, err := apiFilepath(tc.filename) + if !tc.isError { + require.NoError(t, err) + assert.Equal(t, tc.expected, actual) + } else { + require.Error(t, err) + } + }) + } +}