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 }