github.com/prebid/prebid-server/v2@v2.18.0/util/jsonutil/merge_test.go (about) 1 package jsonutil 2 3 import ( 4 "encoding/json" 5 "testing" 6 7 "github.com/prebid/prebid-server/v2/util/sliceutil" 8 9 "github.com/prebid/openrtb/v20/openrtb2" 10 "github.com/stretchr/testify/assert" 11 "github.com/stretchr/testify/require" 12 ) 13 14 func TestMergeClonePtr(t *testing.T) { 15 t.Run("root", func(t *testing.T) { 16 var ( 17 banner = &openrtb2.Banner{ID: "1"} 18 imp = &openrtb2.Imp{Banner: banner} 19 impOriginal = imp 20 ) 21 22 // root objects are not cloned 23 err := MergeClone(imp, []byte(`{"banner":{"id":"4"}}`)) 24 require.NoError(t, err) 25 26 assert.Same(t, impOriginal, imp, "imp-ref") 27 assert.NotSame(t, imp.Banner, banner, "banner-ref") 28 }) 29 30 t.Run("embedded-nil", func(t *testing.T) { 31 var ( 32 banner = &openrtb2.Banner{ID: "1"} 33 video = &openrtb2.Video{PodID: "a"} 34 imp = &openrtb2.Imp{Banner: banner, Video: video} 35 ) 36 37 err := MergeClone(imp, []byte(`{"banner":null}`)) 38 require.NoError(t, err) 39 40 assert.NotSame(t, banner, imp.Banner, "banner-ref") 41 assert.Same(t, video, imp.Video, "video") 42 assert.Nil(t, imp.Banner, "banner-nil") 43 }) 44 45 t.Run("embedded-struct", func(t *testing.T) { 46 var ( 47 banner = &openrtb2.Banner{ID: "1"} 48 video = &openrtb2.Video{PodID: "a"} 49 imp = &openrtb2.Imp{Banner: banner, Video: video} 50 ) 51 52 err := MergeClone(imp, []byte(`{"banner":{"id":"2"}}`)) 53 require.NoError(t, err) 54 55 assert.NotSame(t, banner, imp.Banner, "banner-ref") 56 assert.Same(t, video, imp.Video, "video-ref") 57 assert.Equal(t, "1", banner.ID, "id-original") 58 assert.Equal(t, "2", imp.Banner.ID, "id-clone") 59 }) 60 61 t.Run("embedded-int", func(t *testing.T) { 62 var ( 63 clickbrowser = int8(1) 64 imp = &openrtb2.Imp{ClickBrowser: &clickbrowser} 65 ) 66 67 err := MergeClone(imp, []byte(`{"clickbrowser":2}`)) 68 require.NoError(t, err) 69 70 require.NotNil(t, imp.ClickBrowser, "clickbrowser-nil") 71 assert.NotSame(t, clickbrowser, imp.ClickBrowser, "clickbrowser-ref") 72 assert.Equal(t, int8(2), *imp.ClickBrowser, "clickbrowser-val") 73 }) 74 75 t.Run("invalid-null", func(t *testing.T) { 76 var ( 77 banner = &openrtb2.Banner{ID: "1"} 78 imp = &openrtb2.Imp{Banner: banner} 79 ) 80 81 err := MergeClone(imp, []byte(`{"banner":nul}`)) 82 83 // json-iter will produce an error since "nul" is not a valid json value. the 84 // parsing code will see the "n" and then expect "ull" to follow. the strange 85 // "expect ull" error being asserted is generated by json-iter. 86 require.EqualError(t, err, "cannot unmarshal openrtb2.Imp.Banner: expect ull") 87 }) 88 89 t.Run("invalid-malformed", func(t *testing.T) { 90 var ( 91 banner = &openrtb2.Banner{ID: "1"} 92 imp = &openrtb2.Imp{Banner: banner} 93 ) 94 95 err := MergeClone(imp, []byte(`{"banner":malformed}`)) 96 require.EqualError(t, err, "cannot unmarshal openrtb2.Imp.Banner: expect { or n, but found m") 97 }) 98 } 99 100 func TestMergeCloneSlice(t *testing.T) { 101 t.Run("null", func(t *testing.T) { 102 var ( 103 iframeBuster = []string{"a", "b"} 104 imp = &openrtb2.Imp{IframeBuster: iframeBuster} 105 ) 106 107 err := MergeClone(imp, []byte(`{"iframeBuster":null}`)) 108 require.NoError(t, err) 109 110 assert.Equal(t, []string{"a", "b"}, iframeBuster, "iframeBuster-val") 111 assert.Nil(t, imp.IframeBuster, "iframeBuster-nil") 112 }) 113 114 t.Run("one", func(t *testing.T) { 115 var ( 116 iframeBuster = []string{"a"} 117 imp = &openrtb2.Imp{IframeBuster: iframeBuster} 118 ) 119 120 err := MergeClone(imp, []byte(`{"iframeBuster":["b"]}`)) 121 require.NoError(t, err) 122 123 assert.NotSame(t, iframeBuster, imp.IframeBuster, "ref") 124 assert.Equal(t, []string{"a"}, iframeBuster, "original-val") 125 assert.Equal(t, []string{"b"}, imp.IframeBuster, "new-val") 126 }) 127 128 t.Run("many", func(t *testing.T) { 129 var ( 130 iframeBuster = []string{"a"} 131 imp = &openrtb2.Imp{IframeBuster: iframeBuster} 132 ) 133 134 err := MergeClone(imp, []byte(`{"iframeBuster":["b", "c"]}`)) 135 require.NoError(t, err) 136 137 assert.NotSame(t, iframeBuster, imp.IframeBuster, "ref") 138 assert.Equal(t, []string{"a"}, iframeBuster, "original-val") 139 assert.Equal(t, []string{"b", "c"}, imp.IframeBuster, "new-val") 140 }) 141 142 t.Run("invalid-null", func(t *testing.T) { 143 var ( 144 iframeBuster = []string{"a"} 145 imp = &openrtb2.Imp{IframeBuster: iframeBuster} 146 ) 147 148 err := MergeClone(imp, []byte(`{"iframeBuster":nul}`)) 149 150 // json-iter will produce an error since "nul" is not a valid json value. the 151 // parsing code will see the "n" and then expect "ull" to follow. the strange 152 // "expect ull" error being asserted is generated by json-iter. 153 require.EqualError(t, err, "cannot unmarshal openrtb2.Imp.IframeBuster: expect ull") 154 }) 155 156 t.Run("invalid-malformed", func(t *testing.T) { 157 var ( 158 iframeBuster = []string{"a"} 159 imp = &openrtb2.Imp{IframeBuster: iframeBuster} 160 ) 161 162 err := MergeClone(imp, []byte(`{"iframeBuster":malformed}`)) 163 require.EqualError(t, err, "cannot unmarshal openrtb2.Imp.IframeBuster: decode slice: expect [ or n, but found m") 164 }) 165 } 166 167 func TestMergeCloneMap(t *testing.T) { 168 t.Run("null", func(t *testing.T) { 169 var ( 170 testMap = map[string]int{"a": 1, "b": 2} 171 test = &struct { 172 Foo map[string]int `json:"foo"` 173 }{Foo: testMap} 174 ) 175 176 err := MergeClone(test, []byte(`{"foo":null}`)) 177 require.NoError(t, err) 178 179 assert.NotSame(t, testMap, test.Foo, "ref") 180 assert.Equal(t, map[string]int{"a": 1, "b": 2}, testMap, "val") 181 assert.Nil(t, test.Foo, "nil") 182 }) 183 184 t.Run("key-string", func(t *testing.T) { 185 var ( 186 testMap = map[string]int{"a": 1, "b": 2} 187 test = &struct { 188 Foo map[string]int `json:"foo"` 189 }{Foo: testMap} 190 ) 191 192 err := MergeClone(test, []byte(`{"foo":{"c":3}}`)) 193 require.NoError(t, err) 194 195 assert.NotSame(t, testMap, test.Foo) 196 assert.Equal(t, map[string]int{"a": 1, "b": 2}, testMap, "original-val") 197 assert.Equal(t, map[string]int{"a": 1, "b": 2, "c": 3}, test.Foo, "new-val") 198 199 // verify modifications don't corrupt original 200 testMap["a"] = 10 201 assert.Equal(t, map[string]int{"a": 10, "b": 2}, testMap, "mod-original-val") 202 assert.Equal(t, map[string]int{"a": 1, "b": 2, "c": 3}, test.Foo, "mod-ew-val") 203 }) 204 205 t.Run("key-numeric", func(t *testing.T) { 206 var ( 207 testMap = map[int]string{1: "a", 2: "b"} 208 test = &struct { 209 Foo map[int]string `json:"foo"` 210 }{Foo: testMap} 211 ) 212 213 err := MergeClone(test, []byte(`{"foo":{"3":"c"}}`)) 214 require.NoError(t, err) 215 216 assert.NotSame(t, testMap, test.Foo) 217 assert.Equal(t, map[int]string{1: "a", 2: "b"}, testMap, "original-val") 218 assert.Equal(t, map[int]string{1: "a", 2: "b", 3: "c"}, test.Foo, "new-val") 219 220 // verify modifications don't corrupt original 221 testMap[1] = "z" 222 assert.Equal(t, map[int]string{1: "z", 2: "b"}, testMap, "mod-original-val") 223 assert.Equal(t, map[int]string{1: "a", 2: "b", 3: "c"}, test.Foo, "mod-ew-val") 224 }) 225 226 t.Run("invalid-null", func(t *testing.T) { 227 var ( 228 testMap = map[int]string{1: "a", 2: "b"} 229 test = &struct { 230 Foo map[int]string `json:"foo"` 231 }{Foo: testMap} 232 ) 233 234 err := MergeClone(test, []byte(`{"foo":nul}`)) 235 236 // json-iter will produce an error since "nul" is not a valid json value. the 237 // parsing code will see the "n" and then expect "ull" to follow. the strange 238 // "expect ull" error being asserted is generated by json-iter. 239 require.EqualError(t, err, "cannot unmarshal Foo: expect ull") 240 }) 241 242 t.Run("invalid-malformed", func(t *testing.T) { 243 var ( 244 testMap = map[int]string{1: "a", 2: "b"} 245 test = &struct { 246 Foo map[int]string `json:"foo"` 247 }{Foo: testMap} 248 ) 249 250 err := MergeClone(test, []byte(`{"foo":malformed}`)) 251 require.EqualError(t, err, "cannot unmarshal Foo: expect { or n, but found m") 252 }) 253 } 254 255 func TestMergeCloneExt(t *testing.T) { 256 testCases := []struct { 257 name string 258 givenExisting json.RawMessage 259 givenIncoming json.RawMessage 260 expectedExt json.RawMessage 261 expectedErr string 262 }{ 263 { 264 name: "both-populated", 265 givenExisting: json.RawMessage(`{"a":1,"b":2}`), 266 givenIncoming: json.RawMessage(`{"b":200,"c":3}`), 267 expectedExt: json.RawMessage(`{"a":1,"b":200,"c":3}`), 268 }, 269 { 270 name: "both-omitted", 271 givenExisting: nil, 272 givenIncoming: nil, 273 expectedExt: nil, 274 }, 275 { 276 name: "both-nil", 277 givenExisting: nil, 278 givenIncoming: json.RawMessage(`null`), 279 expectedExt: nil, 280 }, 281 { 282 name: "both-empty", 283 givenExisting: nil, 284 givenIncoming: json.RawMessage(`{}`), 285 expectedExt: json.RawMessage(`{}`), 286 }, 287 { 288 name: "ext-omitted", 289 givenExisting: json.RawMessage(`{"b":2}`), 290 givenIncoming: nil, 291 expectedExt: json.RawMessage(`{"b":2}`), 292 }, 293 { 294 name: "ext-nil", 295 givenExisting: json.RawMessage(`{"b":2}`), 296 givenIncoming: json.RawMessage(`null`), 297 expectedExt: json.RawMessage(`{"b":2}`), 298 }, 299 { 300 name: "ext-empty", 301 givenExisting: json.RawMessage(`{"b":2}`), 302 givenIncoming: json.RawMessage(`{}`), 303 expectedExt: json.RawMessage(`{"b":2}`), 304 }, 305 { 306 name: "ext-malformed", 307 givenExisting: json.RawMessage(`{"b":2}`), 308 givenIncoming: json.RawMessage(`malformed`), 309 expectedErr: "openrtb2.BidRequest.Ext", 310 }, 311 { 312 name: "existing-nil", 313 givenExisting: nil, 314 givenIncoming: json.RawMessage(`{"a":1}`), 315 expectedExt: json.RawMessage(`{"a":1}`), 316 }, 317 { 318 name: "existing-empty", 319 givenExisting: json.RawMessage(`{}`), 320 givenIncoming: json.RawMessage(`{"a":1}`), 321 expectedExt: json.RawMessage(`{"a":1}`), 322 }, 323 { 324 name: "existing-omitted", 325 givenExisting: nil, 326 givenIncoming: json.RawMessage(`{"b":2}`), 327 expectedExt: json.RawMessage(`{"b":2}`), 328 }, 329 { 330 name: "existing-malformed", 331 givenExisting: json.RawMessage(`malformed`), 332 givenIncoming: json.RawMessage(`{"a":1}`), 333 expectedErr: "cannot unmarshal openrtb2.BidRequest.Ext: invalid json on existing object", 334 }, 335 } 336 337 for _, test := range testCases { 338 t.Run(test.name, func(t *testing.T) { 339 // copy original values to check at the end for no modification 340 originalExisting := sliceutil.Clone(test.givenExisting) 341 originalIncoming := sliceutil.Clone(test.givenIncoming) 342 343 // build request 344 request := &openrtb2.BidRequest{Ext: test.givenExisting} 345 346 // build data 347 data := test.givenIncoming 348 if len(data) > 0 { 349 data = []byte(`{"ext":` + string(data) + `}`) // wrap in ext 350 } else { 351 data = []byte(`{}`) // omit ext 352 } 353 354 err := MergeClone(request, data) 355 356 // assert error 357 if test.expectedErr == "" { 358 assert.NoError(t, err, "err") 359 } else { 360 assert.ErrorContains(t, err, test.expectedErr, "err") 361 } 362 363 // assert ext 364 if test.expectedErr != "" { 365 // expect no change in case of error 366 assert.Equal(t, string(test.givenExisting), string(request.Ext), "json") 367 } else { 368 // compare as strings instead of json in case of nil or malformed ext 369 assert.Equal(t, string(test.expectedExt), string(request.Ext), "json") 370 } 371 372 // assert no modifications 373 // - can't use `assert.Same`` comparison checks since that's expected if 374 // either existing or incoming are nil / omitted / empty. 375 assert.Equal(t, originalExisting, []byte(test.givenExisting), "existing") 376 assert.Equal(t, originalIncoming, []byte(test.givenIncoming), "incoming") 377 }) 378 } 379 } 380 381 func TestMergeCloneCombinations(t *testing.T) { 382 t.Run("slice-of-ptr", func(t *testing.T) { 383 var ( 384 imp = &openrtb2.Imp{ID: "1"} 385 impSlice = []*openrtb2.Imp{imp} 386 test = &struct { 387 Imps []*openrtb2.Imp `json:"imps"` 388 }{Imps: impSlice} 389 ) 390 391 err := MergeClone(test, []byte(`{"imps":[{"id":"2"}]}`)) 392 require.NoError(t, err) 393 394 assert.NotSame(t, impSlice, test.Imps, "slice-ref") 395 require.Len(t, test.Imps, 1, "slice-len") 396 397 assert.NotSame(t, imp, test.Imps[0], "item-ref") 398 assert.Equal(t, "1", imp.ID, "original-val") 399 assert.Equal(t, "2", test.Imps[0].ID, "new-val") 400 }) 401 402 // special case of "slice-of-ptr" 403 t.Run("jsonrawmessage-ptr", func(t *testing.T) { 404 var ( 405 testJson = json.RawMessage(`{"a":1}`) 406 test = &struct { 407 Foo *json.RawMessage `json:"foo"` 408 }{Foo: &testJson} 409 ) 410 411 err := MergeClone(test, []byte(`{"foo":{"b":2}}`)) 412 require.NoError(t, err) 413 414 assert.NotSame(t, &testJson, test.Foo, "ref") 415 assert.Equal(t, json.RawMessage(`{"a":1}`), testJson) 416 assert.Equal(t, json.RawMessage(`{"a":1,"b":2}`), *test.Foo) 417 }) 418 419 t.Run("struct-ptr", func(t *testing.T) { 420 var ( 421 imp = &openrtb2.Imp{ID: "1"} 422 test = &struct { 423 Imp *openrtb2.Imp `json:"imp"` 424 }{Imp: imp} 425 ) 426 427 err := MergeClone(test, []byte(`{"imp":{"id":"2"}}`)) 428 require.NoError(t, err) 429 430 assert.NotSame(t, imp, test.Imp, "ref") 431 assert.Equal(t, "1", imp.ID, "original-val") 432 assert.Equal(t, "2", test.Imp.ID, "new-val") 433 }) 434 435 t.Run("map-of-ptrs", func(t *testing.T) { 436 var ( 437 imp = &openrtb2.Imp{ID: "1"} 438 impMap = map[string]*openrtb2.Imp{"a": imp} 439 test = &struct { 440 Imps map[string]*openrtb2.Imp `json:"imps"` 441 }{Imps: impMap} 442 ) 443 444 err := MergeClone(test, []byte(`{"imps":{"a":{"id":"2"}}}`)) 445 require.NoError(t, err) 446 447 assert.NotSame(t, impMap, test.Imps, "map-ref") 448 assert.NotSame(t, imp, test.Imps["a"], "imp-ref") 449 450 assert.Same(t, impMap["a"], imp, "imp-map-ref") 451 452 assert.Equal(t, "1", imp.ID, "original-val") 453 assert.Equal(t, "2", test.Imps["a"].ID, "new-val") 454 }) 455 }