1
0
mirror of https://github.com/rancher/steve.git synced 2025-09-03 08:25:13 +00:00

Adding tests for schemaDefinitions

This commit is contained in:
Michael Bolot
2023-12-01 12:12:51 -06:00
parent 40f6b10fc7
commit 099ebc509f
3 changed files with 683 additions and 0 deletions

View File

@@ -0,0 +1,325 @@
package definitions
import (
"fmt"
"testing"
"time"
openapi_v2 "github.com/google/gnostic-models/openapiv2"
"github.com/rancher/apiserver/pkg/apierror"
"github.com/rancher/apiserver/pkg/types"
wschemas "github.com/rancher/wrangler/v2/pkg/schemas"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/version"
"k8s.io/client-go/discovery"
"k8s.io/client-go/openapi"
restclient "k8s.io/client-go/rest"
)
var globalRoleObject = types.APIObject{
ID: "management.cattle.io.globalrole",
Type: "schemaDefinition",
Object: schemaDefinition{
DefinitionType: "io.cattle.management.v2.GlobalRole",
Definitions: map[string]definition{
"io.cattle.management.v2.GlobalRole": {
ResourceFields: map[string]definitionField{
"apiVersion": {
Type: "string",
Description: "The APIVersion of this resource",
},
"kind": {
Type: "string",
Description: "The kind",
},
"metadata": {
Type: "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta",
Description: "The metadata",
},
"spec": {
Type: "io.cattle.management.v2.GlobalRole.spec", Description: "The spec for the project",
},
},
Type: "io.cattle.management.v2.GlobalRole",
Description: "A Global Role V2 provides Global Permissions in Rancher",
},
"io.cattle.management.v2.GlobalRole.spec": {
ResourceFields: map[string]definitionField{
"clusterName": {
Type: "string",
Description: "The name of the cluster",
Required: true,
},
"displayName": {
Type: "string",
Description: "The UI readable name",
Required: true,
},
"newField": {
Type: "string",
Description: "A new field not present in v1",
},
"notRequired": {
Type: "boolean",
Description: "Some field that isn't required",
},
},
Type: "io.cattle.management.v2.GlobalRole.spec",
Description: "The spec for the project",
},
"io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta": {
ResourceFields: map[string]definitionField{
"annotations": {
Type: "map",
SubType: "string",
Description: "annotations of the resource",
},
"name": {
Type: "string",
SubType: "",
Description: "name of the resource",
},
},
Type: "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta",
Description: "Object Metadata",
},
},
},
}
func TestByID(t *testing.T) {
schemas := types.EmptyAPISchemas()
addBaseSchema := func(names ...string) {
for _, name := range names {
schemas.MustAddSchema(types.APISchema{
Schema: &wschemas.Schema{
ID: name,
CollectionMethods: []string{"get"},
ResourceMethods: []string{"get"},
},
})
}
}
intPtr := func(input int) *int {
return &input
}
addBaseSchema("management.cattle.io.globalrole", "management.cattle.io.missingfrommodel")
tests := []struct {
name string
schemaName string
needsRefresh bool
openapiError error
serverGroupsResourcesErr error
useBadOpenApiDoc bool
unparseableGV bool
wantObject *types.APIObject
wantError bool
wantErrorCode *int
}{
{
name: "global role definition",
schemaName: "management.cattle.io.globalrole",
needsRefresh: true,
wantObject: &globalRoleObject,
},
{
name: "missing definition",
schemaName: "management.cattle.io.cluster",
needsRefresh: true,
wantError: true,
wantErrorCode: intPtr(404),
},
{
name: "not refreshed",
schemaName: "management.cattle.io.globalrole",
needsRefresh: false,
wantError: true,
wantErrorCode: intPtr(503),
},
{
name: "missing from model",
schemaName: "management.cattle.io.missingfrommodel",
needsRefresh: true,
wantError: true,
wantErrorCode: intPtr(503),
},
{
name: "refresh error - openapi doc unavailable",
schemaName: "management.cattle.io.globalrole",
needsRefresh: true,
openapiError: fmt.Errorf("server unavailable"),
wantError: true,
wantErrorCode: intPtr(500),
},
{
name: "refresh error - unable to parse openapi doc",
schemaName: "management.cattle.io.globalrole",
needsRefresh: true,
useBadOpenApiDoc: true,
wantError: true,
wantErrorCode: intPtr(500),
},
{
name: "refresh error - unable to retrieve groups and resources",
schemaName: "management.cattle.io.globalrole",
needsRefresh: true,
serverGroupsResourcesErr: fmt.Errorf("server not available"),
wantError: true,
wantErrorCode: intPtr(500),
},
{
name: "refresh error - unable to retrieve all groups and resources",
schemaName: "management.cattle.io.globalrole",
needsRefresh: true,
serverGroupsResourcesErr: &discovery.ErrGroupDiscoveryFailed{
Groups: map[schema.GroupVersion]error{
{
Group: "other.cattle.io",
Version: "v1",
}: fmt.Errorf("some group error"),
},
},
wantError: true,
wantErrorCode: intPtr(500),
},
{
name: "refresh error - unparesable gv",
schemaName: "management.cattle.io.globalrole",
needsRefresh: true,
unparseableGV: true,
wantError: true,
wantErrorCode: intPtr(500),
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
client, err := buildDefaultDiscovery()
client.DocumentErr = test.openapiError
client.GroupResourcesErr = test.serverGroupsResourcesErr
if test.useBadOpenApiDoc {
schema := client.Document.Definitions.AdditionalProperties[0]
schema.Value.Type = &openapi_v2.TypeItem{
Value: []string{"multiple", "entries"},
}
}
if test.unparseableGV {
client.Resources = append(client.Resources, &metav1.APIResourceList{
GroupVersion: "not/parse/able",
})
}
require.Nil(t, err)
handler := schemaDefinitionHandler{
client: client,
}
if !test.needsRefresh {
handler.lastRefresh = time.Now()
handler.refreshStale = time.Minute * 1
}
request := types.APIRequest{
Schemas: schemas,
Name: test.schemaName,
}
response, err := handler.byIDHandler(&request)
if test.wantError {
require.Error(t, err)
if test.wantErrorCode != nil {
require.True(t, apierror.IsAPIError(err))
apiErr, _ := err.(*apierror.APIError)
require.Equal(t, *test.wantErrorCode, apiErr.Code.Status)
}
} else {
require.NoError(t, err)
require.Equal(t, *test.wantObject, response)
}
})
}
}
func buildDefaultDiscovery() (*fakeDiscovery, error) {
document, err := openapi_v2.ParseDocument([]byte(openapi_raw))
if err != nil {
return nil, fmt.Errorf("unable to parse openapi document %w", err)
}
groups := []*metav1.APIGroup{
{
Name: "management.cattle.io",
PreferredVersion: metav1.GroupVersionForDiscovery{
Version: "v2",
},
},
}
resources := []*metav1.APIResourceList{
{
GroupVersion: schema.GroupVersion{
Group: "management.cattle.io",
Version: "v2",
}.String(),
APIResources: []metav1.APIResource{
{
Group: "management.cattle.io",
Kind: "GlobalRole",
Version: "v2",
},
},
},
{
GroupVersion: schema.GroupVersion{
Group: "management.cattle.io",
Version: "v1",
}.String(),
APIResources: []metav1.APIResource{
{
Group: "management.cattle.io",
Kind: "GlobalRole",
Version: "v2",
},
},
},
nil,
}
return &fakeDiscovery{
Groups: groups,
Resources: resources,
Document: document,
}, nil
}
type fakeDiscovery struct {
Groups []*metav1.APIGroup
Resources []*metav1.APIResourceList
Document *openapi_v2.Document
GroupResourcesErr error
DocumentErr error
}
// ServerGroupsAndResources is the only method we actually need for the test - just returns what is on the struct
func (f *fakeDiscovery) ServerGroupsAndResources() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
return f.Groups, f.Resources, f.GroupResourcesErr
}
// The rest of these methods are just here to conform to discovery.DiscoveryInterface
func (f *fakeDiscovery) RESTClient() restclient.Interface { return nil }
func (f *fakeDiscovery) ServerGroups() (*metav1.APIGroupList, error) { return nil, nil }
func (f *fakeDiscovery) ServerResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error) {
return nil, nil
}
func (f *fakeDiscovery) ServerPreferredResources() ([]*metav1.APIResourceList, error) {
return nil, nil
}
func (f *fakeDiscovery) ServerPreferredNamespacedResources() ([]*metav1.APIResourceList, error) {
return nil, nil
}
func (f *fakeDiscovery) ServerVersion() (*version.Info, error) { return nil, nil }
func (f *fakeDiscovery) OpenAPISchema() (*openapi_v2.Document, error) {
return f.Document, f.DocumentErr
}
func (f *fakeDiscovery) OpenAPIV3() openapi.Client { return nil }
func (f *fakeDiscovery) WithLegacy() discovery.DiscoveryInterface { return f }

