github.com/prebid/prebid-server/v2@v2.18.0/exchange/auction_test.go (about) 1 package exchange 2 3 import ( 4 "context" 5 "encoding/xml" 6 "fmt" 7 "os" 8 "path/filepath" 9 "regexp" 10 "sort" 11 "strconv" 12 "testing" 13 14 "github.com/prebid/openrtb/v20/openrtb2" 15 "github.com/prebid/prebid-server/v2/config" 16 "github.com/prebid/prebid-server/v2/exchange/entities" 17 "github.com/prebid/prebid-server/v2/openrtb_ext" 18 "github.com/prebid/prebid-server/v2/prebid_cache_client" 19 "github.com/prebid/prebid-server/v2/util/jsonutil" 20 "github.com/prebid/prebid-server/v2/util/ptrutil" 21 22 "github.com/stretchr/testify/assert" 23 ) 24 25 func TestMakeVASTGiven(t *testing.T) { 26 const expect = `<VAST version="3.0"></VAST>` 27 bid := &openrtb2.Bid{ 28 AdM: expect, 29 } 30 vast := makeVAST(bid) 31 assert.Equal(t, expect, vast) 32 } 33 34 func TestMakeVASTNurl(t *testing.T) { 35 const url = "http://domain.com/win-notify/1" 36 const expect = `<VAST version="3.0"><Ad><Wrapper>` + 37 `<AdSystem>prebid.org wrapper</AdSystem>` + 38 `<VASTAdTagURI><![CDATA[` + url + `]]></VASTAdTagURI>` + 39 `<Impression></Impression><Creatives></Creatives>` + 40 `</Wrapper></Ad></VAST>` 41 bid := &openrtb2.Bid{ 42 NURL: url, 43 } 44 vast := makeVAST(bid) 45 assert.Equal(t, expect, vast) 46 } 47 48 func TestBuildCacheString(t *testing.T) { 49 testCases := []struct { 50 description string 51 debugLog DebugLog 52 expectedDebugLog DebugLog 53 }{ 54 { 55 description: "DebugLog strings should have tags and be formatted", 56 debugLog: DebugLog{ 57 Data: DebugData{ 58 Request: "test request string", 59 Headers: "test headers string", 60 Response: "test response string", 61 }, 62 Regexp: regexp.MustCompile(`[<>]`), 63 }, 64 expectedDebugLog: DebugLog{ 65 Data: DebugData{ 66 Request: "<Request>test request string</Request>", 67 Headers: "<Headers>test headers string</Headers>", 68 Response: "<Response>test response string</Response>", 69 }, 70 Regexp: regexp.MustCompile(`[<>]`), 71 }, 72 }, 73 { 74 description: "DebugLog strings should have no < or > characters", 75 debugLog: DebugLog{ 76 Data: DebugData{ 77 Request: "<test>test request string</test>", 78 Headers: "test <headers string", 79 Response: "test <response> string", 80 }, 81 Regexp: regexp.MustCompile(`[<>]`), 82 }, 83 expectedDebugLog: DebugLog{ 84 Data: DebugData{ 85 Request: "<Request>testtest request string/test</Request>", 86 Headers: "<Headers>test headers string</Headers>", 87 Response: "<Response>test response string</Response>", 88 }, 89 Regexp: regexp.MustCompile(`[<>]`), 90 }, 91 }, 92 } 93 94 for _, test := range testCases { 95 test.expectedDebugLog.CacheString = fmt.Sprintf("%s<Log>%s%s%s</Log>", xml.Header, test.expectedDebugLog.Data.Request, test.expectedDebugLog.Data.Headers, test.expectedDebugLog.Data.Response) 96 97 test.debugLog.BuildCacheString() 98 99 assert.Equal(t, test.expectedDebugLog, test.debugLog, test.description) 100 } 101 } 102 103 // TestCacheJSON executes tests for all the *.json files in cachetest. 104 // customcachekey.json test here verifies custom cache key not used for non-vast video 105 func TestCacheJSON(t *testing.T) { 106 for _, dir := range []string{"cachetest", "customcachekeytest", "impcustomcachekeytest", "eventscachetest"} { 107 if specFiles, err := os.ReadDir(dir); err == nil { 108 for _, specFile := range specFiles { 109 fileName := filepath.Join(dir, specFile.Name()) 110 fileDisplayName := "exchange/" + fileName 111 t.Run(fileDisplayName, func(t *testing.T) { 112 specData, err := loadCacheSpec(fileName) 113 if assert.NoError(t, err, "Failed to load contents of file %s: %v", fileDisplayName, err) { 114 runCacheSpec(t, fileDisplayName, specData) 115 } 116 }) 117 } 118 } else { 119 t.Fatalf("Failed to read contents of directory exchange/%s: %v", dir, err) 120 } 121 } 122 } 123 124 func TestIsDebugOverrideEnabled(t *testing.T) { 125 type inTest struct { 126 debugHeader string 127 configToken string 128 } 129 type aTest struct { 130 desc string 131 in inTest 132 result bool 133 } 134 testCases := []aTest{ 135 { 136 desc: "test debug header is empty, config token is empty", 137 in: inTest{debugHeader: "", configToken: ""}, 138 result: false, 139 }, 140 { 141 desc: "test debug header is present, config token is empty", 142 in: inTest{debugHeader: "TestToken", configToken: ""}, 143 result: false, 144 }, 145 { 146 desc: "test debug header is empty, config token is present", 147 in: inTest{debugHeader: "", configToken: "TestToken"}, 148 result: false, 149 }, 150 { 151 desc: "test debug header is present, config token is present, not equal", 152 in: inTest{debugHeader: "TestToken123", configToken: "TestToken"}, 153 result: false, 154 }, 155 { 156 desc: "test debug header is present, config token is present, equal", 157 in: inTest{debugHeader: "TestToken", configToken: "TestToken"}, 158 result: true, 159 }, 160 { 161 desc: "test debug header is present, config token is present, not case equal", 162 in: inTest{debugHeader: "TestTokeN", configToken: "TestToken"}, 163 result: false, 164 }, 165 } 166 167 for _, test := range testCases { 168 result := IsDebugOverrideEnabled(test.in.debugHeader, test.in.configToken) 169 assert.Equal(t, test.result, result, test.desc) 170 } 171 172 } 173 174 // LoadCacheSpec reads and parses a file as a test case. If something goes wrong, it returns an error. 175 func loadCacheSpec(filename string) (*cacheSpec, error) { 176 specData, err := os.ReadFile(filename) 177 if err != nil { 178 return nil, fmt.Errorf("Failed to read file %s: %v", filename, err) 179 } 180 181 var spec cacheSpec 182 if err := jsonutil.UnmarshalValid(specData, &spec); err != nil { 183 return nil, fmt.Errorf("Failed to unmarshal JSON from file: %v", err) 184 } 185 186 return &spec, nil 187 } 188 189 // runCacheSpec cycles through the bids found in the json test cases and 190 // finds the highest bid of every Imp, then tests doCache() with resulting auction object 191 func runCacheSpec(t *testing.T, fileDisplayName string, specData *cacheSpec) { 192 var bid *entities.PbsOrtbBid 193 winningBidsByImp := make(map[string]*entities.PbsOrtbBid) 194 allBidsByBidder := make(map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid) 195 roundedPrices := make(map[*entities.PbsOrtbBid]string) 196 bidCategory := make(map[string]string) 197 198 // Traverse through the bid list found in the parsed in Json file 199 for _, pbsBid := range specData.PbsBids { 200 bid = &entities.PbsOrtbBid{ 201 Bid: pbsBid.Bid, 202 BidType: pbsBid.BidType, 203 } 204 cpm := bid.Bid.Price 205 206 // Map this bid if it's the highest we've seen from this Imp so far 207 wbid, ok := winningBidsByImp[bid.Bid.ImpID] 208 if !ok || cpm > wbid.Bid.Price { 209 winningBidsByImp[bid.Bid.ImpID] = bid 210 } 211 212 // Map this bid if it's the highest we've seen from this bidder so far 213 if bidMap, ok := allBidsByBidder[bid.Bid.ImpID]; ok { 214 bidMap[pbsBid.Bidder] = append(bidMap[pbsBid.Bidder], bid) 215 } else { 216 allBidsByBidder[bid.Bid.ImpID] = map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ 217 pbsBid.Bidder: {bid}, 218 } 219 } 220 221 for _, topBidsPerBidder := range allBidsByBidder { 222 for _, topBids := range topBidsPerBidder { 223 sort.Slice(topBids, func(i, j int) bool { 224 return isNewWinningBid(topBids[i].Bid, topBids[j].Bid, true) 225 }) 226 } 227 } 228 229 if len(pbsBid.Bid.Cat) == 1 { 230 bidCategory[pbsBid.Bid.ID] = pbsBid.Bid.Cat[0] 231 } 232 roundedPrices[bid] = strconv.FormatFloat(bid.Bid.Price, 'f', 2, 64) 233 } 234 235 ctx := context.Background() 236 cache := &mockCache{} 237 238 targData := &targetData{ 239 priceGranularity: openrtb_ext.PriceGranularity{ 240 Precision: ptrutil.ToPtr(2), 241 Ranges: []openrtb_ext.GranularityRange{ 242 { 243 Min: 0, 244 Max: 5, 245 Increment: 0.05, 246 }, 247 { 248 Min: 5, 249 Max: 10, 250 Increment: 0.1, 251 }, 252 { 253 Min: 10, 254 Max: 20, 255 Increment: 0.5, 256 }, 257 }, 258 }, 259 includeWinners: specData.TargetDataIncludeWinners, 260 includeBidderKeys: specData.TargetDataIncludeBidderKeys, 261 includeCacheBids: specData.TargetDataIncludeCacheBids, 262 includeCacheVast: specData.TargetDataIncludeCacheVast, 263 } 264 265 testAuction := &auction{ 266 winningBids: winningBidsByImp, 267 allBidsByBidder: allBidsByBidder, 268 roundedPrices: roundedPrices, 269 } 270 evTracking := &eventTracking{ 271 accountID: "TEST_ACC_ID", 272 enabledForAccount: specData.EventsDataEnabledForAccount, 273 enabledForRequest: specData.EventsDataEnabledForRequest, 274 externalURL: "http://localhost", 275 auctionTimestampMs: 1234567890, 276 } 277 _ = testAuction.doCache(ctx, cache, targData, evTracking, &specData.BidRequest, 60, &specData.DefaultTTLs, bidCategory, &specData.DebugLog) 278 279 if len(specData.ExpectedCacheables) > len(cache.items) { 280 t.Errorf("%s: [CACHE_ERROR] Less elements were cached than expected \n", fileDisplayName) 281 } else if len(specData.ExpectedCacheables) < len(cache.items) { 282 t.Errorf("%s: [CACHE_ERROR] More elements were cached than expected \n", fileDisplayName) 283 } else { // len(specData.ExpectedCacheables) == len(cache.items) 284 // We cached the exact number of elements we expected, now we compare them side by side in n^2 285 var matched int = 0 286 for i, expectedCacheable := range specData.ExpectedCacheables { 287 found := false 288 var expectedData interface{} 289 if err := jsonutil.UnmarshalValid(expectedCacheable.Data, &expectedData); err != nil { 290 t.Fatalf("Failed to decode expectedCacheables[%d].value: %v", i, err) 291 } 292 if s, ok := expectedData.(string); ok && expectedCacheable.Type == prebid_cache_client.TypeJSON { 293 // decode again if we have pre-encoded json string values 294 if err := jsonutil.UnmarshalValid([]byte(s), &expectedData); err != nil { 295 t.Fatalf("Failed to re-decode expectedCacheables[%d].value :%v", i, err) 296 } 297 } 298 for j, cachedItem := range cache.items { 299 var actualData interface{} 300 if err := jsonutil.UnmarshalValid(cachedItem.Data, &actualData); err != nil { 301 t.Fatalf("Failed to decode actual cache[%d].value: %s", j, err) 302 } 303 if assert.ObjectsAreEqual(expectedData, actualData) && 304 expectedCacheable.TTLSeconds == cachedItem.TTLSeconds && 305 expectedCacheable.Type == cachedItem.Type && 306 len(expectedCacheable.Key) <= len(cachedItem.Key) && 307 expectedCacheable.Key == cachedItem.Key[:len(expectedCacheable.Key)] { 308 found = true 309 cache.items = append(cache.items[:j], cache.items[j+1:]...) // remove matched item 310 break 311 } 312 } 313 if found { 314 matched++ 315 } else { 316 t.Errorf("%s: [CACHE_ERROR] Did not see expected cacheable #%d: type=%s, ttl=%d, value=%s", fileDisplayName, i, expectedCacheable.Type, expectedCacheable.TTLSeconds, string(expectedCacheable.Data)) 317 } 318 } 319 if matched != len(specData.ExpectedCacheables) { 320 for i, item := range cache.items { 321 t.Errorf("%s: [CACHE_ERROR] Got unexpected cached item #%d: type=%s, ttl=%d, value=%s", fileDisplayName, i, item.Type, item.TTLSeconds, string(item.Data)) 322 } 323 t.FailNow() 324 } 325 } 326 } 327 328 func TestNewAuction(t *testing.T) { 329 bid1p077 := entities.PbsOrtbBid{ 330 Bid: &openrtb2.Bid{ 331 ImpID: "imp1", 332 Price: 0.77, 333 }, 334 } 335 bid1p123 := entities.PbsOrtbBid{ 336 Bid: &openrtb2.Bid{ 337 ImpID: "imp1", 338 Price: 1.23, 339 }, 340 } 341 bid1p230 := entities.PbsOrtbBid{ 342 Bid: &openrtb2.Bid{ 343 ImpID: "imp1", 344 Price: 2.30, 345 }, 346 } 347 bid1p088d := entities.PbsOrtbBid{ 348 Bid: &openrtb2.Bid{ 349 ImpID: "imp1", 350 Price: 0.88, 351 DealID: "SpecialDeal", 352 }, 353 } 354 bid1p166d := entities.PbsOrtbBid{ 355 Bid: &openrtb2.Bid{ 356 ImpID: "imp1", 357 Price: 1.66, 358 DealID: "BigDeal", 359 }, 360 } 361 bid2p123 := entities.PbsOrtbBid{ 362 Bid: &openrtb2.Bid{ 363 ImpID: "imp2", 364 Price: 1.23, 365 }, 366 } 367 bid2p144 := entities.PbsOrtbBid{ 368 Bid: &openrtb2.Bid{ 369 ImpID: "imp2", 370 Price: 1.44, 371 }, 372 } 373 bid2p155 := entities.PbsOrtbBid{ 374 Bid: &openrtb2.Bid{ 375 ImpID: "imp2", 376 Price: 1.55, 377 }, 378 } 379 bid2p166 := entities.PbsOrtbBid{ 380 Bid: &openrtb2.Bid{ 381 ImpID: "imp2", 382 Price: 1.66, 383 }, 384 } 385 tests := []struct { 386 description string 387 seatBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid 388 numImps int 389 preferDeals bool 390 expectedAuction auction 391 }{ 392 { 393 description: "Basic auction test", 394 seatBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ 395 "appnexus": { 396 Bids: []*entities.PbsOrtbBid{&bid1p123}, 397 }, 398 "rubicon": { 399 Bids: []*entities.PbsOrtbBid{&bid1p230}, 400 }, 401 }, 402 numImps: 1, 403 preferDeals: false, 404 expectedAuction: auction{ 405 winningBids: map[string]*entities.PbsOrtbBid{ 406 "imp1": &bid1p230, 407 }, 408 allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ 409 "imp1": { 410 "appnexus": []*entities.PbsOrtbBid{&bid1p123}, 411 "rubicon": []*entities.PbsOrtbBid{&bid1p230}, 412 }, 413 }, 414 }, 415 }, 416 { 417 description: "Multi-imp auction", 418 seatBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ 419 "appnexus": { 420 Bids: []*entities.PbsOrtbBid{&bid1p230, &bid2p123}, 421 }, 422 "rubicon": { 423 Bids: []*entities.PbsOrtbBid{&bid1p077, &bid2p144}, 424 }, 425 "openx": { 426 Bids: []*entities.PbsOrtbBid{&bid1p123}, 427 }, 428 }, 429 numImps: 2, 430 preferDeals: false, 431 expectedAuction: auction{ 432 winningBids: map[string]*entities.PbsOrtbBid{ 433 "imp1": &bid1p230, 434 "imp2": &bid2p144, 435 }, 436 allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ 437 "imp1": { 438 "appnexus": []*entities.PbsOrtbBid{&bid1p230}, 439 "rubicon": []*entities.PbsOrtbBid{&bid1p077}, 440 "openx": []*entities.PbsOrtbBid{&bid1p123}, 441 }, 442 "imp2": { 443 "appnexus": []*entities.PbsOrtbBid{&bid2p123}, 444 "rubicon": []*entities.PbsOrtbBid{&bid2p144}, 445 }, 446 }, 447 }, 448 }, 449 { 450 description: "Basic auction with deals, no preference", 451 seatBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ 452 "appnexus": { 453 Bids: []*entities.PbsOrtbBid{&bid1p123}, 454 }, 455 "rubicon": { 456 Bids: []*entities.PbsOrtbBid{&bid1p088d}, 457 }, 458 }, 459 numImps: 1, 460 preferDeals: false, 461 expectedAuction: auction{ 462 winningBids: map[string]*entities.PbsOrtbBid{ 463 "imp1": &bid1p123, 464 }, 465 allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ 466 "imp1": { 467 "appnexus": []*entities.PbsOrtbBid{&bid1p123}, 468 "rubicon": []*entities.PbsOrtbBid{&bid1p088d}, 469 }, 470 }, 471 }, 472 }, 473 { 474 description: "Basic auction with deals, prefer deals", 475 seatBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ 476 "appnexus": { 477 Bids: []*entities.PbsOrtbBid{&bid1p123}, 478 }, 479 "rubicon": { 480 Bids: []*entities.PbsOrtbBid{&bid1p088d}, 481 }, 482 }, 483 numImps: 1, 484 preferDeals: true, 485 expectedAuction: auction{ 486 winningBids: map[string]*entities.PbsOrtbBid{ 487 "imp1": &bid1p088d, 488 }, 489 allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ 490 "imp1": { 491 "appnexus": []*entities.PbsOrtbBid{&bid1p123}, 492 "rubicon": []*entities.PbsOrtbBid{&bid1p088d}, 493 }, 494 }, 495 }, 496 }, 497 { 498 description: "Auction with 2 deals", 499 seatBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ 500 "appnexus": { 501 Bids: []*entities.PbsOrtbBid{&bid1p166d}, 502 }, 503 "rubicon": { 504 Bids: []*entities.PbsOrtbBid{&bid1p088d}, 505 }, 506 }, 507 numImps: 1, 508 preferDeals: true, 509 expectedAuction: auction{ 510 winningBids: map[string]*entities.PbsOrtbBid{ 511 "imp1": &bid1p166d, 512 }, 513 allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ 514 "imp1": { 515 "appnexus": []*entities.PbsOrtbBid{&bid1p166d}, 516 "rubicon": []*entities.PbsOrtbBid{&bid1p088d}, 517 }, 518 }, 519 }, 520 }, 521 { 522 description: "Auction with 3 bids and 2 deals", 523 seatBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ 524 "appnexus": { 525 Bids: []*entities.PbsOrtbBid{&bid1p166d}, 526 }, 527 "rubicon": { 528 Bids: []*entities.PbsOrtbBid{&bid1p088d}, 529 }, 530 "openx": { 531 Bids: []*entities.PbsOrtbBid{&bid1p230}, 532 }, 533 }, 534 numImps: 1, 535 preferDeals: true, 536 expectedAuction: auction{ 537 winningBids: map[string]*entities.PbsOrtbBid{ 538 "imp1": &bid1p166d, 539 }, 540 allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ 541 "imp1": { 542 "appnexus": []*entities.PbsOrtbBid{&bid1p166d}, 543 "rubicon": []*entities.PbsOrtbBid{&bid1p088d}, 544 "openx": []*entities.PbsOrtbBid{&bid1p230}, 545 }, 546 }, 547 }, 548 }, 549 { 550 description: "Auction with 3 bids and 2 deals - multiple bids under each seatBids", 551 seatBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ 552 "appnexus": { 553 Bids: []*entities.PbsOrtbBid{&bid1p166d, &bid1p077, &bid2p123, &bid2p144}, 554 }, 555 "pubmatic": { 556 Bids: []*entities.PbsOrtbBid{&bid1p088d, &bid1p123, &bid2p155, &bid2p166}, 557 }, 558 }, 559 numImps: 1, 560 preferDeals: true, 561 expectedAuction: auction{ 562 winningBids: map[string]*entities.PbsOrtbBid{ 563 "imp1": &bid1p166d, 564 "imp2": &bid2p166, 565 }, 566 allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ 567 "imp1": { 568 "appnexus": []*entities.PbsOrtbBid{&bid1p166d, &bid1p077}, 569 "pubmatic": []*entities.PbsOrtbBid{&bid1p088d, &bid1p123}, 570 }, 571 "imp2": { 572 "appnexus": []*entities.PbsOrtbBid{&bid2p123, &bid2p144}, 573 "pubmatic": []*entities.PbsOrtbBid{&bid2p155, &bid2p166}, 574 }, 575 }, 576 }, 577 }, 578 } 579 580 for _, test := range tests { 581 auc := newAuction(test.seatBids, test.numImps, test.preferDeals) 582 583 assert.Equal(t, test.expectedAuction, *auc, test.description) 584 } 585 586 } 587 588 func TestValidateAndUpdateMultiBid(t *testing.T) { 589 // create new bids for new test cases since the last one changes a few bids. Ex marks bid1p001.Bid = nil 590 bid1p001 := entities.PbsOrtbBid{ 591 Bid: &openrtb2.Bid{ 592 ImpID: "imp1", 593 Price: 0.01, 594 }, 595 } 596 bid1p077 := entities.PbsOrtbBid{ 597 Bid: &openrtb2.Bid{ 598 ImpID: "imp1", 599 Price: 0.77, 600 }, 601 } 602 bid1p123 := entities.PbsOrtbBid{ 603 Bid: &openrtb2.Bid{ 604 ImpID: "imp1", 605 Price: 1.23, 606 }, 607 } 608 bid1p088d := entities.PbsOrtbBid{ 609 Bid: &openrtb2.Bid{ 610 ImpID: "imp1", 611 Price: 0.88, 612 DealID: "SpecialDeal", 613 }, 614 } 615 bid1p166d := entities.PbsOrtbBid{ 616 Bid: &openrtb2.Bid{ 617 ImpID: "imp1", 618 Price: 1.66, 619 DealID: "BigDeal", 620 }, 621 } 622 bid2p123 := entities.PbsOrtbBid{ 623 Bid: &openrtb2.Bid{ 624 ImpID: "imp2", 625 Price: 1.23, 626 }, 627 } 628 bid2p144 := entities.PbsOrtbBid{ 629 Bid: &openrtb2.Bid{ 630 ImpID: "imp2", 631 Price: 1.44, 632 }, 633 } 634 bid2p155 := entities.PbsOrtbBid{ 635 Bid: &openrtb2.Bid{ 636 ImpID: "imp2", 637 Price: 1.55, 638 }, 639 } 640 bid2p166 := entities.PbsOrtbBid{ 641 Bid: &openrtb2.Bid{ 642 ImpID: "imp2", 643 Price: 1.66, 644 }, 645 } 646 647 type fields struct { 648 winningBids map[string]*entities.PbsOrtbBid 649 allBidsByBidder map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid 650 roundedPrices map[*entities.PbsOrtbBid]string 651 cacheIds map[*openrtb2.Bid]string 652 vastCacheIds map[*openrtb2.Bid]string 653 } 654 type args struct { 655 adapterBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid 656 preferDeals bool 657 accountDefaultBidLimit int 658 } 659 type want struct { 660 allBidsByBidder map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid 661 adapterBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid 662 } 663 tests := []struct { 664 description string 665 fields fields 666 args args 667 want want 668 }{ 669 { 670 description: "DefaultBidLimit is 0 (default value)", 671 fields: fields{ 672 winningBids: map[string]*entities.PbsOrtbBid{ 673 "imp1": &bid1p166d, 674 "imp2": &bid2p166, 675 }, 676 allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ 677 "imp1": { 678 "appnexus": []*entities.PbsOrtbBid{&bid1p001, &bid1p166d, &bid1p077}, 679 "pubmatic": []*entities.PbsOrtbBid{&bid1p088d, &bid1p123}, 680 }, 681 "imp2": { 682 "appnexus": []*entities.PbsOrtbBid{&bid2p123, &bid2p144}, 683 "pubmatic": []*entities.PbsOrtbBid{&bid2p155, &bid2p166}, 684 }, 685 }, 686 }, 687 args: args{ 688 adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ 689 "appnexus": { 690 Bids: []*entities.PbsOrtbBid{&bid1p001, &bid1p166d, &bid1p077, &bid2p123, &bid2p144}, 691 }, 692 "pubmatic": { 693 Bids: []*entities.PbsOrtbBid{&bid1p088d, &bid1p123, &bid2p155, &bid2p166}, 694 }, 695 }, 696 accountDefaultBidLimit: 0, 697 preferDeals: true, 698 }, 699 want: want{ 700 allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ 701 "imp1": { 702 "appnexus": []*entities.PbsOrtbBid{&bid1p166d, &bid1p077, &bid1p001}, 703 "pubmatic": []*entities.PbsOrtbBid{&bid1p088d, &bid1p123}, 704 }, 705 "imp2": { 706 "appnexus": []*entities.PbsOrtbBid{&bid2p144, &bid2p123}, 707 "pubmatic": []*entities.PbsOrtbBid{&bid2p166, &bid2p155}, 708 }, 709 }, 710 adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ 711 "appnexus": { 712 Bids: []*entities.PbsOrtbBid{&bid1p001, &bid1p166d, &bid1p077, &bid2p123, &bid2p144}, 713 }, 714 "pubmatic": { 715 Bids: []*entities.PbsOrtbBid{&bid1p088d, &bid1p123, &bid2p155, &bid2p166}, 716 }, 717 }, 718 }, 719 }, 720 { 721 description: "Adapters bid count per imp within DefaultBidLimit", 722 fields: fields{ 723 winningBids: map[string]*entities.PbsOrtbBid{ 724 "imp1": &bid1p166d, 725 "imp2": &bid2p166, 726 }, 727 allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ 728 "imp1": { 729 "appnexus": []*entities.PbsOrtbBid{&bid1p001, &bid1p166d, &bid1p077}, 730 "pubmatic": []*entities.PbsOrtbBid{&bid1p088d, &bid1p123}, 731 }, 732 "imp2": { 733 "appnexus": []*entities.PbsOrtbBid{&bid2p123, &bid2p144}, 734 "pubmatic": []*entities.PbsOrtbBid{&bid2p155, &bid2p166}, 735 }, 736 }, 737 }, 738 args: args{ 739 adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ 740 "appnexus": { 741 Bids: []*entities.PbsOrtbBid{&bid1p001, &bid1p166d, &bid1p077, &bid2p123, &bid2p144}, 742 }, 743 "pubmatic": { 744 Bids: []*entities.PbsOrtbBid{&bid1p088d, &bid1p123, &bid2p155, &bid2p166}, 745 }, 746 }, 747 accountDefaultBidLimit: 3, 748 preferDeals: true, 749 }, 750 want: want{ 751 allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ 752 "imp1": { 753 "appnexus": []*entities.PbsOrtbBid{&bid1p166d, &bid1p077, &bid1p001}, 754 "pubmatic": []*entities.PbsOrtbBid{&bid1p088d, &bid1p123}, 755 }, 756 "imp2": { 757 "appnexus": []*entities.PbsOrtbBid{&bid2p144, &bid2p123}, 758 "pubmatic": []*entities.PbsOrtbBid{&bid2p166, &bid2p155}, 759 }, 760 }, 761 adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ 762 "appnexus": { 763 Bids: []*entities.PbsOrtbBid{&bid1p001, &bid1p166d, &bid1p077, &bid2p123, &bid2p144}, 764 }, 765 "pubmatic": { 766 Bids: []*entities.PbsOrtbBid{&bid1p088d, &bid1p123, &bid2p155, &bid2p166}, 767 }, 768 }, 769 }, 770 }, 771 { 772 description: "Adapters bid count per imp more than DefaultBidLimit", 773 fields: fields{ 774 winningBids: map[string]*entities.PbsOrtbBid{ 775 "imp1": &bid1p166d, 776 "imp2": &bid2p166, 777 }, 778 allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ 779 "imp1": { 780 "appnexus": []*entities.PbsOrtbBid{&bid1p001, &bid1p166d, &bid1p077}, 781 "pubmatic": []*entities.PbsOrtbBid{&bid1p088d, &bid1p123}, 782 }, 783 "imp2": { 784 "appnexus": []*entities.PbsOrtbBid{&bid2p123, &bid2p144}, 785 "pubmatic": []*entities.PbsOrtbBid{&bid2p155, &bid2p166}, 786 }, 787 }, 788 }, 789 args: args{ 790 adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ 791 "appnexus": { 792 Bids: []*entities.PbsOrtbBid{&bid1p001, &bid1p166d, &bid1p077, &bid2p123, &bid2p144}, 793 }, 794 "pubmatic": { 795 Bids: []*entities.PbsOrtbBid{&bid1p088d, &bid1p123, &bid2p155, &bid2p166}, 796 }, 797 }, 798 accountDefaultBidLimit: 2, 799 preferDeals: true, 800 }, 801 want: want{ 802 allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ 803 "imp1": { 804 "appnexus": []*entities.PbsOrtbBid{&bid1p166d, &bid1p077}, 805 "pubmatic": []*entities.PbsOrtbBid{&bid1p088d, &bid1p123}, 806 }, 807 "imp2": { 808 "appnexus": []*entities.PbsOrtbBid{&bid2p144, &bid2p123}, 809 "pubmatic": []*entities.PbsOrtbBid{&bid2p166, &bid2p155}, 810 }, 811 }, 812 adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ 813 "appnexus": { 814 Bids: []*entities.PbsOrtbBid{&bid1p166d, &bid1p077, &bid2p123, &bid2p144}, 815 }, 816 "pubmatic": { 817 Bids: []*entities.PbsOrtbBid{&bid1p088d, &bid1p123, &bid2p155, &bid2p166}, 818 }, 819 }, 820 }, 821 }, 822 } 823 for _, tt := range tests { 824 t.Run(tt.description, func(t *testing.T) { 825 a := &auction{ 826 winningBids: tt.fields.winningBids, 827 allBidsByBidder: tt.fields.allBidsByBidder, 828 roundedPrices: tt.fields.roundedPrices, 829 cacheIds: tt.fields.cacheIds, 830 vastCacheIds: tt.fields.vastCacheIds, 831 } 832 a.validateAndUpdateMultiBid(tt.args.adapterBids, tt.args.preferDeals, tt.args.accountDefaultBidLimit) 833 assert.Equal(t, tt.want.allBidsByBidder, tt.fields.allBidsByBidder, tt.description) 834 assert.Equal(t, tt.want.adapterBids, tt.args.adapterBids, tt.description) 835 }) 836 } 837 } 838 839 type cacheSpec struct { 840 BidRequest openrtb2.BidRequest `json:"bidRequest"` 841 PbsBids []pbsBid `json:"pbsBids"` 842 ExpectedCacheables []prebid_cache_client.Cacheable `json:"expectedCacheables"` 843 DefaultTTLs config.DefaultTTLs `json:"defaultTTLs"` 844 TargetDataIncludeWinners bool `json:"targetDataIncludeWinners"` 845 TargetDataIncludeBidderKeys bool `json:"targetDataIncludeBidderKeys"` 846 TargetDataIncludeCacheBids bool `json:"targetDataIncludeCacheBids"` 847 TargetDataIncludeCacheVast bool `json:"targetDataIncludeCacheVast"` 848 EventsDataEnabledForAccount bool `json:"eventsDataEnabledForAccount"` 849 EventsDataEnabledForRequest bool `json:"eventsDataEnabledForRequest"` 850 DebugLog DebugLog `json:"debugLog,omitempty"` 851 } 852 853 type pbsBid struct { 854 Bid *openrtb2.Bid `json:"bid"` 855 BidType openrtb_ext.BidType `json:"bidType"` 856 Bidder openrtb_ext.BidderName `json:"bidder"` 857 } 858 859 type mockCache struct { 860 scheme string 861 host string 862 path string 863 items []prebid_cache_client.Cacheable 864 } 865 866 func (c *mockCache) GetExtCacheData() (scheme string, host string, path string) { 867 return c.scheme, c.host, c.path 868 } 869 870 func (c *mockCache) GetPutUrl() string { 871 return "" 872 } 873 874 func (c *mockCache) PutJson(ctx context.Context, values []prebid_cache_client.Cacheable) ([]string, []error) { 875 c.items = values 876 return []string{"", "", "", "", ""}, nil 877 }