knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/apis/duck/patch_test.go (about)

     1  /*
     2  Copyright 2018 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 duck
    18  
    19  import (
    20  	"encoding/json"
    21  	"errors"
    22  	"testing"
    23  
    24  	"github.com/google/go-cmp/cmp"
    25  	"k8s.io/apimachinery/pkg/runtime"
    26  	"k8s.io/apimachinery/pkg/runtime/schema"
    27  )
    28  
    29  func TestCreateMergePatch(t *testing.T) {
    30  	tests := []struct {
    31  		name    string
    32  		before  interface{}
    33  		after   interface{}
    34  		wantErr bool
    35  		want    []byte
    36  	}{{
    37  		name: "patch single field",
    38  		before: &Patch{
    39  			Spec: PatchSpec{
    40  				Patchable: &Patchable{
    41  					Field1: 12,
    42  				},
    43  			},
    44  		},
    45  		after: &Patch{
    46  			Spec: PatchSpec{
    47  				Patchable: &Patchable{
    48  					Field1: 13,
    49  				},
    50  			},
    51  		},
    52  		want: []byte(`{"status":{"patchable":{"field1":13}}}`),
    53  	}, {
    54  		name: "patch two fields",
    55  		before: &Patch{
    56  			Spec: PatchSpec{
    57  				Patchable: &Patchable{
    58  					Field1: 12,
    59  					Field2: true,
    60  				},
    61  			},
    62  		},
    63  		after: &Patch{
    64  			Spec: PatchSpec{
    65  				Patchable: &Patchable{
    66  					Field1: 42,
    67  					Field2: false,
    68  				},
    69  			},
    70  		},
    71  		want: []byte(`{"status":{"patchable":{"field1":42,"field2":null}}}`),
    72  	}, {
    73  		name: "patch slice bigger",
    74  		before: &Patch{
    75  			Spec: PatchSpec{
    76  				Patchable: &Patchable{
    77  					Array: []string{"foo", "baz"},
    78  				},
    79  			},
    80  		},
    81  		after: &Patch{
    82  			Spec: PatchSpec{
    83  				Patchable: &Patchable{
    84  					Array: []string{"foo", "bar", "baz"},
    85  				},
    86  			},
    87  		},
    88  		want: []byte(`{"status":{"patchable":{"array":["foo","bar","baz"]}}}`),
    89  	}, {
    90  		name: "patch array smaller",
    91  		before: &Patch{
    92  			Spec: PatchSpec{
    93  				Patchable: &Patchable{
    94  					Array: []string{"foo", "bar", "baz", "jimmy"},
    95  				},
    96  			},
    97  		},
    98  		after: &Patch{
    99  			Spec: PatchSpec{
   100  				Patchable: &Patchable{
   101  					Array: []string{"foo", "baz"},
   102  				},
   103  			},
   104  		},
   105  		want: []byte(`{"status":{"patchable":{"array":["foo","baz"]}}}`),
   106  	}, {
   107  		name:    "before doesn't marshal",
   108  		before:  &DoesntMarshal{},
   109  		after:   &Patch{},
   110  		wantErr: true,
   111  	}, {
   112  		name:    "after doesn't marshal",
   113  		before:  &Patch{},
   114  		after:   &DoesntMarshal{},
   115  		wantErr: true,
   116  	}}
   117  
   118  	for _, test := range tests {
   119  		t.Run(test.name, func(t *testing.T) {
   120  			got, err := CreateMergePatch(test.before, test.after)
   121  			if err != nil {
   122  				if !test.wantErr {
   123  					t.Error("CreateMergePatch() =", err)
   124  				}
   125  				return
   126  			} else if test.wantErr {
   127  				t.Errorf("CreateMergePatch() = %v, wanted error", got)
   128  				return
   129  			}
   130  
   131  			if !cmp.Equal(test.want, got) {
   132  				t.Errorf("CreatePatch = ->%s<-, diff: (-want, +got) =\n%s", string(got), cmp.Diff(string(test.want), string(got)))
   133  			}
   134  		})
   135  	}
   136  }
   137  
   138  func TestCreatePatch(t *testing.T) {
   139  	tests := []struct {
   140  		name    string
   141  		before  interface{}
   142  		after   interface{}
   143  		wantErr bool
   144  		want    JSONPatch
   145  	}{{
   146  		name: "patch single field",
   147  		before: &Patch{
   148  			Spec: PatchSpec{
   149  				Patchable: &Patchable{
   150  					Field1: 12,
   151  				},
   152  			},
   153  		},
   154  		after: &Patch{
   155  			Spec: PatchSpec{
   156  				Patchable: &Patchable{
   157  					Field1: 13,
   158  				},
   159  			},
   160  		},
   161  		want: JSONPatch{{
   162  			Operation: "replace",
   163  			Path:      "/status/patchable/field1",
   164  			Value:     json.Number("13"),
   165  		}},
   166  	}, {
   167  		name: "patch two fields",
   168  		before: &Patch{
   169  			Spec: PatchSpec{
   170  				Patchable: &Patchable{
   171  					Field1: 12,
   172  					Field2: true,
   173  				},
   174  			},
   175  		},
   176  		after: &Patch{
   177  			Spec: PatchSpec{
   178  				Patchable: &Patchable{
   179  					Field1: 42,
   180  					Field2: false,
   181  				},
   182  			},
   183  		},
   184  		want: JSONPatch{{
   185  			Operation: "replace",
   186  			Path:      "/status/patchable/field1",
   187  			Value:     json.Number("42"),
   188  		}, {
   189  			Operation: "remove",
   190  			Path:      "/status/patchable/field2",
   191  		}},
   192  	}, {
   193  		name: "patch array",
   194  		before: &Patch{
   195  			Spec: PatchSpec{
   196  				Patchable: &Patchable{
   197  					Array: []string{"foo", "baz"},
   198  				},
   199  			},
   200  		},
   201  		after: &Patch{
   202  			Spec: PatchSpec{
   203  				Patchable: &Patchable{
   204  					Array: []string{"foo", "bar", "baz"},
   205  				},
   206  			},
   207  		},
   208  		want: JSONPatch{{
   209  			Operation: "add",
   210  			Path:      "/status/patchable/array/2",
   211  			Value:     "baz",
   212  		}, {
   213  			Operation: "replace",
   214  			Path:      "/status/patchable/array/1",
   215  			Value:     "bar",
   216  		}},
   217  	}, {
   218  		name: "patch with remove elements from array",
   219  		before: &Patch{
   220  			Spec: PatchSpec{
   221  				Patchable: &Patchable{
   222  					Array: []string{"foo", "bar"},
   223  				},
   224  			},
   225  		},
   226  		after: &Patch{
   227  			Spec: PatchSpec{
   228  				Patchable: &Patchable{
   229  					Array: []string{"bar"},
   230  				},
   231  			},
   232  		},
   233  		want: JSONPatch{{
   234  			Operation: "remove",
   235  			Path:      "/status/patchable/array/1",
   236  		}, {
   237  			Operation: "replace",
   238  			Path:      "/status/patchable/array/0",
   239  			Value:     "bar",
   240  		}},
   241  	}, {
   242  		name: "patch with remove collection",
   243  		before: &Patch{
   244  			Spec: PatchSpec{
   245  				Patchable: &Patchable{
   246  					Array: []string{"foo", "bar"},
   247  				},
   248  			},
   249  		},
   250  		after: &Patch{
   251  			Spec: PatchSpec{
   252  				Patchable: &Patchable{},
   253  			},
   254  		},
   255  		want: JSONPatch{{
   256  			Operation: "remove",
   257  			Path:      "/status/patchable/array",
   258  		}},
   259  	}, {
   260  		name: "patch with add elements to array",
   261  		before: &Patch{
   262  			Spec: PatchSpec{
   263  				Patchable: &Patchable{
   264  					Array: []string{"bar"},
   265  				},
   266  			},
   267  		},
   268  		after: &Patch{
   269  			Spec: PatchSpec{
   270  				Patchable: &Patchable{
   271  					Array: []string{"foo", "bar"},
   272  				},
   273  			},
   274  		},
   275  		want: JSONPatch{{
   276  			Operation: "add",
   277  			Path:      "/status/patchable/array/1",
   278  			Value:     "bar",
   279  		}, {
   280  			Operation: "replace",
   281  			Path:      "/status/patchable/array/0",
   282  			Value:     "foo",
   283  		}},
   284  	}, {
   285  		name: "patch with add array",
   286  		before: &Patch{
   287  			Spec: PatchSpec{
   288  				Patchable: &Patchable{},
   289  			},
   290  		},
   291  		after: &Patch{
   292  			Spec: PatchSpec{
   293  				Patchable: &Patchable{
   294  					Array: []string{"foo"},
   295  				},
   296  			},
   297  		},
   298  		want: JSONPatch{{
   299  			Operation: "add",
   300  			Path:      "/status/patchable/array",
   301  			Value:     []interface{}{string("foo")},
   302  		}},
   303  	}, {
   304  		name:    "before doesn't marshal",
   305  		before:  &DoesntMarshal{},
   306  		after:   &Patch{},
   307  		wantErr: true,
   308  	}, {
   309  		name:    "after doesn't marshal",
   310  		before:  &Patch{},
   311  		after:   &DoesntMarshal{},
   312  		wantErr: true,
   313  	}}
   314  
   315  	for _, test := range tests {
   316  		t.Run(test.name+" (CreateBytePatch)", func(t *testing.T) {
   317  			got, err := CreateBytePatch(test.before, test.after)
   318  			if err != nil {
   319  				if !test.wantErr {
   320  					t.Error("CreateBytePatch() =", err)
   321  				}
   322  				return
   323  			} else if test.wantErr {
   324  				t.Errorf("CreateBytePatch() = %v, wanted error", got)
   325  				return
   326  			}
   327  
   328  			want, err := test.want.MarshalJSON()
   329  			if err != nil {
   330  				t.Errorf("Error marshaling 'want' condition. want: %v error: %v", want, err)
   331  			}
   332  
   333  			if diff := cmp.Diff(want, got); diff != "" {
   334  				t.Error("CreateBytePatch (-want, +got) =", diff)
   335  			}
   336  		})
   337  		t.Run(test.name, func(t *testing.T) {
   338  			got, err := CreatePatch(test.before, test.after)
   339  			if err != nil {
   340  				if !test.wantErr {
   341  					t.Error("CreatePatch() =", err)
   342  				}
   343  				return
   344  			} else if test.wantErr {
   345  				t.Errorf("CreatePatch() = %v, wanted error", got)
   346  				return
   347  			}
   348  
   349  			if diff := cmp.Diff(test.want, got); diff != "" {
   350  				t.Error("CreatePatch (-want, +got) =", diff)
   351  			}
   352  		})
   353  	}
   354  }
   355  
   356  func TestPatchToJSON(t *testing.T) {
   357  	input := JSONPatch{{
   358  		Operation: "replace",
   359  		Path:      "/status/patchable/field1",
   360  		Value:     42.0,
   361  	}, {
   362  		Operation: "remove",
   363  		Path:      "/status/patchable/field2",
   364  	}}
   365  
   366  	b, err := input.MarshalJSON()
   367  	if err != nil {
   368  		t.Error("MarshalJSON() =", err)
   369  	}
   370  
   371  	want := `[{"op":"replace","path":"/status/patchable/field1","value":42},{"op":"remove","path":"/status/patchable/field2"}]`
   372  
   373  	got := string(b)
   374  	if got != want {
   375  		t.Errorf("MarshalJSON() = %v, wanted %v", got, want)
   376  	}
   377  }
   378  
   379  type DoesntMarshal struct{}
   380  
   381  var _ json.Marshaler = (*DoesntMarshal)(nil)
   382  
   383  func (*DoesntMarshal) MarshalJSON() ([]byte, error) {
   384  	return nil, errors.New("what did you expect?")
   385  }
   386  
   387  // Define a "Patchable" duck type.
   388  type Patchable struct {
   389  	Field1 int      `json:"field1,omitempty"`
   390  	Field2 bool     `json:"field2,omitempty"`
   391  	Array  []string `json:"array,omitempty"`
   392  }
   393  type Patch struct {
   394  	Spec PatchSpec `json:"status"`
   395  }
   396  type PatchSpec struct {
   397  	Patchable *Patchable `json:"patchable,omitempty"`
   398  }
   399  
   400  var (
   401  	_ Implementable = (*Patchable)(nil)
   402  	_ Populatable   = (*Patch)(nil)
   403  )
   404  
   405  func (*Patch) GetObjectKind() schema.ObjectKind {
   406  	return nil // not used
   407  }
   408  
   409  func (*Patch) DeepCopyObject() runtime.Object {
   410  	return nil // not used
   411  }
   412  
   413  func (*Patch) GetListType() runtime.Object {
   414  	return nil // not used
   415  }
   416  
   417  func (*Patchable) GetFullType() Populatable {
   418  	return &Patch{}
   419  }
   420  
   421  func (f *Patch) Populate() {
   422  	f.Spec.Patchable = &Patchable{
   423  		// Populate ALL fields
   424  		Field1: 42,
   425  		Field2: true,
   426  	}
   427  }