knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/webhook/json/decode_test.go (about)

     1  /*
     2  Copyright 2021 The Knative Authors
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package json
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"errors"
    23  	"testing"
    24  
    25  	"github.com/google/go-cmp/cmp"
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"k8s.io/apimachinery/pkg/runtime"
    28  	"knative.dev/pkg/apis"
    29  	"knative.dev/pkg/webhook/resourcesemantics"
    30  )
    31  
    32  func TestDecode_KnownFields(t *testing.T) {
    33  	// We ignore type meta since inline only works with k8s yaml parsers
    34  	cases := []struct {
    35  		name  string
    36  		input string
    37  
    38  		want    fixture
    39  		wantErr bool
    40  	}{{
    41  		name:  "no metadata",
    42  		input: `{}`,
    43  		want:  fixture{},
    44  	}, {
    45  		name:  "known fields",
    46  		input: `{ "metadata":{"name":"some-name", "namespace":"some-namespace"} }`,
    47  		want: fixture{
    48  			ObjectMeta: metav1.ObjectMeta{
    49  				Name:      "some-name",
    50  				Namespace: "some-namespace",
    51  			},
    52  		},
    53  	}, {
    54  		name: "with spec",
    55  		input: `{
    56  			"metadata":{"name":"some-name", "namespace":"some-namespace"},
    57  			"spec":{"key":"value"}
    58  		}`,
    59  		want: fixture{
    60  			ObjectMeta: metav1.ObjectMeta{
    61  				Name:      "some-name",
    62  				Namespace: "some-namespace",
    63  			},
    64  			Spec: map[string]string{"key": "value"},
    65  		},
    66  	}, {
    67  		name: "unknown metadata field",
    68  		input: `{
    69  			"metadata":{"name":"some-name", "namespace":"some-namespace", "bomba":"boom"},
    70  			"spec":{"key":"value"}
    71  		}`,
    72  		want: fixture{
    73  			ObjectMeta: metav1.ObjectMeta{
    74  				Name:      "some-name",
    75  				Namespace: "some-namespace",
    76  			},
    77  			Spec: map[string]string{"key": "value"},
    78  		},
    79  	}, {
    80  		name: "unknown field top level",
    81  		input: `{
    82  			"metadata":{"name":"some-name", "namespace":"some-namespace"},
    83  			"bomba": "boom",
    84  			"spec":{"key":"value"}
    85  		}`,
    86  		wantErr: true,
    87  	}, {
    88  		name: "multiple metadata keys in the JSON",
    89  		input: `{
    90  			"metadata":{"name":"some-name", "namespace":"some-namespace", "bomba":"boom"},
    91  			"spec":{"metadata":"value"}
    92  		}`,
    93  		want: fixture{
    94  			ObjectMeta: metav1.ObjectMeta{
    95  				Name:      "some-name",
    96  				Namespace: "some-namespace",
    97  			},
    98  			Spec: map[string]string{"metadata": "value"},
    99  		},
   100  	}, {
   101  		name: "nested object in metadata",
   102  		input: `{
   103  			"metadata":{"name":"some-name", "namespace":"some-namespace", "labels":{"key":"value"}}
   104  		}`,
   105  		want: fixture{
   106  			ObjectMeta: metav1.ObjectMeta{
   107  				Name:      "some-name",
   108  				Namespace: "some-namespace",
   109  				Labels: map[string]string{
   110  					"key": "value",
   111  				},
   112  			},
   113  		},
   114  	}, {
   115  		name: "nested array in metadata",
   116  		input: `{
   117  			"metadata":{"name":"some-name", "namespace":"some-namespace", "finalizers":["first","second"]}
   118  		}`,
   119  		want: fixture{
   120  			ObjectMeta: metav1.ObjectMeta{
   121  				Name:       "some-name",
   122  				Namespace:  "some-namespace",
   123  				Finalizers: []string{"first", "second"},
   124  			},
   125  		},
   126  	}, {
   127  		name: "bad input",
   128  		// note use two characters so our decoder fails on the second token lookup
   129  		input:   "{{",
   130  		wantErr: true,
   131  	}}
   132  
   133  	for _, tc := range cases {
   134  		t.Run(tc.name, func(t *testing.T) {
   135  			got := &fixture{}
   136  			err := Decode([]byte(tc.input), got, true)
   137  
   138  			if tc.wantErr && err == nil {
   139  				t.Fatal("DecodeTo() expected an error")
   140  			} else if !tc.wantErr && err != nil {
   141  				t.Fatal("unexpected error", err)
   142  			}
   143  
   144  			// Don't bother checking against the fixture if
   145  			// we expected an error
   146  			if tc.wantErr {
   147  				return
   148  			}
   149  
   150  			if diff := cmp.Diff(&tc.want, got); diff != "" {
   151  				t.Error("unexpected diff", diff)
   152  			}
   153  		})
   154  	}
   155  }
   156  
   157  func TestDecode_AllowUnknownFields(t *testing.T) {
   158  	input := `{
   159  			"metadata":{"name":"some-name", "namespace":"some-namespace"},
   160  			"bomba": "boom",
   161  			"spec": {"some":"value"}
   162  		}`
   163  
   164  	got := &fixture{}
   165  	want := &fixture{
   166  		ObjectMeta: metav1.ObjectMeta{
   167  			Name:      "some-name",
   168  			Namespace: "some-namespace",
   169  		},
   170  		Spec: map[string]string{"some": "value"},
   171  	}
   172  
   173  	err := Decode([]byte(input), got, false)
   174  	if err != nil {
   175  		t.Fatal("unexpected error", err)
   176  	}
   177  
   178  	if diff := cmp.Diff(want, got); diff != "" {
   179  		t.Error("unexpected diff", diff)
   180  	}
   181  }
   182  
   183  // Note: this test is paired with the failingFixture and knows
   184  // that the implementation of Decode parses the json in two passes
   185  // the first being with an empty metadata '{}' - the second being the real
   186  // input
   187  func TestDecode_UnmarshalMetadataFailed(t *testing.T) {
   188  	input := `{ "metadata":{"name":"some-name", "namespace":"some-namespace"} }`
   189  	err := Decode([]byte(input), &failingFixture{}, true)
   190  	if err == nil {
   191  		t.Fatal("expected error")
   192  	}
   193  }
   194  
   195  type fixture struct {
   196  	// Our decoder doesn't support `inline` that's a sig.k8s.io/yaml feature
   197  	// So we skip parsing this property
   198  	metav1.TypeMeta   `json:"-"`
   199  	metav1.ObjectMeta `json:"metadata"`
   200  	Spec              map[string]string `json:"spec"`
   201  }
   202  
   203  var _ resourcesemantics.GenericCRD = (*fixture)(nil)
   204  
   205  func (f *fixture) DeepCopyObject() runtime.Object {
   206  	panic("not implemented")
   207  }
   208  
   209  func (f *fixture) SetDefaults(context.Context) {
   210  	panic("not implemented")
   211  }
   212  
   213  func (f *fixture) Validate(context.Context) *apis.FieldError {
   214  	panic("not implemented")
   215  }
   216  
   217  type failingFixture struct {
   218  	fixture
   219  	Meta failingMeta `json:"metadata"`
   220  }
   221  
   222  type failingMeta struct {
   223  	metav1.ObjectMeta
   224  }
   225  
   226  func (f *failingMeta) UnmarshalJSON(bites []byte) error {
   227  	if bytes.Equal([]byte("{}"), bites) {
   228  		return nil
   229  	}
   230  	return errors.New("bomba")
   231  }