mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-05 18:24:07 +00:00
cover additional types in unstructured roundtrip test
Co-authored-by: Ben Luddy <bluddy@redhat.com>
This commit is contained in:
parent
3d6c99e1a7
commit
aaa7364f60
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
Loading…
Reference in New Issue
Block a user