// Copyright 2020 Google LLC // // 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 // // https://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 model import ( "reflect" "testing" "github.com/google/cel-go/common/types" "google.golang.org/protobuf/proto" "k8s.io/apiextensions-apiserver/pkg/apiserver/schema" ) func TestSchemaDeclType(t *testing.T) { ts := testSchema() cust := SchemaDeclType(ts, false) if cust.TypeName() != "object" { t.Errorf("incorrect type name, got %v, wanted object", cust.TypeName()) } if len(cust.Fields) != 4 { t.Errorf("incorrect number of fields, got %d, wanted 4", len(cust.Fields)) } for _, f := range cust.Fields { prop, found := ts.Properties[f.Name] if !found { t.Errorf("type field not found in schema, field: %s", f.Name) } fdv := f.DefaultValue() if prop.Default.Object != nil { pdv := types.DefaultTypeAdapter.NativeToValue(prop.Default.Object) if !reflect.DeepEqual(fdv, pdv) { t.Errorf("field and schema do not agree on default value for field: %s, field value: %v, schema default: %v", f.Name, fdv, pdv) } } if (prop.ValueValidation == nil || len(prop.ValueValidation.Enum) == 0) && len(f.EnumValues()) != 0 { t.Errorf("field had more enum values than the property. field: %s", f.Name) } if prop.ValueValidation != nil { fevs := f.EnumValues() for _, fev := range fevs { found := false for _, pev := range prop.ValueValidation.Enum { celpev := types.DefaultTypeAdapter.NativeToValue(pev.Object) if reflect.DeepEqual(fev, celpev) { found = true break } } if !found { t.Errorf( "could not find field enum value in property definition. field: %s, enum: %v", f.Name, fev) } } } } if ts.ValueValidation != nil { for _, name := range ts.ValueValidation.Required { df, found := cust.FindField(name) if !found { t.Errorf("custom type missing required field. field=%s", name) } if !df.Required { t.Errorf("field marked as required in schema, but optional in type. field=%s", df.Name) } } } } func TestSchemaDeclTypes(t *testing.T) { ts := testSchema() cust := SchemaDeclType(ts, true).MaybeAssignTypeName("CustomObject") typeMap := FieldTypeMap("CustomObject", cust) nested, _ := cust.FindField("nested") metadata, _ := cust.FindField("metadata") expectedObjTypeMap := map[string]*DeclType{ "CustomObject": cust, "CustomObject.nested": nested.Type, "CustomObject.metadata": metadata.Type, } objTypeMap := map[string]*DeclType{} for name, t := range typeMap { if t.IsObject() { objTypeMap[name] = t } } if len(objTypeMap) != len(expectedObjTypeMap) { t.Errorf("got different type set. got=%v, wanted=%v", objTypeMap, expectedObjTypeMap) } for exp, expType := range expectedObjTypeMap { actType, found := objTypeMap[exp] if !found { t.Errorf("missing type in rule types: %s", exp) continue } if !proto.Equal(expType.ExprType(), actType.ExprType()) { t.Errorf("incompatible CEL types. got=%v, wanted=%v", actType.ExprType(), expType.ExprType()) } } } func testSchema() *schema.Structural { // Manual construction of a schema with the following definition: // // schema: // type: object // metadata: // custom_type: "CustomObject" // required: // - name // - value // properties: // name: // type: string // nested: // type: object // properties: // subname: // type: string // flags: // type: object // additionalProperties: // type: boolean // dates: // type: array // items: // type: string // format: date-time // metadata: // type: object // additionalProperties: // type: object // properties: // key: // type: string // values: // type: array // items: string // value: // type: integer // format: int64 // default: 1 // enum: [1,2,3] ts := &schema.Structural{ Generic: schema.Generic{ Type: "object", }, Properties: map[string]schema.Structural{ "name": { Generic: schema.Generic{ Type: "string", }, }, "value": { Generic: schema.Generic{ Type: "integer", Default: schema.JSON{Object: int64(1)}, }, ValueValidation: &schema.ValueValidation{ Format: "int64", Enum: []schema.JSON{{Object: int64(1)}, {Object: int64(2)}, {Object: int64(3)}}, }, }, "nested": { Generic: schema.Generic{ Type: "object", }, Properties: map[string]schema.Structural{ "subname": { Generic: schema.Generic{ Type: "string", }, }, "flags": { Generic: schema.Generic{ Type: "object", AdditionalProperties: &schema.StructuralOrBool{ Structural: &schema.Structural{ Generic: schema.Generic{ Type: "boolean", }, }, }, }, }, "dates": { Generic: schema.Generic{ Type: "array", }, Items: &schema.Structural{ Generic: schema.Generic{ Type: "string", }, ValueValidation: &schema.ValueValidation{ Format: "date-time", }, }, }, }, }, "metadata": { Generic: schema.Generic{ Type: "object", }, Properties: map[string]schema.Structural{ "name": { Generic: schema.Generic{ Type: "string", }, }, "value": { Generic: schema.Generic{ Type: "array", }, Items: &schema.Structural{ Generic: schema.Generic{ Type: "string", }, }, }, }, }, }, } return ts } func arraySchema(arrayType, format string, maxItems *int64) *schema.Structural { return &schema.Structural{ Generic: schema.Generic{ Type: "array", }, Items: &schema.Structural{ Generic: schema.Generic{ Type: arrayType, }, ValueValidation: &schema.ValueValidation{ Format: format, }, }, ValueValidation: &schema.ValueValidation{ MaxItems: maxItems, }, } } func TestEstimateMaxLengthJSON(t *testing.T) { type maxLengthTest struct { Name string InputSchema *schema.Structural ExpectedMaxElements int64 } tests := []maxLengthTest{ { Name: "booleanArray", InputSchema: arraySchema("boolean", "", nil), // expected JSON is [true,true,...], so our length should be (maxRequestSizeBytes - 2) / 5 ExpectedMaxElements: 629145, }, { Name: "durationArray", InputSchema: arraySchema("string", "duration", nil), // expected JSON is ["0","0",...] so our length should be (maxRequestSizeBytes - 2) / 4 ExpectedMaxElements: 786431, }, { Name: "datetimeArray", InputSchema: arraySchema("string", "date-time", nil), // expected JSON is ["2000-01-01T01:01:01","2000-01-01T01:01:01",...] so our length should be (maxRequestSizeBytes - 2) / 22 ExpectedMaxElements: 142987, }, { Name: "dateArray", InputSchema: arraySchema("string", "date", nil), // expected JSON is ["2000-01-01","2000-01-02",...] so our length should be (maxRequestSizeBytes - 2) / 13 ExpectedMaxElements: 241978, }, { Name: "numberArray", InputSchema: arraySchema("integer", "", nil), // expected JSON is [0,0,...] so our length should be (maxRequestSizeBytes - 2) / 2 ExpectedMaxElements: 1572863, }, { Name: "stringArray", InputSchema: arraySchema("string", "", nil), // expected JSON is ["","",...] so our length should be (maxRequestSizeBytes - 2) / 3 ExpectedMaxElements: 1048575, }, { Name: "stringMap", InputSchema: &schema.Structural{ Generic: schema.Generic{ Type: "object", AdditionalProperties: &schema.StructuralOrBool{Structural: &schema.Structural{ Generic: schema.Generic{ Type: "string", }, }}, }, }, // expected JSON is {"":"","":"",...} so our length should be (3000000 - 2) / 6 ExpectedMaxElements: 393215, }, { Name: "objectOptionalPropertyArray", InputSchema: &schema.Structural{ Generic: schema.Generic{ Type: "array", }, Items: &schema.Structural{ Generic: schema.Generic{ Type: "object", }, Properties: map[string]schema.Structural{ "required": schema.Structural{ Generic: schema.Generic{ Type: "string", }, }, "optional": schema.Structural{ Generic: schema.Generic{ Type: "string", }, }, }, ValueValidation: &schema.ValueValidation{ Required: []string{"required"}, }, }, }, // expected JSON is [{"required":"",},{"required":"",},...] so our length should be (maxRequestSizeBytes - 2) / 17 ExpectedMaxElements: 185042, }, { Name: "arrayWithLength", InputSchema: arraySchema("integer", "int64", maxPtr(10)), // manually set by MaxItems ExpectedMaxElements: 10, }, { Name: "stringWithLength", InputSchema: &schema.Structural{ Generic: schema.Generic{ Type: "string", }, ValueValidation: &schema.ValueValidation{ MaxLength: maxPtr(20), }, }, // manually set by MaxLength, but we expect a 4x multiplier compared to the original input // since OpenAPIv3 maxLength uses code points, but DeclType works with bytes ExpectedMaxElements: 80, }, { Name: "mapWithLength", InputSchema: &schema.Structural{ Generic: schema.Generic{ Type: "object", AdditionalProperties: &schema.StructuralOrBool{Structural: &schema.Structural{ Generic: schema.Generic{ Type: "string", }, }}, }, ValueValidation: &schema.ValueValidation{ Format: "string", MaxProperties: maxPtr(15), }, }, // manually set by MaxProperties ExpectedMaxElements: 15, }, { Name: "durationMaxSize", InputSchema: &schema.Structural{ Generic: schema.Generic{ Type: "string", }, ValueValidation: &schema.ValueValidation{ Format: "duration", }, }, // should be exactly equal to maxDurationSizeJSON ExpectedMaxElements: maxDurationSizeJSON, }, { Name: "dateSize", InputSchema: &schema.Structural{ Generic: schema.Generic{ Type: "string", }, ValueValidation: &schema.ValueValidation{ Format: "date", }, }, // should be exactly equal to dateSizeJSON ExpectedMaxElements: dateSizeJSON, }, { Name: "maxdatetimeSize", InputSchema: &schema.Structural{ Generic: schema.Generic{ Type: "string", }, ValueValidation: &schema.ValueValidation{ Format: "date-time", }, }, // should be exactly equal to maxDatetimeSizeJSON ExpectedMaxElements: maxDatetimeSizeJSON, }, { Name: "maxintOrStringSize", InputSchema: &schema.Structural{ Extensions: schema.Extensions{ XIntOrString: true, }, }, // should be exactly equal to maxRequestSizeBytes - 2 (to allow for quotes in the case of a string) ExpectedMaxElements: maxRequestSizeBytes - 2, }, { Name: "objectDefaultFieldArray", InputSchema: &schema.Structural{ Generic: schema.Generic{ Type: "array", }, Items: &schema.Structural{ Generic: schema.Generic{ Type: "object", }, Properties: map[string]schema.Structural{ "field": schema.Structural{ Generic: schema.Generic{ Type: "string", Default: schema.JSON{Object: "default"}, }, }, }, ValueValidation: &schema.ValueValidation{ Required: []string{"field"}, }, }, }, // expected JSON is [{},{},...] so our length should be (maxRequestSizeBytes - 2) / 3 ExpectedMaxElements: 1048575, }, { Name: "byteStringSize", InputSchema: &schema.Structural{ Generic: schema.Generic{ Type: "string", }, ValueValidation: &schema.ValueValidation{ Format: "byte", }, }, // expected JSON is "" so our length should be (maxRequestSizeBytes - 2) ExpectedMaxElements: 3145726, }, { Name: "byteStringSetMaxLength", InputSchema: &schema.Structural{ Generic: schema.Generic{ Type: "string", }, ValueValidation: &schema.ValueValidation{ Format: "byte", MaxLength: maxPtr(20), }, }, // note that unlike regular strings we don't have to take unicode into account, // so we we expect the max length to be exactly equal to the user-supplied one ExpectedMaxElements: 20, }, } for _, testCase := range tests { t.Run(testCase.Name, func(t *testing.T) { decl := SchemaDeclType(testCase.InputSchema, false) if decl.MaxElements != testCase.ExpectedMaxElements { t.Errorf("wrong maxElements (got %d, expected %d)", decl.MaxElements, testCase.ExpectedMaxElements) } }) } } func maxPtr(max int64) *int64 { return &max }