github.com/prebid/prebid-server@v0.275.0/firstpartydata/first_party_data_test.go (about) 1 package firstpartydata 2 3 import ( 4 "encoding/json" 5 "os" 6 "reflect" 7 "testing" 8 9 "github.com/prebid/openrtb/v19/openrtb2" 10 "github.com/prebid/prebid-server/errortypes" 11 "github.com/prebid/prebid-server/openrtb_ext" 12 "github.com/stretchr/testify/assert" 13 "github.com/stretchr/testify/require" 14 ) 15 16 func TestExtractGlobalFPD(t *testing.T) { 17 testCases := []struct { 18 description string 19 input openrtb_ext.RequestWrapper 20 expectedReq openrtb_ext.RequestWrapper 21 expectedFpd map[string][]byte 22 }{ 23 { 24 description: "Site, app and user data present", 25 input: openrtb_ext.RequestWrapper{ 26 BidRequest: &openrtb2.BidRequest{ 27 ID: "bid_id", 28 Site: &openrtb2.Site{ 29 ID: "reqSiteId", 30 Page: "http://www.foobar.com/1234.html", 31 Publisher: &openrtb2.Publisher{ 32 ID: "1", 33 }, 34 Ext: json.RawMessage(`{"data": {"somesitefpd": "sitefpdDataTest"}}`), 35 }, 36 User: &openrtb2.User{ 37 ID: "reqUserID", 38 Yob: 1982, 39 Gender: "M", 40 Ext: json.RawMessage(`{"data": {"someuserfpd": "userfpdDataTest"}}`), 41 }, 42 App: &openrtb2.App{ 43 ID: "appId", 44 Ext: json.RawMessage(`{"data": {"someappfpd": "appfpdDataTest"}}`), 45 }, 46 }, 47 }, 48 expectedReq: openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{ 49 ID: "bid_id", 50 Site: &openrtb2.Site{ 51 ID: "reqSiteId", 52 Page: "http://www.foobar.com/1234.html", 53 Publisher: &openrtb2.Publisher{ 54 ID: "1", 55 }, 56 }, 57 User: &openrtb2.User{ 58 ID: "reqUserID", 59 Yob: 1982, 60 Gender: "M", 61 }, 62 App: &openrtb2.App{ 63 ID: "appId", 64 }, 65 }}, 66 expectedFpd: map[string][]byte{ 67 "site": []byte(`{"somesitefpd": "sitefpdDataTest"}`), 68 "user": []byte(`{"someuserfpd": "userfpdDataTest"}`), 69 "app": []byte(`{"someappfpd": "appfpdDataTest"}`), 70 }, 71 }, 72 { 73 description: "App FPD only present", 74 input: openrtb_ext.RequestWrapper{ 75 BidRequest: &openrtb2.BidRequest{ 76 ID: "bid_id", 77 Site: &openrtb2.Site{ 78 ID: "reqSiteId", 79 Page: "http://www.foobar.com/1234.html", 80 Publisher: &openrtb2.Publisher{ 81 ID: "1", 82 }, 83 }, 84 App: &openrtb2.App{ 85 ID: "appId", 86 Ext: json.RawMessage(`{"data": {"someappfpd": "appfpdDataTest"}}`), 87 }, 88 }, 89 }, 90 expectedReq: openrtb_ext.RequestWrapper{ 91 BidRequest: &openrtb2.BidRequest{ 92 ID: "bid_id", 93 Site: &openrtb2.Site{ 94 ID: "reqSiteId", 95 Page: "http://www.foobar.com/1234.html", 96 Publisher: &openrtb2.Publisher{ 97 ID: "1", 98 }, 99 }, 100 App: &openrtb2.App{ 101 ID: "appId", 102 }, 103 }, 104 }, 105 expectedFpd: map[string][]byte{ 106 "app": []byte(`{"someappfpd": "appfpdDataTest"}`), 107 "user": nil, 108 "site": nil, 109 }, 110 }, 111 { 112 description: "User FPD only present", 113 input: openrtb_ext.RequestWrapper{ 114 BidRequest: &openrtb2.BidRequest{ 115 ID: "bid_id", 116 Site: &openrtb2.Site{ 117 ID: "reqSiteId", 118 Page: "http://www.foobar.com/1234.html", 119 Publisher: &openrtb2.Publisher{ 120 ID: "1", 121 }, 122 }, 123 User: &openrtb2.User{ 124 ID: "reqUserID", 125 Yob: 1982, 126 Gender: "M", 127 Ext: json.RawMessage(`{"data": {"someuserfpd": "userfpdDataTest"}}`), 128 }, 129 }, 130 }, 131 expectedReq: openrtb_ext.RequestWrapper{ 132 BidRequest: &openrtb2.BidRequest{ 133 ID: "bid_id", 134 Site: &openrtb2.Site{ 135 ID: "reqSiteId", 136 Page: "http://www.foobar.com/1234.html", 137 Publisher: &openrtb2.Publisher{ 138 ID: "1", 139 }, 140 }, 141 User: &openrtb2.User{ 142 ID: "reqUserID", 143 Yob: 1982, 144 Gender: "M", 145 }, 146 }, 147 }, 148 expectedFpd: map[string][]byte{ 149 "app": nil, 150 "user": []byte(`{"someuserfpd": "userfpdDataTest"}`), 151 "site": nil, 152 }, 153 }, 154 { 155 description: "No FPD present in req", 156 input: openrtb_ext.RequestWrapper{ 157 BidRequest: &openrtb2.BidRequest{ 158 ID: "bid_id", 159 Site: &openrtb2.Site{ 160 ID: "reqSiteId", 161 Page: "http://www.foobar.com/1234.html", 162 Publisher: &openrtb2.Publisher{ 163 ID: "1", 164 }, 165 }, 166 User: &openrtb2.User{ 167 ID: "reqUserID", 168 Yob: 1982, 169 Gender: "M", 170 }, 171 App: &openrtb2.App{ 172 ID: "appId", 173 }, 174 }, 175 }, 176 expectedReq: openrtb_ext.RequestWrapper{ 177 BidRequest: &openrtb2.BidRequest{ 178 ID: "bid_id", 179 Site: &openrtb2.Site{ 180 ID: "reqSiteId", 181 Page: "http://www.foobar.com/1234.html", 182 Publisher: &openrtb2.Publisher{ 183 ID: "1", 184 }, 185 }, 186 User: &openrtb2.User{ 187 ID: "reqUserID", 188 Yob: 1982, 189 Gender: "M", 190 }, 191 App: &openrtb2.App{ 192 ID: "appId", 193 }, 194 }, 195 }, 196 expectedFpd: map[string][]byte{ 197 "app": nil, 198 "user": nil, 199 "site": nil, 200 }, 201 }, 202 { 203 description: "Site FPD only present", 204 input: openrtb_ext.RequestWrapper{ 205 BidRequest: &openrtb2.BidRequest{ 206 ID: "bid_id", 207 Site: &openrtb2.Site{ 208 ID: "reqSiteId", 209 Page: "http://www.foobar.com/1234.html", 210 Publisher: &openrtb2.Publisher{ 211 ID: "1", 212 }, 213 Ext: json.RawMessage(`{"data": {"someappfpd": true}}`), 214 }, 215 App: &openrtb2.App{ 216 ID: "appId", 217 }, 218 }, 219 }, 220 expectedReq: openrtb_ext.RequestWrapper{ 221 BidRequest: &openrtb2.BidRequest{ 222 ID: "bid_id", 223 Site: &openrtb2.Site{ 224 ID: "reqSiteId", 225 Page: "http://www.foobar.com/1234.html", 226 Publisher: &openrtb2.Publisher{ 227 ID: "1", 228 }, 229 }, 230 App: &openrtb2.App{ 231 ID: "appId", 232 }, 233 }, 234 }, 235 expectedFpd: map[string][]byte{ 236 "app": nil, 237 "user": nil, 238 "site": []byte(`{"someappfpd": true}`), 239 }, 240 }, 241 } 242 for _, test := range testCases { 243 244 inputReq := &test.input 245 fpd, err := ExtractGlobalFPD(inputReq) 246 assert.NoError(t, err, "Error should be nil") 247 err = inputReq.RebuildRequest() 248 assert.NoError(t, err, "Error should be nil") 249 250 assert.Equal(t, test.expectedReq.BidRequest, inputReq.BidRequest, "Incorrect input request after global fpd extraction") 251 252 assert.Equal(t, test.expectedFpd[userKey], fpd[userKey], "Incorrect User FPD") 253 assert.Equal(t, test.expectedFpd[appKey], fpd[appKey], "Incorrect App FPD") 254 assert.Equal(t, test.expectedFpd[siteKey], fpd[siteKey], "Incorrect Site FPDt") 255 } 256 } 257 258 func TestExtractOpenRtbGlobalFPD(t *testing.T) { 259 testCases := []struct { 260 description string 261 input openrtb2.BidRequest 262 output openrtb2.BidRequest 263 expectedFpdData map[string][]openrtb2.Data 264 }{ 265 { 266 description: "Site, app and user data present", 267 input: openrtb2.BidRequest{ 268 ID: "bid_id", 269 Imp: []openrtb2.Imp{ 270 {ID: "impid"}, 271 }, 272 Site: &openrtb2.Site{ 273 ID: "reqSiteId", 274 Content: &openrtb2.Content{ 275 Data: []openrtb2.Data{ 276 {ID: "siteDataId1", Name: "siteDataName1"}, 277 {ID: "siteDataId2", Name: "siteDataName2"}, 278 }, 279 }, 280 }, 281 User: &openrtb2.User{ 282 ID: "reqUserID", 283 Yob: 1982, 284 Gender: "M", 285 Data: []openrtb2.Data{ 286 {ID: "userDataId1", Name: "userDataName1"}, 287 }, 288 }, 289 App: &openrtb2.App{ 290 ID: "appId", 291 Content: &openrtb2.Content{ 292 Data: []openrtb2.Data{ 293 {ID: "appDataId1", Name: "appDataName1"}, 294 }, 295 }, 296 }, 297 }, 298 output: openrtb2.BidRequest{ 299 ID: "bid_id", 300 Imp: []openrtb2.Imp{ 301 {ID: "impid"}, 302 }, 303 Site: &openrtb2.Site{ 304 ID: "reqSiteId", 305 Content: &openrtb2.Content{}, 306 }, 307 User: &openrtb2.User{ 308 ID: "reqUserID", 309 Yob: 1982, 310 Gender: "M", 311 }, 312 App: &openrtb2.App{ 313 ID: "appId", 314 Content: &openrtb2.Content{}, 315 }, 316 }, 317 expectedFpdData: map[string][]openrtb2.Data{ 318 siteContentDataKey: {{ID: "siteDataId1", Name: "siteDataName1"}, {ID: "siteDataId2", Name: "siteDataName2"}}, 319 userDataKey: {{ID: "userDataId1", Name: "userDataName1"}}, 320 appContentDataKey: {{ID: "appDataId1", Name: "appDataName1"}}, 321 }, 322 }, 323 { 324 description: "No Site, app or user data present", 325 input: openrtb2.BidRequest{ 326 ID: "bid_id", 327 Imp: []openrtb2.Imp{ 328 {ID: "impid"}, 329 }, 330 }, 331 output: openrtb2.BidRequest{ 332 ID: "bid_id", 333 Imp: []openrtb2.Imp{ 334 {ID: "impid"}, 335 }, 336 }, 337 expectedFpdData: map[string][]openrtb2.Data{ 338 siteContentDataKey: nil, 339 userDataKey: nil, 340 appContentDataKey: nil, 341 }, 342 }, 343 { 344 description: "Site only data present", 345 input: openrtb2.BidRequest{ 346 ID: "bid_id", 347 Imp: []openrtb2.Imp{ 348 {ID: "impid"}, 349 }, 350 Site: &openrtb2.Site{ 351 ID: "reqSiteId", 352 Page: "test/page", 353 Content: &openrtb2.Content{ 354 Data: []openrtb2.Data{ 355 {ID: "siteDataId1", Name: "siteDataName1"}, 356 }, 357 }, 358 }, 359 }, 360 output: openrtb2.BidRequest{ 361 ID: "bid_id", 362 Imp: []openrtb2.Imp{ 363 {ID: "impid"}, 364 }, 365 Site: &openrtb2.Site{ 366 ID: "reqSiteId", 367 Page: "test/page", 368 Content: &openrtb2.Content{}, 369 }, 370 }, 371 expectedFpdData: map[string][]openrtb2.Data{ 372 siteContentDataKey: {{ID: "siteDataId1", Name: "siteDataName1"}}, 373 userDataKey: nil, 374 appContentDataKey: nil, 375 }, 376 }, 377 { 378 description: "App only data present", 379 input: openrtb2.BidRequest{ 380 ID: "bid_id", 381 Imp: []openrtb2.Imp{ 382 {ID: "impid"}, 383 }, 384 App: &openrtb2.App{ 385 ID: "reqAppId", 386 Content: &openrtb2.Content{ 387 Data: []openrtb2.Data{ 388 {ID: "appDataId1", Name: "appDataName1"}, 389 }, 390 }, 391 }, 392 }, 393 output: openrtb2.BidRequest{ 394 ID: "bid_id", 395 Imp: []openrtb2.Imp{ 396 {ID: "impid"}, 397 }, 398 App: &openrtb2.App{ 399 ID: "reqAppId", 400 Content: &openrtb2.Content{}, 401 }, 402 }, 403 expectedFpdData: map[string][]openrtb2.Data{ 404 siteContentDataKey: nil, 405 userDataKey: nil, 406 appContentDataKey: {{ID: "appDataId1", Name: "appDataName1"}}, 407 }, 408 }, 409 { 410 description: "User only data present", 411 input: openrtb2.BidRequest{ 412 ID: "bid_id", 413 Imp: []openrtb2.Imp{ 414 {ID: "impid"}, 415 }, 416 Site: &openrtb2.Site{ 417 ID: "reqSiteId", 418 }, 419 App: &openrtb2.App{ 420 ID: "reqAppId", 421 }, 422 User: &openrtb2.User{ 423 ID: "reqUserId", 424 Yob: 1982, 425 Gender: "M", 426 Data: []openrtb2.Data{ 427 {ID: "userDataId1", Name: "userDataName1"}, 428 }, 429 }, 430 }, 431 output: openrtb2.BidRequest{ 432 ID: "bid_id", 433 Imp: []openrtb2.Imp{ 434 {ID: "impid"}, 435 }, 436 Site: &openrtb2.Site{ 437 ID: "reqSiteId", 438 }, 439 App: &openrtb2.App{ 440 ID: "reqAppId", 441 }, 442 User: &openrtb2.User{ 443 ID: "reqUserId", 444 Yob: 1982, 445 Gender: "M", 446 }, 447 }, 448 expectedFpdData: map[string][]openrtb2.Data{ 449 siteContentDataKey: nil, 450 userDataKey: {{ID: "userDataId1", Name: "userDataName1"}}, 451 appContentDataKey: nil, 452 }, 453 }, 454 } 455 for _, test := range testCases { 456 457 inputReq := &test.input 458 459 res := ExtractOpenRtbGlobalFPD(inputReq) 460 461 assert.Equal(t, &test.output, inputReq, "Result request is incorrect") 462 assert.Equal(t, test.expectedFpdData[siteContentDataKey], res[siteContentDataKey], "siteContentData data is incorrect") 463 assert.Equal(t, test.expectedFpdData[userDataKey], res[userDataKey], "userData is incorrect") 464 assert.Equal(t, test.expectedFpdData[appContentDataKey], res[appContentDataKey], "appContentData is incorrect") 465 466 } 467 } 468 469 func TestExtractBidderConfigFPD(t *testing.T) { 470 testPath := "tests/extractbidderconfigfpd" 471 472 tests, err := os.ReadDir(testPath) 473 require.NoError(t, err, "Cannot Discover Tests") 474 475 for _, test := range tests { 476 t.Run(test.Name(), func(t *testing.T) { 477 filePath := testPath + "/" + test.Name() 478 479 fpdFile, err := loadFpdFile(filePath) 480 require.NoError(t, err, "Cannot Load Test") 481 482 givenRequestExtPrebid := &openrtb_ext.ExtRequestPrebid{} 483 err = json.Unmarshal(fpdFile.InputRequestData, givenRequestExtPrebid) 484 require.NoError(t, err, "Cannot Load Test Conditions") 485 486 testRequest := &openrtb_ext.RequestExt{} 487 testRequest.SetPrebid(givenRequestExtPrebid) 488 489 // run test 490 results, err := ExtractBidderConfigFPD(testRequest) 491 492 // assert errors 493 if len(fpdFile.ValidationErrors) > 0 { 494 require.EqualError(t, err, fpdFile.ValidationErrors[0].Message, "Expected Error Not Received") 495 } else { 496 require.NoError(t, err, "Error Not Expected") 497 assert.Nil(t, testRequest.GetPrebid().BidderConfigs, "Bidder specific FPD config should be removed from request") 498 } 499 500 // assert fpd (with normalization for nicer looking tests) 501 for bidderName, expectedFPD := range fpdFile.BidderConfigFPD { 502 if expectedFPD.App != nil { 503 assert.JSONEq(t, string(expectedFPD.App), string(results[bidderName].App), "app is incorrect") 504 } else { 505 assert.Nil(t, results[bidderName].App, "app expected to be nil") 506 } 507 508 if expectedFPD.Site != nil { 509 assert.JSONEq(t, string(expectedFPD.Site), string(results[bidderName].Site), "site is incorrect") 510 } else { 511 assert.Nil(t, results[bidderName].Site, "site expected to be nil") 512 } 513 514 if expectedFPD.User != nil { 515 assert.JSONEq(t, string(expectedFPD.User), string(results[bidderName].User), "user is incorrect") 516 } else { 517 assert.Nil(t, results[bidderName].User, "user expected to be nil") 518 } 519 } 520 }) 521 } 522 } 523 524 func TestResolveFPD(t *testing.T) { 525 testPath := "tests/resolvefpd" 526 527 tests, err := os.ReadDir(testPath) 528 require.NoError(t, err, "Cannot Discover Tests") 529 530 for _, test := range tests { 531 t.Run(test.Name(), func(t *testing.T) { 532 filePath := testPath + "/" + test.Name() 533 534 fpdFile, err := loadFpdFile(filePath) 535 require.NoError(t, err, "Cannot Load Test") 536 537 request := &openrtb2.BidRequest{} 538 err = json.Unmarshal(fpdFile.InputRequestData, &request) 539 require.NoError(t, err, "Cannot Load Request") 540 541 originalRequest := &openrtb2.BidRequest{} 542 err = json.Unmarshal(fpdFile.InputRequestData, &originalRequest) 543 require.NoError(t, err, "Cannot Load Request") 544 545 outputReq := &openrtb2.BidRequest{} 546 err = json.Unmarshal(fpdFile.OutputRequestData, &outputReq) 547 require.NoError(t, err, "Cannot Load Output Request") 548 549 reqExtFPD := make(map[string][]byte) 550 reqExtFPD["site"] = fpdFile.GlobalFPD["site"] 551 reqExtFPD["app"] = fpdFile.GlobalFPD["app"] 552 reqExtFPD["user"] = fpdFile.GlobalFPD["user"] 553 554 reqFPD := make(map[string][]openrtb2.Data, 3) 555 556 reqFPDSiteContentData := fpdFile.GlobalFPD[siteContentDataKey] 557 if len(reqFPDSiteContentData) > 0 { 558 var siteConData []openrtb2.Data 559 err = json.Unmarshal(reqFPDSiteContentData, &siteConData) 560 if err != nil { 561 t.Errorf("Unable to unmarshal site.content.data:") 562 } 563 reqFPD[siteContentDataKey] = siteConData 564 } 565 566 reqFPDAppContentData := fpdFile.GlobalFPD[appContentDataKey] 567 if len(reqFPDAppContentData) > 0 { 568 var appConData []openrtb2.Data 569 err = json.Unmarshal(reqFPDAppContentData, &appConData) 570 if err != nil { 571 t.Errorf("Unable to unmarshal app.content.data: ") 572 } 573 reqFPD[appContentDataKey] = appConData 574 } 575 576 reqFPDUserData := fpdFile.GlobalFPD[userDataKey] 577 if len(reqFPDUserData) > 0 { 578 var userData []openrtb2.Data 579 err = json.Unmarshal(reqFPDUserData, &userData) 580 if err != nil { 581 t.Errorf("Unable to unmarshal app.content.data: ") 582 } 583 reqFPD[userDataKey] = userData 584 } 585 if fpdFile.BidderConfigFPD == nil { 586 fpdFile.BidderConfigFPD = make(map[openrtb_ext.BidderName]*openrtb_ext.ORTB2) 587 fpdFile.BidderConfigFPD["appnexus"] = &openrtb_ext.ORTB2{} 588 } 589 590 // run test 591 resultFPD, errL := ResolveFPD(request, fpdFile.BidderConfigFPD, reqExtFPD, reqFPD, []string{"appnexus"}) 592 593 if len(errL) == 0 { 594 assert.Equal(t, request, originalRequest, "Original request should not be modified") 595 596 bidderFPD := resultFPD["appnexus"] 597 598 if outputReq.Site != nil && len(outputReq.Site.Ext) > 0 { 599 resSiteExt := bidderFPD.Site.Ext 600 expectedSiteExt := outputReq.Site.Ext 601 bidderFPD.Site.Ext = nil 602 outputReq.Site.Ext = nil 603 assert.JSONEq(t, string(expectedSiteExt), string(resSiteExt), "site.ext is incorrect") 604 605 assert.Equal(t, outputReq.Site, bidderFPD.Site, "Site is incorrect") 606 } 607 if outputReq.App != nil && len(outputReq.App.Ext) > 0 { 608 resAppExt := bidderFPD.App.Ext 609 expectedAppExt := outputReq.App.Ext 610 bidderFPD.App.Ext = nil 611 outputReq.App.Ext = nil 612 613 assert.JSONEq(t, string(expectedAppExt), string(resAppExt), "app.ext is incorrect") 614 615 assert.Equal(t, outputReq.App, bidderFPD.App, "App is incorrect") 616 } 617 if outputReq.User != nil && len(outputReq.User.Ext) > 0 { 618 resUserExt := bidderFPD.User.Ext 619 expectedUserExt := outputReq.User.Ext 620 bidderFPD.User.Ext = nil 621 outputReq.User.Ext = nil 622 assert.JSONEq(t, string(expectedUserExt), string(resUserExt), "user.ext is incorrect") 623 624 assert.Equal(t, outputReq.User, bidderFPD.User, "User is incorrect") 625 } 626 } else { 627 assert.ElementsMatch(t, errL, fpdFile.ValidationErrors, "Incorrect first party data warning message") 628 } 629 }) 630 } 631 } 632 633 func TestExtractFPDForBidders(t *testing.T) { 634 if specFiles, err := os.ReadDir("./tests/extractfpdforbidders"); err == nil { 635 for _, specFile := range specFiles { 636 fileName := "./tests/extractfpdforbidders/" + specFile.Name() 637 638 fpdFile, err := loadFpdFile(fileName) 639 640 if err != nil { 641 t.Errorf("Unable to load file: %s", fileName) 642 } 643 644 var expectedRequest openrtb2.BidRequest 645 err = json.Unmarshal(fpdFile.OutputRequestData, &expectedRequest) 646 if err != nil { 647 t.Errorf("Unable to unmarshal input request: %s", fileName) 648 } 649 650 resultRequest := &openrtb_ext.RequestWrapper{} 651 resultRequest.BidRequest = &openrtb2.BidRequest{} 652 err = json.Unmarshal(fpdFile.InputRequestData, resultRequest.BidRequest) 653 assert.NoError(t, err, "Error should be nil") 654 655 resultFPD, errL := ExtractFPDForBidders(resultRequest) 656 657 if len(fpdFile.ValidationErrors) > 0 { 658 assert.Equal(t, len(fpdFile.ValidationErrors), len(errL), "Incorrect number of errors was returned") 659 assert.ElementsMatch(t, errL, fpdFile.ValidationErrors, "Incorrect errors were returned") 660 //in case or error no further assertions needed 661 continue 662 } 663 assert.Empty(t, errL, "Error should be empty") 664 assert.Equal(t, len(resultFPD), len(fpdFile.BiddersFPDResolved)) 665 666 for bidderName, expectedValue := range fpdFile.BiddersFPDResolved { 667 actualValue := resultFPD[bidderName] 668 if expectedValue.Site != nil { 669 if len(expectedValue.Site.Ext) > 0 { 670 assert.JSONEq(t, string(expectedValue.Site.Ext), string(actualValue.Site.Ext), "Incorrect first party data") 671 expectedValue.Site.Ext = nil 672 actualValue.Site.Ext = nil 673 } 674 assert.Equal(t, expectedValue.Site, actualValue.Site, "Incorrect first party data") 675 } 676 if expectedValue.App != nil { 677 if len(expectedValue.App.Ext) > 0 { 678 assert.JSONEq(t, string(expectedValue.App.Ext), string(actualValue.App.Ext), "Incorrect first party data") 679 expectedValue.App.Ext = nil 680 actualValue.App.Ext = nil 681 } 682 assert.Equal(t, expectedValue.App, actualValue.App, "Incorrect first party data") 683 } 684 if expectedValue.User != nil { 685 if len(expectedValue.User.Ext) > 0 { 686 assert.JSONEq(t, string(expectedValue.User.Ext), string(actualValue.User.Ext), "Incorrect first party data") 687 expectedValue.User.Ext = nil 688 actualValue.User.Ext = nil 689 } 690 assert.Equal(t, expectedValue.User, actualValue.User, "Incorrect first party data") 691 } 692 } 693 694 if expectedRequest.Site != nil { 695 if len(expectedRequest.Site.Ext) > 0 { 696 assert.JSONEq(t, string(expectedRequest.Site.Ext), string(resultRequest.BidRequest.Site.Ext), "Incorrect site in request") 697 expectedRequest.Site.Ext = nil 698 resultRequest.BidRequest.Site.Ext = nil 699 } 700 assert.Equal(t, expectedRequest.Site, resultRequest.BidRequest.Site, "Incorrect site in request") 701 } 702 if expectedRequest.App != nil { 703 if len(expectedRequest.App.Ext) > 0 { 704 assert.JSONEq(t, string(expectedRequest.App.Ext), string(resultRequest.BidRequest.App.Ext), "Incorrect app in request") 705 expectedRequest.App.Ext = nil 706 resultRequest.BidRequest.App.Ext = nil 707 } 708 assert.Equal(t, expectedRequest.App, resultRequest.BidRequest.App, "Incorrect app in request") 709 } 710 if expectedRequest.User != nil { 711 if len(expectedRequest.User.Ext) > 0 { 712 assert.JSONEq(t, string(expectedRequest.User.Ext), string(resultRequest.BidRequest.User.Ext), "Incorrect user in request") 713 expectedRequest.User.Ext = nil 714 resultRequest.BidRequest.User.Ext = nil 715 } 716 assert.Equal(t, expectedRequest.User, resultRequest.BidRequest.User, "Incorrect user in request") 717 } 718 719 } 720 } 721 } 722 723 func loadFpdFile(filename string) (fpdFile, error) { 724 var fileData fpdFile 725 fileContents, err := os.ReadFile(filename) 726 if err != nil { 727 return fileData, err 728 } 729 err = json.Unmarshal(fileContents, &fileData) 730 if err != nil { 731 return fileData, err 732 } 733 734 return fileData, nil 735 } 736 737 type fpdFile struct { 738 InputRequestData json.RawMessage `json:"inputRequestData,omitempty"` 739 OutputRequestData json.RawMessage `json:"outputRequestData,omitempty"` 740 BidderConfigFPD map[openrtb_ext.BidderName]*openrtb_ext.ORTB2 `json:"bidderConfigFPD,omitempty"` 741 BiddersFPDResolved map[openrtb_ext.BidderName]*ResolvedFirstPartyData `json:"biddersFPDResolved,omitempty"` 742 GlobalFPD map[string]json.RawMessage `json:"globalFPD,omitempty"` 743 ValidationErrors []*errortypes.BadInput `json:"validationErrors,omitempty"` 744 } 745 746 func TestResolveUser(t *testing.T) { 747 testCases := []struct { 748 description string 749 fpdConfig *openrtb_ext.ORTB2 750 bidRequestUser *openrtb2.User 751 globalFPD map[string][]byte 752 openRtbGlobalFPD map[string][]openrtb2.Data 753 expectedUser *openrtb2.User 754 expectedError string 755 }{ 756 { 757 description: "FPD config and bid request user are not specified", 758 expectedUser: nil, 759 }, 760 { 761 description: "FPD config user only is specified", 762 fpdConfig: &openrtb_ext.ORTB2{User: json.RawMessage(`{"id": "test"}`)}, 763 expectedUser: &openrtb2.User{ID: "test"}, 764 }, 765 { 766 description: "FPD config and bid request user are specified", 767 fpdConfig: &openrtb_ext.ORTB2{User: json.RawMessage(`{"id": "test1"}`)}, 768 bidRequestUser: &openrtb2.User{ID: "test2"}, 769 expectedUser: &openrtb2.User{ID: "test1"}, 770 }, 771 { 772 description: "FPD config, bid request and global fpd user are specified, no input user ext", 773 fpdConfig: &openrtb_ext.ORTB2{User: json.RawMessage(`{"id": "test1"}`)}, 774 bidRequestUser: &openrtb2.User{ID: "test2"}, 775 globalFPD: map[string][]byte{userKey: []byte(`{"globalFPDUserData": "globalFPDUserDataValue"}`)}, 776 expectedUser: &openrtb2.User{ID: "test1", Ext: json.RawMessage(`{"data":{"globalFPDUserData":"globalFPDUserDataValue"}}`)}, 777 }, 778 { 779 description: "FPD config, bid request user with ext and global fpd user are specified, no input user ext", 780 fpdConfig: &openrtb_ext.ORTB2{User: json.RawMessage(`{"id": "test1"}`)}, 781 bidRequestUser: &openrtb2.User{ID: "test2", Ext: json.RawMessage(`{"test":{"inputFPDUserData":"inputFPDUserDataValue"}}`)}, 782 globalFPD: map[string][]byte{userKey: []byte(`{"globalFPDUserData": "globalFPDUserDataValue"}`)}, 783 expectedUser: &openrtb2.User{ID: "test1", Ext: json.RawMessage(`{"data":{"globalFPDUserData":"globalFPDUserDataValue"},"test":{"inputFPDUserData":"inputFPDUserDataValue"}}`)}, 784 }, 785 { 786 description: "FPD config, bid request and global fpd user are specified, with input user ext.data", 787 fpdConfig: &openrtb_ext.ORTB2{User: json.RawMessage(`{"id": "test1"}`)}, 788 bidRequestUser: &openrtb2.User{ID: "test2", Ext: json.RawMessage(`{"data":{"inputFPDUserData":"inputFPDUserDataValue"}}`)}, 789 globalFPD: map[string][]byte{userKey: []byte(`{"globalFPDUserData": "globalFPDUserDataValue"}`)}, 790 expectedUser: &openrtb2.User{ID: "test1", Ext: json.RawMessage(`{"data":{"globalFPDUserData":"globalFPDUserDataValue","inputFPDUserData":"inputFPDUserDataValue"}}`)}, 791 }, 792 { 793 description: "FPD config, bid request and global fpd user are specified, with input user ext.data malformed", 794 fpdConfig: &openrtb_ext.ORTB2{User: json.RawMessage(`{"id": "test1"}`)}, 795 bidRequestUser: &openrtb2.User{ID: "test2", Ext: json.RawMessage(`{"data":{"inputFPDUserData":"inputFPDUserDataValue"}}`)}, 796 globalFPD: map[string][]byte{userKey: []byte(`malformed`)}, 797 expectedError: "Invalid JSON Patch", 798 }, 799 { 800 description: "bid request and openrtb global fpd user are specified, no input user ext", 801 bidRequestUser: &openrtb2.User{ID: "test2"}, 802 openRtbGlobalFPD: map[string][]openrtb2.Data{userDataKey: { 803 {ID: "DataId1", Name: "Name1"}, 804 {ID: "DataId2", Name: "Name2"}, 805 }}, 806 expectedUser: &openrtb2.User{ID: "test2", Data: []openrtb2.Data{ 807 {ID: "DataId1", Name: "Name1"}, 808 {ID: "DataId2", Name: "Name2"}, 809 }}, 810 }, 811 { 812 description: "fpd config user, bid request and openrtb global fpd user are specified, no input user ext", 813 fpdConfig: &openrtb_ext.ORTB2{User: json.RawMessage(`{"id": "test1"}`)}, 814 bidRequestUser: &openrtb2.User{ID: "test2"}, 815 openRtbGlobalFPD: map[string][]openrtb2.Data{userDataKey: { 816 {ID: "DataId1", Name: "Name1"}, 817 {ID: "DataId2", Name: "Name2"}, 818 }}, 819 expectedUser: &openrtb2.User{ID: "test1", Data: []openrtb2.Data{ 820 {ID: "DataId1", Name: "Name1"}, 821 {ID: "DataId2", Name: "Name2"}, 822 }}, 823 }, 824 { 825 description: "fpd config user with ext, bid request and openrtb global fpd user are specified, no input user ext", 826 fpdConfig: &openrtb_ext.ORTB2{User: json.RawMessage(`{"id": "test1", "ext":{"test":1}}`)}, 827 bidRequestUser: &openrtb2.User{ID: "test2"}, 828 openRtbGlobalFPD: map[string][]openrtb2.Data{userDataKey: { 829 {ID: "DataId1", Name: "Name1"}, 830 {ID: "DataId2", Name: "Name2"}, 831 }}, 832 expectedUser: &openrtb2.User{ID: "test1", Data: []openrtb2.Data{ 833 {ID: "DataId1", Name: "Name1"}, 834 {ID: "DataId2", Name: "Name2"}, 835 }, 836 Ext: json.RawMessage(`{"test":1}`)}, 837 }, 838 { 839 description: "fpd config user with ext, bid requestuser with ext and openrtb global fpd user are specified, no input user ext", 840 fpdConfig: &openrtb_ext.ORTB2{User: json.RawMessage(`{"id": "test1", "ext":{"test":1}}`)}, 841 bidRequestUser: &openrtb2.User{ID: "test2", Ext: json.RawMessage(`{"test":2, "key": "value"}`)}, 842 openRtbGlobalFPD: map[string][]openrtb2.Data{userDataKey: { 843 {ID: "DataId1", Name: "Name1"}, 844 {ID: "DataId2", Name: "Name2"}, 845 }}, 846 expectedUser: &openrtb2.User{ID: "test1", Data: []openrtb2.Data{ 847 {ID: "DataId1", Name: "Name1"}, 848 {ID: "DataId2", Name: "Name2"}, 849 }, 850 Ext: json.RawMessage(`{"key":"value","test":1}`)}, 851 }, 852 { 853 description: "fpd config user with malformed ext, bid requestuser with ext and openrtb global fpd user are specified, no input user ext", 854 fpdConfig: &openrtb_ext.ORTB2{User: json.RawMessage(`{"id": "test1", "ext":{malformed}}`)}, 855 bidRequestUser: &openrtb2.User{ID: "test2", Ext: json.RawMessage(`{"test":2, "key": "value"}`)}, 856 openRtbGlobalFPD: map[string][]openrtb2.Data{userDataKey: { 857 {ID: "DataId1", Name: "Name1"}, 858 {ID: "DataId2", Name: "Name2"}, 859 }}, 860 expectedUser: &openrtb2.User{ID: "test1", Data: []openrtb2.Data{ 861 {ID: "DataId1", Name: "Name1"}, 862 {ID: "DataId2", Name: "Name2"}, 863 }, 864 Ext: json.RawMessage(`{"key":"value","test":1}`), 865 }, 866 expectedError: "invalid character 'm' looking for beginning of object key string", 867 }, 868 } 869 for _, test := range testCases { 870 t.Run(test.description, func(t *testing.T) { 871 resultUser, err := resolveUser(test.fpdConfig, test.bidRequestUser, test.globalFPD, test.openRtbGlobalFPD, "bidderA") 872 873 if test.expectedError == "" { 874 assert.NoError(t, err, "unexpected error returned") 875 assert.Equal(t, test.expectedUser, resultUser, "Result user is incorrect") 876 } else { 877 assert.EqualError(t, err, test.expectedError, "expected error incorrect") 878 } 879 }) 880 } 881 } 882 883 func TestResolveSite(t *testing.T) { 884 testCases := []struct { 885 description string 886 fpdConfig *openrtb_ext.ORTB2 887 bidRequestSite *openrtb2.Site 888 globalFPD map[string][]byte 889 openRtbGlobalFPD map[string][]openrtb2.Data 890 expectedSite *openrtb2.Site 891 expectedError string 892 }{ 893 { 894 description: "FPD config and bid request site are not specified", 895 expectedSite: nil, 896 }, 897 { 898 description: "FPD config site only is specified", 899 fpdConfig: &openrtb_ext.ORTB2{Site: json.RawMessage(`{"id": "test"}`)}, 900 expectedError: "incorrect First Party Data for bidder bidderA: Site object is not defined in request, but defined in FPD config", 901 }, 902 { 903 description: "FPD config and bid request site are specified", 904 fpdConfig: &openrtb_ext.ORTB2{Site: json.RawMessage(`{"id": "test1"}`)}, 905 bidRequestSite: &openrtb2.Site{ID: "test2"}, 906 expectedSite: &openrtb2.Site{ID: "test1"}, 907 }, 908 { 909 description: "FPD config, bid request and global fpd site are specified, no input site ext", 910 fpdConfig: &openrtb_ext.ORTB2{Site: json.RawMessage(`{"id": "test1"}`)}, 911 bidRequestSite: &openrtb2.Site{ID: "test2"}, 912 globalFPD: map[string][]byte{siteKey: []byte(`{"globalFPDSiteData": "globalFPDSiteDataValue"}`)}, 913 expectedSite: &openrtb2.Site{ID: "test1", Ext: json.RawMessage(`{"data":{"globalFPDSiteData":"globalFPDSiteDataValue"}}`)}, 914 }, 915 { 916 description: "FPD config, bid request site with ext and global fpd site are specified, no input site ext", 917 fpdConfig: &openrtb_ext.ORTB2{Site: json.RawMessage(`{"id": "test1"}`)}, 918 bidRequestSite: &openrtb2.Site{ID: "test2", Ext: json.RawMessage(`{"test":{"inputFPDSiteData":"inputFPDSiteDataValue"}}`)}, 919 globalFPD: map[string][]byte{siteKey: []byte(`{"globalFPDSiteData": "globalFPDSiteDataValue"}`)}, 920 expectedSite: &openrtb2.Site{ID: "test1", Ext: json.RawMessage(`{"data":{"globalFPDSiteData":"globalFPDSiteDataValue"},"test":{"inputFPDSiteData":"inputFPDSiteDataValue"}}`)}, 921 }, 922 { 923 description: "FPD config, bid request and global fpd site are specified, with input site ext.data", 924 fpdConfig: &openrtb_ext.ORTB2{Site: json.RawMessage(`{"id": "test1"}`)}, 925 bidRequestSite: &openrtb2.Site{ID: "test2", Ext: json.RawMessage(`{"data":{"inputFPDSiteData":"inputFPDSiteDataValue"}}`)}, 926 globalFPD: map[string][]byte{siteKey: []byte(`{"globalFPDSiteData": "globalFPDSiteDataValue"}`)}, 927 expectedSite: &openrtb2.Site{ID: "test1", Ext: json.RawMessage(`{"data":{"globalFPDSiteData":"globalFPDSiteDataValue","inputFPDSiteData":"inputFPDSiteDataValue"}}`)}, 928 }, 929 { 930 description: "FPD config, bid request and global fpd site are specified, with input site ext.data malformed", 931 fpdConfig: &openrtb_ext.ORTB2{Site: json.RawMessage(`{"id": "test1"}`)}, 932 bidRequestSite: &openrtb2.Site{ID: "test2", Ext: json.RawMessage(`{"data":{"inputFPDSiteData":"inputFPDSiteDataValue"}}`)}, 933 globalFPD: map[string][]byte{siteKey: []byte(`malformed`)}, 934 expectedError: "Invalid JSON Patch", 935 }, 936 { 937 description: "bid request and openrtb global fpd site are specified, no input site ext", 938 bidRequestSite: &openrtb2.Site{ID: "test2"}, 939 openRtbGlobalFPD: map[string][]openrtb2.Data{siteContentDataKey: { 940 {ID: "DataId1", Name: "Name1"}, 941 {ID: "DataId2", Name: "Name2"}, 942 }}, 943 expectedSite: &openrtb2.Site{ID: "test2", Content: &openrtb2.Content{Data: []openrtb2.Data{ 944 {ID: "DataId1", Name: "Name1"}, 945 {ID: "DataId2", Name: "Name2"}, 946 }}}, 947 }, 948 { 949 description: "bid request with content and openrtb global fpd site are specified, no input site ext", 950 bidRequestSite: &openrtb2.Site{ID: "test2", Content: &openrtb2.Content{ 951 ID: "InputSiteContentId", 952 Data: []openrtb2.Data{ 953 {ID: "1", Name: "N1"}, 954 {ID: "2", Name: "N2"}, 955 }, 956 Ext: json.RawMessage(`{"contentPresent":true}`), 957 }}, 958 openRtbGlobalFPD: map[string][]openrtb2.Data{siteContentDataKey: { 959 {ID: "DataId1", Name: "Name1"}, 960 {ID: "DataId2", Name: "Name2"}, 961 }}, 962 expectedSite: &openrtb2.Site{ID: "test2", Content: &openrtb2.Content{ 963 ID: "InputSiteContentId", 964 Data: []openrtb2.Data{ 965 {ID: "DataId1", Name: "Name1"}, 966 {ID: "DataId2", Name: "Name2"}, 967 }, 968 Ext: json.RawMessage(`{"contentPresent":true}`), 969 }}, 970 }, 971 { 972 description: "fpd config site, bid request and openrtb global fpd site are specified, no input site ext", 973 fpdConfig: &openrtb_ext.ORTB2{Site: json.RawMessage(`{"id": "test1"}`)}, 974 bidRequestSite: &openrtb2.Site{ID: "test2"}, 975 openRtbGlobalFPD: map[string][]openrtb2.Data{siteContentDataKey: { 976 {ID: "DataId1", Name: "Name1"}, 977 {ID: "DataId2", Name: "Name2"}, 978 }}, 979 expectedSite: &openrtb2.Site{ID: "test1", Content: &openrtb2.Content{Data: []openrtb2.Data{ 980 {ID: "DataId1", Name: "Name1"}, 981 {ID: "DataId2", Name: "Name2"}, 982 }}}, 983 }, 984 { 985 description: "fpd config site with ext, bid request and openrtb global fpd site are specified, no input site ext", 986 fpdConfig: &openrtb_ext.ORTB2{Site: json.RawMessage(`{"id": "test1", "ext":{"test":1}}`)}, 987 bidRequestSite: &openrtb2.Site{ID: "test2"}, 988 openRtbGlobalFPD: map[string][]openrtb2.Data{siteContentDataKey: { 989 {ID: "DataId1", Name: "Name1"}, 990 {ID: "DataId2", Name: "Name2"}, 991 }}, 992 expectedSite: &openrtb2.Site{ID: "test1", Content: &openrtb2.Content{Data: []openrtb2.Data{ 993 {ID: "DataId1", Name: "Name1"}, 994 {ID: "DataId2", Name: "Name2"}, 995 }}, 996 Ext: json.RawMessage(`{"test":1}`)}, 997 }, 998 { 999 description: "fpd config site with ext, bid request site with ext and openrtb global fpd site are specified, no input site ext", 1000 fpdConfig: &openrtb_ext.ORTB2{Site: json.RawMessage(`{"id": "test1", "ext":{"test":1}}`)}, 1001 bidRequestSite: &openrtb2.Site{ID: "test2", Ext: json.RawMessage(`{"test":2, "key": "value"}`)}, 1002 openRtbGlobalFPD: map[string][]openrtb2.Data{siteContentDataKey: { 1003 {ID: "DataId1", Name: "Name1"}, 1004 {ID: "DataId2", Name: "Name2"}, 1005 }}, 1006 expectedSite: &openrtb2.Site{ID: "test1", Content: &openrtb2.Content{Data: []openrtb2.Data{ 1007 {ID: "DataId1", Name: "Name1"}, 1008 {ID: "DataId2", Name: "Name2"}, 1009 }}, 1010 Ext: json.RawMessage(`{"key":"value","test":1}`)}, 1011 }, 1012 { 1013 description: "fpd config site with malformed ext, bid request site with ext and openrtb global fpd site are specified, no input site ext", 1014 fpdConfig: &openrtb_ext.ORTB2{Site: json.RawMessage(`{"id": "test1", "ext":{malformed}}`)}, 1015 bidRequestSite: &openrtb2.Site{ID: "test2", Ext: json.RawMessage(`{"test":2, "key": "value"}`)}, 1016 openRtbGlobalFPD: map[string][]openrtb2.Data{siteContentDataKey: { 1017 {ID: "DataId1", Name: "Name1"}, 1018 {ID: "DataId2", Name: "Name2"}, 1019 }}, 1020 expectedSite: &openrtb2.Site{ID: "test1", Content: &openrtb2.Content{Data: []openrtb2.Data{ 1021 {ID: "DataId1", Name: "Name1"}, 1022 {ID: "DataId2", Name: "Name2"}, 1023 }}, 1024 Ext: json.RawMessage(`{"key":"value","test":1}`), 1025 }, 1026 expectedError: "invalid character 'm' looking for beginning of object key string", 1027 }, 1028 } 1029 for _, test := range testCases { 1030 t.Run(test.description, func(t *testing.T) { 1031 resultSite, err := resolveSite(test.fpdConfig, test.bidRequestSite, test.globalFPD, test.openRtbGlobalFPD, "bidderA") 1032 1033 if test.expectedError == "" { 1034 assert.NoError(t, err, "unexpected error returned") 1035 assert.Equal(t, test.expectedSite, resultSite, "Result site is incorrect") 1036 } else { 1037 assert.EqualError(t, err, test.expectedError, "expected error incorrect") 1038 } 1039 }) 1040 } 1041 } 1042 1043 func TestResolveApp(t *testing.T) { 1044 testCases := []struct { 1045 description string 1046 fpdConfig *openrtb_ext.ORTB2 1047 bidRequestApp *openrtb2.App 1048 globalFPD map[string][]byte 1049 openRtbGlobalFPD map[string][]openrtb2.Data 1050 expectedApp *openrtb2.App 1051 expectedError string 1052 }{ 1053 { 1054 description: "FPD config and bid request app are not specified", 1055 expectedApp: nil, 1056 }, 1057 { 1058 description: "FPD config app only is specified", 1059 fpdConfig: &openrtb_ext.ORTB2{App: json.RawMessage(`{"id": "test"}`)}, 1060 expectedError: "incorrect First Party Data for bidder bidderA: App object is not defined in request, but defined in FPD config", 1061 }, 1062 { 1063 description: "FPD config and bid request app are specified", 1064 fpdConfig: &openrtb_ext.ORTB2{App: json.RawMessage(`{"id": "test1"}`)}, 1065 bidRequestApp: &openrtb2.App{ID: "test2"}, 1066 expectedApp: &openrtb2.App{ID: "test1"}, 1067 }, 1068 { 1069 description: "FPD config, bid request and global fpd app are specified, no input app ext", 1070 fpdConfig: &openrtb_ext.ORTB2{App: json.RawMessage(`{"id": "test1"}`)}, 1071 bidRequestApp: &openrtb2.App{ID: "test2"}, 1072 globalFPD: map[string][]byte{appKey: []byte(`{"globalFPDAppData": "globalFPDAppDataValue"}`)}, 1073 expectedApp: &openrtb2.App{ID: "test1", Ext: json.RawMessage(`{"data":{"globalFPDAppData":"globalFPDAppDataValue"}}`)}, 1074 }, 1075 { 1076 description: "FPD config, bid request app with ext and global fpd app are specified, no input app ext", 1077 fpdConfig: &openrtb_ext.ORTB2{App: json.RawMessage(`{"id": "test1"}`)}, 1078 bidRequestApp: &openrtb2.App{ID: "test2", Ext: json.RawMessage(`{"test":{"inputFPDAppData":"inputFPDAppDataValue"}}`)}, 1079 globalFPD: map[string][]byte{appKey: []byte(`{"globalFPDAppData": "globalFPDAppDataValue"}`)}, 1080 expectedApp: &openrtb2.App{ID: "test1", Ext: json.RawMessage(`{"data":{"globalFPDAppData":"globalFPDAppDataValue"},"test":{"inputFPDAppData":"inputFPDAppDataValue"}}`)}, 1081 }, 1082 { 1083 description: "FPD config, bid request and global fpd app are specified, with input app ext.data", 1084 fpdConfig: &openrtb_ext.ORTB2{App: json.RawMessage(`{"id": "test1"}`)}, 1085 bidRequestApp: &openrtb2.App{ID: "test2", Ext: json.RawMessage(`{"data":{"inputFPDAppData":"inputFPDAppDataValue"}}`)}, 1086 globalFPD: map[string][]byte{appKey: []byte(`{"globalFPDAppData": "globalFPDAppDataValue"}`)}, 1087 expectedApp: &openrtb2.App{ID: "test1", Ext: json.RawMessage(`{"data":{"globalFPDAppData":"globalFPDAppDataValue","inputFPDAppData":"inputFPDAppDataValue"}}`)}, 1088 }, 1089 { 1090 description: "FPD config, bid request and global fpd app are specified, with input app ext.data malformed", 1091 fpdConfig: &openrtb_ext.ORTB2{App: json.RawMessage(`{"id": "test1"}`)}, 1092 bidRequestApp: &openrtb2.App{ID: "test2", Ext: json.RawMessage(`{"data":{"inputFPDAppData":"inputFPDAppDataValue"}}`)}, 1093 globalFPD: map[string][]byte{appKey: []byte(`malformed`)}, 1094 expectedError: "Invalid JSON Patch", 1095 }, 1096 { 1097 description: "bid request and openrtb global fpd app are specified, no input app ext", 1098 bidRequestApp: &openrtb2.App{ID: "test2"}, 1099 openRtbGlobalFPD: map[string][]openrtb2.Data{appContentDataKey: { 1100 {ID: "DataId1", Name: "Name1"}, 1101 {ID: "DataId2", Name: "Name2"}, 1102 }}, 1103 expectedApp: &openrtb2.App{ID: "test2", Content: &openrtb2.Content{Data: []openrtb2.Data{ 1104 {ID: "DataId1", Name: "Name1"}, 1105 {ID: "DataId2", Name: "Name2"}, 1106 }}}, 1107 }, 1108 { 1109 description: "bid request with content and openrtb global fpd app are specified, no input app ext", 1110 bidRequestApp: &openrtb2.App{ID: "test2", Content: &openrtb2.Content{ 1111 ID: "InputAppContentId", 1112 Data: []openrtb2.Data{ 1113 {ID: "1", Name: "N1"}, 1114 {ID: "2", Name: "N2"}, 1115 }, 1116 Ext: json.RawMessage(`{"contentPresent":true}`), 1117 }}, 1118 openRtbGlobalFPD: map[string][]openrtb2.Data{appContentDataKey: { 1119 {ID: "DataId1", Name: "Name1"}, 1120 {ID: "DataId2", Name: "Name2"}, 1121 }}, 1122 expectedApp: &openrtb2.App{ID: "test2", Content: &openrtb2.Content{ 1123 ID: "InputAppContentId", 1124 Data: []openrtb2.Data{ 1125 {ID: "DataId1", Name: "Name1"}, 1126 {ID: "DataId2", Name: "Name2"}, 1127 }, 1128 Ext: json.RawMessage(`{"contentPresent":true}`), 1129 }}, 1130 }, 1131 { 1132 description: "fpd config app, bid request and openrtb global fpd app are specified, no input app ext", 1133 fpdConfig: &openrtb_ext.ORTB2{App: json.RawMessage(`{"id": "test1"}`)}, 1134 bidRequestApp: &openrtb2.App{ID: "test2"}, 1135 openRtbGlobalFPD: map[string][]openrtb2.Data{appContentDataKey: { 1136 {ID: "DataId1", Name: "Name1"}, 1137 {ID: "DataId2", Name: "Name2"}, 1138 }}, 1139 expectedApp: &openrtb2.App{ID: "test1", Content: &openrtb2.Content{Data: []openrtb2.Data{ 1140 {ID: "DataId1", Name: "Name1"}, 1141 {ID: "DataId2", Name: "Name2"}, 1142 }}}, 1143 }, 1144 { 1145 description: "fpd config app with ext, bid request and openrtb global fpd app are specified, no input app ext", 1146 fpdConfig: &openrtb_ext.ORTB2{App: json.RawMessage(`{"id": "test1", "ext":{"test":1}}`)}, 1147 bidRequestApp: &openrtb2.App{ID: "test2"}, 1148 openRtbGlobalFPD: map[string][]openrtb2.Data{appContentDataKey: { 1149 {ID: "DataId1", Name: "Name1"}, 1150 {ID: "DataId2", Name: "Name2"}, 1151 }}, 1152 expectedApp: &openrtb2.App{ID: "test1", Content: &openrtb2.Content{Data: []openrtb2.Data{ 1153 {ID: "DataId1", Name: "Name1"}, 1154 {ID: "DataId2", Name: "Name2"}, 1155 }}, 1156 Ext: json.RawMessage(`{"test":1}`)}, 1157 }, 1158 { 1159 description: "fpd config app with ext, bid request app with ext and openrtb global fpd app are specified, no input app ext", 1160 fpdConfig: &openrtb_ext.ORTB2{App: json.RawMessage(`{"id": "test1", "ext":{"test":1}}`)}, 1161 bidRequestApp: &openrtb2.App{ID: "test2", Ext: json.RawMessage(`{"test":2, "key": "value"}`)}, 1162 openRtbGlobalFPD: map[string][]openrtb2.Data{appContentDataKey: { 1163 {ID: "DataId1", Name: "Name1"}, 1164 {ID: "DataId2", Name: "Name2"}, 1165 }}, 1166 expectedApp: &openrtb2.App{ID: "test1", Content: &openrtb2.Content{Data: []openrtb2.Data{ 1167 {ID: "DataId1", Name: "Name1"}, 1168 {ID: "DataId2", Name: "Name2"}, 1169 }}, 1170 Ext: json.RawMessage(`{"key":"value","test":1}`)}, 1171 }, 1172 { 1173 description: "fpd config app with malformed ext, bid request app with ext and openrtb global fpd app are specified, no input app ext", 1174 fpdConfig: &openrtb_ext.ORTB2{App: json.RawMessage(`{"id": "test1", "ext":{malformed}}`)}, 1175 bidRequestApp: &openrtb2.App{ID: "test2", Ext: json.RawMessage(`{"test":2, "key": "value"}`)}, 1176 openRtbGlobalFPD: map[string][]openrtb2.Data{appContentDataKey: { 1177 {ID: "DataId1", Name: "Name1"}, 1178 {ID: "DataId2", Name: "Name2"}, 1179 }}, 1180 expectedApp: &openrtb2.App{ID: "test1", Content: &openrtb2.Content{Data: []openrtb2.Data{ 1181 {ID: "DataId1", Name: "Name1"}, 1182 {ID: "DataId2", Name: "Name2"}, 1183 }}, 1184 Ext: json.RawMessage(`{"key":"value","test":1}`), 1185 }, 1186 expectedError: "invalid character 'm' looking for beginning of object key string", 1187 }, 1188 } 1189 for _, test := range testCases { 1190 t.Run(test.description, func(t *testing.T) { 1191 resultApp, err := resolveApp(test.fpdConfig, test.bidRequestApp, test.globalFPD, test.openRtbGlobalFPD, "bidderA") 1192 1193 if test.expectedError == "" { 1194 assert.NoError(t, err, "unexpected error returned") 1195 assert.Equal(t, test.expectedApp, resultApp, "Result app is incorrect") 1196 } else { 1197 assert.EqualError(t, err, test.expectedError, "expected error incorrect") 1198 } 1199 }) 1200 } 1201 } 1202 1203 func TestBuildExtData(t *testing.T) { 1204 testCases := []struct { 1205 description string 1206 input []byte 1207 expectedRes string 1208 }{ 1209 { 1210 description: "Input object with int value", 1211 input: []byte(`{"someData": 123}`), 1212 expectedRes: `{"data": {"someData": 123}}`, 1213 }, 1214 { 1215 description: "Input object with bool value", 1216 input: []byte(`{"someData": true}`), 1217 expectedRes: `{"data": {"someData": true}}`, 1218 }, 1219 { 1220 description: "Input object with string value", 1221 input: []byte(`{"someData": "true"}`), 1222 expectedRes: `{"data": {"someData": "true"}}`, 1223 }, 1224 { 1225 description: "No input object", 1226 input: []byte(`{}`), 1227 expectedRes: `{"data": {}}`, 1228 }, 1229 { 1230 description: "Input object with object value", 1231 input: []byte(`{"someData": {"moreFpdData": "fpddata"}}`), 1232 expectedRes: `{"data": {"someData": {"moreFpdData": "fpddata"}}}`, 1233 }, 1234 } 1235 1236 for _, test := range testCases { 1237 actualRes := buildExtData(test.input) 1238 assert.JSONEq(t, test.expectedRes, string(actualRes), "Incorrect result data") 1239 } 1240 } 1241 1242 func TestMergeUser(t *testing.T) { 1243 testCases := []struct { 1244 name string 1245 givenUser openrtb2.User 1246 givenFPD json.RawMessage 1247 expectedUser openrtb2.User 1248 expectedErr string 1249 }{ 1250 { 1251 name: "empty", 1252 givenUser: openrtb2.User{}, 1253 givenFPD: []byte(`{}`), 1254 expectedUser: openrtb2.User{}, 1255 }, 1256 { 1257 name: "toplevel", 1258 givenUser: openrtb2.User{ID: "1"}, 1259 givenFPD: []byte(`{"id":"2"}`), 1260 expectedUser: openrtb2.User{ID: "2"}, 1261 }, 1262 { 1263 name: "toplevel-ext", 1264 givenUser: openrtb2.User{Ext: []byte(`{"a":1,"b":2}`)}, 1265 givenFPD: []byte(`{"ext":{"b":100,"c":3}}`), 1266 expectedUser: openrtb2.User{Ext: []byte(`{"a":1,"b":100,"c":3}`)}, 1267 }, 1268 { 1269 name: "toplevel-ext-err", 1270 givenUser: openrtb2.User{ID: "1", Ext: []byte(`malformed`)}, 1271 givenFPD: []byte(`{"id":"2"}`), 1272 expectedErr: "invalid request ext", 1273 }, 1274 { 1275 name: "nested-geo", 1276 givenUser: openrtb2.User{Geo: &openrtb2.Geo{Lat: 1}}, 1277 givenFPD: []byte(`{"geo":{"lat": 2}}`), 1278 expectedUser: openrtb2.User{Geo: &openrtb2.Geo{Lat: 2}}, 1279 }, 1280 { 1281 name: "nested-geo-ext", 1282 givenUser: openrtb2.User{Geo: &openrtb2.Geo{Ext: []byte(`{"a":1,"b":2}`)}}, 1283 givenFPD: []byte(`{"geo":{"ext":{"b":100,"c":3}}}`), 1284 expectedUser: openrtb2.User{Geo: &openrtb2.Geo{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}, 1285 }, 1286 { 1287 name: "toplevel-ext-and-nested-geo-ext", 1288 givenUser: openrtb2.User{Ext: []byte(`{"a":1,"b":2}`), Geo: &openrtb2.Geo{Ext: []byte(`{"a":10,"b":20}`)}}, 1289 givenFPD: []byte(`{"ext":{"b":100,"c":3}, "geo":{"ext":{"b":100,"c":3}}}`), 1290 expectedUser: openrtb2.User{Ext: []byte(`{"a":1,"b":100,"c":3}`), Geo: &openrtb2.Geo{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}, 1291 }, 1292 { 1293 name: "nested-geo-ext-err", 1294 givenUser: openrtb2.User{Geo: &openrtb2.Geo{Ext: []byte(`malformed`)}}, 1295 givenFPD: []byte(`{"geo":{"ext":{"b":100,"c":3}}}`), 1296 expectedErr: "invalid request ext", 1297 }, 1298 { 1299 name: "fpd-err", 1300 givenUser: openrtb2.User{ID: "1", Ext: []byte(`{"a":1}`)}, 1301 givenFPD: []byte(`malformed`), 1302 expectedErr: "invalid character 'm' looking for beginning of value", 1303 }, 1304 } 1305 1306 for _, test := range testCases { 1307 t.Run(test.name, func(t *testing.T) { 1308 err := mergeUser(&test.givenUser, test.givenFPD) 1309 1310 if test.expectedErr == "" { 1311 assert.NoError(t, err, "unexpected error returned") 1312 assert.Equal(t, test.expectedUser, test.givenUser, "result user is incorrect") 1313 } else { 1314 assert.EqualError(t, err, test.expectedErr, "expected error incorrect") 1315 } 1316 }) 1317 } 1318 } 1319 1320 func TestMergeApp(t *testing.T) { 1321 testCases := []struct { 1322 name string 1323 givenApp openrtb2.App 1324 givenFPD json.RawMessage 1325 expectedApp openrtb2.App 1326 expectedErr string 1327 }{ 1328 { 1329 name: "empty", 1330 givenApp: openrtb2.App{}, 1331 givenFPD: []byte(`{}`), 1332 expectedApp: openrtb2.App{}, 1333 }, 1334 { 1335 name: "toplevel", 1336 givenApp: openrtb2.App{ID: "1"}, 1337 givenFPD: []byte(`{"id":"2"}`), 1338 expectedApp: openrtb2.App{ID: "2"}, 1339 }, 1340 { 1341 name: "toplevel-ext", 1342 givenApp: openrtb2.App{Ext: []byte(`{"a":1,"b":2}`)}, 1343 givenFPD: []byte(`{"ext":{"b":100,"c":3}}`), 1344 expectedApp: openrtb2.App{Ext: []byte(`{"a":1,"b":100,"c":3}`)}, 1345 }, 1346 { 1347 name: "toplevel-ext-err", 1348 givenApp: openrtb2.App{ID: "1", Ext: []byte(`malformed`)}, 1349 givenFPD: []byte(`{"id":"2"}`), 1350 expectedErr: "invalid request ext", 1351 }, 1352 { 1353 name: "nested-publisher", 1354 givenApp: openrtb2.App{Publisher: &openrtb2.Publisher{Name: "pub1"}}, 1355 givenFPD: []byte(`{"publisher":{"name": "pub2"}}`), 1356 expectedApp: openrtb2.App{Publisher: &openrtb2.Publisher{Name: "pub2"}}, 1357 }, 1358 { 1359 name: "nested-content", 1360 givenApp: openrtb2.App{Content: &openrtb2.Content{Title: "content1"}}, 1361 givenFPD: []byte(`{"content":{"title": "content2"}}`), 1362 expectedApp: openrtb2.App{Content: &openrtb2.Content{Title: "content2"}}, 1363 }, 1364 { 1365 name: "nested-content-producer", 1366 givenApp: openrtb2.App{Content: &openrtb2.Content{Title: "content1", Producer: &openrtb2.Producer{Name: "producer1"}}}, 1367 givenFPD: []byte(`{"content":{"title": "content2", "producer":{"name":"producer2"}}}`), 1368 expectedApp: openrtb2.App{Content: &openrtb2.Content{Title: "content2", Producer: &openrtb2.Producer{Name: "producer2"}}}, 1369 }, 1370 { 1371 name: "nested-content-network", 1372 givenApp: openrtb2.App{Content: &openrtb2.Content{Title: "content1", Network: &openrtb2.Network{Name: "network1"}}}, 1373 givenFPD: []byte(`{"content":{"title": "content2", "network":{"name":"network2"}}}`), 1374 expectedApp: openrtb2.App{Content: &openrtb2.Content{Title: "content2", Network: &openrtb2.Network{Name: "network2"}}}, 1375 }, 1376 { 1377 name: "nested-content-channel", 1378 givenApp: openrtb2.App{Content: &openrtb2.Content{Title: "content1", Channel: &openrtb2.Channel{Name: "channel1"}}}, 1379 givenFPD: []byte(`{"content":{"title": "content2", "channel":{"name":"channel2"}}}`), 1380 expectedApp: openrtb2.App{Content: &openrtb2.Content{Title: "content2", Channel: &openrtb2.Channel{Name: "channel2"}}}, 1381 }, 1382 { 1383 name: "nested-publisher-ext", 1384 givenApp: openrtb2.App{Publisher: &openrtb2.Publisher{Ext: []byte(`{"a":1,"b":2}`)}}, 1385 givenFPD: []byte(`{"publisher":{"ext":{"b":100,"c":3}}}`), 1386 expectedApp: openrtb2.App{Publisher: &openrtb2.Publisher{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}, 1387 }, 1388 { 1389 name: "nested-content-ext", 1390 givenApp: openrtb2.App{Content: &openrtb2.Content{Ext: []byte(`{"a":1,"b":2}`)}}, 1391 givenFPD: []byte(`{"content":{"ext":{"b":100,"c":3}}}`), 1392 expectedApp: openrtb2.App{Content: &openrtb2.Content{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}, 1393 }, 1394 { 1395 name: "nested-content-producer-ext", 1396 givenApp: openrtb2.App{Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`{"a":1,"b":2}`)}}}, 1397 givenFPD: []byte(`{"content":{"producer":{"ext":{"b":100,"c":3}}}}`), 1398 expectedApp: openrtb2.App{Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}}, 1399 }, 1400 { 1401 name: "nested-content-network-ext", 1402 givenApp: openrtb2.App{Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`{"a":1,"b":2}`)}}}, 1403 givenFPD: []byte(`{"content":{"network":{"ext":{"b":100,"c":3}}}}`), 1404 expectedApp: openrtb2.App{Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}}, 1405 }, 1406 { 1407 name: "nested-content-channel-ext", 1408 givenApp: openrtb2.App{Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`{"a":1,"b":2}`)}}}, 1409 givenFPD: []byte(`{"content":{"channel":{"ext":{"b":100,"c":3}}}}`), 1410 expectedApp: openrtb2.App{Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}}, 1411 }, 1412 { 1413 name: "toplevel-ext-and-nested-publisher-ext", 1414 givenApp: openrtb2.App{Ext: []byte(`{"a":1,"b":2}`), Publisher: &openrtb2.Publisher{Ext: []byte(`{"a":10,"b":20}`)}}, 1415 givenFPD: []byte(`{"ext":{"b":100,"c":3}, "publisher":{"ext":{"b":100,"c":3}}}`), 1416 expectedApp: openrtb2.App{Ext: []byte(`{"a":1,"b":100,"c":3}`), Publisher: &openrtb2.Publisher{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}, 1417 }, 1418 { 1419 name: "toplevel-ext-and-nested-content-ext", 1420 givenApp: openrtb2.App{Ext: []byte(`{"a":1,"b":2}`), Content: &openrtb2.Content{Ext: []byte(`{"a":10,"b":20}`)}}, 1421 givenFPD: []byte(`{"ext":{"b":100,"c":3}, "content":{"ext":{"b":100,"c":3}}}`), 1422 expectedApp: openrtb2.App{Ext: []byte(`{"a":1,"b":100,"c":3}`), Content: &openrtb2.Content{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}, 1423 }, 1424 { 1425 name: "toplevel-ext-and-nested-content-producer-ext", 1426 givenApp: openrtb2.App{Ext: []byte(`{"a":1,"b":2}`), Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`{"a":10,"b":20}`)}}}, 1427 givenFPD: []byte(`{"ext":{"b":100,"c":3}, "content":{"producer": {"ext":{"b":100,"c":3}}}}`), 1428 expectedApp: openrtb2.App{Ext: []byte(`{"a":1,"b":100,"c":3}`), Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}}, 1429 }, 1430 { 1431 name: "toplevel-ext-and-nested-content-network-ext", 1432 givenApp: openrtb2.App{Ext: []byte(`{"a":1,"b":2}`), Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`{"a":10,"b":20}`)}}}, 1433 givenFPD: []byte(`{"ext":{"b":100,"c":3}, "content":{"network": {"ext":{"b":100,"c":3}}}}`), 1434 expectedApp: openrtb2.App{Ext: []byte(`{"a":1,"b":100,"c":3}`), Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}}, 1435 }, 1436 { 1437 name: "toplevel-ext-and-nested-content-channel-ext", 1438 givenApp: openrtb2.App{Ext: []byte(`{"a":1,"b":2}`), Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`{"a":10,"b":20}`)}}}, 1439 givenFPD: []byte(`{"ext":{"b":100,"c":3}, "content":{"channel": {"ext":{"b":100,"c":3}}}}`), 1440 expectedApp: openrtb2.App{Ext: []byte(`{"a":1,"b":100,"c":3}`), Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}}, 1441 }, 1442 { 1443 name: "nested-publisher-ext-err", 1444 givenApp: openrtb2.App{Publisher: &openrtb2.Publisher{Ext: []byte(`malformed`)}}, 1445 givenFPD: []byte(`{"publisher":{"ext":{"b":100,"c":3}}}`), 1446 expectedErr: "invalid request ext", 1447 }, 1448 { 1449 name: "nested-content-ext-err", 1450 givenApp: openrtb2.App{Content: &openrtb2.Content{Ext: []byte(`malformed`)}}, 1451 givenFPD: []byte(`{"content":{"ext":{"b":100,"c":3}}}`), 1452 expectedErr: "invalid request ext", 1453 }, 1454 { 1455 name: "nested-content-producer-ext-err", 1456 givenApp: openrtb2.App{Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`malformed`)}}}, 1457 givenFPD: []byte(`{"content":{"producer": {"ext":{"b":100,"c":3}}}}`), 1458 expectedErr: "invalid request ext", 1459 }, 1460 { 1461 name: "nested-content-network-ext-err", 1462 givenApp: openrtb2.App{Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`malformed`)}}}, 1463 givenFPD: []byte(`{"content":{"network": {"ext":{"b":100,"c":3}}}}`), 1464 expectedErr: "invalid request ext", 1465 }, 1466 { 1467 name: "nested-content-channel-ext-err", 1468 givenApp: openrtb2.App{Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`malformed`)}}}, 1469 givenFPD: []byte(`{"content":{"channelx": {"ext":{"b":100,"c":3}}}}`), 1470 expectedErr: "invalid request ext", 1471 }, 1472 { 1473 name: "fpd-err", 1474 givenApp: openrtb2.App{ID: "1", Ext: []byte(`{"a":1}`)}, 1475 givenFPD: []byte(`malformed`), 1476 expectedErr: "invalid character 'm' looking for beginning of value", 1477 }, 1478 } 1479 1480 for _, test := range testCases { 1481 t.Run(test.name, func(t *testing.T) { 1482 err := mergeApp(&test.givenApp, test.givenFPD) 1483 1484 if test.expectedErr == "" { 1485 assert.NoError(t, err, "unexpected error returned") 1486 assert.Equal(t, test.expectedApp, test.givenApp, " result app is incorrect") 1487 } else { 1488 assert.EqualError(t, err, test.expectedErr, "expected error incorrect") 1489 } 1490 }) 1491 } 1492 } 1493 1494 func TestMergeSite(t *testing.T) { 1495 testCases := []struct { 1496 name string 1497 givenSite openrtb2.Site 1498 givenFPD json.RawMessage 1499 expectedSite openrtb2.Site 1500 expectedErr string 1501 }{ 1502 { 1503 name: "empty", 1504 givenSite: openrtb2.Site{}, 1505 givenFPD: []byte(`{}`), 1506 expectedErr: "incorrect First Party Data for bidder BidderA: Site object cannot set empty page if req.site.id is empty", 1507 }, 1508 { 1509 name: "toplevel", 1510 givenSite: openrtb2.Site{ID: "1"}, 1511 givenFPD: []byte(`{"id":"2"}`), 1512 expectedSite: openrtb2.Site{ID: "2"}, 1513 }, 1514 { 1515 name: "toplevel-ext", 1516 givenSite: openrtb2.Site{Page: "test.com/page", Ext: []byte(`{"a":1,"b":2}`)}, 1517 givenFPD: []byte(`{"ext":{"b":100,"c":3}}`), 1518 expectedSite: openrtb2.Site{Page: "test.com/page", Ext: []byte(`{"a":1,"b":100,"c":3}`)}, 1519 }, 1520 { 1521 name: "toplevel-ext-err", 1522 givenSite: openrtb2.Site{ID: "1", Ext: []byte(`malformed`)}, 1523 givenFPD: []byte(`{"id":"2"}`), 1524 expectedErr: "invalid request ext", 1525 }, 1526 { 1527 name: "nested-publisher", 1528 givenSite: openrtb2.Site{Page: "test.com/page", Publisher: &openrtb2.Publisher{Name: "pub1"}}, 1529 givenFPD: []byte(`{"publisher":{"name": "pub2"}}`), 1530 expectedSite: openrtb2.Site{Page: "test.com/page", Publisher: &openrtb2.Publisher{Name: "pub2"}}, 1531 }, 1532 { 1533 name: "nested-content", 1534 givenSite: openrtb2.Site{Page: "test.com/page", Content: &openrtb2.Content{Title: "content1"}}, 1535 givenFPD: []byte(`{"content":{"title": "content2"}}`), 1536 expectedSite: openrtb2.Site{Page: "test.com/page", Content: &openrtb2.Content{Title: "content2"}}, 1537 }, 1538 { 1539 name: "nested-content-producer", 1540 givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Title: "content1", Producer: &openrtb2.Producer{Name: "producer1"}}}, 1541 givenFPD: []byte(`{"content":{"title": "content2", "producer":{"name":"producer2"}}}`), 1542 expectedSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Title: "content2", Producer: &openrtb2.Producer{Name: "producer2"}}}, 1543 }, 1544 { 1545 name: "nested-content-network", 1546 givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Title: "content1", Network: &openrtb2.Network{Name: "network1"}}}, 1547 givenFPD: []byte(`{"content":{"title": "content2", "network":{"name":"network2"}}}`), 1548 expectedSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Title: "content2", Network: &openrtb2.Network{Name: "network2"}}}, 1549 }, 1550 { 1551 name: "nested-content-channel", 1552 givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Title: "content1", Channel: &openrtb2.Channel{Name: "channel1"}}}, 1553 givenFPD: []byte(`{"content":{"title": "content2", "channel":{"name":"channel2"}}}`), 1554 expectedSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Title: "content2", Channel: &openrtb2.Channel{Name: "channel2"}}}, 1555 }, 1556 { 1557 name: "nested-publisher-ext", 1558 givenSite: openrtb2.Site{ID: "1", Publisher: &openrtb2.Publisher{Ext: []byte(`{"a":1,"b":2}`)}}, 1559 givenFPD: []byte(`{"publisher":{"ext":{"b":100,"c":3}}}`), 1560 expectedSite: openrtb2.Site{ID: "1", Publisher: &openrtb2.Publisher{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}, 1561 }, 1562 { 1563 name: "nested-content-ext", 1564 givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Ext: []byte(`{"a":1,"b":2}`)}}, 1565 givenFPD: []byte(`{"content":{"ext":{"b":100,"c":3}}}`), 1566 expectedSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}, 1567 }, 1568 { 1569 name: "nested-content-producer-ext", 1570 givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`{"a":1,"b":2}`)}}}, 1571 givenFPD: []byte(`{"content":{"producer":{"ext":{"b":100,"c":3}}}}`), 1572 expectedSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}}, 1573 }, 1574 { 1575 name: "nested-content-network-ext", 1576 givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`{"a":1,"b":2}`)}}}, 1577 givenFPD: []byte(`{"content":{"network":{"ext":{"b":100,"c":3}}}}`), 1578 expectedSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}}, 1579 }, 1580 { 1581 name: "nested-content-channel-ext", 1582 givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`{"a":1,"b":2}`)}}}, 1583 givenFPD: []byte(`{"content":{"channel":{"ext":{"b":100,"c":3}}}}`), 1584 expectedSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}}, 1585 }, 1586 { 1587 name: "toplevel-ext-and-nested-publisher-ext", 1588 givenSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":2}`), Publisher: &openrtb2.Publisher{Ext: []byte(`{"a":10,"b":20}`)}}, 1589 givenFPD: []byte(`{"ext":{"b":100,"c":3}, "publisher":{"ext":{"b":100,"c":3}}}`), 1590 expectedSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":100,"c":3}`), Publisher: &openrtb2.Publisher{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}, 1591 }, 1592 { 1593 name: "toplevel-ext-and-nested-content-ext", 1594 givenSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":2}`), Content: &openrtb2.Content{Ext: []byte(`{"a":10,"b":20}`)}}, 1595 givenFPD: []byte(`{"ext":{"b":100,"c":3}, "content":{"ext":{"b":100,"c":3}}}`), 1596 expectedSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":100,"c":3}`), Content: &openrtb2.Content{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}, 1597 }, 1598 { 1599 name: "toplevel-ext-and-nested-content-producer-ext", 1600 givenSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":2}`), Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`{"a":10,"b":20}`)}}}, 1601 givenFPD: []byte(`{"ext":{"b":100,"c":3}, "content":{"producer": {"ext":{"b":100,"c":3}}}}`), 1602 expectedSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":100,"c":3}`), Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}}, 1603 }, 1604 { 1605 name: "toplevel-ext-and-nested-content-network-ext", 1606 givenSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":2}`), Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`{"a":10,"b":20}`)}}}, 1607 givenFPD: []byte(`{"ext":{"b":100,"c":3}, "content":{"network": {"ext":{"b":100,"c":3}}}}`), 1608 expectedSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":100,"c":3}`), Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}}, 1609 }, 1610 { 1611 name: "toplevel-ext-and-nested-content-channel-ext", 1612 givenSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":2}`), Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`{"a":10,"b":20}`)}}}, 1613 givenFPD: []byte(`{"ext":{"b":100,"c":3}, "content":{"channel": {"ext":{"b":100,"c":3}}}}`), 1614 expectedSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":100,"c":3}`), Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}}, 1615 }, 1616 { 1617 name: "nested-publisher-ext-err", 1618 givenSite: openrtb2.Site{ID: "1", Publisher: &openrtb2.Publisher{Ext: []byte(`malformed`)}}, 1619 givenFPD: []byte(`{"publisher":{"ext":{"b":100,"c":3}}}`), 1620 expectedErr: "invalid request ext", 1621 }, 1622 { 1623 name: "nested-content-ext-err", 1624 givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Ext: []byte(`malformed`)}}, 1625 givenFPD: []byte(`{"content":{"ext":{"b":100,"c":3}}}`), 1626 expectedErr: "invalid request ext", 1627 }, 1628 { 1629 name: "nested-content-producer-ext-err", 1630 givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`malformed`)}}}, 1631 givenFPD: []byte(`{"content":{"producer": {"ext":{"b":100,"c":3}}}}`), 1632 expectedErr: "invalid request ext", 1633 }, 1634 { 1635 name: "nested-content-network-ext-err", 1636 givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`malformed`)}}}, 1637 givenFPD: []byte(`{"content":{"network": {"ext":{"b":100,"c":3}}}}`), 1638 expectedErr: "invalid request ext", 1639 }, 1640 { 1641 name: "nested-content-channel-ext-err", 1642 givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`malformed`)}}}, 1643 givenFPD: []byte(`{"content":{"channelx": {"ext":{"b":100,"c":3}}}}`), 1644 expectedErr: "invalid request ext", 1645 }, 1646 { 1647 name: "fpd-err", 1648 givenSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1}`)}, 1649 givenFPD: []byte(`malformed`), 1650 expectedErr: "invalid character 'm' looking for beginning of value", 1651 }, 1652 } 1653 1654 for _, test := range testCases { 1655 t.Run(test.name, func(t *testing.T) { 1656 err := mergeSite(&test.givenSite, test.givenFPD, "BidderA") 1657 1658 if test.expectedErr == "" { 1659 assert.NoError(t, err, "unexpected error returned") 1660 assert.Equal(t, test.expectedSite, test.givenSite, " result Site is incorrect") 1661 } else { 1662 assert.EqualError(t, err, test.expectedErr, "expected error incorrect") 1663 } 1664 }) 1665 } 1666 } 1667 1668 // TestMergeObjectStructure detects when new nested objects are added to First Party Data supported 1669 // fields, as these will invalidate the mergeSite, mergeApp, and mergeUser methods. If this test fails, 1670 // fix the merge methods to add support and update this test to set a new baseline. 1671 func TestMergeObjectStructure(t *testing.T) { 1672 testCases := []struct { 1673 name string 1674 kind any 1675 knownStructs []string 1676 }{ 1677 { 1678 name: "Site", 1679 kind: openrtb2.Site{}, 1680 knownStructs: []string{ 1681 "Publisher", 1682 "Content", 1683 "Content.Producer", 1684 "Content.Network", 1685 "Content.Channel", 1686 }, 1687 }, 1688 { 1689 name: "App", 1690 kind: openrtb2.App{}, 1691 knownStructs: []string{ 1692 "Publisher", 1693 "Content", 1694 "Content.Producer", 1695 "Content.Network", 1696 "Content.Channel", 1697 }, 1698 }, 1699 { 1700 name: "User", 1701 kind: openrtb2.User{}, 1702 knownStructs: []string{ 1703 "Geo", 1704 }, 1705 }, 1706 } 1707 1708 for _, test := range testCases { 1709 t.Run(test.name, func(t *testing.T) { 1710 nestedStructs := []string{} 1711 1712 var discover func(parent string, t reflect.Type) 1713 discover = func(parent string, t reflect.Type) { 1714 fields := reflect.VisibleFields(t) 1715 for _, field := range fields { 1716 if field.Type.Kind() == reflect.Pointer && field.Type.Elem().Kind() == reflect.Struct { 1717 nestedStructs = append(nestedStructs, parent+field.Name) 1718 discover(parent+field.Name+".", field.Type.Elem()) 1719 } 1720 } 1721 } 1722 discover("", reflect.TypeOf(test.kind)) 1723 1724 assert.ElementsMatch(t, test.knownStructs, nestedStructs) 1725 }) 1726 } 1727 } 1728 1729 // user memory protect test 1730 func TestMergeUserMemoryProtection(t *testing.T) { 1731 inputGeo := &openrtb2.Geo{ 1732 Ext: json.RawMessage(`{"a":1,"b":2}`), 1733 } 1734 input := openrtb2.User{ 1735 ID: "1", 1736 Geo: inputGeo, 1737 } 1738 1739 err := mergeUser(&input, userFPD) 1740 assert.NoError(t, err) 1741 1742 // Input user object is expected to be a copy. Changes are ok. 1743 assert.Equal(t, "2", input.ID, "user-id-copied") 1744 1745 // Nested objects must be copied before changes. 1746 assert.JSONEq(t, `{"a":1,"b":2}`, string(inputGeo.Ext), "geo-input") 1747 assert.JSONEq(t, `{"a":1,"b":100,"c":3}`, string(input.Geo.Ext), "geo-copied") 1748 } 1749 1750 // app memory protect test 1751 func TestMergeAppMemoryProtection(t *testing.T) { 1752 inputPublisher := &openrtb2.Publisher{ 1753 ID: "InPubId", 1754 Ext: json.RawMessage(`{"a": "inputPubExt", "b": 1}`), 1755 } 1756 inputContent := &openrtb2.Content{ 1757 ID: "InContentId", 1758 Ext: json.RawMessage(`{"a": "inputContentExt", "b": 1}`), 1759 Producer: &openrtb2.Producer{ 1760 ID: "InProducerId", 1761 Ext: json.RawMessage(`{"a": "inputProducerExt", "b": 1}`), 1762 }, 1763 Network: &openrtb2.Network{ 1764 ID: "InNetworkId", 1765 Ext: json.RawMessage(`{"a": "inputNetworkExt", "b": 1}`), 1766 }, 1767 Channel: &openrtb2.Channel{ 1768 ID: "InChannelId", 1769 Ext: json.RawMessage(`{"a": "inputChannelExt", "b": 1}`), 1770 }, 1771 } 1772 input := openrtb2.App{ 1773 ID: "InAppID", 1774 Publisher: inputPublisher, 1775 Content: inputContent, 1776 Ext: json.RawMessage(`{"a": "inputAppExt", "b": 1}`), 1777 } 1778 1779 err := mergeApp(&input, fpdWithPublisherAndContent) 1780 assert.NoError(t, err) 1781 1782 // Input app object is expected to be a copy. Changes are ok. 1783 assert.Equal(t, "FPDID", input.ID, "app-id-copied") 1784 assert.JSONEq(t, `{"a": "FPDExt", "b": 2}`, string(input.Ext), "app-ext-copied") 1785 1786 // Nested objects must be copied before changes. 1787 assert.Equal(t, "InPubId", inputPublisher.ID, "app-pub-id-input") 1788 assert.Equal(t, "FPDPubId", input.Publisher.ID, "app-pub-id-copied") 1789 assert.JSONEq(t, `{"a": "inputPubExt", "b": 1}`, string(inputPublisher.Ext), "app-pub-ext-input") 1790 assert.JSONEq(t, `{"a": "FPDPubExt", "b": 2}`, string(input.Publisher.Ext), "app-pub-ext-copied") 1791 1792 assert.Equal(t, "InContentId", inputContent.ID, "app-content-id-input") 1793 assert.Equal(t, "FPDContentId", input.Content.ID, "app-content-id-copied") 1794 assert.JSONEq(t, `{"a": "inputContentExt", "b": 1}`, string(inputContent.Ext), "app-content-ext-input") 1795 assert.JSONEq(t, `{"a": "FPDContentExt", "b": 2}`, string(input.Content.Ext), "app-content-ext-copied") 1796 1797 assert.Equal(t, "InProducerId", inputContent.Producer.ID, "app-content-producer-id-input") 1798 assert.Equal(t, "FPDProducerId", input.Content.Producer.ID, "app-content-producer-id-copied") 1799 assert.JSONEq(t, `{"a": "inputProducerExt", "b": 1}`, string(inputContent.Producer.Ext), "app-content-producer-ext-input") 1800 assert.JSONEq(t, `{"a": "FPDProducerExt", "b": 2}`, string(input.Content.Producer.Ext), "app-content-producer-ext-copied") 1801 1802 assert.Equal(t, "InNetworkId", inputContent.Network.ID, "app-content-network-id-input") 1803 assert.Equal(t, "FPDNetworkId", input.Content.Network.ID, "app-content-network-id-copied") 1804 assert.JSONEq(t, `{"a": "inputNetworkExt", "b": 1}`, string(inputContent.Network.Ext), "app-content-network-ext-input") 1805 assert.JSONEq(t, `{"a": "FPDNetworkExt", "b": 2}`, string(input.Content.Network.Ext), "app-content-network-ext-copied") 1806 1807 assert.Equal(t, "InChannelId", inputContent.Channel.ID, "app-content-channel-id-input") 1808 assert.Equal(t, "FPDChannelId", input.Content.Channel.ID, "app-content-channel-id-copied") 1809 assert.JSONEq(t, `{"a": "inputChannelExt", "b": 1}`, string(inputContent.Channel.Ext), "app-content-channel-ext-input") 1810 assert.JSONEq(t, `{"a": "FPDChannelExt", "b": 2}`, string(input.Content.Channel.Ext), "app-content-channel-ext-copied") 1811 } 1812 1813 // site memory protect test 1814 func TestMergeSiteMemoryProtection(t *testing.T) { 1815 inputPublisher := &openrtb2.Publisher{ 1816 ID: "InPubId", 1817 Ext: json.RawMessage(`{"a": "inputPubExt", "b": 1}`), 1818 } 1819 inputContent := &openrtb2.Content{ 1820 ID: "InContentId", 1821 Ext: json.RawMessage(`{"a": "inputContentExt", "b": 1}`), 1822 Producer: &openrtb2.Producer{ 1823 ID: "InProducerId", 1824 Ext: json.RawMessage(`{"a": "inputProducerExt", "b": 1}`), 1825 }, 1826 Network: &openrtb2.Network{ 1827 ID: "InNetworkId", 1828 Ext: json.RawMessage(`{"a": "inputNetworkExt", "b": 1}`), 1829 }, 1830 Channel: &openrtb2.Channel{ 1831 ID: "InChannelId", 1832 Ext: json.RawMessage(`{"a": "inputChannelExt", "b": 1}`), 1833 }, 1834 } 1835 input := openrtb2.Site{ 1836 ID: "InSiteID", 1837 Publisher: inputPublisher, 1838 Content: inputContent, 1839 Ext: json.RawMessage(`{"a": "inputSiteExt", "b": 1}`), 1840 } 1841 1842 err := mergeSite(&input, fpdWithPublisherAndContent, "BidderA") 1843 assert.NoError(t, err) 1844 1845 // Input app object is expected to be a copy. Changes are ok. 1846 assert.Equal(t, "FPDID", input.ID, "site-id-copied") 1847 assert.JSONEq(t, `{"a": "FPDExt", "b": 2}`, string(input.Ext), "site-ext-copied") 1848 1849 // Nested objects must be copied before changes. 1850 assert.Equal(t, "InPubId", inputPublisher.ID, "site-pub-id-input") 1851 assert.Equal(t, "FPDPubId", input.Publisher.ID, "site-pub-id-copied") 1852 assert.JSONEq(t, `{"a": "inputPubExt", "b": 1}`, string(inputPublisher.Ext), "site-pub-ext-input") 1853 assert.JSONEq(t, `{"a": "FPDPubExt", "b": 2}`, string(input.Publisher.Ext), "site-pub-ext-copied") 1854 1855 assert.Equal(t, "InContentId", inputContent.ID, "site-content-id-input") 1856 assert.Equal(t, "FPDContentId", input.Content.ID, "site-content-id-copied") 1857 assert.JSONEq(t, `{"a": "inputContentExt", "b": 1}`, string(inputContent.Ext), "site-content-ext-input") 1858 assert.JSONEq(t, `{"a": "FPDContentExt", "b": 2}`, string(input.Content.Ext), "site-content-ext-copied") 1859 1860 assert.Equal(t, "InProducerId", inputContent.Producer.ID, "site-content-producer-id-input") 1861 assert.Equal(t, "FPDProducerId", input.Content.Producer.ID, "site-content-producer-id-copied") 1862 assert.JSONEq(t, `{"a": "inputProducerExt", "b": 1}`, string(inputContent.Producer.Ext), "site-content-producer-ext-input") 1863 assert.JSONEq(t, `{"a": "FPDProducerExt", "b": 2}`, string(input.Content.Producer.Ext), "site-content-producer-ext-copied") 1864 1865 assert.Equal(t, "InNetworkId", inputContent.Network.ID, "site-content-network-id-input") 1866 assert.Equal(t, "FPDNetworkId", input.Content.Network.ID, "site-content-network-id-copied") 1867 assert.JSONEq(t, `{"a": "inputNetworkExt", "b": 1}`, string(inputContent.Network.Ext), "site-content-network-ext-input") 1868 assert.JSONEq(t, `{"a": "FPDNetworkExt", "b": 2}`, string(input.Content.Network.Ext), "site-content-network-ext-copied") 1869 1870 assert.Equal(t, "InChannelId", inputContent.Channel.ID, "site-content-channel-id-input") 1871 assert.Equal(t, "FPDChannelId", input.Content.Channel.ID, "site-content-channel-id-copied") 1872 assert.JSONEq(t, `{"a": "inputChannelExt", "b": 1}`, string(inputContent.Channel.Ext), "site-content-channel-ext-input") 1873 assert.JSONEq(t, `{"a": "FPDChannelExt", "b": 2}`, string(input.Content.Channel.Ext), "site-content-channel-ext-copied") 1874 } 1875 1876 var ( 1877 userFPD = []byte(` 1878 { 1879 "id": "2", 1880 "geo": { 1881 "ext": { 1882 "b": 100, 1883 "c": 3 1884 } 1885 } 1886 } 1887 `) 1888 1889 fpdWithPublisherAndContent = []byte(` 1890 { 1891 "id": "FPDID", 1892 "ext": {"a": "FPDExt", "b": 2}, 1893 "publisher": { 1894 "id": "FPDPubId", 1895 "ext": {"a": "FPDPubExt", "b": 2} 1896 }, 1897 "content": { 1898 "id": "FPDContentId", 1899 "ext": {"a": "FPDContentExt", "b": 2}, 1900 "producer": { 1901 "id": "FPDProducerId", 1902 "ext": {"a": "FPDProducerExt", "b": 2} 1903 }, 1904 "network": { 1905 "id": "FPDNetworkId", 1906 "ext": {"a": "FPDNetworkExt", "b": 2} 1907 }, 1908 "channel": { 1909 "id": "FPDChannelId", 1910 "ext": {"a": "FPDChannelExt", "b": 2} 1911 } 1912 } 1913 } 1914 `) 1915 1916 user = []byte(` 1917 { 1918 "id": "2", 1919 "yob": 2000, 1920 "geo": { 1921 "city": "LA", 1922 "ext": { 1923 "b": 100, 1924 "c": 3 1925 } 1926 } 1927 } 1928 `) 1929 )