View File

@@ -0,0 +1,99 @@
package definitions
const openapi_raw = `
swagger: "2.0"
info:
title: "Test openapi spec"
version: "v1.0.0"
paths:
/apis/management.cattle.io/v3/globalroles:
get:
description: "get a global role"
responses:
200:
description: "OK"
definitions:
io.cattle.management.v1.GlobalRole:
description: "A Global Role V1 provides Global Permissions in Rancher"
type: "object"
properties:
apiVersion:
description: "The APIVersion of this resource"
type: "string"
kind:
description: "The kind"
type: "string"
metadata:
description: "The metadata"
$ref: "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"
spec:
description: "The spec for the project"
type: "object"
required:
- "clusterName"
- "displayName"
properties:
clusterName:
description: "The name of the cluster"
type: "string"
displayName:
description: "The UI readable name"
type: "string"
notRequired:
description: "Some field that isn't required"
type: "boolean"
x-kubernetes-group-version-kind:
- group: "management.cattle.io"
version: "v1"
kind: "GlobalRole"
io.cattle.management.v2.GlobalRole:
description: "A Global Role V2 provides Global Permissions in Rancher"
type: "object"
properties:
apiVersion:
description: "The APIVersion of this resource"
type: "string"
kind:
description: "The kind"
type: "string"
metadata:
description: "The metadata"
$ref: "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"
spec:
description: "The spec for the project"
type: "object"
required:
- "clusterName"
- "displayName"
properties:
clusterName:
description: "The name of the cluster"
type: "string"
displayName:
description: "The UI readable name"
type: "string"
notRequired:
description: "Some field that isn't required"
type: "boolean"
newField:
description: "A new field not present in v1"
type: "string"
x-kubernetes-group-version-kind:
- group: "management.cattle.io"
version: "v2"
kind: "GlobalRole"
io.management.cattle.NotAKind:
type: "string"
description: "Some string which isn't a kind"
io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta:
description: "Object Metadata"
properties:
annotations:
description: "annotations of the resource"
type: "object"
additionalProperties:
type: "string"
name:
description: "name of the resource"
type: "string"
`

