github.com/prebid/prebid-server/v2@v2.18.0/adservertargeting/respdataprocessor_test.go (about) 1 package adservertargeting 2 3 import ( 4 "encoding/json" 5 "reflect" 6 "strings" 7 "testing" 8 9 "github.com/prebid/openrtb/v20/openrtb2" 10 "github.com/prebid/openrtb/v20/openrtb3" 11 "github.com/prebid/prebid-server/v2/openrtb_ext" 12 "github.com/stretchr/testify/assert" 13 ) 14 15 func TestProcessRequestTargetingData(t *testing.T) { 16 17 testCases := []struct { 18 description string 19 inputAdServerTargeting adServerTargetingData 20 inputTargetingData map[string]string 21 expectedTargetingData map[string]string 22 }{ 23 24 { 25 description: "Add single and targeting value by imp to not empty input targeting data", 26 inputAdServerTargeting: adServerTargetingData{ 27 RequestTargetingData: map[string]RequestTargetingData{ 28 "key1": {SingleVal: json.RawMessage(`val1`)}, 29 "key2": { 30 TargetingValueByImpId: map[string][]byte{ 31 "impId1": []byte(`impId1val`), "impId2": []byte(`impId2val`), 32 }, 33 }, 34 }, 35 }, 36 inputTargetingData: map[string]string{"inKey1": "inVal1"}, 37 expectedTargetingData: map[string]string{ 38 "inKey1": "inVal1", "key1": "val1", "key2": "impId1val", 39 }, 40 }, 41 { 42 description: "Add single and targeting value by imp to empty input targeting data", 43 inputAdServerTargeting: adServerTargetingData{ 44 RequestTargetingData: map[string]RequestTargetingData{ 45 "key1": {SingleVal: json.RawMessage(`val1`)}, 46 "key2": { 47 TargetingValueByImpId: map[string][]byte{ 48 "impId1": []byte(`impId1val`), "impId2": []byte(`impId2val`), 49 }, 50 }, 51 }, 52 }, 53 inputTargetingData: map[string]string{}, 54 expectedTargetingData: map[string]string{ 55 "key1": "val1", "key2": "impId1val", 56 }, 57 }, 58 { 59 description: "Add single value by imp to not empty input targeting data", 60 inputAdServerTargeting: adServerTargetingData{ 61 RequestTargetingData: map[string]RequestTargetingData{ 62 "key1": {SingleVal: json.RawMessage(`val1`)}, 63 }, 64 }, 65 inputTargetingData: map[string]string{"inKey1": "inVal1"}, 66 expectedTargetingData: map[string]string{ 67 "inKey1": "inVal1", "key1": "val1", 68 }, 69 }, 70 { 71 description: "Add targeting value by imp to not empty input targeting data", 72 inputAdServerTargeting: adServerTargetingData{ 73 RequestTargetingData: map[string]RequestTargetingData{ 74 "key2": { 75 TargetingValueByImpId: map[string][]byte{ 76 "impId1": []byte(`impId1val`), "impId2": []byte(`impId2val`), 77 }, 78 }, 79 }, 80 }, 81 inputTargetingData: map[string]string{"inKey1": "inVal1"}, 82 expectedTargetingData: map[string]string{ 83 "inKey1": "inVal1", "key2": "impId1val", 84 }, 85 }, 86 } 87 88 bidImpId := "impId1" 89 for _, test := range testCases { 90 processRequestTargetingData(&test.inputAdServerTargeting, test.inputTargetingData, bidImpId) 91 assert.Equal(t, test.expectedTargetingData, test.inputTargetingData, "incorrect targeting data") 92 } 93 94 } 95 96 func TestProcessResponseTargetingData(t *testing.T) { 97 98 testCases := []struct { 99 description string 100 inputAdServerTargeting adServerTargetingData 101 inputTargetingData map[string]string 102 inputBid openrtb2.Bid 103 inputResponse *openrtb2.BidResponse 104 inputSeatExt json.RawMessage 105 106 expectedTargetingData map[string]string 107 expectedWarnings []openrtb_ext.ExtBidderMessage 108 }{ 109 110 { 111 description: "get value from seatbid.bid", 112 inputAdServerTargeting: adServerTargetingData{ 113 ResponseTargetingData: []ResponseTargetingData{ 114 {Key: "{{BIDDER}}_custom1", HasMacro: true, Path: "seatbid.bid.impid"}, 115 }, 116 }, 117 inputTargetingData: map[string]string{"inKey1": "inVal1"}, 118 inputBid: openrtb2.Bid{ID: "testBidId", ImpID: "testBidImpId1"}, 119 inputResponse: nil, 120 inputSeatExt: nil, 121 122 expectedTargetingData: map[string]string{ 123 "inKey1": "inVal1", "bidderA_custom1": "testBidImpId1", 124 }, 125 expectedWarnings: []openrtb_ext.ExtBidderMessage(nil), 126 }, 127 { 128 description: "get value from seatbid.bid.ext.prebid.foo", 129 inputAdServerTargeting: adServerTargetingData{ 130 ResponseTargetingData: []ResponseTargetingData{ 131 {Key: "{{BIDDER}}_custom1", HasMacro: true, Path: "seatbid.bid.ext.prebid.foo"}, 132 }, 133 }, 134 inputTargetingData: map[string]string{"inKey1": "inVal1"}, 135 inputBid: openrtb2.Bid{ID: "testBidId", ImpID: "testBidImpId1"}, 136 inputResponse: nil, 137 inputSeatExt: nil, 138 139 expectedTargetingData: map[string]string{ 140 "inKey1": "inVal1", "bidderA_custom1": "bar1", 141 }, 142 expectedWarnings: []openrtb_ext.ExtBidderMessage(nil), 143 }, 144 { 145 description: "get value from ext.testData.foo", 146 inputAdServerTargeting: adServerTargetingData{ 147 ResponseTargetingData: []ResponseTargetingData{ 148 {Key: "{{BIDDER}}_custom1", HasMacro: true, Path: "ext.testData.foo"}, 149 }, 150 }, 151 inputTargetingData: map[string]string{"inKey1": "inVal1"}, 152 inputBid: openrtb2.Bid{ID: "testBidId", ImpID: "testBidImpId1"}, 153 inputResponse: &openrtb2.BidResponse{Cur: "UAH", Ext: json.RawMessage(`{"testData": {"foo": "barExt"}}`)}, 154 inputSeatExt: nil, 155 156 expectedTargetingData: map[string]string{ 157 "inKey1": "inVal1", "bidderA_custom1": "barExt", 158 }, 159 expectedWarnings: []openrtb_ext.ExtBidderMessage(nil), 160 }, 161 { 162 description: "get value from resp", 163 inputAdServerTargeting: adServerTargetingData{ 164 ResponseTargetingData: []ResponseTargetingData{ 165 {Key: "{{BIDDER}}_custom1", HasMacro: true, Path: "cur"}, 166 }, 167 }, 168 inputTargetingData: map[string]string{"inKey1": "inVal1"}, 169 inputBid: openrtb2.Bid{ID: "testBidId", ImpID: "testBidImpId1"}, 170 inputResponse: &openrtb2.BidResponse{Cur: "UAH", Ext: json.RawMessage(`{"testData": {"foo": "barExt"}}`)}, 171 inputSeatExt: nil, 172 173 expectedTargetingData: map[string]string{ 174 "inKey1": "inVal1", "bidderA_custom1": "UAH", 175 }, 176 expectedWarnings: []openrtb_ext.ExtBidderMessage(nil), 177 }, 178 { 179 description: "get value from resp ext", 180 inputAdServerTargeting: adServerTargetingData{ 181 ResponseTargetingData: []ResponseTargetingData{ 182 {Key: "{{BIDDER}}_custom1", HasMacro: true, Path: "seatbid.ext.testData.foo"}, 183 }, 184 }, 185 inputTargetingData: map[string]string{"inKey1": "inVal1"}, 186 inputBid: openrtb2.Bid{ID: "testBidId", ImpID: "testBidImpId1"}, 187 inputResponse: nil, 188 inputSeatExt: json.RawMessage(`{"testData": {"foo": "barBidderA"}}`), 189 190 expectedTargetingData: map[string]string{ 191 "inKey1": "inVal1", "bidderA_custom1": "barBidderA", 192 }, 193 expectedWarnings: []openrtb_ext.ExtBidderMessage(nil), 194 }, 195 { 196 description: "get value from resp ext with incorrect format", 197 inputAdServerTargeting: adServerTargetingData{ 198 ResponseTargetingData: []ResponseTargetingData{ 199 {Key: "{{BIDDER}}_custom1", HasMacro: true, Path: "seatbid.ext.testData"}, 200 }, 201 }, 202 inputTargetingData: map[string]string{"inKey1": "inVal1"}, 203 inputBid: openrtb2.Bid{ID: "testBidId", ImpID: "testBidImpId1"}, 204 inputResponse: &openrtb2.BidResponse{Cur: "UAH", Ext: json.RawMessage(`{"testData": {"foo": "barExt"}}`)}, 205 inputSeatExt: json.RawMessage(`{"testData": {"foo": "barBidderA"}}`), 206 207 expectedTargetingData: map[string]string{ 208 "inKey1": "inVal1", 209 }, 210 expectedWarnings: []openrtb_ext.ExtBidderMessage{ 211 {Code: 10007, Message: "incorrect value type for path: testData, value can only be string or number for bidder: bidderA, bid id: testBidId"}, 212 }, 213 }, 214 } 215 216 bidderName := "bidderA" 217 218 inputBidsCache := bidsCache{ 219 bids: map[string]map[string][]byte{ 220 "bidderA": {"testBidId": []byte(`{"id":"testBidId","impid":"testBidImpId1","price":10,"cat":["cat11","cat12"],"ext":{"prebid":{"foo":"bar1"}}}`)}, 221 }, 222 } 223 224 for _, test := range testCases { 225 actualWarnings := processResponseTargetingData(&test.inputAdServerTargeting, 226 test.inputTargetingData, bidderName, test.inputBid, inputBidsCache, 227 test.inputResponse, test.inputSeatExt) 228 assert.Equal(t, test.expectedWarnings, actualWarnings, "incorrect warnings returned") 229 assert.Equal(t, test.expectedTargetingData, test.inputTargetingData, "incorrect targeting data") 230 } 231 } 232 233 func TestBuildBidExt(t *testing.T) { 234 235 testCases := []struct { 236 description string 237 inputTargetingData map[string]string 238 inputBid openrtb2.Bid 239 inputWarnings []openrtb_ext.ExtBidderMessage 240 truncateTargetAttribute int 241 expectedExt json.RawMessage 242 expectedWarnings []openrtb_ext.ExtBidderMessage 243 }{ 244 245 { 246 description: "build valid bid ext with existing warnings", 247 inputTargetingData: map[string]string{"inKey1": "inVal1"}, 248 inputBid: openrtb2.Bid{ID: "testBidId", ImpID: "testBidImpId1", Ext: json.RawMessage(`{"prebid": {"test": 1}}`)}, 249 inputWarnings: []openrtb_ext.ExtBidderMessage{ 250 {Code: 10007, Message: "incorrect value type for path: testData, value can only be string or number for bidder: bidderA, bid id: testBidId"}, 251 }, 252 truncateTargetAttribute: 20, 253 expectedExt: json.RawMessage(`{"prebid":{"targeting":{"inKey1":"inVal1"},"test":1}}`), 254 expectedWarnings: []openrtb_ext.ExtBidderMessage{ 255 {Code: 10007, Message: "incorrect value type for path: testData, value can only be string or number for bidder: bidderA, bid id: testBidId"}, 256 }, 257 }, 258 { 259 description: "build valid bid ext without existing warnings", 260 inputTargetingData: map[string]string{"inKey1": "inVal1"}, 261 inputBid: openrtb2.Bid{ID: "testBidId", ImpID: "testBidImpId1", Ext: json.RawMessage(`{"prebid": {"test": 1}}`)}, 262 inputWarnings: []openrtb_ext.ExtBidderMessage(nil), 263 truncateTargetAttribute: 20, 264 expectedExt: json.RawMessage(`{"prebid":{"targeting":{"inKey1":"inVal1"},"test":1}}`), 265 expectedWarnings: []openrtb_ext.ExtBidderMessage(nil), 266 }, 267 } 268 269 for _, test := range testCases { 270 actualExt := buildBidExt(test.inputTargetingData, test.inputBid, test.inputWarnings, &test.truncateTargetAttribute) 271 assert.Equal(t, test.expectedWarnings, test.inputWarnings, "incorrect warnings returned") 272 assert.JSONEq(t, string(test.expectedExt), string(actualExt), "incorrect result extension") 273 } 274 } 275 276 func TestResolveKey(t *testing.T) { 277 278 testCases := []struct { 279 description string 280 inputResponseTargetingData ResponseTargetingData 281 inputBidderName string 282 expectedKey string 283 }{ 284 285 { 286 description: "resolve key with macro", 287 inputResponseTargetingData: ResponseTargetingData{Key: "{{BIDDER}}_custom1", HasMacro: true, Path: "ext"}, 288 inputBidderName: "bidderA", 289 expectedKey: "bidderA_custom1", 290 }, 291 { 292 description: "resolve key without macro", 293 inputResponseTargetingData: ResponseTargetingData{Key: "key_custom1", HasMacro: true, Path: "ext"}, 294 inputBidderName: "bidderA", 295 expectedKey: "key_custom1", 296 }, 297 { 298 description: "resolve key with macro only", 299 inputResponseTargetingData: ResponseTargetingData{Key: "{{BIDDER}}", HasMacro: true, Path: "ext"}, 300 inputBidderName: "bidderA", 301 expectedKey: "bidderA", 302 }, 303 { 304 description: "resolve key with macro and empty bidder name", 305 inputResponseTargetingData: ResponseTargetingData{Key: "{{BIDDER}}_custom1", HasMacro: true, Path: "ext"}, 306 inputBidderName: "", 307 expectedKey: "_custom1", 308 }, 309 } 310 311 for _, test := range testCases { 312 actualKey := resolveKey(test.inputResponseTargetingData, test.inputBidderName) 313 assert.Equal(t, test.expectedKey, actualKey, "incorrect resolved key") 314 } 315 } 316 317 func TestTruncateTargetingKeys(t *testing.T) { 318 319 testCases := []struct { 320 description string 321 inputTargetingData map[string]string 322 truncateTargetAttribute int 323 expectedTargetingData map[string]string 324 }{ 325 326 { 327 description: "truncate targeting keys", 328 inputTargetingData: map[string]string{"inKey1": "inVal1"}, 329 truncateTargetAttribute: 5, 330 expectedTargetingData: map[string]string{"inKey": "inVal1"}, 331 }, 332 { 333 description: "do not truncate targeting keys", 334 inputTargetingData: map[string]string{"inKey1": "inVal1"}, 335 truncateTargetAttribute: 10, 336 expectedTargetingData: map[string]string{"inKey1": "inVal1"}, 337 }, 338 { 339 description: "exceed truncate limit", 340 inputTargetingData: map[string]string{"inKey1": "inVal1"}, 341 truncateTargetAttribute: 100, 342 expectedTargetingData: map[string]string{"inKey1": "inVal1"}, 343 }, 344 { 345 description: "limit long key to default length", 346 inputTargetingData: map[string]string{"very_long_targeting_key_should_be_truncated_to_default": "inVal1"}, 347 truncateTargetAttribute: 0, 348 expectedTargetingData: map[string]string{"very_long_targeting_": "inVal1"}, 349 }, 350 } 351 352 for _, test := range testCases { 353 actualTargetingData := truncateTargetingKeys(test.inputTargetingData, &test.truncateTargetAttribute) 354 assert.Equal(t, test.expectedTargetingData, actualTargetingData, "incorrect targeting data") 355 } 356 } 357 358 func TestGetValueFromSeatBidBid(t *testing.T) { 359 360 testCases := []struct { 361 description string 362 inputPath string 363 expectedValue string 364 expectError bool 365 }{ 366 367 { 368 description: "get existing valid value from bid", 369 inputPath: "seatbid.bid.price", 370 expectedValue: "10", 371 expectError: false, 372 }, 373 { 374 description: "get existing invalid value from bid", 375 inputPath: "seatbid.bid.cat", 376 expectedValue: "", 377 expectError: true, 378 }, 379 { 380 description: "get non-existing value from bid", 381 inputPath: "seatbid.bid.test", 382 expectedValue: "", 383 expectError: true, 384 }, 385 } 386 387 inputBidsCache := bidsCache{ 388 bids: map[string]map[string][]byte{ 389 "bidderA": {"testBidId": []byte(`{"id":"testBidId","impid":"testBidImpId1","price":10,"cat":["cat11","cat12"],"ext":{"prebid":{"foo":"bar1"}}}`)}, 390 }, 391 } 392 393 for _, test := range testCases { 394 actualValue, actualErr := getValueFromSeatBidBid(test.inputPath, inputBidsCache, "bidderA", openrtb2.Bid{ID: "testBidId"}) 395 assert.Equal(t, test.expectedValue, actualValue, "incorrect value returned") 396 if test.expectError { 397 assert.Error(t, actualErr, "unexpected error returned") 398 } else { 399 assert.NoError(t, actualErr, "expected error not returned") 400 } 401 } 402 } 403 404 func TestGetValueFromExt(t *testing.T) { 405 406 testCases := []struct { 407 description string 408 inputPath string 409 inputSeparator string 410 expectedValue string 411 expectError bool 412 }{ 413 414 { 415 description: "get existing valid value from bid.ext", 416 inputPath: "seatbid.ext.prebid.foo", 417 inputSeparator: "seatbid.ext.", 418 expectedValue: "bar1", 419 expectError: false, 420 }, 421 { 422 description: "get non-existing valid value from bid.ext", 423 inputPath: "seatbid.ext.prebid.foo1", 424 inputSeparator: "seatbid.ext.", 425 expectedValue: "", 426 expectError: true, 427 }, 428 { 429 description: "get existing valid with incorrect type value from bid.ext", 430 inputPath: "seatbid.ext.prebid", 431 inputSeparator: "seatbid.ext.", 432 expectedValue: "", 433 expectError: true, 434 }, 435 { 436 description: "get existing valid with unexpected separator from bid.ext", 437 inputPath: "seatbid.ext.prebid", 438 inputSeparator: ".ext.", 439 expectedValue: "", 440 expectError: true, 441 }, 442 } 443 inputExt := json.RawMessage(`{"prebid":{"foo":"bar1"}}`) 444 445 for _, test := range testCases { 446 actualValue, actualErr := getValueFromExt(test.inputPath, test.inputSeparator, inputExt) 447 assert.Equal(t, test.expectedValue, actualValue, "incorrect value returned") 448 if test.expectError { 449 assert.Error(t, actualErr, "unexpected error returned") 450 } else { 451 assert.NoError(t, actualErr, "expected error not returned") 452 } 453 } 454 } 455 456 func TestGetValueFromResp(t *testing.T) { 457 458 testCases := []struct { 459 description string 460 inputPath string 461 inputResponse *openrtb2.BidResponse 462 expectedValue string 463 expectError bool 464 }{ 465 466 { 467 description: "get existing valid value from response", 468 inputPath: "cur", 469 inputResponse: &openrtb2.BidResponse{Cur: "UAH"}, 470 expectedValue: "UAH", 471 expectError: false, 472 }, 473 { 474 description: "get empty valid value from response", 475 inputPath: "id", 476 inputResponse: &openrtb2.BidResponse{}, 477 expectedValue: "", 478 expectError: false, 479 }, 480 { 481 description: "get non-existing valid value from response", 482 inputPath: "test", 483 inputResponse: &openrtb2.BidResponse{}, 484 expectedValue: "", 485 expectError: true, 486 }, 487 } 488 489 for _, test := range testCases { 490 actualValue, actualErr := getValueFromResp(test.inputPath, test.inputResponse) 491 assert.Equal(t, test.expectedValue, actualValue, "incorrect value returned") 492 if test.expectError { 493 assert.Error(t, actualErr, "unexpected error returned") 494 } else { 495 assert.NoError(t, actualErr, "expected error not returned") 496 } 497 } 498 } 499 500 func TestGetRespData(t *testing.T) { 501 502 nbr := openrtb3.NoBidProxy 503 testCases := []struct { 504 description string 505 inputField string 506 inputResponse *openrtb2.BidResponse 507 expectedValue string 508 expectError bool 509 }{ 510 { 511 description: "get id from response", 512 inputField: "id", 513 inputResponse: &openrtb2.BidResponse{ID: "testId"}, 514 expectedValue: "testId", 515 expectError: false, 516 }, 517 { 518 description: "get bidid from response", 519 inputField: "bidid", 520 inputResponse: &openrtb2.BidResponse{BidID: "testBidId"}, 521 expectedValue: "testBidId", 522 expectError: false, 523 }, 524 { 525 description: "get cur from response", 526 inputField: "cur", 527 inputResponse: &openrtb2.BidResponse{Cur: "UAH"}, 528 expectedValue: "UAH", 529 expectError: false, 530 }, 531 { 532 description: "get customdata from response", 533 inputField: "customdata", 534 inputResponse: &openrtb2.BidResponse{CustomData: "testCustomdata"}, 535 expectedValue: "testCustomdata", 536 expectError: false, 537 }, 538 { 539 description: "get nbr from response", 540 inputField: "nbr", 541 inputResponse: &openrtb2.BidResponse{NBR: &nbr}, 542 expectedValue: "5", 543 expectError: false, 544 }, 545 { 546 description: "get non-existing value from response", 547 inputField: "test", 548 inputResponse: &openrtb2.BidResponse{ID: "testId"}, 549 expectedValue: "", 550 expectError: true, 551 }, 552 } 553 554 for _, test := range testCases { 555 actualValue, actualErr := getRespData(test.inputResponse, test.inputField) 556 assert.Equal(t, test.expectedValue, actualValue, "incorrect value returned") 557 if test.expectError { 558 assert.Error(t, actualErr, "unexpected error returned") 559 } else { 560 assert.NoError(t, actualErr, "expected error not returned") 561 } 562 } 563 564 } 565 566 func TestResponseObjectStructure(t *testing.T) { 567 // in case BidResponse format will change in next versions this test will show the error 568 // current implementation is up to date with OpenRTB 2.5 and OpenRTB 2.6 formats 569 fieldsToCheck := map[string]reflect.Kind{ 570 "id": reflect.String, 571 "bidid": reflect.String, 572 "cur": reflect.String, 573 "customdata": reflect.String, 574 "nbr": reflect.Pointer, 575 } 576 577 tt := reflect.TypeOf(openrtb2.BidResponse{}) 578 fields := reflect.VisibleFields(tt) 579 580 for fieldName, fieldType := range fieldsToCheck { 581 fieldFound := false 582 for _, field := range fields { 583 if fieldName == strings.ToLower(field.Name) { 584 fieldFound = true 585 assert.Equal(t, fieldType, field.Type.Kind(), "incorrect type for field: %s", fieldName) 586 break 587 } 588 } 589 assert.True(t, fieldFound, "field %s is not found in bidResponse object", fieldName) 590 } 591 }