cover additional types in unstructured roundtrip test

Co-authored-by: Ben Luddy <bluddy@redhat.com>
This commit is contained in:
Luis Sanchez 2024-07-05 15:40:52 -04:00
parent 3d6c99e1a7
commit aaa7364f60
10 changed files with 1470 additions and 243 deletions

View File

@ -144,7 +144,7 @@ func TestRoundtripToUnstructured(t *testing.T) {
}
}
roundtrip.RoundtripToUnstructured(t, legacyscheme.Scheme, FuzzerFuncs, skipped)
roundtrip.RoundtripToUnstructured(t, legacyscheme.Scheme, FuzzerFuncs, skipped, nil)
}
func TestRoundTripWithEmptyCreationTimestamp(t *testing.T) {

View File

@ -8,6 +8,7 @@ godebug default=go1.23
require (
github.com/emicklei/go-restful/v3 v3.11.0
github.com/fxamacker/cbor/v2 v2.7.0
github.com/gogo/protobuf v1.3.2
github.com/google/cel-go v0.21.0
github.com/google/gnostic-models v0.6.8
@ -53,7 +54,6 @@ require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect

View File

@ -20,12 +20,42 @@ import (
"bytes"
"errors"
cbor "k8s.io/apimachinery/pkg/runtime/serializer/cbor/direct"
"k8s.io/apimachinery/pkg/util/json"
)
var jsTrue = []byte("true")
var jsFalse = []byte("false")
// The CBOR parsing related constants and functions below are not exported so they can be
// easily removed at a future date when the CBOR library provides equivalent functionality.
type cborMajorType int
const (
// https://www.rfc-editor.org/rfc/rfc8949.html#section-3.1
cborUnsignedInteger cborMajorType = 0
cborNegativeInteger cborMajorType = 1
cborByteString cborMajorType = 2
cborTextString cborMajorType = 3
cborArray cborMajorType = 4
cborMap cborMajorType = 5
cborTag cborMajorType = 6
cborOther cborMajorType = 7
)
const (
// from https://www.rfc-editor.org/rfc/rfc8949.html#name-jump-table-for-initial-byte.
// additionally, see https://www.rfc-editor.org/rfc/rfc8949.html#section-3.3-5.
cborFalseValue = 0xf4
cborTrueValue = 0xf5
cborNullValue = 0xf6
)
func cborType(b byte) cborMajorType {
return cborMajorType(b >> 5)
}
func (s JSONSchemaPropsOrBool) MarshalJSON() ([]byte, error) {
if s.Schema != nil {
return json.Marshal(s.Schema)
@ -59,6 +89,39 @@ func (s *JSONSchemaPropsOrBool) UnmarshalJSON(data []byte) error {
return nil
}
func (s JSONSchemaPropsOrBool) MarshalCBOR() ([]byte, error) {
if s.Schema != nil {
return cbor.Marshal(s.Schema)
}
return cbor.Marshal(s.Allows)
}
func (s *JSONSchemaPropsOrBool) UnmarshalCBOR(data []byte) error {
switch {
case len(data) == 0:
// ideally we would avoid modifying *s here, but we are matching the behavior of UnmarshalJSON
*s = JSONSchemaPropsOrBool{}
return nil
case cborType(data[0]) == cborMap:
var p JSONSchemaProps
if err := cbor.Unmarshal(data, &p); err != nil {
return err
}
*s = JSONSchemaPropsOrBool{Allows: true, Schema: &p}
return nil
case data[0] == cborTrueValue:
*s = JSONSchemaPropsOrBool{Allows: true}
return nil
case data[0] == cborFalseValue:
*s = JSONSchemaPropsOrBool{Allows: false}
return nil
default:
// ideally, this case would not also capture a null input value,
// but we are matching the behavior of the UnmarshalJSON
return errors.New("boolean or JSON schema expected")
}
}
func (s JSONSchemaPropsOrStringArray) MarshalJSON() ([]byte, error) {
if len(s.Property) > 0 {
return json.Marshal(s.Property)
@ -91,6 +154,40 @@ func (s *JSONSchemaPropsOrStringArray) UnmarshalJSON(data []byte) error {
return nil
}
func (s JSONSchemaPropsOrStringArray) MarshalCBOR() ([]byte, error) {
if len(s.Property) > 0 {
return cbor.Marshal(s.Property)
}
if s.Schema != nil {
return cbor.Marshal(s.Schema)
}
return cbor.Marshal(nil)
}
func (s *JSONSchemaPropsOrStringArray) UnmarshalCBOR(data []byte) error {
if len(data) > 0 && cborType(data[0]) == cborArray {
var a []string
if err := cbor.Unmarshal(data, &a); err != nil {
return err
}
*s = JSONSchemaPropsOrStringArray{Property: a}
return nil
}
if len(data) > 0 && cborType(data[0]) == cborMap {
var p JSONSchemaProps
if err := cbor.Unmarshal(data, &p); err != nil {
return err
}
*s = JSONSchemaPropsOrStringArray{Schema: &p}
return nil
}
// At this point we either have: empty data, a null value, or an
// unexpected type. In order to match the behavior of the existing
// UnmarshalJSON, no error is returned and *s is overwritten here.
*s = JSONSchemaPropsOrStringArray{}
return nil
}
func (s JSONSchemaPropsOrArray) MarshalJSON() ([]byte, error) {
if len(s.JSONSchemas) > 0 {
return json.Marshal(s.JSONSchemas)
@ -120,6 +217,37 @@ func (s *JSONSchemaPropsOrArray) UnmarshalJSON(data []byte) error {
return nil
}
func (s JSONSchemaPropsOrArray) MarshalCBOR() ([]byte, error) {
if len(s.JSONSchemas) > 0 {
return cbor.Marshal(s.JSONSchemas)
}
return cbor.Marshal(s.Schema)
}
func (s *JSONSchemaPropsOrArray) UnmarshalCBOR(data []byte) error {
if len(data) > 0 && cborType(data[0]) == cborMap {
var p JSONSchemaProps
if err := cbor.Unmarshal(data, &p); err != nil {
return err
}
*s = JSONSchemaPropsOrArray{Schema: &p}
return nil
}
if len(data) > 0 && cborType(data[0]) == cborArray {
var a []JSONSchemaProps
if err := cbor.Unmarshal(data, &a); err != nil {
return err
}
*s = JSONSchemaPropsOrArray{JSONSchemas: a}
return nil
}
// At this point we either have: empty data, a null value, or an
// unexpected type. In order to match the behavior of the existing
// UnmarshalJSON, no error is returned and *s is overwritten here.
*s = JSONSchemaPropsOrArray{}
return nil
}
func (s JSON) MarshalJSON() ([]byte, error) {
if len(s.Raw) > 0 {
return s.Raw, nil
@ -134,3 +262,34 @@ func (s *JSON) UnmarshalJSON(data []byte) error {
}
return nil
}
func (s JSON) MarshalCBOR() ([]byte, error) {
// Note that non-semantic whitespace is lost during the transcoding performed here.
// We do not forsee this to be a problem given the current known uses of this type.
// Other limitations that arise when roundtripping JSON via dynamic clients also apply
// here, for example: insignificant whitespace handling, number handling, and map key ordering.
if len(s.Raw) == 0 {
return []byte{cborNullValue}, nil
}
var u any
if err := json.Unmarshal(s.Raw, &u); err != nil {
return nil, err
}
return cbor.Marshal(u)
}
func (s *JSON) UnmarshalCBOR(data []byte) error {
if len(data) == 0 || data[0] == cborNullValue {
return nil
}
var u any
if err := cbor.Unmarshal(data, &u); err != nil {
return err
}
raw, err := json.Marshal(u)
if err != nil {
return err
}
s.Raw = raw
return nil
}

View File

@ -18,133 +18,522 @@ package v1
import (
"encoding/json"
"reflect"
"math"
"testing"
"github.com/fxamacker/cbor/v2"
"github.com/google/go-cmp/cmp"
)
type JSONSchemaPropsOrBoolHolder struct {
JSPoB JSONSchemaPropsOrBool `json:"val1"`
JSPoBOmitEmpty *JSONSchemaPropsOrBool `json:"val2,omitempty"`
type marshalTestable interface {
json.Marshaler
cbor.Marshaler
}
func TestJSONSchemaPropsOrBoolUnmarshalJSON(t *testing.T) {
cases := []struct {
input string
result JSONSchemaPropsOrBoolHolder
}{
{`{}`, JSONSchemaPropsOrBoolHolder{}},
{`{"val1": {}}`, JSONSchemaPropsOrBoolHolder{JSPoB: JSONSchemaPropsOrBool{Allows: true, Schema: &JSONSchemaProps{}}}},
{`{"val1": {"type":"string"}}`, JSONSchemaPropsOrBoolHolder{JSPoB: JSONSchemaPropsOrBool{Allows: true, Schema: &JSONSchemaProps{Type: "string"}}}},
{`{"val1": false}`, JSONSchemaPropsOrBoolHolder{JSPoB: JSONSchemaPropsOrBool{}}},
{`{"val1": true}`, JSONSchemaPropsOrBoolHolder{JSPoB: JSONSchemaPropsOrBool{Allows: true}}},
{`{"val2": {}}`, JSONSchemaPropsOrBoolHolder{JSPoBOmitEmpty: &JSONSchemaPropsOrBool{Allows: true, Schema: &JSONSchemaProps{}}}},
{`{"val2": {"type":"string"}}`, JSONSchemaPropsOrBoolHolder{JSPoBOmitEmpty: &JSONSchemaPropsOrBool{Allows: true, Schema: &JSONSchemaProps{Type: "string"}}}},
{`{"val2": false}`, JSONSchemaPropsOrBoolHolder{JSPoBOmitEmpty: &JSONSchemaPropsOrBool{}}},
{`{"val2": true}`, JSONSchemaPropsOrBoolHolder{JSPoBOmitEmpty: &JSONSchemaPropsOrBool{Allows: true}}},
}
for _, c := range cases {
var result JSONSchemaPropsOrBoolHolder
if err := json.Unmarshal([]byte(c.input), &result); err != nil {
t.Errorf("Failed to unmarshal input '%v': %v", c.input, err)
}
if !reflect.DeepEqual(result, c.result) {
t.Errorf("Failed to unmarshal input '%v': expected %+v, got %+v", c.input, c.result, result)
}
}
type marshalTestCase struct {
name string
input marshalTestable
wantJSONError bool
wantCBORError bool
wantJSON []byte
wantCBOR []byte
}
func TestStringArrayOrStringMarshalJSON(t *testing.T) {
cases := []struct {
input JSONSchemaPropsOrBoolHolder
result string
}{
{JSONSchemaPropsOrBoolHolder{}, `{"val1":false}`},
type unmarshalTestable interface {
json.Unmarshaler
cbor.Unmarshaler
}
{JSONSchemaPropsOrBoolHolder{JSPoB: JSONSchemaPropsOrBool{Schema: &JSONSchemaProps{}}}, `{"val1":{}}`},
{JSONSchemaPropsOrBoolHolder{JSPoB: JSONSchemaPropsOrBool{Schema: &JSONSchemaProps{Type: "string"}}}, `{"val1":{"type":"string"}}`},
{JSONSchemaPropsOrBoolHolder{JSPoB: JSONSchemaPropsOrBool{}}, `{"val1":false}`},
{JSONSchemaPropsOrBoolHolder{JSPoB: JSONSchemaPropsOrBool{Allows: true}}, `{"val1":true}`},
type unmarshalTestCase struct {
name string
inputJSON []byte
inputCBOR []byte
wantJSONError bool
wantCBORError bool
wantDecoded unmarshalTestable
}
{JSONSchemaPropsOrBoolHolder{JSPoBOmitEmpty: &JSONSchemaPropsOrBool{Schema: &JSONSchemaProps{}}}, `{"val1":false,"val2":{}}`},
{JSONSchemaPropsOrBoolHolder{JSPoBOmitEmpty: &JSONSchemaPropsOrBool{Schema: &JSONSchemaProps{Type: "string"}}}, `{"val1":false,"val2":{"type":"string"}}`},
{JSONSchemaPropsOrBoolHolder{JSPoBOmitEmpty: &JSONSchemaPropsOrBool{}}, `{"val1":false,"val2":false}`},
{JSONSchemaPropsOrBoolHolder{JSPoBOmitEmpty: &JSONSchemaPropsOrBool{Allows: true}}, `{"val1":false,"val2":true}`},
}
type roundTripTestable interface {
marshalTestable
unmarshalTestable
}
for _, c := range cases {
result, err := json.Marshal(&c.input)
type roundTripTestCase struct {
name string
input roundTripTestable
wantJSON []byte
wantCBOR []byte
wantDecoded roundTripTestable
}
func TestJSONSchemaPropsOrBool(t *testing.T) {
nan := math.NaN()
t.Run("Marshal", func(t *testing.T) {
testCases := []marshalTestCase{
{
name: "unsupported value",
input: &JSONSchemaPropsOrBool{
Schema: &JSONSchemaProps{Maximum: &nan},
},
wantJSONError: true,
wantCBORError: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("json", marshalJSONTest(tc.input, tc.wantJSONError, tc.wantJSON))
t.Run("cbor", marshalCBORTest(tc.input, tc.wantCBORError, tc.wantCBOR))
})
}
})
t.Run("RoundTrip", func(t *testing.T) {
testCases := []roundTripTestCase{
{
name: "zero value",
input: &JSONSchemaPropsOrBool{},
wantDecoded: &JSONSchemaPropsOrBool{},
wantJSON: []byte(`false`),
wantCBOR: []byte{cborFalseValue},
},
{
name: "bool false",
input: &JSONSchemaPropsOrBool{Allows: false},
wantDecoded: &JSONSchemaPropsOrBool{Allows: false},
wantJSON: []byte(`false`),
wantCBOR: []byte{cborFalseValue},
},
{
name: "bool true",
input: &JSONSchemaPropsOrBool{Allows: true},
wantDecoded: &JSONSchemaPropsOrBool{Allows: true},
wantJSON: []byte(`true`),
wantCBOR: []byte{cborTrueValue},
},
{
name: "with props",
input: &JSONSchemaPropsOrBool{Schema: &JSONSchemaProps{Type: "string"}},
wantDecoded: &JSONSchemaPropsOrBool{Allows: true, Schema: &JSONSchemaProps{Type: "string"}},
wantJSON: []byte(`{"type":"string"}`),
wantCBOR: []byte{0xA1, 0x44, 't', 'y', 'p', 'e', 0x46, 's', 't', 'r', 'i', 'n', 'g'},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("json", roundTripJSONTest(tc.input, tc.wantJSON, tc.wantDecoded, &JSONSchemaPropsOrBool{}))
t.Run("cbor", roundTripCBORTest(tc.input, tc.wantCBOR, tc.wantDecoded, &JSONSchemaPropsOrBool{}))
})
}
})
t.Run("Unmarshal", func(t *testing.T) {
testCases := []unmarshalTestCase{
{
name: "legacy behavior",
inputJSON: []byte(`{}`),
inputCBOR: []byte{0xA0},
wantDecoded: &JSONSchemaPropsOrBool{Allows: true, Schema: &JSONSchemaProps{}},
},
{
name: "null",
inputJSON: []byte(`null`),
inputCBOR: []byte{cborNullValue},
wantJSONError: true,
wantCBORError: true,
},
{
name: "zero len input",
inputJSON: []byte{},
inputCBOR: []byte{},
wantDecoded: &JSONSchemaPropsOrBool{},
},
{
name: "unsupported type",
inputJSON: []byte(`42`),
inputCBOR: []byte{0x18, 42},
wantJSONError: true,
wantCBORError: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("json", unmarshalJSONTest(tc.inputJSON, tc.wantJSONError, tc.wantDecoded, &JSONSchemaPropsOrBool{}))
t.Run("cbor", unmarshalCBORTest(tc.inputCBOR, tc.wantCBORError, tc.wantDecoded, &JSONSchemaPropsOrBool{}))
})
}
})
}
func TestJSONSchemaPropsOrArray(t *testing.T) {
nan := math.NaN()
t.Run("Marshal", func(t *testing.T) {
testCases := []marshalTestCase{
{
name: "unsupported value",
input: &JSONSchemaPropsOrArray{Schema: &JSONSchemaProps{Maximum: &nan}},
wantJSONError: true,
wantCBORError: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("json", marshalJSONTest(tc.input, tc.wantJSONError, tc.wantJSON))
t.Run("cbor", marshalCBORTest(tc.input, tc.wantCBORError, tc.wantCBOR))
})
}
})
t.Run("RoundTrip", func(t *testing.T) {
testCases := []roundTripTestCase{
{
name: "empty props",
input: &JSONSchemaPropsOrArray{Schema: &JSONSchemaProps{}},
wantDecoded: &JSONSchemaPropsOrArray{Schema: &JSONSchemaProps{}},
wantJSON: []byte(`{}`),
wantCBOR: []byte{0xA0},
},
{
name: "props",
input: &JSONSchemaPropsOrArray{Schema: &JSONSchemaProps{Type: "string"}},
wantDecoded: &JSONSchemaPropsOrArray{Schema: &JSONSchemaProps{Type: "string"}},
wantJSON: []byte(`{"type":"string"}`),
wantCBOR: []byte{0xA1, 0x44, 't', 'y', 'p', 'e', 0x46, 's', 't', 'r', 'i', 'n', 'g'},
},
{
name: "array with empty props",
input: &JSONSchemaPropsOrArray{JSONSchemas: []JSONSchemaProps{{}}},
wantDecoded: &JSONSchemaPropsOrArray{JSONSchemas: []JSONSchemaProps{{}}},
wantJSON: []byte(`[{}]`),
wantCBOR: []byte{0x81, 0xA0},
},
{
name: "array with empty props and props",
input: &JSONSchemaPropsOrArray{JSONSchemas: []JSONSchemaProps{{}, {Type: "string"}}},
wantDecoded: &JSONSchemaPropsOrArray{JSONSchemas: []JSONSchemaProps{{}, {Type: "string"}}},
wantJSON: []byte(`[{},{"type":"string"}]`),
wantCBOR: []byte{0x82, 0xA0, 0xA1, 0x44, 't', 'y', 'p', 'e', 0x46, 's', 't', 'r', 'i', 'n', 'g'},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("json", roundTripJSONTest(tc.input, tc.wantJSON, tc.wantDecoded, &JSONSchemaPropsOrArray{}))
t.Run("cbor", roundTripCBORTest(tc.input, tc.wantCBOR, tc.wantDecoded, &JSONSchemaPropsOrArray{}))
})
}
})
t.Run("Unmarshal", func(t *testing.T) {
testCases := []unmarshalTestCase{
{
name: "null",
inputJSON: []byte(`null`),
inputCBOR: []byte{cborNullValue},
wantDecoded: &JSONSchemaPropsOrArray{},
},
{
name: "zero len input",
inputJSON: []byte{},
inputCBOR: []byte{},
wantDecoded: &JSONSchemaPropsOrArray{},
},
{
name: "unsupported type",
inputJSON: []byte(`42`),
inputCBOR: []byte{0x18, 42},
wantDecoded: &JSONSchemaPropsOrArray{},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("json", unmarshalJSONTest(tc.inputJSON, tc.wantJSONError, tc.wantDecoded, &JSONSchemaPropsOrArray{}))
t.Run("cbor", unmarshalCBORTest(tc.inputCBOR, tc.wantCBORError, tc.wantDecoded, &JSONSchemaPropsOrArray{}))
})
}
})
}
func TestJSONSchemaPropsOrStringArray(t *testing.T) {
nan := math.NaN()
t.Run("Marshal", func(t *testing.T) {
testCases := []marshalTestCase{
{
name: "unsupported value",
input: JSONSchemaPropsOrStringArray{Schema: &JSONSchemaProps{Maximum: &nan}},
wantJSONError: true,
wantCBORError: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("json", marshalJSONTest(tc.input, tc.wantJSONError, tc.wantJSON))
t.Run("cbor", marshalCBORTest(tc.input, tc.wantCBORError, tc.wantCBOR))
})
}
})
t.Run("RoundTrip", func(t *testing.T) {
testCases := []roundTripTestCase{
{
name: "empty props",
input: &JSONSchemaPropsOrStringArray{Schema: &JSONSchemaProps{}},
wantDecoded: &JSONSchemaPropsOrStringArray{Schema: &JSONSchemaProps{}},
wantJSON: []byte(`{}`),
wantCBOR: []byte{0xA0},
},
{
name: "props",
input: &JSONSchemaPropsOrStringArray{Schema: &JSONSchemaProps{Type: "string"}},
wantDecoded: &JSONSchemaPropsOrStringArray{Schema: &JSONSchemaProps{Type: "string"}},
wantJSON: []byte(`{"type":"string"}`),
wantCBOR: []byte{0xA1, 0x44, 't', 'y', 'p', 'e', 0x46, 's', 't', 'r', 'i', 'n', 'g'},
},
{
name: "empty array",
input: &JSONSchemaPropsOrStringArray{Property: []string{}},
wantDecoded: &JSONSchemaPropsOrStringArray{Property: nil},
wantJSON: []byte(`null`),
wantCBOR: []byte{cborNullValue},
},
{
name: "array value",
input: &JSONSchemaPropsOrStringArray{Property: []string{"string"}},
wantDecoded: &JSONSchemaPropsOrStringArray{Property: []string{"string"}},
wantJSON: []byte(`["string"]`),
wantCBOR: []byte{0x81, 0x46, 's', 't', 'r', 'i', 'n', 'g'},
},
{
name: "both props and array",
input: &JSONSchemaPropsOrStringArray{Schema: &JSONSchemaProps{Type: "props"}, Property: []string{"string"}},
wantDecoded: &JSONSchemaPropsOrStringArray{Schema: nil, Property: []string{"string"}},
wantJSON: []byte(`["string"]`),
wantCBOR: []byte{0x81, 0x46, 's', 't', 'r', 'i', 'n', 'g'},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("json", roundTripJSONTest(tc.input, tc.wantJSON, tc.wantDecoded, &JSONSchemaPropsOrStringArray{}))
t.Run("cbor", roundTripCBORTest(tc.input, tc.wantCBOR, tc.wantDecoded, &JSONSchemaPropsOrStringArray{}))
})
}
})
t.Run("Unmarshal", func(t *testing.T) {
testCases := []unmarshalTestCase{
{
name: "empty array",
inputJSON: []byte(`[]`),
inputCBOR: []byte{0x80},
wantDecoded: &JSONSchemaPropsOrStringArray{Property: []string{}},
},
{
name: "null",
inputJSON: []byte(`null`),
inputCBOR: []byte{cborNullValue},
wantDecoded: &JSONSchemaPropsOrStringArray{},
},
{
name: "zero len input",
inputJSON: []byte{},
inputCBOR: []byte{},
wantDecoded: &JSONSchemaPropsOrStringArray{},
},
{
name: "unsupported type",
inputJSON: []byte(`42`),
inputCBOR: []byte{0x18, 42},
wantDecoded: &JSONSchemaPropsOrStringArray{},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("json", unmarshalJSONTest(tc.inputJSON, tc.wantJSONError, tc.wantDecoded, &JSONSchemaPropsOrStringArray{}))
t.Run("cbor", unmarshalCBORTest(tc.inputCBOR, tc.wantCBORError, tc.wantDecoded, &JSONSchemaPropsOrStringArray{}))
})
}
})
}
func TestJSON(t *testing.T) {
t.Run("RoundTrip", func(t *testing.T) {
testCases := []roundTripTestCase{
{
name: "nil raw",
input: &JSON{Raw: nil},
wantDecoded: &JSON{Raw: nil},
wantJSON: []byte(`null`),
wantCBOR: []byte{cborNullValue},
},
{
name: "zero len raw",
input: &JSON{Raw: []byte{}},
wantDecoded: &JSON{Raw: nil},
wantJSON: []byte(`null`),
wantCBOR: []byte{cborNullValue},
},
{
name: "empty",
input: &JSON{},
wantDecoded: &JSON{},
wantJSON: []byte(`null`),
wantCBOR: []byte{cborNullValue},
},
{
name: "string",
input: &JSON{Raw: []byte(`"string"`)},
wantDecoded: &JSON{Raw: []byte(`"string"`)},
wantJSON: []byte(`"string"`),
wantCBOR: []byte{0x46, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67},
},
{
name: "number",
input: &JSON{Raw: []byte(`42.01`)},
wantDecoded: &JSON{Raw: []byte(`42.01`)},
wantJSON: []byte(`42.01`),
wantCBOR: []byte{0xFB, 0x40, 0x45, 0x01, 0x47, 0xAE, 0x14, 0x7A, 0xE1},
},
{
name: "bool",
input: &JSON{Raw: []byte(`true`)},
wantDecoded: &JSON{Raw: []byte(`true`)},
wantJSON: []byte(`true`),
wantCBOR: []byte{0xF5},
},
{
name: "array",
input: &JSON{Raw: []byte(`[1,2,3]`)},
wantDecoded: &JSON{Raw: []byte(`[1,2,3]`)},
wantJSON: []byte(`[1,2,3]`),
wantCBOR: []byte{0x83, 1, 2, 3},
},
{
name: "map",
input: &JSON{Raw: []byte(`{"foo":"bar"}`)},
wantDecoded: &JSON{Raw: []byte(`{"foo":"bar"}`)},
wantJSON: []byte(`{"foo":"bar"}`),
wantCBOR: []byte{0xA1, 0x43, 'f', 'o', 'o', 0x43, 'b', 'a', 'r'},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("json", roundTripJSONTest(tc.input, tc.wantJSON, tc.wantDecoded, &JSON{}))
t.Run("cbor", roundTripCBORTest(tc.input, tc.wantCBOR, tc.wantDecoded, &JSON{}))
})
}
})
}
func marshalJSONTest(input marshalTestable, wantErr bool, expected []byte) func(t *testing.T) {
return func(t *testing.T) {
actual, err := input.MarshalJSON()
if (err != nil) != wantErr {
if wantErr {
t.Fatal("expected error")
}
t.Fatalf("unexpected error: %v", err)
}
if err != nil {
t.Errorf("Unexpected error marshaling input '%v': %v", c.input, err)
return
}
if string(result) != c.result {
t.Errorf("Failed to marshal input '%v': expected: %q, got %q", c.input, c.result, string(result))
if diff := cmp.Diff(string(expected), string(actual)); len(diff) > 0 {
t.Fatal(diff)
}
}
}
type JSONSchemaPropsOrArrayHolder struct {
JSPoA JSONSchemaPropsOrArray `json:"val1"`
JSPoAOmitEmpty *JSONSchemaPropsOrArray `json:"val2,omitempty"`
}
func TestJSONSchemaPropsOrArrayUnmarshalJSON(t *testing.T) {
cases := []struct {
input string
result JSONSchemaPropsOrArrayHolder
}{
{`{}`, JSONSchemaPropsOrArrayHolder{}},
{`{"val1": {}}`, JSONSchemaPropsOrArrayHolder{JSPoA: JSONSchemaPropsOrArray{Schema: &JSONSchemaProps{}}}},
{`{"val1": {"type":"string"}}`, JSONSchemaPropsOrArrayHolder{JSPoA: JSONSchemaPropsOrArray{Schema: &JSONSchemaProps{Type: "string"}}}},
{`{"val1": [{}]}`, JSONSchemaPropsOrArrayHolder{JSPoA: JSONSchemaPropsOrArray{JSONSchemas: []JSONSchemaProps{{}}}}},
{`{"val1": [{},{"type":"string"}]}`, JSONSchemaPropsOrArrayHolder{JSPoA: JSONSchemaPropsOrArray{JSONSchemas: []JSONSchemaProps{{}, {Type: "string"}}}}},
{`{"val2": {}}`, JSONSchemaPropsOrArrayHolder{JSPoAOmitEmpty: &JSONSchemaPropsOrArray{Schema: &JSONSchemaProps{}}}},
{`{"val2": {"type":"string"}}`, JSONSchemaPropsOrArrayHolder{JSPoAOmitEmpty: &JSONSchemaPropsOrArray{Schema: &JSONSchemaProps{Type: "string"}}}},
{`{"val2": [{}]}`, JSONSchemaPropsOrArrayHolder{JSPoAOmitEmpty: &JSONSchemaPropsOrArray{JSONSchemas: []JSONSchemaProps{{}}}}},
{`{"val2": [{},{"type":"string"}]}`, JSONSchemaPropsOrArrayHolder{JSPoAOmitEmpty: &JSONSchemaPropsOrArray{JSONSchemas: []JSONSchemaProps{{}, {Type: "string"}}}}},
}
for _, c := range cases {
var result JSONSchemaPropsOrArrayHolder
if err := json.Unmarshal([]byte(c.input), &result); err != nil {
t.Errorf("Failed to unmarshal input '%v': %v", c.input, err)
func marshalCBORTest(input marshalTestable, wantErr bool, expected []byte) func(t *testing.T) {
return func(t *testing.T) {
actual, err := input.MarshalCBOR()
if (err != nil) != wantErr {
if wantErr {
t.Fatal("expected error")
}
t.Fatalf("unexpected error: %v", err)
}
if !reflect.DeepEqual(result, c.result) {
t.Errorf("Failed to unmarshal input '%v': expected %+v, got %+v", c.input, c.result, result)
}
}
}
func TestJSONSchemaPropsOrArrayMarshalJSON(t *testing.T) {
cases := []struct {
input JSONSchemaPropsOrArrayHolder
result string
}{
{JSONSchemaPropsOrArrayHolder{}, `{"val1":null}`},
{JSONSchemaPropsOrArrayHolder{JSPoA: JSONSchemaPropsOrArray{Schema: &JSONSchemaProps{}}}, `{"val1":{}}`},
{JSONSchemaPropsOrArrayHolder{JSPoA: JSONSchemaPropsOrArray{Schema: &JSONSchemaProps{Type: "string"}}}, `{"val1":{"type":"string"}}`},
{JSONSchemaPropsOrArrayHolder{JSPoA: JSONSchemaPropsOrArray{JSONSchemas: []JSONSchemaProps{{}}}}, `{"val1":[{}]}`},
{JSONSchemaPropsOrArrayHolder{JSPoA: JSONSchemaPropsOrArray{JSONSchemas: []JSONSchemaProps{{}, {Type: "string"}}}}, `{"val1":[{},{"type":"string"}]}`},
{JSONSchemaPropsOrArrayHolder{JSPoAOmitEmpty: &JSONSchemaPropsOrArray{}}, `{"val1":null,"val2":null}`},
{JSONSchemaPropsOrArrayHolder{JSPoAOmitEmpty: &JSONSchemaPropsOrArray{Schema: &JSONSchemaProps{}}}, `{"val1":null,"val2":{}}`},
{JSONSchemaPropsOrArrayHolder{JSPoAOmitEmpty: &JSONSchemaPropsOrArray{Schema: &JSONSchemaProps{Type: "string"}}}, `{"val1":null,"val2":{"type":"string"}}`},
{JSONSchemaPropsOrArrayHolder{JSPoAOmitEmpty: &JSONSchemaPropsOrArray{JSONSchemas: []JSONSchemaProps{{}}}}, `{"val1":null,"val2":[{}]}`},
{JSONSchemaPropsOrArrayHolder{JSPoAOmitEmpty: &JSONSchemaPropsOrArray{JSONSchemas: []JSONSchemaProps{{}, {Type: "string"}}}}, `{"val1":null,"val2":[{},{"type":"string"}]}`},
}
for i, c := range cases {
result, err := json.Marshal(&c.input)
if err != nil {
t.Errorf("%d: Unexpected error marshaling input '%v': %v", i, c.input, err)
return
}
if string(result) != c.result {
t.Errorf("%d: Failed to marshal input '%v': expected: %q, got %q", i, c.input, c.result, string(result))
if diff := cmp.Diff(expected, actual); len(diff) > 0 {
t.Fatal(diff)
}
}
}
func unmarshalJSONTest(input []byte, wantErr bool, expectedDecoded unmarshalTestable, actualDecoded unmarshalTestable) func(t *testing.T) {
return func(t *testing.T) {
err := actualDecoded.UnmarshalJSON(input)
if (err != nil) != wantErr {
if wantErr {
t.Fatal("expected error")
}
t.Fatalf("unexpected error: %v", err)
}
if err != nil {
return
}
if diff := cmp.Diff(expectedDecoded, actualDecoded); len(diff) > 0 {
t.Error("unexpected decoded value")
t.Fatal(diff)
}
}
}
func unmarshalCBORTest(input []byte, wantErr bool, expectedDecoded unmarshalTestable, actualDecoded unmarshalTestable) func(t *testing.T) {
return func(t *testing.T) {
err := actualDecoded.UnmarshalCBOR(input)
if (err != nil) != wantErr {
if wantErr {
t.Fatal("expected error")
}
t.Fatalf("unexpected error: %v", err)
}
if err != nil {
return
}
if diff := cmp.Diff(expectedDecoded, actualDecoded); len(diff) != 0 {
t.Error("unexpected decoded value")
t.Fatal(diff)
}
}
}
func roundTripJSONTest(input roundTripTestable, expectedEncoded []byte, expectedDecoded roundTripTestable, actualDecoded roundTripTestable) func(t *testing.T) {
return func(t *testing.T) {
actualEncoded, err := input.MarshalJSON()
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(string(expectedEncoded), string(actualEncoded)); len(diff) > 0 {
t.Error("unexpected encoded value")
t.Fatal(diff)
}
err = actualDecoded.UnmarshalJSON(actualEncoded)
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(expectedDecoded, actualDecoded); len(diff) > 0 {
t.Error("unexpected decoded value")
t.Fatal(diff)
}
}
}
func roundTripCBORTest(input roundTripTestable, expectedEncoded []byte, expectedDecoded roundTripTestable, actualDecoded roundTripTestable) func(t *testing.T) {
return func(t *testing.T) {
actualEncoded, err := input.MarshalCBOR()
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(expectedEncoded, actualEncoded); len(diff) > 0 {
t.Error("unexpected encoded value")
t.Fatal(diff)
}
err = actualDecoded.UnmarshalCBOR(actualEncoded)
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(expectedDecoded, actualDecoded); len(diff) > 0 {
t.Error("unexpected decoded value")
t.Fatal(diff)
}
}
}

View File

@ -20,12 +20,40 @@ import (
"bytes"
"errors"
cbor "k8s.io/apimachinery/pkg/runtime/serializer/cbor/direct"
"k8s.io/apimachinery/pkg/util/json"
)
var jsTrue = []byte("true")
var jsFalse = []byte("false")
// The CBOR parsing related constants and functions below are not exported so they can be
// easily removed at a future date when the CBOR library provides equivalent functionality.
type cborMajorType int
const (
// https://www.rfc-editor.org/rfc/rfc8949.html#section-3.1
cborUnsignedInteger cborMajorType = 0
cborNegativeInteger cborMajorType = 1
cborByteString cborMajorType = 2
cborTextString cborMajorType = 3
cborArray cborMajorType = 4
cborMap cborMajorType = 5
cborTag cborMajorType = 6
cborOther cborMajorType = 7
)
const (
cborFalseValue = 0xf4
cborTrueValue = 0xf5
cborNullValue = 0xf6
)
func cborType(b byte) cborMajorType {
return cborMajorType(b >> 5)
}
func (s JSONSchemaPropsOrBool) MarshalJSON() ([]byte, error) {
if s.Schema != nil {
return json.Marshal(s.Schema)
@ -59,6 +87,39 @@ func (s *JSONSchemaPropsOrBool) UnmarshalJSON(data []byte) error {
return nil
}
func (s JSONSchemaPropsOrBool) MarshalCBOR() ([]byte, error) {
if s.Schema != nil {
return cbor.Marshal(s.Schema)
}
return cbor.Marshal(s.Allows)
}
func (s *JSONSchemaPropsOrBool) UnmarshalCBOR(data []byte) error {
switch {
case len(data) == 0:
// ideally we would avoid modifying *s here, but we are matching the behavior of UnmarshalJSON
*s = JSONSchemaPropsOrBool{}
return nil
case cborType(data[0]) == cborMap:
var p JSONSchemaProps
if err := cbor.Unmarshal(data, &p); err != nil {
return err
}
*s = JSONSchemaPropsOrBool{Allows: true, Schema: &p}
return nil
case data[0] == cborTrueValue:
*s = JSONSchemaPropsOrBool{Allows: true}
return nil
case data[0] == cborFalseValue:
*s = JSONSchemaPropsOrBool{Allows: false}
return nil
default:
// ideally, this case would not also capture a null input value,
// but we are matching the behavior of the UnmarshalJSON
return errors.New("boolean or JSON schema expected")
}
}
func (s JSONSchemaPropsOrStringArray) MarshalJSON() ([]byte, error) {
if len(s.Property) > 0 {
return json.Marshal(s.Property)
@ -91,6 +152,40 @@ func (s *JSONSchemaPropsOrStringArray) UnmarshalJSON(data []byte) error {
return nil
}
func (s JSONSchemaPropsOrStringArray) MarshalCBOR() ([]byte, error) {
if len(s.Property) > 0 {
return cbor.Marshal(s.Property)
}
if s.Schema != nil {
return cbor.Marshal(s.Schema)
}
return cbor.Marshal(nil)
}
func (s *JSONSchemaPropsOrStringArray) UnmarshalCBOR(data []byte) error {
if len(data) > 0 && cborType(data[0]) == cborArray {
var a []string
if err := cbor.Unmarshal(data, &a); err != nil {
return err
}
*s = JSONSchemaPropsOrStringArray{Property: a}
return nil
}
if len(data) > 0 && cborType(data[0]) == cborMap {
var p JSONSchemaProps
if err := cbor.Unmarshal(data, &p); err != nil {
return err
}
*s = JSONSchemaPropsOrStringArray{Schema: &p}
return nil
}
// At this point we either have: empty data, a null value, or an
// unexpected type. In order to match the behavior of the existing
// UnmarshalJSON, no error is returned and *s is overwritten here.
*s = JSONSchemaPropsOrStringArray{}
return nil
}
func (s JSONSchemaPropsOrArray) MarshalJSON() ([]byte, error) {
if len(s.JSONSchemas) > 0 {
return json.Marshal(s.JSONSchemas)
@ -120,6 +215,37 @@ func (s *JSONSchemaPropsOrArray) UnmarshalJSON(data []byte) error {
return nil
}
func (s JSONSchemaPropsOrArray) MarshalCBOR() ([]byte, error) {
if len(s.JSONSchemas) > 0 {
return cbor.Marshal(s.JSONSchemas)
}
return cbor.Marshal(s.Schema)
}
func (s *JSONSchemaPropsOrArray) UnmarshalCBOR(data []byte) error {
if len(data) > 0 && cborType(data[0]) == cborMap {
var p JSONSchemaProps
if err := cbor.Unmarshal(data, &p); err != nil {
return err
}
*s = JSONSchemaPropsOrArray{Schema: &p}
return nil
}
if len(data) > 0 && cborType(data[0]) == cborArray {
var a []JSONSchemaProps
if err := cbor.Unmarshal(data, &a); err != nil {
return err
}
*s = JSONSchemaPropsOrArray{JSONSchemas: a}
return nil
}
// At this point we either have: empty data, a null value, or an
// unexpected type. In order to match the behavior of the existing
// UnmarshalJSON, no error is returned and *s is overwritten here.
*s = JSONSchemaPropsOrArray{}
return nil
}
func (s JSON) MarshalJSON() ([]byte, error) {
if len(s.Raw) > 0 {
return s.Raw, nil
@ -134,3 +260,34 @@ func (s *JSON) UnmarshalJSON(data []byte) error {
}
return nil
}
func (s JSON) MarshalCBOR() ([]byte, error) {
// Note that non-semantic whitespace is lost during the transcoding performed here.
// We do not forsee this to be a problem given the current known uses of this type.
// Other limitations that arise when roundtripping JSON via dynamic clients also apply
// here, for example: insignificant whitespace handling, number handling, and map key ordering.
if len(s.Raw) == 0 {
return []byte{cborNullValue}, nil
}
var u any
if err := json.Unmarshal(s.Raw, &u); err != nil {
return nil, err
}
return cbor.Marshal(u)
}
func (s *JSON) UnmarshalCBOR(data []byte) error {
if len(data) == 0 || data[0] == cborNullValue {
return nil
}
var u any
if err := cbor.Unmarshal(data, &u); err != nil {
return err
}
raw, err := json.Marshal(u)
if err != nil {
return err
}
s.Raw = raw
return nil
}

View File

@ -18,133 +18,522 @@ package v1beta1
import (
"encoding/json"
"reflect"
"math"
"testing"
"github.com/fxamacker/cbor/v2"
"github.com/google/go-cmp/cmp"
)
type JSONSchemaPropsOrBoolHolder struct {
JSPoB JSONSchemaPropsOrBool `json:"val1"`
JSPoBOmitEmpty *JSONSchemaPropsOrBool `json:"val2,omitempty"`
type marshalTestable interface {
json.Marshaler
cbor.Marshaler
}
func TestJSONSchemaPropsOrBoolUnmarshalJSON(t *testing.T) {
cases := []struct {
input string
result JSONSchemaPropsOrBoolHolder
}{
{`{}`, JSONSchemaPropsOrBoolHolder{}},
{`{"val1": {}}`, JSONSchemaPropsOrBoolHolder{JSPoB: JSONSchemaPropsOrBool{Allows: true, Schema: &JSONSchemaProps{}}}},
{`{"val1": {"type":"string"}}`, JSONSchemaPropsOrBoolHolder{JSPoB: JSONSchemaPropsOrBool{Allows: true, Schema: &JSONSchemaProps{Type: "string"}}}},
{`{"val1": false}`, JSONSchemaPropsOrBoolHolder{JSPoB: JSONSchemaPropsOrBool{}}},
{`{"val1": true}`, JSONSchemaPropsOrBoolHolder{JSPoB: JSONSchemaPropsOrBool{Allows: true}}},
{`{"val2": {}}`, JSONSchemaPropsOrBoolHolder{JSPoBOmitEmpty: &JSONSchemaPropsOrBool{Allows: true, Schema: &JSONSchemaProps{}}}},
{`{"val2": {"type":"string"}}`, JSONSchemaPropsOrBoolHolder{JSPoBOmitEmpty: &JSONSchemaPropsOrBool{Allows: true, Schema: &JSONSchemaProps{Type: "string"}}}},
{`{"val2": false}`, JSONSchemaPropsOrBoolHolder{JSPoBOmitEmpty: &JSONSchemaPropsOrBool{}}},
{`{"val2": true}`, JSONSchemaPropsOrBoolHolder{JSPoBOmitEmpty: &JSONSchemaPropsOrBool{Allows: true}}},
}
for _, c := range cases {
var result JSONSchemaPropsOrBoolHolder
if err := json.Unmarshal([]byte(c.input), &result); err != nil {
t.Errorf("Failed to unmarshal input '%v': %v", c.input, err)
}
if !reflect.DeepEqual(result, c.result) {
t.Errorf("Failed to unmarshal input '%v': expected %+v, got %+v", c.input, c.result, result)
}
}
type marshalTestCase struct {
name string
input marshalTestable
wantJSONError bool
wantCBORError bool
wantJSON []byte
wantCBOR []byte
}
func TestStringArrayOrStringMarshalJSON(t *testing.T) {
cases := []struct {
input JSONSchemaPropsOrBoolHolder
result string
}{
{JSONSchemaPropsOrBoolHolder{}, `{"val1":false}`},
type unmarshalTestable interface {
json.Unmarshaler
cbor.Unmarshaler
}
{JSONSchemaPropsOrBoolHolder{JSPoB: JSONSchemaPropsOrBool{Schema: &JSONSchemaProps{}}}, `{"val1":{}}`},
{JSONSchemaPropsOrBoolHolder{JSPoB: JSONSchemaPropsOrBool{Schema: &JSONSchemaProps{Type: "string"}}}, `{"val1":{"type":"string"}}`},
{JSONSchemaPropsOrBoolHolder{JSPoB: JSONSchemaPropsOrBool{}}, `{"val1":false}`},
{JSONSchemaPropsOrBoolHolder{JSPoB: JSONSchemaPropsOrBool{Allows: true}}, `{"val1":true}`},
type unmarshalTestCase struct {
name string
inputJSON []byte
inputCBOR []byte
wantJSONError bool
wantCBORError bool
wantDecoded unmarshalTestable
}
{JSONSchemaPropsOrBoolHolder{JSPoBOmitEmpty: &JSONSchemaPropsOrBool{Schema: &JSONSchemaProps{}}}, `{"val1":false,"val2":{}}`},
{JSONSchemaPropsOrBoolHolder{JSPoBOmitEmpty: &JSONSchemaPropsOrBool{Schema: &JSONSchemaProps{Type: "string"}}}, `{"val1":false,"val2":{"type":"string"}}`},
{JSONSchemaPropsOrBoolHolder{JSPoBOmitEmpty: &JSONSchemaPropsOrBool{}}, `{"val1":false,"val2":false}`},
{JSONSchemaPropsOrBoolHolder{JSPoBOmitEmpty: &JSONSchemaPropsOrBool{Allows: true}}, `{"val1":false,"val2":true}`},
}
type roundTripTestable interface {
marshalTestable
unmarshalTestable
}
for _, c := range cases {
result, err := json.Marshal(&c.input)
type roundTripTestCase struct {
name string
input roundTripTestable
wantJSON []byte
wantCBOR []byte
wantDecoded roundTripTestable
}
func TestJSONSchemaPropsOrBool(t *testing.T) {
nan := math.NaN()
t.Run("Marshal", func(t *testing.T) {
testCases := []marshalTestCase{
{
name: "unsupported value",
input: &JSONSchemaPropsOrBool{
Schema: &JSONSchemaProps{Maximum: &nan},
},
wantJSONError: true,
wantCBORError: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("json", marshalJSONTest(tc.input, tc.wantJSONError, tc.wantJSON))
t.Run("cbor", marshalCBORTest(tc.input, tc.wantCBORError, tc.wantCBOR))
})
}
})
t.Run("RoundTrip", func(t *testing.T) {
testCases := []roundTripTestCase{
{
name: "zero value",
input: &JSONSchemaPropsOrBool{},
wantDecoded: &JSONSchemaPropsOrBool{},
wantJSON: []byte(`false`),
wantCBOR: []byte{cborFalseValue},
},
{
name: "bool false",
input: &JSONSchemaPropsOrBool{Allows: false},
wantDecoded: &JSONSchemaPropsOrBool{Allows: false},
wantJSON: []byte(`false`),
wantCBOR: []byte{cborFalseValue},
},
{
name: "bool true",
input: &JSONSchemaPropsOrBool{Allows: true},
wantDecoded: &JSONSchemaPropsOrBool{Allows: true},
wantJSON: []byte(`true`),
wantCBOR: []byte{cborTrueValue},
},
{
name: "with props",
input: &JSONSchemaPropsOrBool{Schema: &JSONSchemaProps{Type: "string"}},
wantDecoded: &JSONSchemaPropsOrBool{Allows: true, Schema: &JSONSchemaProps{Type: "string"}},
wantJSON: []byte(`{"type":"string"}`),
wantCBOR: []byte{0xA1, 0x44, 't', 'y', 'p', 'e', 0x46, 's', 't', 'r', 'i', 'n', 'g'},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("json", roundTripJSONTest(tc.input, tc.wantJSON, tc.wantDecoded, &JSONSchemaPropsOrBool{}))
t.Run("cbor", roundTripCBORTest(tc.input, tc.wantCBOR, tc.wantDecoded, &JSONSchemaPropsOrBool{}))
})
}
})
t.Run("Unmarshal", func(t *testing.T) {
testCases := []unmarshalTestCase{
{
name: "legacy behavior",
inputJSON: []byte(`{}`),
inputCBOR: []byte{0xA0},
wantDecoded: &JSONSchemaPropsOrBool{Allows: true, Schema: &JSONSchemaProps{}},
},
{
name: "null",
inputJSON: []byte(`null`),
inputCBOR: []byte{cborNullValue},
wantJSONError: true,
wantCBORError: true,
},
{
name: "zero len input",
inputJSON: []byte{},
inputCBOR: []byte{},
wantDecoded: &JSONSchemaPropsOrBool{},
},
{
name: "unsupported type",
inputJSON: []byte(`42`),
inputCBOR: []byte{0x18, 42},
wantJSONError: true,
wantCBORError: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("json", unmarshalJSONTest(tc.inputJSON, tc.wantJSONError, tc.wantDecoded, &JSONSchemaPropsOrBool{}))
t.Run("cbor", unmarshalCBORTest(tc.inputCBOR, tc.wantCBORError, tc.wantDecoded, &JSONSchemaPropsOrBool{}))
})
}
})
}
func TestJSONSchemaPropsOrArray(t *testing.T) {
nan := math.NaN()
t.Run("Marshal", func(t *testing.T) {
testCases := []marshalTestCase{
{
name: "unsupported value",
input: &JSONSchemaPropsOrArray{Schema: &JSONSchemaProps{Maximum: &nan}},
wantJSONError: true,
wantCBORError: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("json", marshalJSONTest(tc.input, tc.wantJSONError, tc.wantJSON))
t.Run("cbor", marshalCBORTest(tc.input, tc.wantCBORError, tc.wantCBOR))
})
}
})
t.Run("RoundTrip", func(t *testing.T) {
testCases := []roundTripTestCase{
{
name: "empty props",
input: &JSONSchemaPropsOrArray{Schema: &JSONSchemaProps{}},
wantDecoded: &JSONSchemaPropsOrArray{Schema: &JSONSchemaProps{}},
wantJSON: []byte(`{}`),
wantCBOR: []byte{0xA0},
},
{
name: "props",
input: &JSONSchemaPropsOrArray{Schema: &JSONSchemaProps{Type: "string"}},
wantDecoded: &JSONSchemaPropsOrArray{Schema: &JSONSchemaProps{Type: "string"}},
wantJSON: []byte(`{"type":"string"}`),
wantCBOR: []byte{0xA1, 0x44, 't', 'y', 'p', 'e', 0x46, 's', 't', 'r', 'i', 'n', 'g'},
},
{
name: "array with empty props",
input: &JSONSchemaPropsOrArray{JSONSchemas: []JSONSchemaProps{{}}},
wantDecoded: &JSONSchemaPropsOrArray{JSONSchemas: []JSONSchemaProps{{}}},
wantJSON: []byte(`[{}]`),
wantCBOR: []byte{0x81, 0xA0},
},
{
name: "array with empty props and props",
input: &JSONSchemaPropsOrArray{JSONSchemas: []JSONSchemaProps{{}, {Type: "string"}}},
wantDecoded: &JSONSchemaPropsOrArray{JSONSchemas: []JSONSchemaProps{{}, {Type: "string"}}},
wantJSON: []byte(`[{},{"type":"string"}]`),
wantCBOR: []byte{0x82, 0xA0, 0xA1, 0x44, 't', 'y', 'p', 'e', 0x46, 's', 't', 'r', 'i', 'n', 'g'},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("json", roundTripJSONTest(tc.input, tc.wantJSON, tc.wantDecoded, &JSONSchemaPropsOrArray{}))
t.Run("cbor", roundTripCBORTest(tc.input, tc.wantCBOR, tc.wantDecoded, &JSONSchemaPropsOrArray{}))
})
}
})
t.Run("Unmarshal", func(t *testing.T) {
testCases := []unmarshalTestCase{
{
name: "null",
inputJSON: []byte(`null`),
inputCBOR: []byte{cborNullValue},
wantDecoded: &JSONSchemaPropsOrArray{},
},
{
name: "zero len input",
inputJSON: []byte{},
inputCBOR: []byte{},
wantDecoded: &JSONSchemaPropsOrArray{},
},
{
name: "unsupported type",
inputJSON: []byte(`42`),
inputCBOR: []byte{0x18, 42},
wantDecoded: &JSONSchemaPropsOrArray{},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("json", unmarshalJSONTest(tc.inputJSON, tc.wantJSONError, tc.wantDecoded, &JSONSchemaPropsOrArray{}))
t.Run("cbor", unmarshalCBORTest(tc.inputCBOR, tc.wantCBORError, tc.wantDecoded, &JSONSchemaPropsOrArray{}))
})
}
})
}
func TestJSONSchemaPropsOrStringArray(t *testing.T) {
nan := math.NaN()
t.Run("Marshal", func(t *testing.T) {
testCases := []marshalTestCase{
{
name: "unsupported value",
input: JSONSchemaPropsOrStringArray{Schema: &JSONSchemaProps{Maximum: &nan}},
wantJSONError: true,
wantCBORError: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("json", marshalJSONTest(tc.input, tc.wantJSONError, tc.wantJSON))
t.Run("cbor", marshalCBORTest(tc.input, tc.wantCBORError, tc.wantCBOR))
})
}
})
t.Run("RoundTrip", func(t *testing.T) {
testCases := []roundTripTestCase{
{
name: "empty props",
input: &JSONSchemaPropsOrStringArray{Schema: &JSONSchemaProps{}},
wantDecoded: &JSONSchemaPropsOrStringArray{Schema: &JSONSchemaProps{}},
wantJSON: []byte(`{}`),
wantCBOR: []byte{0xA0},
},
{
name: "props",
input: &JSONSchemaPropsOrStringArray{Schema: &JSONSchemaProps{Type: "string"}},
wantDecoded: &JSONSchemaPropsOrStringArray{Schema: &JSONSchemaProps{Type: "string"}},
wantJSON: []byte(`{"type":"string"}`),
wantCBOR: []byte{0xA1, 0x44, 't', 'y', 'p', 'e', 0x46, 's', 't', 'r', 'i', 'n', 'g'},
},
{
name: "empty array",
input: &JSONSchemaPropsOrStringArray{Property: []string{}},
wantDecoded: &JSONSchemaPropsOrStringArray{Property: nil},
wantJSON: []byte(`null`),
wantCBOR: []byte{cborNullValue},
},
{
name: "array value",
input: &JSONSchemaPropsOrStringArray{Property: []string{"string"}},
wantDecoded: &JSONSchemaPropsOrStringArray{Property: []string{"string"}},
wantJSON: []byte(`["string"]`),
wantCBOR: []byte{0x81, 0x46, 's', 't', 'r', 'i', 'n', 'g'},
},
{
name: "both props and array",
input: &JSONSchemaPropsOrStringArray{Schema: &JSONSchemaProps{Type: "props"}, Property: []string{"string"}},
wantDecoded: &JSONSchemaPropsOrStringArray{Schema: nil, Property: []string{"string"}},
wantJSON: []byte(`["string"]`),
wantCBOR: []byte{0x81, 0x46, 's', 't', 'r', 'i', 'n', 'g'},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("json", roundTripJSONTest(tc.input, tc.wantJSON, tc.wantDecoded, &JSONSchemaPropsOrStringArray{}))
t.Run("cbor", roundTripCBORTest(tc.input, tc.wantCBOR, tc.wantDecoded, &JSONSchemaPropsOrStringArray{}))
})
}
})
t.Run("Unmarshal", func(t *testing.T) {
testCases := []unmarshalTestCase{
{
name: "empty array",
inputJSON: []byte(`[]`),
inputCBOR: []byte{0x80},
wantDecoded: &JSONSchemaPropsOrStringArray{Property: []string{}},
},
{
name: "null",
inputJSON: []byte(`null`),
inputCBOR: []byte{cborNullValue},
wantDecoded: &JSONSchemaPropsOrStringArray{},
},
{
name: "zero len input",
inputJSON: []byte{},
inputCBOR: []byte{},
wantDecoded: &JSONSchemaPropsOrStringArray{},
},
{
name: "unsupported type",
inputJSON: []byte(`42`),
inputCBOR: []byte{0x18, 42},
wantDecoded: &JSONSchemaPropsOrStringArray{},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("json", unmarshalJSONTest(tc.inputJSON, tc.wantJSONError, tc.wantDecoded, &JSONSchemaPropsOrStringArray{}))
t.Run("cbor", unmarshalCBORTest(tc.inputCBOR, tc.wantCBORError, tc.wantDecoded, &JSONSchemaPropsOrStringArray{}))
})
}
})
}
func TestJSON(t *testing.T) {
t.Run("RoundTrip", func(t *testing.T) {
testCases := []roundTripTestCase{
{
name: "nil raw",
input: &JSON{Raw: nil},
wantDecoded: &JSON{Raw: nil},
wantJSON: []byte(`null`),
wantCBOR: []byte{cborNullValue},
},
{
name: "zero len raw",
input: &JSON{Raw: []byte{}},
wantDecoded: &JSON{Raw: nil},
wantJSON: []byte(`null`),
wantCBOR: []byte{cborNullValue},
},
{
name: "empty",
input: &JSON{},
wantDecoded: &JSON{},
wantJSON: []byte(`null`),
wantCBOR: []byte{cborNullValue},
},
{
name: "string",
input: &JSON{Raw: []byte(`"string"`)},
wantDecoded: &JSON{Raw: []byte(`"string"`)},
wantJSON: []byte(`"string"`),
wantCBOR: []byte{0x46, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67},
},
{
name: "number",
input: &JSON{Raw: []byte(`42.01`)},
wantDecoded: &JSON{Raw: []byte(`42.01`)},
wantJSON: []byte(`42.01`),
wantCBOR: []byte{0xFB, 0x40, 0x45, 0x01, 0x47, 0xAE, 0x14, 0x7A, 0xE1},
},
{
name: "bool",
input: &JSON{Raw: []byte(`true`)},
wantDecoded: &JSON{Raw: []byte(`true`)},
wantJSON: []byte(`true`),
wantCBOR: []byte{0xF5},
},
{
name: "array",
input: &JSON{Raw: []byte(`[1,2,3]`)},
wantDecoded: &JSON{Raw: []byte(`[1,2,3]`)},
wantJSON: []byte(`[1,2,3]`),
wantCBOR: []byte{0x83, 1, 2, 3},
},
{
name: "map",
input: &JSON{Raw: []byte(`{"foo":"bar"}`)},
wantDecoded: &JSON{Raw: []byte(`{"foo":"bar"}`)},
wantJSON: []byte(`{"foo":"bar"}`),
wantCBOR: []byte{0xA1, 0x43, 'f', 'o', 'o', 0x43, 'b', 'a', 'r'},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("json", roundTripJSONTest(tc.input, tc.wantJSON, tc.wantDecoded, &JSON{}))
t.Run("cbor", roundTripCBORTest(tc.input, tc.wantCBOR, tc.wantDecoded, &JSON{}))
})
}
})
}
func marshalJSONTest(input marshalTestable, wantErr bool, expected []byte) func(t *testing.T) {
return func(t *testing.T) {
actual, err := input.MarshalJSON()
if (err != nil) != wantErr {
if wantErr {
t.Fatal("expected error")
}
t.Fatalf("unexpected error: %v", err)
}
if err != nil {
t.Errorf("Unexpected error marshaling input '%v': %v", c.input, err)
return
}
if string(result) != c.result {
t.Errorf("Failed to marshal input '%v': expected: %q, got %q", c.input, c.result, string(result))
if diff := cmp.Diff(string(expected), string(actual)); len(diff) > 0 {
t.Fatal(diff)
}
}
}
type JSONSchemaPropsOrArrayHolder struct {
JSPoA JSONSchemaPropsOrArray `json:"val1"`
JSPoAOmitEmpty *JSONSchemaPropsOrArray `json:"val2,omitempty"`
}
func TestJSONSchemaPropsOrArrayUnmarshalJSON(t *testing.T) {
cases := []struct {
input string
result JSONSchemaPropsOrArrayHolder
}{
{`{}`, JSONSchemaPropsOrArrayHolder{}},
{`{"val1": {}}`, JSONSchemaPropsOrArrayHolder{JSPoA: JSONSchemaPropsOrArray{Schema: &JSONSchemaProps{}}}},
{`{"val1": {"type":"string"}}`, JSONSchemaPropsOrArrayHolder{JSPoA: JSONSchemaPropsOrArray{Schema: &JSONSchemaProps{Type: "string"}}}},
{`{"val1": [{}]}`, JSONSchemaPropsOrArrayHolder{JSPoA: JSONSchemaPropsOrArray{JSONSchemas: []JSONSchemaProps{{}}}}},
{`{"val1": [{},{"type":"string"}]}`, JSONSchemaPropsOrArrayHolder{JSPoA: JSONSchemaPropsOrArray{JSONSchemas: []JSONSchemaProps{{}, {Type: "string"}}}}},
{`{"val2": {}}`, JSONSchemaPropsOrArrayHolder{JSPoAOmitEmpty: &JSONSchemaPropsOrArray{Schema: &JSONSchemaProps{}}}},
{`{"val2": {"type":"string"}}`, JSONSchemaPropsOrArrayHolder{JSPoAOmitEmpty: &JSONSchemaPropsOrArray{Schema: &JSONSchemaProps{Type: "string"}}}},
{`{"val2": [{}]}`, JSONSchemaPropsOrArrayHolder{JSPoAOmitEmpty: &JSONSchemaPropsOrArray{JSONSchemas: []JSONSchemaProps{{}}}}},
{`{"val2": [{},{"type":"string"}]}`, JSONSchemaPropsOrArrayHolder{JSPoAOmitEmpty: &JSONSchemaPropsOrArray{JSONSchemas: []JSONSchemaProps{{}, {Type: "string"}}}}},
}
for _, c := range cases {
var result JSONSchemaPropsOrArrayHolder
if err := json.Unmarshal([]byte(c.input), &result); err != nil {
t.Errorf("Failed to unmarshal input '%v': %v", c.input, err)
func marshalCBORTest(input marshalTestable, wantErr bool, expected []byte) func(t *testing.T) {
return func(t *testing.T) {
actual, err := input.MarshalCBOR()
if (err != nil) != wantErr {
if wantErr {
t.Fatal("expected error")
}
t.Fatalf("unexpected error: %v", err)
}
if !reflect.DeepEqual(result, c.result) {
t.Errorf("Failed to unmarshal input '%v': expected %+v, got %+v", c.input, c.result, result)
}
}
}
func TestJSONSchemaPropsOrArrayMarshalJSON(t *testing.T) {
cases := []struct {
input JSONSchemaPropsOrArrayHolder
result string
}{
{JSONSchemaPropsOrArrayHolder{}, `{"val1":null}`},
{JSONSchemaPropsOrArrayHolder{JSPoA: JSONSchemaPropsOrArray{Schema: &JSONSchemaProps{}}}, `{"val1":{}}`},
{JSONSchemaPropsOrArrayHolder{JSPoA: JSONSchemaPropsOrArray{Schema: &JSONSchemaProps{Type: "string"}}}, `{"val1":{"type":"string"}}`},
{JSONSchemaPropsOrArrayHolder{JSPoA: JSONSchemaPropsOrArray{JSONSchemas: []JSONSchemaProps{{}}}}, `{"val1":[{}]}`},
{JSONSchemaPropsOrArrayHolder{JSPoA: JSONSchemaPropsOrArray{JSONSchemas: []JSONSchemaProps{{}, {Type: "string"}}}}, `{"val1":[{},{"type":"string"}]}`},
{JSONSchemaPropsOrArrayHolder{JSPoAOmitEmpty: &JSONSchemaPropsOrArray{}}, `{"val1":null,"val2":null}`},
{JSONSchemaPropsOrArrayHolder{JSPoAOmitEmpty: &JSONSchemaPropsOrArray{Schema: &JSONSchemaProps{}}}, `{"val1":null,"val2":{}}`},
{JSONSchemaPropsOrArrayHolder{JSPoAOmitEmpty: &JSONSchemaPropsOrArray{Schema: &JSONSchemaProps{Type: "string"}}}, `{"val1":null,"val2":{"type":"string"}}`},
{JSONSchemaPropsOrArrayHolder{JSPoAOmitEmpty: &JSONSchemaPropsOrArray{JSONSchemas: []JSONSchemaProps{{}}}}, `{"val1":null,"val2":[{}]}`},
{JSONSchemaPropsOrArrayHolder{JSPoAOmitEmpty: &JSONSchemaPropsOrArray{JSONSchemas: []JSONSchemaProps{{}, {Type: "string"}}}}, `{"val1":null,"val2":[{},{"type":"string"}]}`},
}
for i, c := range cases {
result, err := json.Marshal(&c.input)
if err != nil {
t.Errorf("%d: Unexpected error marshaling input '%v': %v", i, c.input, err)
return
}
if string(result) != c.result {
t.Errorf("%d: Failed to marshal input '%v': expected: %q, got %q", i, c.input, c.result, string(result))
if diff := cmp.Diff(expected, actual); len(diff) > 0 {
t.Fatal(diff)
}
}
}
func unmarshalJSONTest(input []byte, wantErr bool, expectedDecoded unmarshalTestable, actualDecoded unmarshalTestable) func(t *testing.T) {
return func(t *testing.T) {
err := actualDecoded.UnmarshalJSON(input)
if (err != nil) != wantErr {
if wantErr {
t.Fatal("expected error")
}
t.Fatalf("unexpected error: %v", err)
}
if err != nil {
return
}
if diff := cmp.Diff(expectedDecoded, actualDecoded); len(diff) > 0 {
t.Error("unexpected decoded value")
t.Fatal(diff)
}
}
}
func unmarshalCBORTest(input []byte, wantErr bool, expectedDecoded unmarshalTestable, actualDecoded unmarshalTestable) func(t *testing.T) {
return func(t *testing.T) {
err := actualDecoded.UnmarshalCBOR(input)
if (err != nil) != wantErr {
if wantErr {
t.Fatal("expected error")
}
t.Fatalf("unexpected error: %v", err)
}
if err != nil {
return
}
if diff := cmp.Diff(expectedDecoded, actualDecoded); len(diff) != 0 {
t.Error("unexpected decoded value")
t.Fatal(diff)
}
}
}
func roundTripJSONTest(input roundTripTestable, expectedEncoded []byte, expectedDecoded roundTripTestable, actualDecoded roundTripTestable) func(t *testing.T) {
return func(t *testing.T) {
actualEncoded, err := input.MarshalJSON()
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(string(expectedEncoded), string(actualEncoded)); len(diff) > 0 {
t.Error("unexpected encoded value")
t.Fatal(diff)
}
err = actualDecoded.UnmarshalJSON(actualEncoded)
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(expectedDecoded, actualDecoded); len(diff) > 0 {
t.Error("unexpected decoded value")
t.Fatal(diff)
}
}
}
func roundTripCBORTest(input roundTripTestable, expectedEncoded []byte, expectedDecoded roundTripTestable, actualDecoded roundTripTestable) func(t *testing.T) {
return func(t *testing.T) {
actualEncoded, err := input.MarshalCBOR()
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(expectedEncoded, actualEncoded); len(diff) > 0 {
t.Error("unexpected encoded value")
t.Fatal(diff)
}
err = actualDecoded.UnmarshalCBOR(actualEncoded)
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(expectedDecoded, actualDecoded); len(diff) > 0 {
t.Error("unexpected decoded value")
t.Fatal(diff)
}
}
}

View File

@ -21,19 +21,24 @@ import (
"strconv"
"testing"
fuzz "github.com/google/gofuzz"
"github.com/stretchr/testify/require"
apiextensionv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apiextensionv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
apiextensionsfuzzer "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/fuzzer"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/install"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
"k8s.io/apimachinery/pkg/api/apitesting/roundtrip"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
_ "k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/util/sets"
)
var groups = []runtime.SchemeBuilder{
apiextensionv1.SchemeBuilder,
apiextensionv1beta1.SchemeBuilder,
apiextensionsv1.SchemeBuilder,
apiextensionsv1beta1.SchemeBuilder,
}
func TestCompatibility(t *testing.T) {
@ -46,11 +51,11 @@ func TestCompatibility(t *testing.T) {
// Fill unstructured JSON field types
opts.FillFuncs = map[reflect.Type]roundtrip.FillFunc{
reflect.TypeOf(&apiextensionv1.JSON{}): func(s string, i int, obj interface{}) {
obj.(*apiextensionv1.JSON).Raw = []byte(strconv.Quote(s + "Value"))
reflect.TypeOf(&apiextensionsv1.JSON{}): func(s string, i int, obj interface{}) {
obj.(*apiextensionsv1.JSON).Raw = []byte(strconv.Quote(s + "Value"))
},
reflect.TypeOf(&apiextensionv1beta1.JSON{}): func(s string, i int, obj interface{}) {
obj.(*apiextensionv1beta1.JSON).Raw = []byte(strconv.Quote(s + "Value"))
reflect.TypeOf(&apiextensionsv1beta1.JSON{}): func(s string, i int, obj interface{}) {
obj.(*apiextensionsv1beta1.JSON).Raw = []byte(strconv.Quote(s + "Value"))
},
}
@ -59,7 +64,7 @@ func TestCompatibility(t *testing.T) {
// limit to types in apiextensions.k8s.io
filteredKinds := []schema.GroupVersionKind{}
for _, gvk := range opts.Kinds {
if gvk.Group == apiextensionv1.SchemeGroupVersion.Group {
if gvk.Group == apiextensionsv1.SchemeGroupVersion.Group {
filteredKinds = append(filteredKinds, gvk)
}
}
@ -67,3 +72,59 @@ func TestCompatibility(t *testing.T) {
opts.Run(t)
}
func TestRoundtripToUnstructured(t *testing.T) {
scheme := runtime.NewScheme()
install.Install(scheme)
roundtrip.RoundtripToUnstructured(t, scheme,
fuzzer.MergeFuzzerFuncs(
apiextensionsfuzzer.Funcs,
func(_ serializer.CodecFactory) []any {
return []any{
func(obj *apiextensionsv1.ConversionReview, c fuzz.Continue) {
c.FuzzNoCustom(obj)
if obj.Request != nil {
for i := range obj.Request.Objects {
fuzzer.NormalizeJSONRawExtension(&obj.Request.Objects[i])
}
}
if obj.Response != nil {
for i := range obj.Response.ConvertedObjects {
fuzzer.NormalizeJSONRawExtension(&obj.Response.ConvertedObjects[i])
}
}
},
func(obj *apiextensionsv1beta1.ConversionReview, c fuzz.Continue) {
c.FuzzNoCustom(obj)
if obj.Request != nil {
for i := range obj.Request.Objects {
fuzzer.NormalizeJSONRawExtension(&obj.Request.Objects[i])
}
}
if obj.Response != nil {
for i := range obj.Response.ConvertedObjects {
fuzzer.NormalizeJSONRawExtension(&obj.Response.ConvertedObjects[i])
}
}
},
}
},
),
// skip types that are never serialized in the body of a request/response.
sets.New(
apiextensionsv1.SchemeGroupVersion.WithKind("CreateOptions"),
apiextensionsv1.SchemeGroupVersion.WithKind("PatchOptions"),
apiextensionsv1.SchemeGroupVersion.WithKind("UpdateOptions"),
apiextensionsv1beta1.SchemeGroupVersion.WithKind("CreateOptions"),
apiextensionsv1beta1.SchemeGroupVersion.WithKind("PatchOptions"),
apiextensionsv1beta1.SchemeGroupVersion.WithKind("UpdateOptions"),
),
// the following types do not have an "internal" go type, so we fuzz the
// versioned type directly instead of converting to/from the "internal" version
// during the round-tripping.
sets.New(
apiextensionsv1.SchemeGroupVersion.WithKind("ConversionReview"),
apiextensionsv1beta1.SchemeGroupVersion.WithKind("ConversionReview"),
),
)
}

View File

@ -17,11 +17,15 @@ limitations under the License.
package fuzzer
import (
"encoding/json"
"fmt"
"math/rand"
"github.com/google/gofuzz"
"k8s.io/apimachinery/pkg/runtime"
runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer"
kjson "k8s.io/apimachinery/pkg/util/json"
)
// FuzzerFuncs returns a list of func(*SomeType, c fuzz.Continue) functions.
@ -50,3 +54,20 @@ func MergeFuzzerFuncs(funcs ...FuzzerFuncs) FuzzerFuncs {
return result
})
}
func NormalizeJSONRawExtension(ext *runtime.RawExtension) {
if json.Valid(ext.Raw) {
// RawExtension->JSON encodes struct fields in field index order while map[string]interface{}->JSON encodes
// struct fields (i.e. keys in the map) lexicographically. We have to sort the fields here to ensure the
// JSON in the (RawExtension->)JSON->map[string]interface{}->JSON round trip results in identical JSON.
var u any
err := kjson.Unmarshal(ext.Raw, &u)
if err != nil {
panic(fmt.Sprintf("Failed to encode object: %v", err))
}
ext.Raw, err = kjson.Marshal(&u)
if err != nil {
panic(fmt.Sprintf("Failed to encode object: %v", err))
}
}
}

View File

@ -27,6 +27,7 @@ import (
"k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
apiequality "k8s.io/apimachinery/pkg/api/equality"
metafuzzer "k8s.io/apimachinery/pkg/apis/meta/fuzzer"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
@ -43,7 +44,11 @@ import (
// from native to unstructured and back using both the JSON and CBOR serializers. The intermediate
// unstructured objects produced by both encodings must be identical and be themselves
// roundtrippable to JSON and CBOR.
func RoundtripToUnstructured(t *testing.T, scheme *runtime.Scheme, funcs fuzzer.FuzzerFuncs, skipped sets.Set[schema.GroupVersionKind]) {
//
// Values for all external types in the scheme are generated by fuzzing the a value of the
// corresponding internal type and converting it, except for types whose registered GVK appears in
// the "nointernal" set, which are fuzzed directly.
func RoundtripToUnstructured(t *testing.T, scheme *runtime.Scheme, funcs fuzzer.FuzzerFuncs, skipped sets.Set[schema.GroupVersionKind], nointernal sets.Set[schema.GroupVersionKind]) {
codecs := serializer.NewCodecFactory(scheme)
seed := int64(time.Now().Nanosecond())
@ -63,6 +68,7 @@ func RoundtripToUnstructured(t *testing.T, scheme *runtime.Scheme, funcs fuzzer.
if globalNonRoundTrippableTypes.Has(gvk.Kind) {
continue
}
if gvk.Version == runtime.APIVersionInternal {
continue
}
@ -74,27 +80,29 @@ func RoundtripToUnstructured(t *testing.T, scheme *runtime.Scheme, funcs fuzzer.
t.Run(subtestName, func(t *testing.T) {
if skipped.Has(gvk) {
t.Skip()
t.SkipNow()
}
fuzzer := fuzzer.FuzzerFor(funcs, rand.NewSource(seed), codecs)
for i := 0; i < 50; i++ {
// We do fuzzing on the internal version of the object, and only then
// convert to the external version. This is because custom fuzzing
// function are only supported for internal objects.
internalObj, err := scheme.New(schema.GroupVersion{Group: gvk.Group, Version: runtime.APIVersionInternal}.WithKind(gvk.Kind))
if err != nil {
t.Fatalf("couldn't create internal object %v: %v", gvk.Kind, err)
}
fuzzer.Fuzz(internalObj)
fuzzer := fuzzer.FuzzerFor(fuzzer.MergeFuzzerFuncs(metafuzzer.Funcs, funcs), rand.NewSource(seed), codecs)
for i := 0; i < *FuzzIters; i++ {
item, err := scheme.New(gvk)
if err != nil {
t.Fatalf("couldn't create external object %v: %v", gvk.Kind, err)
}
if err := scheme.Convert(internalObj, item, nil); err != nil {
t.Fatalf("conversion for %v failed: %v", gvk.Kind, err)
if nointernal.Has(gvk) {
fuzzer.Fuzz(item)
} else {
internalObj, err := scheme.New(gvk.GroupKind().WithVersion(runtime.APIVersionInternal))
if err != nil {
t.Fatalf("couldn't create internal object %v: %v", gvk.Kind, err)
}
fuzzer.Fuzz(internalObj)
if err := scheme.Convert(internalObj, item, nil); err != nil {
t.Fatalf("conversion for %v failed: %v", gvk.Kind, err)
}
}
// Decoding into Unstructured requires that apiVersion and kind be

View File

@ -0,0 +1,43 @@
/*
Copyright 2024 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 apis
import (
"testing"
"k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
"k8s.io/apimachinery/pkg/api/apitesting/roundtrip"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/kube-aggregator/pkg/apis/apiregistration/install"
apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
apiregistrationv1beta1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1beta1"
)
func TestRoundtripToUnstructured(t *testing.T) {
scheme := runtime.NewScheme()
install.Install(scheme)
roundtrip.RoundtripToUnstructured(t, scheme, fuzzer.MergeFuzzerFuncs(), sets.New(
apiregistrationv1.SchemeGroupVersion.WithKind("CreateOptions"),
apiregistrationv1.SchemeGroupVersion.WithKind("PatchOptions"),
apiregistrationv1.SchemeGroupVersion.WithKind("UpdateOptions"),
apiregistrationv1beta1.SchemeGroupVersion.WithKind("CreateOptions"),
apiregistrationv1beta1.SchemeGroupVersion.WithKind("PatchOptions"),
apiregistrationv1beta1.SchemeGroupVersion.WithKind("UpdateOptions"),
), nil)
}