View File

@@ -0,0 +1,259 @@
package definitions
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
"k8s.io/kube-openapi/pkg/util/proto"
)
var (
protoPrimitive = proto.Primitive{
BaseSchema: proto.BaseSchema{
Description: "primitive value",
},
Type: "string",
}
protoPrimitiveInt = proto.Primitive{
BaseSchema: proto.BaseSchema{
Description: "primitive value - int",
},
Type: "integer",
}
protoPrimitiveNumber = proto.Primitive{
BaseSchema: proto.BaseSchema{
Description: "primitive value - number",
},
Type: "number",
}
protoArray = proto.Array{
BaseSchema: proto.BaseSchema{
Description: "testArray",
},
SubType: &protoPrimitive,
}
protoMap = proto.Map{
BaseSchema: proto.BaseSchema{
Description: "testMap",
},
SubType: &protoPrimitive,
}
protoKind = proto.Kind{
BaseSchema: proto.BaseSchema{
Description: "testKind",
Path: proto.NewPath("io.cattle.test"),
},
Fields: map[string]proto.Schema{
"protoArray": &protoArray,
"protoPrimitive": &protoPrimitive,
"protoMap": &protoMap,
},
RequiredFields: []string{
"protoArray",
"protoPrimitive",
"missing",
},
}
protoRefNoSubSchema = testRef{
BaseSchema: proto.BaseSchema{
Description: "testRef - no subSchema",
},
reference: "some-other-type",
}
protoRef = testRef{
BaseSchema: proto.BaseSchema{
Description: "testRef",
},
reference: "testKind",
subSchema: &protoKind,
}
protoArbitrary = proto.Arbitrary{
BaseSchema: proto.BaseSchema{
Description: "testArbitrary",
},
}
)
// testRef implements proto.Reference to test VisitReference
type testRef struct {
proto.BaseSchema
reference string
subSchema proto.Schema
}
func (t *testRef) Reference() string {
return t.reference
}
func (t *testRef) SubSchema() proto.Schema {
return t.subSchema
}
func (t *testRef) Accept(v proto.SchemaVisitor) {
v.VisitReference(t)
}
func (t *testRef) GetName() string {
return fmt.Sprintf("Reference to %q", t.reference)
}
func TestSchemaFieldVisitor(t *testing.T) {
protoKind.Fields["protoRef"] = &protoRef
tests := []struct {
name string
inputSchema proto.Schema
wantDefinitions map[string]definition
wantField definitionField
}{
{
name: "array",
inputSchema: &protoArray,
wantDefinitions: map[string]definition{},
wantField: definitionField{
Type: "array",
Description: protoArray.Description,
SubType: protoPrimitive.Type,
},
},
{
name: "map",
inputSchema: &protoMap,
wantDefinitions: map[string]definition{},
wantField: definitionField{
Type: "map",
Description: protoMap.Description,
SubType: protoPrimitive.Type,
},
},
{
name: "string primitive",
inputSchema: &protoPrimitive,
wantDefinitions: map[string]definition{},
wantField: definitionField{
Type: protoPrimitive.Type,
Description: protoPrimitive.Description,
},
},
{
name: "integer primitive",
inputSchema: &protoPrimitiveInt,
wantDefinitions: map[string]definition{},
wantField: definitionField{
Type: "int",
Description: protoPrimitiveInt.Description,
},
},
{
name: "number primitive",
inputSchema: &protoPrimitiveNumber,
wantDefinitions: map[string]definition{},
wantField: definitionField{
Type: "int",
Description: protoPrimitiveNumber.Description,
},
},
{
name: "kind",
inputSchema: &protoKind,
wantDefinitions: map[string]definition{
protoKind.Path.String(): {
ResourceFields: map[string]definitionField{
"protoArray": {
Type: "array",
Description: protoArray.Description,
SubType: protoPrimitive.Type,
Required: true,
},
"protoMap": {
Type: "map",
Description: protoMap.Description,
SubType: protoPrimitive.Type,
},
"protoPrimitive": {
Type: protoPrimitive.Type,
Description: protoPrimitive.Description,
Required: true,
},
"protoRef": {
Type: protoKind.Path.String(),
Description: protoRef.Description,
},
},
Type: protoKind.Path.String(),
Description: protoKind.Description,
},
},
wantField: definitionField{
Description: protoKind.Description,
Type: protoKind.Path.String(),
},
},
{
name: "reference no subschema",
inputSchema: &protoRefNoSubSchema,
wantDefinitions: map[string]definition{},
wantField: definitionField{
Type: protoRefNoSubSchema.reference,
Description: protoRefNoSubSchema.Description,
},
},
{
name: "reference",
inputSchema: &protoRef,
wantDefinitions: map[string]definition{
protoKind.Path.String(): {
ResourceFields: map[string]definitionField{
"protoArray": {
Type: "array",
Description: protoArray.Description,
SubType: protoPrimitive.Type,
Required: true,
},
"protoMap": {
Type: "map",
Description: protoMap.Description,
SubType: protoPrimitive.Type,
},
"protoPrimitive": {
Type: protoPrimitive.Type,
Description: protoPrimitive.Description,
Required: true,
},
"protoRef": {
Type: protoKind.Path.String(),
Description: protoRef.Description,
},
},
Type: protoKind.Path.String(),
Description: protoKind.Description,
},
},
wantField: definitionField{
Type: protoKind.Path.String(),
Description: protoRef.Description,
},
},
{
name: "abitrary schema",
inputSchema: &protoArbitrary,
wantDefinitions: map[string]definition{},
wantField: definitionField{
Type: "string",
Description: protoArbitrary.Description,
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
definitions := map[string]definition{}
visitor := schemaFieldVisitor{
definitions: definitions,
}
test.inputSchema.Accept(&visitor)
require.Equal(t, test.wantField, visitor.field)
require.Equal(t, test.wantDefinitions, visitor.definitions)
})
}
}