github.com/prebid/prebid-server/v2@v2.18.0/exchange/exchange_test.go (about) 1 package exchange 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "math" 10 "net/http" 11 "net/http/httptest" 12 "os" 13 "reflect" 14 "regexp" 15 "sort" 16 "strconv" 17 "strings" 18 "testing" 19 "time" 20 21 "github.com/buger/jsonparser" 22 "github.com/prebid/openrtb/v20/openrtb2" 23 "github.com/prebid/prebid-server/v2/adapters" 24 "github.com/prebid/prebid-server/v2/config" 25 "github.com/prebid/prebid-server/v2/currency" 26 "github.com/prebid/prebid-server/v2/errortypes" 27 "github.com/prebid/prebid-server/v2/exchange/entities" 28 "github.com/prebid/prebid-server/v2/experiment/adscert" 29 "github.com/prebid/prebid-server/v2/gdpr" 30 "github.com/prebid/prebid-server/v2/hooks" 31 "github.com/prebid/prebid-server/v2/hooks/hookexecution" 32 "github.com/prebid/prebid-server/v2/hooks/hookstage" 33 "github.com/prebid/prebid-server/v2/macros" 34 "github.com/prebid/prebid-server/v2/metrics" 35 metricsConf "github.com/prebid/prebid-server/v2/metrics/config" 36 metricsConfig "github.com/prebid/prebid-server/v2/metrics/config" 37 "github.com/prebid/prebid-server/v2/openrtb_ext" 38 pbc "github.com/prebid/prebid-server/v2/prebid_cache_client" 39 "github.com/prebid/prebid-server/v2/privacy" 40 "github.com/prebid/prebid-server/v2/stored_requests" 41 "github.com/prebid/prebid-server/v2/stored_requests/backends/file_fetcher" 42 "github.com/prebid/prebid-server/v2/usersync" 43 "github.com/prebid/prebid-server/v2/util/jsonutil" 44 "github.com/prebid/prebid-server/v2/util/ptrutil" 45 "github.com/stretchr/testify/assert" 46 "github.com/stretchr/testify/mock" 47 jsonpatch "gopkg.in/evanphx/json-patch.v4" 48 ) 49 50 func TestNewExchange(t *testing.T) { 51 respStatus := 200 52 respBody := "{\"bid\":false}" 53 server := httptest.NewServer(mockHandler(respStatus, "getBody", respBody)) 54 defer server.Close() 55 56 knownAdapters := openrtb_ext.CoreBidderNames() 57 58 cfg := &config.Configuration{ 59 CacheURL: config.Cache{ 60 ExpectedTimeMillis: 20, 61 }, 62 GDPR: config.GDPR{ 63 EEACountries: []string{"FIN", "FRA", "GUF"}, 64 }, 65 } 66 67 biddersInfo, err := config.LoadBidderInfoFromDisk("../static/bidder-info") 68 if err != nil { 69 t.Fatal(err) 70 } 71 72 adapters, adaptersErr := BuildAdapters(server.Client(), cfg, biddersInfo, &metricsConf.NilMetricsEngine{}) 73 if adaptersErr != nil { 74 t.Fatalf("Error intializing adapters: %v", adaptersErr) 75 } 76 77 currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) 78 79 gdprPermsBuilder := fakePermissionsBuilder{ 80 permissions: &permissionsMock{ 81 allowAllBidders: true, 82 }, 83 }.Builder 84 85 e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange) 86 for _, bidderName := range knownAdapters { 87 if _, ok := e.adapterMap[bidderName]; !ok { 88 if biddersInfo[string(bidderName)].IsEnabled() { 89 t.Errorf("NewExchange produced an Exchange without bidder %s", bidderName) 90 } 91 } 92 } 93 if e.cacheTime != time.Duration(cfg.CacheURL.ExpectedTimeMillis)*time.Millisecond { 94 t.Errorf("Bad cacheTime. Expected 20 ms, got %s", e.cacheTime.String()) 95 } 96 } 97 98 // The objective is to get to execute e.buildBidResponse(ctx.Background(), liveA... ) (*openrtb2.BidResponse, error) 99 // and check whether the returned request successfully prints any '&' characters as it should 100 // To do so, we: 101 // 1. Write the endpoint adapter URL with an '&' character into a new config,Configuration struct 102 // as specified in https://github.com/prebid/prebid-server/issues/465 103 // 2. Initialize a new exchange with said configuration 104 // 3. Build all the parameters e.buildBidResponse(ctx.Background(), liveA... ) needs including the 105 // sample request as specified in https://github.com/prebid/prebid-server/issues/465 106 // 4. Build a BidResponse struct using exchange.buildBidResponse(ctx.Background(), liveA... ) 107 // 5. Assert we have no '&' characters in the response that exchange.buildBidResponse returns 108 func TestCharacterEscape(t *testing.T) { 109 110 // 1) Adapter with a '& char in its endpoint property 111 // https://github.com/prebid/prebid-server/issues/465 112 cfg := &config.Configuration{} 113 biddersInfo := config.BidderInfos{"appnexus": config.BidderInfo{Endpoint: "http://ib.adnxs.com/openrtb2?query1&query2"}} //Note the '&' character in there 114 115 // 2) Init new exchange with said configuration 116 //Other parameters also needed to create exchange 117 handlerNoBidServer := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(204) } 118 server := httptest.NewServer(http.HandlerFunc(handlerNoBidServer)) 119 120 defer server.Close() 121 122 adapters, adaptersErr := BuildAdapters(server.Client(), cfg, biddersInfo, &metricsConf.NilMetricsEngine{}) 123 if adaptersErr != nil { 124 t.Fatalf("Error intializing adapters: %v", adaptersErr) 125 } 126 127 currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) 128 129 gdprPermsBuilder := fakePermissionsBuilder{ 130 permissions: &permissionsMock{ 131 allowAllBidders: true, 132 }, 133 }.Builder 134 135 e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange) 136 137 // 3) Build all the parameters e.buildBidResponse(ctx.Background(), liveA... ) needs 138 //liveAdapters []openrtb_ext.BidderName, 139 liveAdapters := make([]openrtb_ext.BidderName, 1) 140 liveAdapters[0] = "appnexus" 141 142 //adapterBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, 143 adapterBids := make(map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, 1) 144 adapterBids["appnexus"] = &entities.PbsOrtbSeatBid{Currency: "USD"} 145 146 //An openrtb2.BidRequest struct as specified in https://github.com/prebid/prebid-server/issues/465 147 bidRequest := &openrtb_ext.RequestWrapper{ 148 BidRequest: &openrtb2.BidRequest{ 149 ID: "some-request-id", 150 Imp: []openrtb2.Imp{{ 151 ID: "some-impression-id", 152 Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}, {W: 300, H: 600}}}, 153 Ext: json.RawMessage(`{"appnexus": {"placementId": 1}}`), 154 }}, 155 Site: &openrtb2.Site{Page: "prebid.org", Ext: json.RawMessage(`{"amp":0}`)}, 156 Device: &openrtb2.Device{UA: "curl/7.54.0", IP: "::1"}, 157 AT: 1, 158 TMax: 500, 159 Ext: json.RawMessage(`{"id": "some-request-id","site": {"page": "prebid.org"},"imp": [{"id": "some-impression-id","banner": {"format": [{"w": 300,"h": 250},{"w": 300,"h": 600}]},"ext": {"appnexus": {"placementId": 1}}}],"tmax": 500}`), 160 }, 161 } 162 163 //adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, 164 adapterExtra := make(map[openrtb_ext.BidderName]*seatResponseExtra, 1) 165 adapterExtra["appnexus"] = &seatResponseExtra{ 166 ResponseTimeMillis: 5, 167 Errors: []openrtb_ext.ExtBidderMessage{{Code: 999, Message: "Post ib.adnxs.com/openrtb2?query1&query2: unsupported protocol scheme \"\""}}, 168 } 169 170 var errList []error 171 172 // 4) Build bid response 173 bidResp := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, adapterExtra, nil, nil, true, nil, "", errList, &nonBids{}) 174 175 // 5) Assert we have no errors and one '&' character as we are supposed to 176 if len(errList) > 0 { 177 t.Errorf("exchange.buildBidResponse returned %d errors", len(errList)) 178 } 179 if bytes.Contains(bidResp.Ext, []byte("u0026")) { 180 t.Errorf("exchange.buildBidResponse() did not correctly print the '&' characters %s", string(bidResp.Ext)) 181 } 182 } 183 184 // TestDebugBehaviour asserts the HttpCalls object is included inside the json "debug" field of the bidResponse extension when the 185 // openrtb2.BidRequest "Test" value is set to 1 or the openrtb2.BidRequest.Ext.Debug boolean field is set to true 186 func TestDebugBehaviour(t *testing.T) { 187 188 // Define test cases 189 type inTest struct { 190 test int8 191 debug bool 192 } 193 type outTest struct { 194 debugInfoIncluded bool 195 } 196 197 type debugData struct { 198 bidderLevelDebugAllowed bool 199 accountLevelDebugAllowed bool 200 headerOverrideDebugAllowed bool 201 } 202 203 type aTest struct { 204 desc string 205 in inTest 206 out outTest 207 debugData debugData 208 generateWarnings bool 209 } 210 testCases := []aTest{ 211 { 212 desc: "test flag equals zero, ext debug flag false, no debug info expected", 213 in: inTest{test: 0, debug: false}, 214 out: outTest{debugInfoIncluded: false}, 215 debugData: debugData{true, true, false}, 216 generateWarnings: false, 217 }, 218 { 219 desc: "test flag equals zero, ext debug flag true, debug info expected", 220 in: inTest{test: 0, debug: true}, 221 out: outTest{debugInfoIncluded: true}, 222 debugData: debugData{true, true, false}, 223 generateWarnings: false, 224 }, 225 { 226 desc: "test flag equals 1, ext debug flag false, debug info expected", 227 in: inTest{test: 1, debug: false}, 228 out: outTest{debugInfoIncluded: true}, 229 debugData: debugData{true, true, false}, 230 generateWarnings: false, 231 }, 232 { 233 desc: "test flag equals 1, ext debug flag true, debug info expected", 234 in: inTest{test: 1, debug: true}, 235 out: outTest{debugInfoIncluded: true}, 236 debugData: debugData{true, true, false}, 237 generateWarnings: false, 238 }, 239 { 240 desc: "test flag not equal to 0 nor 1, ext debug flag false, no debug info expected", 241 in: inTest{test: 2, debug: false}, 242 out: outTest{debugInfoIncluded: false}, 243 debugData: debugData{true, true, false}, 244 generateWarnings: false, 245 }, 246 { 247 desc: "test flag not equal to 0 nor 1, ext debug flag true, debug info expected", 248 in: inTest{test: -1, debug: true}, 249 out: outTest{debugInfoIncluded: true}, 250 debugData: debugData{true, true, false}, 251 generateWarnings: true, 252 }, 253 { 254 desc: "test account level debug disabled", 255 in: inTest{test: -1, debug: true}, 256 out: outTest{debugInfoIncluded: false}, 257 debugData: debugData{true, false, false}, 258 generateWarnings: true, 259 }, 260 { 261 desc: "test header override enabled when all other debug options are disabled", 262 in: inTest{test: -1, debug: false}, 263 out: outTest{debugInfoIncluded: true}, 264 debugData: debugData{false, false, true}, 265 generateWarnings: false, 266 }, 267 { 268 desc: "test header override and url debug options are enabled when all other debug options are disabled", 269 in: inTest{test: -1, debug: true}, 270 out: outTest{debugInfoIncluded: true}, 271 debugData: debugData{false, false, true}, 272 generateWarnings: false, 273 }, 274 { 275 desc: "test header override and url and bidder debug options are enabled when account debug option is disabled", 276 in: inTest{test: -1, debug: true}, 277 out: outTest{debugInfoIncluded: true}, 278 debugData: debugData{true, false, true}, 279 generateWarnings: false, 280 }, 281 { 282 desc: "test all debug options are enabled", 283 in: inTest{test: -1, debug: true}, 284 out: outTest{debugInfoIncluded: true}, 285 debugData: debugData{true, true, true}, 286 generateWarnings: false, 287 }, 288 } 289 290 // Set up test 291 noBidServer := func(w http.ResponseWriter, r *http.Request) { 292 w.WriteHeader(204) 293 } 294 server := httptest.NewServer(http.HandlerFunc(noBidServer)) 295 defer server.Close() 296 297 categoriesFetcher, err := newCategoryFetcher("./test/category-mapping") 298 if err != nil { 299 t.Errorf("Failed to create a category Fetcher: %v", err) 300 } 301 302 bidRequest := &openrtb2.BidRequest{ 303 ID: "some-request-id", 304 Imp: []openrtb2.Imp{{ 305 ID: "some-impression-id", 306 Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}, {W: 300, H: 600}}}, 307 Ext: json.RawMessage(`{"prebid":{"bidder":{"appnexus": {"placementId": 1}}}}`), 308 }}, 309 Site: &openrtb2.Site{Page: "prebid.org", Ext: json.RawMessage(`{"amp":0}`)}, 310 Device: &openrtb2.Device{UA: "curl/7.54.0", IP: "::1"}, 311 AT: 1, 312 TMax: 500, 313 } 314 315 bidderImpl := &goodSingleBidder{ 316 httpRequest: &adapters.RequestData{ 317 Method: "POST", 318 Uri: server.URL, 319 Body: []byte("{\"key\":\"val\"}"), 320 Headers: http.Header{}, 321 }, 322 bidResponse: &adapters.BidderResponse{}, 323 } 324 325 e := new(exchange) 326 327 e.cache = &wellBehavedCache{} 328 e.me = &metricsConf.NilMetricsEngine{} 329 e.gdprPermsBuilder = fakePermissionsBuilder{ 330 permissions: &permissionsMock{ 331 allowAllBidders: true, 332 }, 333 }.Builder 334 e.currencyConverter = currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) 335 e.categoriesFetcher = categoriesFetcher 336 e.requestSplitter = requestSplitter{ 337 me: &metricsConf.NilMetricsEngine{}, 338 gdprPermsBuilder: e.gdprPermsBuilder, 339 } 340 ctx := context.Background() 341 342 // Run tests 343 for _, test := range testCases { 344 345 e.adapterMap = map[openrtb_ext.BidderName]AdaptedBidder{ 346 openrtb_ext.BidderAppnexus: AdaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderAppnexus, &config.DebugInfo{Allow: test.debugData.bidderLevelDebugAllowed}, ""), 347 } 348 349 bidRequest.Test = test.in.test 350 351 if test.in.debug { 352 bidRequest.Ext = json.RawMessage(`{"prebid":{"debug":true}}`) 353 } else { 354 bidRequest.Ext = nil 355 } 356 357 auctionRequest := &AuctionRequest{ 358 BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: bidRequest}, 359 Account: config.Account{DebugAllow: test.debugData.accountLevelDebugAllowed}, 360 UserSyncs: &emptyUsersync{}, 361 StartTime: time.Now(), 362 HookExecutor: &hookexecution.EmptyHookExecutor{}, 363 TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), 364 } 365 if test.generateWarnings { 366 var errL []error 367 errL = append(errL, &errortypes.Warning{ 368 Message: fmt.Sprintf("CCPA consent test warning."), 369 WarningCode: errortypes.InvalidPrivacyConsentWarningCode}) 370 auctionRequest.Warnings = errL 371 } 372 debugLog := &DebugLog{} 373 if test.debugData.headerOverrideDebugAllowed { 374 debugLog = &DebugLog{DebugOverride: true, DebugEnabledOrOverridden: true} 375 } 376 // Run test 377 outBidResponse, err := e.HoldAuction(ctx, auctionRequest, debugLog) 378 379 // Assert no HoldAuction error 380 assert.NoErrorf(t, err, "%s. ex.HoldAuction returned an error: %v \n", test.desc, err) 381 assert.NotNilf(t, outBidResponse.Ext, "%s. outBidResponse.Ext should not be nil \n", test.desc) 382 assert.False(t, auctionRequest.BidderResponseStartTime.IsZero()) 383 actualExt := &openrtb_ext.ExtBidResponse{} 384 err = jsonutil.UnmarshalValid(outBidResponse.Ext, actualExt) 385 assert.NoErrorf(t, err, "%s. \"ext\" JSON field could not be unmarshaled. err: \"%v\" \n outBidResponse.Ext: \"%s\" \n", test.desc, err, outBidResponse.Ext) 386 387 assert.NotEmpty(t, actualExt.Prebid, "%s. ext.prebid should not be empty") 388 assert.NotEmpty(t, actualExt.Prebid.AuctionTimestamp, "%s. ext.prebid.auctiontimestamp should not be empty when AuctionRequest.StartTime is set") 389 assert.Equal(t, auctionRequest.StartTime.UnixNano()/1e+6, actualExt.Prebid.AuctionTimestamp, "%s. ext.prebid.auctiontimestamp has incorrect value") 390 391 if test.debugData.headerOverrideDebugAllowed { 392 assert.Empty(t, actualExt.Warnings, "warnings should be empty") 393 assert.Empty(t, actualExt.Errors, "errors should be empty") 394 } 395 396 if test.out.debugInfoIncluded { 397 assert.NotNilf(t, actualExt, "%s. ext.debug field is expected to be included in this outBidResponse.Ext and not be nil. outBidResponse.Ext.Debug = %v \n", test.desc, actualExt.Debug) 398 399 // Assert "Debug fields 400 assert.Greater(t, len(actualExt.Debug.HttpCalls), 0, "%s. ext.debug.httpcalls array should not be empty\n", test.desc) 401 assert.Equal(t, server.URL, actualExt.Debug.HttpCalls["appnexus"][0].Uri, "%s. ext.debug.httpcalls array should not be empty\n", test.desc) 402 assert.NotNilf(t, actualExt.Debug.ResolvedRequest, "%s. ext.debug.resolvedrequest field is expected to be included in this outBidResponse.Ext and not be nil. outBidResponse.Ext.Debug = %v \n", test.desc, actualExt.Debug) 403 404 // If not nil, assert bid extension 405 if test.in.debug { 406 actualResolvedReqExt, _, _, err := jsonparser.Get(actualExt.Debug.ResolvedRequest, "ext") 407 assert.NoError(t, err, "Resolved request should have the correct format") 408 assert.JSONEq(t, string(bidRequest.Ext), string(actualResolvedReqExt), test.desc) 409 } 410 } else if !test.debugData.bidderLevelDebugAllowed && test.debugData.accountLevelDebugAllowed { 411 assert.Equal(t, len(actualExt.Debug.HttpCalls), 0, "%s. ext.debug.httpcalls array should not be empty", "With bidder level debug disable option http calls should be empty") 412 413 } else { 414 assert.Nil(t, actualExt.Debug, "%s. ext.debug.httpcalls array should not be empty", "With bidder level debug disable option http calls should be empty") 415 } 416 417 if test.out.debugInfoIncluded && !test.debugData.accountLevelDebugAllowed && !test.debugData.headerOverrideDebugAllowed { 418 assert.Len(t, actualExt.Warnings, 1, "warnings should have one warning") 419 assert.NotNil(t, actualExt.Warnings["general"], "general warning should be present") 420 assert.Equal(t, "debug turned off for account", actualExt.Warnings["general"][0].Message, "account debug disabled message should be present") 421 } 422 423 if !test.out.debugInfoIncluded && test.in.debug && test.debugData.accountLevelDebugAllowed && !test.debugData.headerOverrideDebugAllowed { 424 if test.generateWarnings { 425 assert.Len(t, actualExt.Warnings, 2, "warnings should have one warning") 426 } else { 427 assert.Len(t, actualExt.Warnings, 1, "warnings should have one warning") 428 } 429 assert.NotNil(t, actualExt.Warnings["appnexus"], "bidder warning should be present") 430 assert.Equal(t, "debug turned off for bidder", actualExt.Warnings["appnexus"][0].Message, "account debug disabled message should be present") 431 } 432 433 if test.generateWarnings { 434 assert.NotNil(t, actualExt.Warnings["general"], "general warning should be present") 435 CCPAWarningPresent := false 436 for _, warn := range actualExt.Warnings["general"] { 437 if warn.Code == errortypes.InvalidPrivacyConsentWarningCode { 438 CCPAWarningPresent = true 439 break 440 } 441 } 442 assert.True(t, CCPAWarningPresent, "CCPA Warning should be present") 443 } 444 445 } 446 } 447 448 func TestTwoBiddersDebugDisabledAndEnabled(t *testing.T) { 449 450 type testCase struct { 451 bidder1DebugEnabled bool 452 bidder2DebugEnabled bool 453 } 454 455 testCases := []testCase{ 456 { 457 bidder1DebugEnabled: true, bidder2DebugEnabled: true, 458 }, 459 { 460 bidder1DebugEnabled: true, bidder2DebugEnabled: false, 461 }, 462 { 463 bidder1DebugEnabled: false, bidder2DebugEnabled: true, 464 }, 465 { 466 bidder1DebugEnabled: false, bidder2DebugEnabled: false, 467 }, 468 } 469 470 // Set up test 471 noBidServer := func(w http.ResponseWriter, r *http.Request) { 472 w.WriteHeader(204) 473 } 474 server := httptest.NewServer(http.HandlerFunc(noBidServer)) 475 defer server.Close() 476 477 categoriesFetcher, err := newCategoryFetcher("./test/category-mapping") 478 if err != nil { 479 t.Errorf("Failed to create a category Fetcher: %v", err) 480 } 481 482 bidderImpl := &goodSingleBidder{ 483 httpRequest: &adapters.RequestData{ 484 Method: "POST", 485 Uri: server.URL, 486 Body: []byte(`{"key":"val"}`), 487 Headers: http.Header{}, 488 }, 489 bidResponse: &adapters.BidderResponse{}, 490 } 491 492 e := new(exchange) 493 e.cache = &wellBehavedCache{} 494 e.me = &metricsConf.NilMetricsEngine{} 495 e.gdprPermsBuilder = fakePermissionsBuilder{ 496 permissions: &permissionsMock{ 497 allowAllBidders: true, 498 }, 499 }.Builder 500 e.currencyConverter = currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) 501 e.categoriesFetcher = categoriesFetcher 502 e.requestSplitter = requestSplitter{ 503 me: e.me, 504 gdprPermsBuilder: e.gdprPermsBuilder, 505 } 506 507 debugLog := DebugLog{Enabled: true} 508 509 for _, testCase := range testCases { 510 bidRequest := &openrtb2.BidRequest{ 511 ID: "some-request-id", 512 Imp: []openrtb2.Imp{{ 513 ID: "some-impression-id", 514 Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}, {W: 300, H: 600}}}, 515 Ext: json.RawMessage(`{"prebid":{"bidder":{"telaria": {"placementId": 1}, "appnexus": {"placementid": 2}}}}`), 516 }}, 517 Site: &openrtb2.Site{Page: "prebid.org", Ext: json.RawMessage(`{"amp":0}`)}, 518 Device: &openrtb2.Device{UA: "curl/7.54.0", IP: "::1"}, 519 AT: 1, 520 TMax: 500, 521 } 522 523 bidRequest.Ext = json.RawMessage(`{"prebid":{"debug":true}}`) 524 525 auctionRequest := &AuctionRequest{ 526 BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: bidRequest}, 527 Account: config.Account{DebugAllow: true}, 528 UserSyncs: &emptyUsersync{}, 529 StartTime: time.Now(), 530 HookExecutor: &hookexecution.EmptyHookExecutor{}, 531 TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), 532 } 533 534 e.adapterMap = map[openrtb_ext.BidderName]AdaptedBidder{ 535 openrtb_ext.BidderAppnexus: AdaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderAppnexus, &config.DebugInfo{Allow: testCase.bidder1DebugEnabled}, ""), 536 openrtb_ext.BidderTelaria: AdaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderAppnexus, &config.DebugInfo{Allow: testCase.bidder2DebugEnabled}, ""), 537 } 538 // Run test 539 outBidResponse, err := e.HoldAuction(context.Background(), auctionRequest, &debugLog) 540 // Assert no HoldAuction err 541 assert.NoErrorf(t, err, "ex.HoldAuction returned an err") 542 assert.NotNilf(t, outBidResponse.Ext, "outBidResponse.Ext should not be nil") 543 assert.False(t, auctionRequest.BidderResponseStartTime.IsZero()) 544 545 actualExt := &openrtb_ext.ExtBidResponse{} 546 err = jsonutil.UnmarshalValid(outBidResponse.Ext, actualExt) 547 assert.NoErrorf(t, err, "JSON field unmarshaling err. ") 548 549 assert.NotEmpty(t, actualExt.Prebid, "ext.prebid should not be empty") 550 assert.NotEmpty(t, actualExt.Prebid.AuctionTimestamp, "ext.prebid.auctiontimestamp should not be empty when AuctionRequest.StartTime is set") 551 assert.Equal(t, auctionRequest.StartTime.UnixNano()/1e+6, actualExt.Prebid.AuctionTimestamp, "ext.prebid.auctiontimestamp has incorrect value") 552 553 assert.NotNilf(t, actualExt, "ext.debug field is expected to be included in this outBidResponse.Ext and not be nil") 554 555 // Assert "Debug fields 556 if testCase.bidder1DebugEnabled { 557 assert.Equal(t, server.URL, actualExt.Debug.HttpCalls["appnexus"][0].Uri, "Url for bidder with debug enabled is incorrect") 558 assert.NotNilf(t, actualExt.Debug.HttpCalls["appnexus"][0].RequestBody, "ext.debug.resolvedrequest field is expected to be included in this outBidResponse.Ext and not be nil") 559 } 560 if testCase.bidder2DebugEnabled { 561 assert.Equal(t, server.URL, actualExt.Debug.HttpCalls["telaria"][0].Uri, "Url for bidder with debug enabled is incorrect") 562 assert.NotNilf(t, actualExt.Debug.HttpCalls["telaria"][0].RequestBody, "ext.debug.resolvedrequest field is expected to be included in this outBidResponse.Ext and not be nil") 563 } 564 if !testCase.bidder1DebugEnabled { 565 assert.Nil(t, actualExt.Debug.HttpCalls["appnexus"], "ext.debug.resolvedrequest field is expected to be included in this outBidResponse.Ext and not be nil") 566 } 567 if !testCase.bidder2DebugEnabled { 568 assert.Nil(t, actualExt.Debug.HttpCalls["telaria"], "ext.debug.resolvedrequest field is expected to be included in this outBidResponse.Ext and not be nil") 569 } 570 if testCase.bidder1DebugEnabled && testCase.bidder2DebugEnabled { 571 assert.Equal(t, 2, len(actualExt.Debug.HttpCalls), "With bidder level debug enable option for both bidders http calls should have 2 elements") 572 } 573 } 574 } 575 576 func TestOverrideWithCustomCurrency(t *testing.T) { 577 mockCurrencyClient := ¤cy.MockCurrencyRatesHttpClient{ 578 ResponseBody: `{"dataAsOf":"2018-09-12","conversions":{"USD":{"MXN":10.00}}}`, 579 } 580 mockCurrencyConverter := currency.NewRateConverter( 581 mockCurrencyClient, 582 "currency.fake.com", 583 24*time.Hour, 584 ) 585 586 type testIn struct { 587 customCurrencyRates json.RawMessage 588 bidRequestCurrency string 589 } 590 type testResults struct { 591 numBids int 592 bidRespPrice float64 593 bidRespCurrency string 594 } 595 596 testCases := []struct { 597 desc string 598 in testIn 599 expected testResults 600 }{ 601 { 602 desc: "Blank currency field in ext. bidRequest comes with a valid currency but conversion rate was not found in PBS. Return no bids", 603 in: testIn{ 604 customCurrencyRates: json.RawMessage(`{ "prebid": { "currency": {} } } `), 605 bidRequestCurrency: "GBP", 606 }, 607 expected: testResults{}, 608 }, 609 { 610 desc: "valid request.ext.prebid.currency, expect custom rates to override those of the currency rate server", 611 in: testIn{ 612 customCurrencyRates: json.RawMessage(`{ 613 "prebid": { 614 "currency": { 615 "rates": { 616 "USD": { 617 "MXN": 20.00, 618 "EUR": 10.95 619 } 620 } 621 } 622 } 623 }`), 624 bidRequestCurrency: "MXN", 625 }, 626 expected: testResults{ 627 numBids: 1, 628 bidRespPrice: 20.00, 629 bidRespCurrency: "MXN", 630 }, 631 }, 632 } 633 634 // Init mock currency conversion service 635 mockCurrencyConverter.Run() 636 637 // Init an exchange to run an auction from 638 noBidServer := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(204) } 639 mockAppnexusBidService := httptest.NewServer(http.HandlerFunc(noBidServer)) 640 defer mockAppnexusBidService.Close() 641 642 categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") 643 if error != nil { 644 t.Errorf("Failed to create a category Fetcher: %v", error) 645 } 646 647 oneDollarBidBidder := &goodSingleBidder{ 648 httpRequest: &adapters.RequestData{ 649 Method: "POST", 650 Uri: mockAppnexusBidService.URL, 651 Body: []byte("{\"key\":\"val\"}"), 652 Headers: http.Header{}, 653 }, 654 } 655 656 e := new(exchange) 657 e.cache = &wellBehavedCache{} 658 e.me = &metricsConf.NilMetricsEngine{} 659 e.gdprPermsBuilder = fakePermissionsBuilder{ 660 permissions: &permissionsMock{ 661 allowAllBidders: true, 662 }, 663 }.Builder 664 e.currencyConverter = mockCurrencyConverter 665 e.categoriesFetcher = categoriesFetcher 666 e.bidIDGenerator = &fakeBidIDGenerator{GenerateBidID: false, ReturnError: false} 667 e.requestSplitter = requestSplitter{ 668 me: e.me, 669 gdprPermsBuilder: e.gdprPermsBuilder, 670 } 671 672 // Define mock incoming bid requeset 673 mockBidRequest := &openrtb2.BidRequest{ 674 ID: "some-request-id", 675 Imp: []openrtb2.Imp{{ 676 ID: "some-impression-id", 677 Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}, {W: 300, H: 600}}}, 678 Ext: json.RawMessage(`{"prebid":{"bidder":{"appnexus":{"placementId":1}}}}`), 679 }}, 680 Site: &openrtb2.Site{Page: "prebid.org", Ext: json.RawMessage(`{"amp":0}`)}, 681 } 682 683 // Run tests 684 for _, test := range testCases { 685 686 oneDollarBidBidder.bidResponse = &adapters.BidderResponse{ 687 Bids: []*adapters.TypedBid{ 688 { 689 Bid: &openrtb2.Bid{Price: 1.00}, 690 }, 691 }, 692 Currency: "USD", 693 } 694 695 e.adapterMap = map[openrtb_ext.BidderName]AdaptedBidder{ 696 openrtb_ext.BidderAppnexus: AdaptBidder(oneDollarBidBidder, mockAppnexusBidService.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderAppnexus, nil, ""), 697 } 698 699 // Set custom rates in extension 700 mockBidRequest.Ext = test.in.customCurrencyRates 701 702 // Set bidRequest currency list 703 mockBidRequest.Cur = []string{test.in.bidRequestCurrency} 704 705 auctionRequest := &AuctionRequest{ 706 BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: mockBidRequest}, 707 Account: config.Account{}, 708 UserSyncs: &emptyUsersync{}, 709 HookExecutor: &hookexecution.EmptyHookExecutor{}, 710 TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), 711 } 712 713 // Run test 714 outBidResponse, err := e.HoldAuction(context.Background(), auctionRequest, &DebugLog{}) 715 716 // Assertions 717 assert.NoErrorf(t, err, "%s. HoldAuction error: %v \n", test.desc, err) 718 assert.False(t, auctionRequest.BidderResponseStartTime.IsZero()) 719 720 if test.expected.numBids > 0 { 721 // Assert out currency 722 assert.Equal(t, test.expected.bidRespCurrency, outBidResponse.Cur, "Bid response currency is wrong: %s \n", test.desc) 723 724 // Assert returned bid 725 if !assert.NotNil(t, outBidResponse, "outBidResponse is nil: %s \n", test.desc) { 726 return 727 } 728 if !assert.NotEmpty(t, outBidResponse.SeatBid, "outBidResponse.SeatBid is empty: %s", test.desc) { 729 return 730 } 731 if !assert.NotEmpty(t, outBidResponse.SeatBid[0].Bid, "outBidResponse.SeatBid[0].Bid is empty: %s", test.desc) { 732 return 733 } 734 735 // Assert returned bid price matches the currency conversion 736 assert.Equal(t, test.expected.bidRespPrice, outBidResponse.SeatBid[0].Bid[0].Price, "Bid response seatBid price is wrong: %s", test.desc) 737 } else { 738 assert.Len(t, outBidResponse.SeatBid, 0, "outBidResponse.SeatBid should be empty: %s", test.desc) 739 } 740 } 741 } 742 743 func TestAdapterCurrency(t *testing.T) { 744 mockCurrencyClient := ¤cy.MockCurrencyRatesHttpClient{ 745 ResponseBody: `{"dataAsOf":"2018-09-12","conversions":{"USD":{"MXN":10.00}}}`, 746 } 747 currencyConverter := currency.NewRateConverter( 748 mockCurrencyClient, 749 "currency.fake.com", 750 24*time.Hour, 751 ) 752 currencyConverter.Run() 753 754 // Initialize Mock Bidder 755 // - Response purposefully causes PBS-Core to stop processing the request, since this test is only 756 // interested in the call to MakeRequests and nothing after. 757 mockBidder := &mockBidder{} 758 mockBidder.On("MakeRequests", mock.Anything, mock.Anything).Return([]*adapters.RequestData(nil), []error(nil)) 759 760 // Initialize Real Exchange 761 e := exchange{ 762 cache: &wellBehavedCache{}, 763 me: &metricsConf.NilMetricsEngine{}, 764 gdprPermsBuilder: fakePermissionsBuilder{ 765 permissions: &permissionsMock{ 766 allowAllBidders: true, 767 }, 768 }.Builder, 769 currencyConverter: currencyConverter, 770 categoriesFetcher: nilCategoryFetcher{}, 771 bidIDGenerator: &fakeBidIDGenerator{GenerateBidID: false, ReturnError: false}, 772 adapterMap: map[openrtb_ext.BidderName]AdaptedBidder{ 773 openrtb_ext.BidderName("appnexus"): AdaptBidder(mockBidder, nil, &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderName("appnexus"), nil, ""), 774 }, 775 } 776 e.requestSplitter = requestSplitter{ 777 me: e.me, 778 gdprPermsBuilder: e.gdprPermsBuilder, 779 } 780 781 // Define Bid Request 782 request := &openrtb2.BidRequest{ 783 ID: "some-request-id", 784 Imp: []openrtb2.Imp{{ 785 ID: "some-impression-id", 786 Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}, {W: 300, H: 600}}}, 787 Ext: json.RawMessage(`{"prebid":{"bidder":{"appnexus":{"placementId":1}}}}`), 788 }}, 789 Site: &openrtb2.Site{ 790 Page: "prebid.org", 791 Ext: json.RawMessage(`{"amp":0}`), 792 }, 793 Cur: []string{"USD"}, 794 Ext: json.RawMessage(`{"prebid": {"currency": {"rates": {"USD": {"MXN": 20.00}}}}}`), 795 } 796 797 // Run Auction 798 auctionRequest := &AuctionRequest{ 799 BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: request}, 800 Account: config.Account{}, 801 UserSyncs: &emptyUsersync{}, 802 HookExecutor: &hookexecution.EmptyHookExecutor{}, 803 TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), 804 } 805 response, err := e.HoldAuction(context.Background(), auctionRequest, &DebugLog{}) 806 assert.NoError(t, err) 807 assert.Equal(t, "some-request-id", response.ID, "Response ID") 808 assert.Empty(t, response.SeatBid, "Response Bids") 809 assert.Contains(t, string(response.Ext), `"errors":{"appnexus":[{"code":5,"message":"The adapter failed to generate any bid requests, but also failed to generate an error explaining why"}]}`, "Response Ext") 810 811 // Test Currency Converter Properly Passed To Adapter 812 if assert.NotNil(t, mockBidder.lastExtraRequestInfo, "Currency Conversion Argument") { 813 converted, err := mockBidder.lastExtraRequestInfo.ConvertCurrency(2.0, "USD", "MXN") 814 assert.NoError(t, err, "Currency Conversion Error") 815 assert.Equal(t, 40.0, converted, "Currency Conversion Response") 816 } 817 } 818 819 type mockPriceFloorFetcher struct{} 820 821 func (mpf *mockPriceFloorFetcher) Fetch(configs config.AccountPriceFloors) (*openrtb_ext.PriceFloorRules, string) { 822 return nil, openrtb_ext.FetchNone 823 } 824 825 func (mpf *mockPriceFloorFetcher) Stop() {} 826 827 func TestFloorsSignalling(t *testing.T) { 828 mockCurrencyClient := ¤cy.MockCurrencyRatesHttpClient{ 829 ResponseBody: `{"dataAsOf":"2023-04-10","conversions":{"USD":{"MXN":10.00}}}`, 830 } 831 currencyConverter := currency.NewRateConverter( 832 mockCurrencyClient, 833 "currency.com", 834 24*time.Hour, 835 ) 836 currencyConverter.Run() 837 838 // Initialize Real Exchange 839 e := exchange{ 840 cache: &wellBehavedCache{}, 841 me: &metricsConf.NilMetricsEngine{}, 842 gdprPermsBuilder: fakePermissionsBuilder{ 843 permissions: &permissionsMock{ 844 allowAllBidders: true, 845 }, 846 }.Builder, 847 currencyConverter: currencyConverter, 848 categoriesFetcher: nilCategoryFetcher{}, 849 bidIDGenerator: &fakeBidIDGenerator{GenerateBidID: false, ReturnError: false}, 850 priceFloorEnabled: true, 851 priceFloorFetcher: &mockPriceFloorFetcher{}, 852 } 853 e.requestSplitter = requestSplitter{ 854 me: e.me, 855 gdprPermsBuilder: e.gdprPermsBuilder, 856 } 857 858 type testResults struct { 859 bidFloor float64 860 bidFloorCur string 861 err error 862 resolvedReq string 863 } 864 865 testCases := []struct { 866 desc string 867 req *openrtb_ext.RequestWrapper 868 floorsEnable bool 869 expected testResults 870 }{ 871 { 872 desc: "no update in imp.bidfloor, floors disabled in account config", 873 floorsEnable: false, 874 req: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{ 875 ID: "some-request-id", 876 Imp: []openrtb2.Imp{{ 877 ID: "some-impression-id", 878 BidFloor: 15, 879 BidFloorCur: "USD", 880 Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}}, 881 Ext: json.RawMessage(`{"prebid":{}}`), 882 }}, 883 Site: &openrtb2.Site{ 884 Page: "prebid.org", 885 Ext: json.RawMessage(`{"amp":0}`), 886 Domain: "www.website.com", 887 }, 888 Cur: []string{"USD"}, 889 Ext: json.RawMessage(`{"prebid":{"floors":{"floormin":1,"floormincur":"USD","data":{"currency":"USD","modelgroups":[{"modelversion":"model 1 from req","values":{"banner|300x250|www.website.com":11,"*|*|www.test.com":15,"*|*|*":20},"Default":50,"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"}}]},"enabled":true}}}`), 890 }}, 891 expected: testResults{ 892 bidFloor: 15.00, 893 bidFloorCur: "USD", 894 }, 895 }, 896 { 897 desc: "no update in imp.bidfloor due to no rule matched", 898 floorsEnable: true, 899 req: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{ 900 ID: "some-request-id", 901 Imp: []openrtb2.Imp{{ 902 ID: "some-impression-id", 903 BidFloor: 15, 904 BidFloorCur: "USD", 905 Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}}, 906 Ext: json.RawMessage(`{"prebid":{}}`), 907 }}, 908 Site: &openrtb2.Site{ 909 Page: "prebid.org", 910 Ext: json.RawMessage(`{"amp":0}`), 911 Domain: "www.website.com", 912 }, 913 Cur: []string{"USD"}, 914 Ext: json.RawMessage(`{"prebid":{"floors":{"floormin":1,"floormincur":"USD","data":{"currency":"USD","modelgroups":[{"modelversion":"model 1 from req","values":{"banner|300x250|www.website123.com":10,"*|*|www.test.com":15},"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"}}]},"enabled":true}}}`), 915 }}, 916 expected: testResults{ 917 bidFloor: 15.00, 918 bidFloorCur: "USD", 919 }, 920 }, 921 { 922 desc: "update imp.bidfloor with matched rule value", 923 floorsEnable: true, 924 req: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{ 925 ID: "some-request-id", 926 Imp: []openrtb2.Imp{{ 927 ID: "some-impression-id", 928 BidFloor: 15, 929 BidFloorCur: "USD", 930 Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}}, 931 Ext: json.RawMessage(`{"prebid":{}}`), 932 }}, 933 Site: &openrtb2.Site{ 934 Page: "prebid.org", 935 Ext: json.RawMessage(`{"amp":0}`), 936 Domain: "www.website.com", 937 }, 938 Cur: []string{"USD"}, 939 Ext: json.RawMessage(`{"prebid":{"floors":{"floormin":1,"floormincur":"USD","data":{"currency":"USD","modelgroups":[{"modelversion":"model 1 from req","values":{"banner|300x250|www.website.com":10,"*|*|www.test.com":15,"*|*|*":20},"Default":50,"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"}}]},"enabled":true}}}`), 940 }}, 941 expected: testResults{ 942 bidFloor: 10.00, 943 bidFloorCur: "USD", 944 }, 945 }, 946 { 947 desc: "update resolved request with floors details", 948 floorsEnable: true, 949 req: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{ 950 ID: "some-request-id", 951 Imp: []openrtb2.Imp{{ 952 ID: "some-impression-id", 953 BidFloor: 15, 954 BidFloorCur: "USD", 955 Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}}, 956 Ext: json.RawMessage(`{"prebid":{}}`), 957 }}, 958 Site: &openrtb2.Site{ 959 Page: "prebid.org", 960 Ext: json.RawMessage(`{"amp":0}`), 961 Domain: "www.website.com", 962 }, 963 Test: 1, 964 Cur: []string{"USD"}, 965 Ext: json.RawMessage(`{"prebid":{"floors":{"floormin":1,"floormincur":"USD","data":{"currency":"USD","modelgroups":[{"modelversion":"model 1 from req","values":{"banner|300x250|www.website.com":11,"*|*|www.test.com":15,"*|*|*":20},"Default":50,"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"}}]},"enabled":true}}}`), 966 }}, 967 expected: testResults{ 968 bidFloor: 11.00, 969 bidFloorCur: "USD", 970 resolvedReq: `{"id":"some-request-id","imp":[{"id":"some-impression-id","banner":{"format":[{"w":300,"h":250}]},"bidfloor":11,"bidfloorcur":"USD","ext":{"prebid":{"floors":{"floorrule":"banner|300x250|www.website.com","floorrulevalue":11,"floorvalue":11}}}}],"site":{"domain":"www.website.com","page":"prebid.org","ext":{"amp":0}},"test":1,"cur":["USD"],"ext":{"prebid":{"floors":{"floormin":1,"floormincur":"USD","data":{"currency":"USD","modelgroups":[{"modelversion":"model 1 from req","schema":{"fields":["mediaType","size","domain"],"delimiter":"|"},"values":{"*|*|*":20,"*|*|www.test.com":15,"banner|300x250|www.website.com":11},"default":50}]},"enabled":true,"skipped":false,"fetchstatus":"none","location":"request"}}}}`, 971 }, 972 }, 973 } 974 975 for _, test := range testCases { 976 auctionRequest := &AuctionRequest{ 977 BidRequestWrapper: test.req, 978 Account: config.Account{DebugAllow: true, PriceFloors: config.AccountPriceFloors{Enabled: test.floorsEnable, MaxRule: 100, MaxSchemaDims: 5}}, 979 UserSyncs: &emptyUsersync{}, 980 HookExecutor: &hookexecution.EmptyHookExecutor{}, 981 TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), 982 } 983 outBidResponse, err := e.HoldAuction(context.Background(), auctionRequest, &DebugLog{}) 984 985 // Assertions 986 assert.Equal(t, test.expected.err, err, "Error") 987 assert.Equal(t, test.expected.bidFloor, auctionRequest.BidRequestWrapper.Imp[0].BidFloor, "Floor Value") 988 assert.Equal(t, test.expected.bidFloorCur, auctionRequest.BidRequestWrapper.Imp[0].BidFloorCur, "Floor Currency") 989 990 if test.req.Test == 1 { 991 actualResolvedRequest, _, _, _ := jsonparser.Get(outBidResponse.Ext, "debug", "resolvedrequest") 992 assert.JSONEq(t, test.expected.resolvedReq, string(actualResolvedRequest), "Resolved request is incorrect") 993 } 994 } 995 996 } 997 998 func TestReturnCreativeEndToEnd(t *testing.T) { 999 sampleAd := "<?xml version=\"1.0\" encoding=\"UTF-8\"?><VAST ...></VAST>" 1000 1001 // Define test cases 1002 type aTest struct { 1003 desc string 1004 inExt json.RawMessage 1005 outAdM string 1006 } 1007 testGroups := []struct { 1008 groupDesc string 1009 testCases []aTest 1010 expectError bool 1011 }{ 1012 { 1013 groupDesc: "Valid bidRequest Ext but no returnCreative value specified, default to returning creative", 1014 testCases: []aTest{ 1015 { 1016 "Nil ext in bidRequest", 1017 nil, 1018 sampleAd, 1019 }, 1020 { 1021 "empty ext", 1022 json.RawMessage(``), 1023 sampleAd, 1024 }, 1025 { 1026 "bids doesn't come with returnCreative value", 1027 json.RawMessage(`{"prebid":{"cache":{"bids":{}}}}`), 1028 sampleAd, 1029 }, 1030 { 1031 "vast doesn't come with returnCreative value", 1032 json.RawMessage(`{"prebid":{"cache":{"vastXml":{}}}}`), 1033 sampleAd, 1034 }, 1035 }, 1036 }, 1037 { 1038 groupDesc: "Bids field comes with returnCreative value", 1039 testCases: []aTest{ 1040 { 1041 "Bids returnCreative set to true, return ad markup in response", 1042 json.RawMessage(`{"prebid":{"cache":{"bids":{"returnCreative":true}}}}`), 1043 sampleAd, 1044 }, 1045 { 1046 "Bids returnCreative set to false, don't return ad markup in response", 1047 json.RawMessage(`{"prebid":{"cache":{"bids":{"returnCreative":false}}}}`), 1048 "", 1049 }, 1050 }, 1051 }, 1052 { 1053 groupDesc: "Vast field comes with returnCreative value", 1054 testCases: []aTest{ 1055 { 1056 "Vast returnCreative set to true, return ad markup in response", 1057 json.RawMessage(`{"prebid":{"cache":{"vastXml":{"returnCreative":true}}}}`), 1058 sampleAd, 1059 }, 1060 { 1061 "Vast returnCreative set to false, don't return ad markup in response", 1062 json.RawMessage(`{"prebid":{"cache":{"vastXml":{"returnCreative":false}}}}`), 1063 "", 1064 }, 1065 }, 1066 }, 1067 { 1068 groupDesc: "Both Bids and Vast come with their own returnCreative value", 1069 testCases: []aTest{ 1070 { 1071 "Both false, expect empty AdM", 1072 json.RawMessage(`{"prebid":{"cache":{"bids":{"returnCreative":false},"vastXml":{"returnCreative":false}}}}`), 1073 "", 1074 }, 1075 { 1076 "Bids returnCreative is true, expect valid AdM", 1077 json.RawMessage(`{"prebid":{"cache":{"bids":{"returnCreative":true},"vastXml":{"returnCreative":false}}}}`), 1078 sampleAd, 1079 }, 1080 { 1081 "Vast returnCreative is true, expect valid AdM", 1082 json.RawMessage(`{"prebid":{"cache":{"bids":{"returnCreative":false},"vastXml":{"returnCreative":true}}}}`), 1083 sampleAd, 1084 }, 1085 { 1086 "Both field's returnCreative set to true, expect valid AdM", 1087 json.RawMessage(`{"prebid":{"cache":{"bids":{"returnCreative":true},"vastXml":{"returnCreative":true}}}}`), 1088 sampleAd, 1089 }, 1090 }, 1091 }, 1092 } 1093 1094 // Init an exchange to run an auction from 1095 noBidServer := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(204) } 1096 server := httptest.NewServer(http.HandlerFunc(noBidServer)) 1097 defer server.Close() 1098 1099 categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") 1100 if error != nil { 1101 t.Errorf("Failed to create a category Fetcher: %v", error) 1102 } 1103 1104 bidderImpl := &goodSingleBidder{ 1105 httpRequest: &adapters.RequestData{ 1106 Method: "POST", 1107 Uri: server.URL, 1108 Body: []byte("{\"key\":\"val\"}"), 1109 Headers: http.Header{}, 1110 }, 1111 bidResponse: &adapters.BidderResponse{ 1112 Bids: []*adapters.TypedBid{ 1113 { 1114 Bid: &openrtb2.Bid{AdM: sampleAd}, 1115 }, 1116 }, 1117 }, 1118 } 1119 1120 e := new(exchange) 1121 e.adapterMap = map[openrtb_ext.BidderName]AdaptedBidder{ 1122 openrtb_ext.BidderAppnexus: AdaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderAppnexus, nil, ""), 1123 } 1124 e.cache = &wellBehavedCache{} 1125 e.me = &metricsConf.NilMetricsEngine{} 1126 e.gdprPermsBuilder = fakePermissionsBuilder{ 1127 permissions: &permissionsMock{ 1128 allowAllBidders: true, 1129 }, 1130 }.Builder 1131 e.currencyConverter = currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) 1132 e.categoriesFetcher = categoriesFetcher 1133 e.bidIDGenerator = &fakeBidIDGenerator{GenerateBidID: false, ReturnError: false} 1134 e.requestSplitter = requestSplitter{ 1135 me: e.me, 1136 gdprPermsBuilder: e.gdprPermsBuilder, 1137 } 1138 1139 // Define mock incoming bid requeset 1140 mockBidRequest := &openrtb2.BidRequest{ 1141 ID: "some-request-id", 1142 Imp: []openrtb2.Imp{{ 1143 ID: "some-impression-id", 1144 Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}, {W: 300, H: 600}}}, 1145 Ext: json.RawMessage(`{"prebid":{"bidder":{"appnexus":{"placementId":1}}}}`), 1146 }}, 1147 Site: &openrtb2.Site{Page: "prebid.org", Ext: json.RawMessage(`{"amp":0}`)}, 1148 } 1149 1150 // Run tests 1151 for _, testGroup := range testGroups { 1152 for _, test := range testGroup.testCases { 1153 mockBidRequest.Ext = test.inExt 1154 1155 auctionRequest := &AuctionRequest{ 1156 BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: mockBidRequest}, 1157 Account: config.Account{}, 1158 UserSyncs: &emptyUsersync{}, 1159 HookExecutor: &hookexecution.EmptyHookExecutor{}, 1160 TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), 1161 } 1162 1163 // Run test 1164 debugLog := DebugLog{} 1165 outBidResponse, err := e.HoldAuction(context.Background(), auctionRequest, &debugLog) 1166 1167 // Assert return error, if any 1168 if testGroup.expectError { 1169 assert.Errorf(t, err, "HoldAuction expected to throw error for: %s - %s. \n", testGroup.groupDesc, test.desc) 1170 continue 1171 } else { 1172 assert.NoErrorf(t, err, "%s: %s. HoldAuction error: %v \n", testGroup.groupDesc, test.desc, err) 1173 assert.False(t, auctionRequest.BidderResponseStartTime.IsZero()) 1174 } 1175 1176 // Assert returned bid 1177 if !assert.NotNil(t, outBidResponse, "%s: %s. outBidResponse is nil \n", testGroup.groupDesc, test.desc) { 1178 return 1179 } 1180 if !assert.NotEmpty(t, outBidResponse.SeatBid, "%s: %s. outBidResponse.SeatBid is empty \n", testGroup.groupDesc, test.desc) { 1181 return 1182 } 1183 if !assert.NotEmpty(t, outBidResponse.SeatBid[0].Bid, "%s: %s. outBidResponse.SeatBid[0].Bid is empty \n", testGroup.groupDesc, test.desc) { 1184 return 1185 } 1186 assert.Equal(t, test.outAdM, outBidResponse.SeatBid[0].Bid[0].AdM, "Ad markup string doesn't match in: %s - %s \n", testGroup.groupDesc, test.desc) 1187 } 1188 } 1189 } 1190 1191 func TestGetBidCacheInfoEndToEnd(t *testing.T) { 1192 testUUID := "CACHE_UUID_1234" 1193 testExternalCacheScheme := "https" 1194 testExternalCacheHost := "www.externalprebidcache.net" 1195 testExternalCachePath := "endpoints/cache" 1196 1197 // 1) An adapter 1198 bidderName := openrtb_ext.BidderName("appnexus") 1199 1200 cfg := &config.Configuration{ 1201 CacheURL: config.Cache{ 1202 Host: "www.internalprebidcache.net", 1203 }, 1204 ExtCacheURL: config.ExternalCache{ 1205 Scheme: testExternalCacheScheme, 1206 Host: testExternalCacheHost, 1207 Path: testExternalCachePath, 1208 }, 1209 } 1210 1211 adapterList := make([]openrtb_ext.BidderName, 0, 2) 1212 syncerKeys := []string{} 1213 var moduleStageNames map[string][]string 1214 testEngine := metricsConf.NewMetricsEngine(cfg, adapterList, syncerKeys, moduleStageNames) 1215 // 2) Init new exchange with said configuration 1216 handlerNoBidServer := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(204) } 1217 server := httptest.NewServer(http.HandlerFunc(handlerNoBidServer)) 1218 defer server.Close() 1219 1220 biddersInfo, err := config.LoadBidderInfoFromDisk("../static/bidder-info") 1221 if err != nil { 1222 t.Fatal(err) 1223 } 1224 1225 adapters, adaptersErr := BuildAdapters(server.Client(), cfg, biddersInfo, &metricsConf.NilMetricsEngine{}) 1226 if adaptersErr != nil { 1227 t.Fatalf("Error intializing adapters: %v", adaptersErr) 1228 } 1229 currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) 1230 pbc := pbc.NewClient(&http.Client{}, &cfg.CacheURL, &cfg.ExtCacheURL, testEngine) 1231 1232 gdprPermsBuilder := fakePermissionsBuilder{ 1233 permissions: &permissionsMock{ 1234 allowAllBidders: true, 1235 }, 1236 }.Builder 1237 1238 e := NewExchange(adapters, pbc, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange) 1239 // 3) Build all the parameters e.buildBidResponse(ctx.Background(), liveA... ) needs 1240 liveAdapters := []openrtb_ext.BidderName{bidderName} 1241 1242 //adapterBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, 1243 bids := []*openrtb2.Bid{ 1244 { 1245 ID: "some-imp-id", 1246 ImpID: "", 1247 Price: 9.517803, 1248 NURL: "", 1249 BURL: "", 1250 LURL: "", 1251 AdM: "", 1252 AdID: "", 1253 ADomain: nil, 1254 Bundle: "", 1255 IURL: "", 1256 CID: "", 1257 CrID: "", 1258 Tactic: "", 1259 Cat: nil, 1260 Attr: nil, 1261 API: 0, 1262 Protocol: 0, 1263 QAGMediaRating: 0, 1264 Language: "", 1265 DealID: "", 1266 W: 300, 1267 H: 250, 1268 WRatio: 0, 1269 HRatio: 0, 1270 Exp: 0, 1271 Ext: nil, 1272 }, 1273 } 1274 auc := &auction{ 1275 cacheIds: map[*openrtb2.Bid]string{ 1276 bids[0]: testUUID, 1277 }, 1278 } 1279 aPbsOrtbBidArr := []*entities.PbsOrtbBid{ 1280 { 1281 Bid: bids[0], 1282 BidType: openrtb_ext.BidTypeBanner, 1283 BidTargets: map[string]string{ 1284 "pricegranularity": "med", 1285 "includewinners": "true", 1286 "includebidderkeys": "false", 1287 }, 1288 }, 1289 } 1290 adapterBids := map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ 1291 bidderName: { 1292 Bids: aPbsOrtbBidArr, 1293 Currency: "USD", 1294 }, 1295 } 1296 1297 //adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, 1298 adapterExtra := map[openrtb_ext.BidderName]*seatResponseExtra{ 1299 bidderName: { 1300 ResponseTimeMillis: 5, 1301 Errors: []openrtb_ext.ExtBidderMessage{ 1302 { 1303 Code: 999, 1304 Message: "Post ib.adnxs.com/openrtb2?query1&query2: unsupported protocol scheme \"\"", 1305 }, 1306 }, 1307 }, 1308 } 1309 bidRequest := &openrtb_ext.RequestWrapper{ 1310 BidRequest: &openrtb2.BidRequest{ 1311 ID: "some-request-id", 1312 TMax: 1000, 1313 Imp: []openrtb2.Imp{ 1314 { 1315 ID: "test-div", 1316 Secure: openrtb2.Int8Ptr(0), 1317 Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}}, 1318 Ext: json.RawMessage(` { 1319 "rubicon": { 1320 "accountId": 1001, 1321 "siteId": 113932, 1322 "zoneId": 535510 1323 }, 1324 "appnexus": { "placementId": 1 }, 1325 "pubmatic": { "publisherId": "156209", "adSlot": "pubmatic_test2@300x250" }, 1326 "pulsepoint": { "cf": "300X250", "cp": 512379, "ct": 486653 }, 1327 "conversant": { "site_id": "108060" }, 1328 "ix": { "siteId": "287415" } 1329 }`), 1330 }, 1331 }, 1332 Site: &openrtb2.Site{ 1333 Page: "http://rubitest.com/index.html", 1334 Publisher: &openrtb2.Publisher{ID: "1001"}, 1335 }, 1336 Test: 1, 1337 Ext: json.RawMessage(`{"prebid": { "cache": { "bids": {}, "vastxml": {} }, "targeting": { "pricegranularity": "med", "includewinners": true, "includebidderkeys": false } }}`), 1338 }, 1339 } 1340 1341 var errList []error 1342 1343 // 4) Build bid response 1344 bid_resp := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, adapterExtra, auc, nil, true, nil, "", errList, &nonBids{}) 1345 1346 expectedBidResponse := &openrtb2.BidResponse{ 1347 SeatBid: []openrtb2.SeatBid{ 1348 { 1349 Seat: string(bidderName), 1350 Bid: []openrtb2.Bid{ 1351 { 1352 Ext: json.RawMessage(`{ "prebid": { "cache": { "bids": { "cacheId": "` + testUUID + `", "url": "` + testExternalCacheScheme + `://` + testExternalCacheHost + `/` + testExternalCachePath + `?uuid=` + testUUID + `" }, "key": "", "url": "" }`), 1353 }, 1354 }, 1355 }, 1356 }, 1357 } 1358 // compare cache UUID 1359 expCacheUUID, err := jsonparser.GetString(expectedBidResponse.SeatBid[0].Bid[0].Ext, "prebid", "cache", "bids", "cacheId") 1360 assert.NoErrorf(t, err, "[TestGetBidCacheInfo] Error found while trying to json parse the cacheId field from expected build response. Message: %v \n", err) 1361 1362 cacheUUID, err := jsonparser.GetString(bid_resp.SeatBid[0].Bid[0].Ext, "prebid", "cache", "bids", "cacheId") 1363 assert.NoErrorf(t, err, "[TestGetBidCacheInfo] bid_resp.SeatBid[0].Bid[0].Ext = %s \n", bid_resp.SeatBid[0].Bid[0].Ext) 1364 1365 assert.Equal(t, expCacheUUID, cacheUUID, "[TestGetBidCacheInfo] cacheId field in ext should equal \"%s\" \n", expCacheUUID) 1366 1367 // compare cache URL 1368 expCacheURL, err := jsonparser.GetString(expectedBidResponse.SeatBid[0].Bid[0].Ext, "prebid", "cache", "bids", "url") 1369 assert.NoErrorf(t, err, "[TestGetBidCacheInfo] Error found while trying to json parse the url field from expected build response. Message: %v \n", err) 1370 1371 cacheURL, err := jsonparser.GetString(bid_resp.SeatBid[0].Bid[0].Ext, "prebid", "cache", "bids", "url") 1372 assert.NoErrorf(t, err, "[TestGetBidCacheInfo] Error found while trying to json parse the url field from actual build response. Message: %v \n", err) 1373 1374 assert.Equal(t, expCacheURL, cacheURL, "[TestGetBidCacheInfo] cacheId field in ext should equal \"%s\" \n", expCacheURL) 1375 } 1376 1377 func TestBidReturnsCreative(t *testing.T) { 1378 sampleAd := "<?xml version=\"1.0\" encoding=\"UTF-8\"?><VAST ...></VAST>" 1379 sampleOpenrtbBid := &openrtb2.Bid{ID: "some-bid-id", AdM: sampleAd} 1380 1381 // Define test cases 1382 testCases := []struct { 1383 description string 1384 inReturnCreative bool 1385 expectedCreativeMarkup string 1386 }{ 1387 { 1388 "returnCreative set to true, expect a full creative markup string in returned bid", 1389 true, 1390 sampleAd, 1391 }, 1392 { 1393 "returnCreative set to false, expect empty creative markup string in returned bid", 1394 false, 1395 "", 1396 }, 1397 } 1398 1399 // Test set up 1400 sampleBids := []*entities.PbsOrtbBid{ 1401 { 1402 Bid: sampleOpenrtbBid, 1403 BidType: openrtb_ext.BidTypeBanner, 1404 BidTargets: map[string]string{}, 1405 GeneratedBidID: "randomId", 1406 }, 1407 } 1408 sampleAuction := &auction{cacheIds: map[*openrtb2.Bid]string{sampleOpenrtbBid: "CACHE_UUID_1234"}} 1409 1410 noBidHandler := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(204) } 1411 server := httptest.NewServer(http.HandlerFunc(noBidHandler)) 1412 defer server.Close() 1413 1414 bidderImpl := &goodSingleBidder{ 1415 httpRequest: &adapters.RequestData{ 1416 Method: "POST", 1417 Uri: server.URL, 1418 Body: []byte("{\"key\":\"val\"}"), 1419 Headers: http.Header{}, 1420 }, 1421 bidResponse: &adapters.BidderResponse{}, 1422 } 1423 e := new(exchange) 1424 e.adapterMap = map[openrtb_ext.BidderName]AdaptedBidder{ 1425 openrtb_ext.BidderAppnexus: AdaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderAppnexus, nil, ""), 1426 } 1427 e.cache = &wellBehavedCache{} 1428 e.me = &metricsConf.NilMetricsEngine{} 1429 1430 e.currencyConverter = currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) 1431 1432 //Run tests 1433 for _, test := range testCases { 1434 resultingBids, resultingErrs := e.makeBid(sampleBids, sampleAuction, test.inReturnCreative, nil, &openrtb_ext.RequestWrapper{}, nil, "", "", &nonBids{}) 1435 1436 assert.Equal(t, 0, len(resultingErrs), "%s. Test should not return errors \n", test.description) 1437 assert.Equal(t, test.expectedCreativeMarkup, resultingBids[0].AdM, "%s. Ad markup string doesn't match expected \n", test.description) 1438 1439 var bidExt openrtb_ext.ExtBid 1440 jsonutil.UnmarshalValid(resultingBids[0].Ext, &bidExt) 1441 assert.Equal(t, 0, bidExt.Prebid.DealPriority, "%s. Test should have DealPriority set to 0", test.description) 1442 assert.Equal(t, false, bidExt.Prebid.DealTierSatisfied, "%s. Test should have DealTierSatisfied set to false", test.description) 1443 } 1444 } 1445 1446 func TestGetBidCacheInfo(t *testing.T) { 1447 bid := &openrtb2.Bid{ID: "42"} 1448 testCases := []struct { 1449 description string 1450 scheme string 1451 host string 1452 path string 1453 bid *entities.PbsOrtbBid 1454 auction *auction 1455 expectedFound bool 1456 expectedCacheID string 1457 expectedCacheURL string 1458 }{ 1459 { 1460 description: "JSON Cache ID", 1461 scheme: "https", 1462 host: "prebid.org", 1463 path: "cache", 1464 bid: &entities.PbsOrtbBid{Bid: bid}, 1465 auction: &auction{cacheIds: map[*openrtb2.Bid]string{bid: "anyID"}}, 1466 expectedFound: true, 1467 expectedCacheID: "anyID", 1468 expectedCacheURL: "https://prebid.org/cache?uuid=anyID", 1469 }, 1470 { 1471 description: "VAST Cache ID", 1472 scheme: "https", 1473 host: "prebid.org", 1474 path: "cache", 1475 bid: &entities.PbsOrtbBid{Bid: bid}, 1476 auction: &auction{vastCacheIds: map[*openrtb2.Bid]string{bid: "anyID"}}, 1477 expectedFound: true, 1478 expectedCacheID: "anyID", 1479 expectedCacheURL: "https://prebid.org/cache?uuid=anyID", 1480 }, 1481 { 1482 description: "Cache ID Not Found", 1483 scheme: "https", 1484 host: "prebid.org", 1485 path: "cache", 1486 bid: &entities.PbsOrtbBid{Bid: bid}, 1487 auction: &auction{}, 1488 expectedFound: false, 1489 expectedCacheID: "", 1490 expectedCacheURL: "", 1491 }, 1492 { 1493 description: "Scheme Not Provided", 1494 host: "prebid.org", 1495 path: "cache", 1496 bid: &entities.PbsOrtbBid{Bid: bid}, 1497 auction: &auction{cacheIds: map[*openrtb2.Bid]string{bid: "anyID"}}, 1498 expectedFound: true, 1499 expectedCacheID: "anyID", 1500 expectedCacheURL: "prebid.org/cache?uuid=anyID", 1501 }, 1502 { 1503 description: "Host And Path Not Provided - Without Scheme", 1504 bid: &entities.PbsOrtbBid{Bid: bid}, 1505 auction: &auction{cacheIds: map[*openrtb2.Bid]string{bid: "anyID"}}, 1506 expectedFound: true, 1507 expectedCacheID: "anyID", 1508 expectedCacheURL: "", 1509 }, 1510 { 1511 description: "Host And Path Not Provided - With Scheme", 1512 scheme: "https", 1513 bid: &entities.PbsOrtbBid{Bid: bid}, 1514 auction: &auction{cacheIds: map[*openrtb2.Bid]string{bid: "anyID"}}, 1515 expectedFound: true, 1516 expectedCacheID: "anyID", 1517 expectedCacheURL: "", 1518 }, 1519 { 1520 description: "Nil Bid", 1521 scheme: "https", 1522 host: "prebid.org", 1523 path: "cache", 1524 bid: nil, 1525 auction: &auction{cacheIds: map[*openrtb2.Bid]string{bid: "anyID"}}, 1526 expectedFound: false, 1527 expectedCacheID: "", 1528 expectedCacheURL: "", 1529 }, 1530 { 1531 description: "Nil Embedded Bid", 1532 scheme: "https", 1533 host: "prebid.org", 1534 path: "cache", 1535 bid: &entities.PbsOrtbBid{Bid: nil}, 1536 auction: &auction{cacheIds: map[*openrtb2.Bid]string{bid: "anyID"}}, 1537 expectedFound: false, 1538 expectedCacheID: "", 1539 expectedCacheURL: "", 1540 }, 1541 { 1542 description: "Nil Auction", 1543 scheme: "https", 1544 host: "prebid.org", 1545 path: "cache", 1546 bid: &entities.PbsOrtbBid{Bid: bid}, 1547 auction: nil, 1548 expectedFound: false, 1549 expectedCacheID: "", 1550 expectedCacheURL: "", 1551 }, 1552 } 1553 1554 for _, test := range testCases { 1555 exchange := &exchange{ 1556 cache: &mockCache{ 1557 scheme: test.scheme, 1558 host: test.host, 1559 path: test.path, 1560 }, 1561 } 1562 1563 cacheInfo, found := exchange.getBidCacheInfo(test.bid, test.auction) 1564 1565 assert.Equal(t, test.expectedFound, found, test.description+":found") 1566 assert.Equal(t, test.expectedCacheID, cacheInfo.CacheId, test.description+":id") 1567 assert.Equal(t, test.expectedCacheURL, cacheInfo.Url, test.description+":url") 1568 } 1569 } 1570 1571 func TestBidResponseCurrency(t *testing.T) { 1572 // Init objects 1573 cfg := &config.Configuration{} 1574 1575 handlerNoBidServer := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(204) } 1576 server := httptest.NewServer(http.HandlerFunc(handlerNoBidServer)) 1577 defer server.Close() 1578 1579 biddersInfo, err := config.LoadBidderInfoFromDisk("../static/bidder-info") 1580 if err != nil { 1581 t.Fatal(err) 1582 } 1583 1584 adapters, adaptersErr := BuildAdapters(server.Client(), cfg, biddersInfo, &metricsConf.NilMetricsEngine{}) 1585 if adaptersErr != nil { 1586 t.Fatalf("Error intializing adapters: %v", adaptersErr) 1587 } 1588 1589 currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) 1590 1591 gdprPermsBuilder := fakePermissionsBuilder{ 1592 permissions: &permissionsMock{ 1593 allowAllBidders: true, 1594 }, 1595 }.Builder 1596 1597 e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange) 1598 1599 liveAdapters := make([]openrtb_ext.BidderName, 1) 1600 liveAdapters[0] = "appnexus" 1601 1602 bidRequest := &openrtb_ext.RequestWrapper{ 1603 BidRequest: &openrtb2.BidRequest{ 1604 ID: "some-request-id", 1605 Imp: []openrtb2.Imp{{ 1606 ID: "some-impression-id", 1607 Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}, {W: 300, H: 600}}}, 1608 Ext: json.RawMessage(`{"appnexus": {"placementId": 10433394}}`), 1609 }}, 1610 Site: &openrtb2.Site{Page: "prebid.org", Ext: json.RawMessage(`{"amp":0}`)}, 1611 Device: &openrtb2.Device{UA: "curl/7.54.0", IP: "::1"}, 1612 AT: 1, 1613 TMax: 500, 1614 Ext: json.RawMessage(`{"id": "some-request-id","site": {"page": "prebid.org"},"imp": [{"id": "some-impression-id","banner": {"format": [{"w": 300,"h": 250},{"w": 300,"h": 600}]},"ext": {"appnexus": {"placementId": 10433394}}}],"tmax": 500}`), 1615 }, 1616 } 1617 1618 adapterExtra := map[openrtb_ext.BidderName]*seatResponseExtra{ 1619 "appnexus": {ResponseTimeMillis: 5}, 1620 } 1621 1622 var errList []error 1623 1624 sampleBid := &openrtb2.Bid{ 1625 ID: "some-imp-id", 1626 Price: 9.517803, 1627 W: 300, 1628 H: 250, 1629 Ext: nil, 1630 } 1631 aPbsOrtbBidArr := []*entities.PbsOrtbBid{{Bid: sampleBid, BidType: openrtb_ext.BidTypeBanner, OriginalBidCPM: 9.517803}} 1632 sampleSeatBid := []openrtb2.SeatBid{ 1633 { 1634 Seat: "appnexus", 1635 Bid: []openrtb2.Bid{ 1636 { 1637 ID: "some-imp-id", 1638 Price: 9.517803, 1639 W: 300, 1640 H: 250, 1641 Ext: json.RawMessage(`{"origbidcpm":9.517803,"prebid":{"meta":{},"type":"banner"}}`), 1642 }, 1643 }, 1644 }, 1645 } 1646 emptySeatBid := []openrtb2.SeatBid{} 1647 1648 // Test cases 1649 type aTest struct { 1650 description string 1651 adapterBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid 1652 expectedBidResponse *openrtb2.BidResponse 1653 } 1654 testCases := []aTest{ 1655 { 1656 description: "1) Adapter to bids map comes with a non-empty currency field and non-empty bid array", 1657 adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ 1658 openrtb_ext.BidderName("appnexus"): { 1659 Bids: aPbsOrtbBidArr, 1660 Currency: "USD", 1661 }, 1662 }, 1663 expectedBidResponse: &openrtb2.BidResponse{ 1664 ID: "some-request-id", 1665 SeatBid: sampleSeatBid, 1666 Cur: "USD", 1667 }, 1668 }, 1669 { 1670 description: "2) Adapter to bids map comes with a non-empty currency field but an empty bid array", 1671 adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ 1672 openrtb_ext.BidderName("appnexus"): { 1673 Bids: nil, 1674 Currency: "USD", 1675 }, 1676 }, 1677 expectedBidResponse: &openrtb2.BidResponse{ 1678 ID: "some-request-id", 1679 SeatBid: emptySeatBid, 1680 Cur: "", 1681 }, 1682 }, 1683 { 1684 description: "3) Adapter to bids map comes with an empty currency string and a non-empty bid array", 1685 adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ 1686 openrtb_ext.BidderName("appnexus"): { 1687 Bids: aPbsOrtbBidArr, 1688 Currency: "", 1689 }, 1690 }, 1691 expectedBidResponse: &openrtb2.BidResponse{ 1692 ID: "some-request-id", 1693 SeatBid: sampleSeatBid, 1694 Cur: "", 1695 }, 1696 }, 1697 { 1698 description: "4) Adapter to bids map comes with an empty currency string and an empty bid array", 1699 adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ 1700 openrtb_ext.BidderName("appnexus"): { 1701 Bids: nil, 1702 Currency: "", 1703 }, 1704 }, 1705 expectedBidResponse: &openrtb2.BidResponse{ 1706 ID: "some-request-id", 1707 SeatBid: emptySeatBid, 1708 Cur: "", 1709 }, 1710 }, 1711 } 1712 1713 bidResponseExt := &openrtb_ext.ExtBidResponse{ 1714 ResponseTimeMillis: map[openrtb_ext.BidderName]int{openrtb_ext.BidderName("appnexus"): 5}, 1715 RequestTimeoutMillis: 500, 1716 } 1717 // Run tests 1718 for i := range testCases { 1719 actualBidResp := e.buildBidResponse(context.Background(), liveAdapters, testCases[i].adapterBids, bidRequest, adapterExtra, nil, bidResponseExt, true, nil, "", errList, &nonBids{}) 1720 assert.Equalf(t, testCases[i].expectedBidResponse, actualBidResp, fmt.Sprintf("[TEST_FAILED] Objects must be equal for test: %s \n Expected: >>%s<< \n Actual: >>%s<< ", testCases[i].description, testCases[i].expectedBidResponse.Ext, actualBidResp.Ext)) 1721 } 1722 } 1723 1724 func TestBidResponseImpExtInfo(t *testing.T) { 1725 // Init objects 1726 cfg := &config.Configuration{} 1727 1728 gdprPermsBuilder := fakePermissionsBuilder{ 1729 permissions: &permissionsMock{ 1730 allowAllBidders: true, 1731 }, 1732 }.Builder 1733 1734 noBidHandler := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(204) } 1735 server := httptest.NewServer(http.HandlerFunc(noBidHandler)) 1736 defer server.Close() 1737 1738 biddersInfo := config.BidderInfos{"appnexus": config.BidderInfo{Endpoint: "http://ib.adnxs.com"}} 1739 1740 adapters, adaptersErr := BuildAdapters(server.Client(), cfg, biddersInfo, &metricsConf.NilMetricsEngine{}) 1741 if adaptersErr != nil { 1742 t.Fatalf("Error intializing adapters: %v", adaptersErr) 1743 } 1744 1745 e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, nil, gdprPermsBuilder, nil, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange) 1746 1747 liveAdapters := make([]openrtb_ext.BidderName, 1) 1748 liveAdapters[0] = "appnexus" 1749 1750 bidRequest := &openrtb_ext.RequestWrapper{ 1751 BidRequest: &openrtb2.BidRequest{ 1752 ID: "some-request-id", 1753 Imp: []openrtb2.Imp{{ 1754 ID: "some-impression-id", 1755 Video: &openrtb2.Video{}, 1756 Ext: json.RawMessage(`{"appnexus": {"placementId": 10433394}}`), 1757 }}, 1758 Ext: json.RawMessage(``), 1759 }, 1760 } 1761 1762 var errList []error 1763 1764 sampleBid := &openrtb2.Bid{ 1765 ID: "some-imp-id", 1766 ImpID: "some-impression-id", 1767 W: 300, 1768 H: 250, 1769 Ext: nil, 1770 } 1771 aPbsOrtbBidArr := []*entities.PbsOrtbBid{{Bid: sampleBid, BidType: openrtb_ext.BidTypeVideo, AdapterCode: openrtb_ext.BidderAppnexus}} 1772 1773 adapterBids := map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ 1774 openrtb_ext.BidderName("appnexus"): { 1775 Bids: aPbsOrtbBidArr, 1776 }, 1777 } 1778 1779 impExtInfo := make(map[string]ImpExtInfo, 1) 1780 impExtInfo["some-impression-id"] = ImpExtInfo{ 1781 true, 1782 []byte(`{"video":{"h":480,"mimes":["video/mp4"]}}`), 1783 json.RawMessage(`{"imp_passthrough_val":1}`)} 1784 1785 expectedBidResponseExt := `{"origbidcpm":0,"prebid":{"meta":{"adaptercode":"appnexus"},"type":"video","passthrough":{"imp_passthrough_val":1}},"storedrequestattributes":{"h":480,"mimes":["video/mp4"]}}` 1786 1787 actualBidResp := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, nil, nil, nil, true, impExtInfo, "", errList, &nonBids{}) 1788 1789 resBidExt := string(actualBidResp.SeatBid[0].Bid[0].Ext) 1790 assert.Equalf(t, expectedBidResponseExt, resBidExt, "Expected bid response extension is incorrect") 1791 1792 } 1793 1794 // TestRaceIntegration runs an integration test using all the sample params from 1795 // adapters/{bidder}/{bidder}test/params/race/*.json files. 1796 // 1797 // Its primary goal is to catch race conditions, since parts of the BidRequest passed into MakeBids() 1798 // are shared across many goroutines. 1799 // 1800 // The "known" file names right now are "banner.json" and "video.json". These files should hold params 1801 // which the Bidder would expect on banner or video Imps, respectively. 1802 func TestRaceIntegration(t *testing.T) { 1803 noBidServer := func(w http.ResponseWriter, r *http.Request) { 1804 w.WriteHeader(204) 1805 } 1806 server := httptest.NewServer(http.HandlerFunc(noBidServer)) 1807 defer server.Close() 1808 1809 cfg := &config.Configuration{} 1810 1811 biddersInfo, err := config.LoadBidderInfoFromDisk("../static/bidder-info") 1812 if err != nil { 1813 t.Fatal(err) 1814 } 1815 1816 adapters, adaptersErr := BuildAdapters(server.Client(), cfg, biddersInfo, &metricsConf.NilMetricsEngine{}) 1817 if adaptersErr != nil { 1818 t.Fatalf("Error intializing adapters: %v", adaptersErr) 1819 } 1820 1821 currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) 1822 1823 auctionRequest := &AuctionRequest{ 1824 BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: getTestBuildRequest(t)}, 1825 Account: config.Account{}, 1826 UserSyncs: &emptyUsersync{}, 1827 HookExecutor: &hookexecution.EmptyHookExecutor{}, 1828 TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), 1829 } 1830 1831 debugLog := DebugLog{} 1832 1833 gdprPermsBuilder := fakePermissionsBuilder{ 1834 permissions: &permissionsMock{ 1835 allowAllBidders: true, 1836 }, 1837 }.Builder 1838 1839 ex := NewExchange(adapters, &wellBehavedCache{}, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, &nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange) 1840 _, err = ex.HoldAuction(context.Background(), auctionRequest, &debugLog) 1841 if err != nil { 1842 t.Errorf("HoldAuction returned unexpected error: %v", err) 1843 } 1844 } 1845 1846 func newCategoryFetcher(directory string) (stored_requests.CategoryFetcher, error) { 1847 fetcher, err := file_fetcher.NewFileFetcher(directory) 1848 if err != nil { 1849 return nil, err 1850 } 1851 catfetcher, ok := fetcher.(stored_requests.CategoryFetcher) 1852 if !ok { 1853 return nil, fmt.Errorf("Failed to type cast fetcher to CategoryFetcher") 1854 } 1855 return catfetcher, nil 1856 } 1857 1858 func getTestBuildRequest(t *testing.T) *openrtb2.BidRequest { 1859 dnt := int8(1) 1860 return &openrtb2.BidRequest{ 1861 Site: &openrtb2.Site{ 1862 Page: "www.some.domain.com", 1863 Domain: "domain.com", 1864 Publisher: &openrtb2.Publisher{ 1865 ID: "some-publisher-id", 1866 }, 1867 }, 1868 Device: &openrtb2.Device{ 1869 UA: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36", 1870 IFA: "ifa", 1871 IP: "132.173.230.74", 1872 DNT: &dnt, 1873 Language: "EN", 1874 }, 1875 Source: &openrtb2.Source{ 1876 TID: "61018dc9-fa61-4c41-b7dc-f90b9ae80e87", 1877 }, 1878 User: &openrtb2.User{ 1879 ID: "our-id", 1880 BuyerUID: "their-id", 1881 Ext: json.RawMessage(`{"consent":"BONciguONcjGKADACHENAOLS1rAHDAFAAEAASABQAMwAeACEAFw"}`), 1882 }, 1883 Regs: &openrtb2.Regs{ 1884 COPPA: 1, 1885 Ext: json.RawMessage(`{"gdpr":1}`), 1886 }, 1887 Imp: []openrtb2.Imp{{ 1888 ID: "some-imp-id", 1889 Banner: &openrtb2.Banner{ 1890 Format: []openrtb2.Format{{ 1891 W: 300, 1892 H: 250, 1893 }, { 1894 W: 300, 1895 H: 600, 1896 }}, 1897 }, 1898 Ext: json.RawMessage(`{"prebid":{"bidder":{"appnexus": {"placementId": 1}}}}`), 1899 }, { 1900 Video: &openrtb2.Video{ 1901 MIMEs: []string{"video/mp4"}, 1902 MinDuration: 1, 1903 MaxDuration: 300, 1904 W: ptrutil.ToPtr[int64](300), 1905 H: ptrutil.ToPtr[int64](600), 1906 }, 1907 Ext: json.RawMessage(`{"prebid":{"bidder":{"appnexus": {"placementId": 1}}}}`), 1908 }}, 1909 } 1910 } 1911 1912 func TestPanicRecovery(t *testing.T) { 1913 cfg := &config.Configuration{ 1914 CacheURL: config.Cache{ 1915 ExpectedTimeMillis: 20, 1916 }, 1917 } 1918 1919 biddersInfo, err := config.LoadBidderInfoFromDisk("../static/bidder-info") 1920 if err != nil { 1921 t.Fatal(err) 1922 } 1923 1924 adapters, adaptersErr := BuildAdapters(&http.Client{}, cfg, biddersInfo, &metricsConf.NilMetricsEngine{}) 1925 if adaptersErr != nil { 1926 t.Fatalf("Error intializing adapters: %v", adaptersErr) 1927 } 1928 1929 currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) 1930 1931 gdprPermsBuilder := fakePermissionsBuilder{ 1932 permissions: &permissionsMock{ 1933 allowAllBidders: true, 1934 }, 1935 }.Builder 1936 1937 e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange) 1938 1939 chBids := make(chan *bidResponseWrapper, 1) 1940 panicker := func(bidderRequest BidderRequest, conversions currency.Conversions) { 1941 panic("panic!") 1942 } 1943 1944 apnLabels := metrics.AdapterLabels{ 1945 Source: metrics.DemandWeb, 1946 RType: metrics.ReqTypeORTB2Web, 1947 Adapter: openrtb_ext.BidderAppnexus, 1948 PubID: "test1", 1949 CookieFlag: metrics.CookieFlagYes, 1950 AdapterBids: metrics.AdapterBidNone, 1951 } 1952 1953 bidderRequests := []BidderRequest{ 1954 { 1955 BidderName: "bidder1", 1956 BidderCoreName: "appnexus", 1957 BidderLabels: apnLabels, 1958 BidRequest: &openrtb2.BidRequest{ 1959 ID: "b-1", 1960 }, 1961 }, 1962 { 1963 BidderName: "bidder2", 1964 BidderCoreName: "bidder2", 1965 BidRequest: &openrtb2.BidRequest{ 1966 ID: "b-2", 1967 }, 1968 }, 1969 } 1970 1971 recovered := e.recoverSafely(bidderRequests, panicker, chBids) 1972 recovered(bidderRequests[0], nil) 1973 } 1974 1975 // TestPanicRecoveryHighLevel calls HoldAuction with a panicingAdapter{} 1976 func TestPanicRecoveryHighLevel(t *testing.T) { 1977 noBidServer := func(w http.ResponseWriter, r *http.Request) { 1978 w.WriteHeader(204) 1979 } 1980 server := httptest.NewServer(http.HandlerFunc(noBidServer)) 1981 defer server.Close() 1982 1983 cfg := &config.Configuration{} 1984 1985 biddersInfo, err := config.LoadBidderInfoFromDisk("../static/bidder-info") 1986 if err != nil { 1987 t.Fatal(err) 1988 } 1989 1990 adapters, adaptersErr := BuildAdapters(server.Client(), cfg, biddersInfo, &metricsConf.NilMetricsEngine{}) 1991 if adaptersErr != nil { 1992 t.Fatalf("Error intializing adapters: %v", adaptersErr) 1993 } 1994 1995 currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) 1996 1997 categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") 1998 if error != nil { 1999 t.Errorf("Failed to create a category Fetcher: %v", error) 2000 } 2001 2002 gdprPermsBuilder := fakePermissionsBuilder{ 2003 permissions: &permissionsMock{ 2004 allowAllBidders: true, 2005 }, 2006 }.Builder 2007 e := NewExchange(adapters, &mockCache{}, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, categoriesFetcher, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange) 2008 2009 e.adapterMap[openrtb_ext.BidderBeachfront] = panicingAdapter{} 2010 e.adapterMap[openrtb_ext.BidderAppnexus] = panicingAdapter{} 2011 2012 request := &openrtb2.BidRequest{ 2013 Site: &openrtb2.Site{ 2014 Page: "www.some.domain.com", 2015 Domain: "domain.com", 2016 Publisher: &openrtb2.Publisher{ 2017 ID: "some-publisher-id", 2018 }, 2019 }, 2020 User: &openrtb2.User{ 2021 ID: "our-id", 2022 BuyerUID: "their-id", 2023 Ext: json.RawMessage(`{"consent":"BONciguONcjGKADACHENAOLS1rAHDAFAAEAASABQAMwAeACEAFw"}`), 2024 }, 2025 Imp: []openrtb2.Imp{{ 2026 ID: "some-imp-id", 2027 Banner: &openrtb2.Banner{ 2028 Format: []openrtb2.Format{{ 2029 W: 300, 2030 H: 250, 2031 }, { 2032 W: 300, 2033 H: 600, 2034 }}, 2035 }, 2036 Ext: json.RawMessage(`{"ext_field": "value"}`), 2037 }}, 2038 } 2039 2040 auctionRequest := &AuctionRequest{ 2041 BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: request}, 2042 Account: config.Account{}, 2043 UserSyncs: &emptyUsersync{}, 2044 HookExecutor: &hookexecution.EmptyHookExecutor{}, 2045 TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), 2046 } 2047 debugLog := DebugLog{} 2048 _, err = e.HoldAuction(context.Background(), auctionRequest, &debugLog) 2049 if err != nil { 2050 t.Errorf("HoldAuction returned unexpected error: %v", err) 2051 } 2052 2053 } 2054 2055 func TestTimeoutComputation(t *testing.T) { 2056 cacheTimeMillis := 10 2057 ex := exchange{ 2058 cacheTime: time.Duration(cacheTimeMillis) * time.Millisecond, 2059 } 2060 deadline := time.Now() 2061 ctx, cancel := context.WithDeadline(context.Background(), deadline) 2062 defer cancel() 2063 2064 auctionCtx, cancel := ex.makeAuctionContext(ctx, true) 2065 defer cancel() 2066 2067 if finalDeadline, ok := auctionCtx.Deadline(); !ok || deadline.Add(-time.Duration(cacheTimeMillis)*time.Millisecond) != finalDeadline { 2068 t.Errorf("The auction should allocate cacheTime amount of time from the whole request timeout.") 2069 } 2070 } 2071 2072 // TestExchangeJSON executes tests for all the *.json files in exchangetest. 2073 func TestExchangeJSON(t *testing.T) { 2074 if specFiles, err := os.ReadDir("./exchangetest"); err == nil { 2075 for _, specFile := range specFiles { 2076 if !strings.HasSuffix(specFile.Name(), ".json") { 2077 continue 2078 } 2079 2080 fileName := "./exchangetest/" + specFile.Name() 2081 fileDisplayName := "exchange/exchangetest/" + specFile.Name() 2082 2083 t.Run(fileDisplayName, func(t *testing.T) { 2084 specData, err := loadFile(fileName) 2085 if assert.NoError(t, err, "Failed to load contents of file %s: %v", fileDisplayName, err) { 2086 assert.NotPanics(t, func() { runSpec(t, fileDisplayName, specData) }, fileDisplayName) 2087 } 2088 }) 2089 } 2090 } 2091 } 2092 2093 // LoadFile reads and parses a file as a test case. If something goes wrong, it returns an error. 2094 func loadFile(filename string) (*exchangeSpec, error) { 2095 specData, err := os.ReadFile(filename) 2096 if err != nil { 2097 return nil, fmt.Errorf("Failed to read file %s: %v", filename, err) 2098 } 2099 2100 var spec exchangeSpec 2101 if err := jsonutil.UnmarshalValid(specData, &spec); err != nil { 2102 return nil, fmt.Errorf("Failed to unmarshal JSON from file: %v", err) 2103 } 2104 2105 return &spec, nil 2106 } 2107 2108 func runSpec(t *testing.T, filename string, spec *exchangeSpec) { 2109 aliases, err := parseRequestAliases(spec.IncomingRequest.OrtbRequest) 2110 if err != nil { 2111 t.Fatalf("%s: Failed to parse aliases", filename) 2112 } 2113 2114 var s struct{} 2115 eeac := make(map[string]struct{}) 2116 for _, c := range []string{"FIN", "FRA", "GUF"} { 2117 eeac[c] = s 2118 } 2119 2120 var gdprDefaultValue string 2121 if spec.AssumeGDPRApplies { 2122 gdprDefaultValue = "1" 2123 } else { 2124 gdprDefaultValue = "0" 2125 } 2126 2127 privacyConfig := config.Privacy{ 2128 CCPA: config.CCPA{ 2129 Enforce: spec.EnforceCCPA, 2130 }, 2131 LMT: config.LMT{ 2132 Enforce: spec.EnforceLMT, 2133 }, 2134 GDPR: config.GDPR{ 2135 Enabled: spec.GDPREnabled, 2136 DefaultValue: gdprDefaultValue, 2137 EEACountriesMap: eeac, 2138 TCF2: config.TCF2{ 2139 Enabled: spec.GDPREnabled, 2140 }, 2141 }, 2142 } 2143 bidIdGenerator := &fakeBidIDGenerator{} 2144 if spec.BidIDGenerator != nil { 2145 bidIdGenerator = spec.BidIDGenerator 2146 } 2147 ex := newExchangeForTests(t, filename, spec.OutgoingRequests, aliases, privacyConfig, bidIdGenerator, spec.HostSChainFlag, spec.FloorsEnabled, spec.HostConfigBidValidation, spec.Server) 2148 biddersInAuction := findBiddersInAuction(t, filename, &spec.IncomingRequest.OrtbRequest) 2149 debugLog := &DebugLog{} 2150 if spec.DebugLog != nil { 2151 *debugLog = *spec.DebugLog 2152 debugLog.Regexp = regexp.MustCompile(`[<>]`) 2153 } 2154 2155 // Passthrough JSON Testing 2156 impExtInfoMap := make(map[string]ImpExtInfo) 2157 if spec.PassthroughFlag { 2158 impPassthrough, impID, err := getInfoFromImp(&openrtb_ext.RequestWrapper{BidRequest: &spec.IncomingRequest.OrtbRequest}) 2159 if err != nil { 2160 t.Errorf("%s: Exchange returned an unexpected error. Got %s", filename, err.Error()) 2161 } 2162 impExtInfoMap[impID] = ImpExtInfo{Passthrough: impPassthrough} 2163 } 2164 2165 // Imp Setting for Bid Validation 2166 if spec.HostConfigBidValidation.SecureMarkup == config.ValidationEnforce || spec.HostConfigBidValidation.SecureMarkup == config.ValidationWarn { 2167 _, impID, err := getInfoFromImp(&openrtb_ext.RequestWrapper{BidRequest: &spec.IncomingRequest.OrtbRequest}) 2168 if err != nil { 2169 t.Errorf("%s: Exchange returned an unexpected error. Got %s", filename, err.Error()) 2170 } 2171 impExtInfoMap[impID] = ImpExtInfo{} 2172 } 2173 2174 if spec.AccountPrivacy.DSA != nil && len(spec.AccountPrivacy.DSA.Default) > 0 { 2175 if err := jsonutil.Unmarshal([]byte(spec.AccountPrivacy.DSA.Default), &spec.AccountPrivacy.DSA.DefaultUnpacked); err != nil { 2176 t.Errorf("%s: Exchange returned an unexpected error. Got %s", filename, err.Error()) 2177 } 2178 } 2179 2180 activityControl := privacy.NewActivityControl(&spec.AccountPrivacy) 2181 2182 auctionRequest := &AuctionRequest{ 2183 BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: &spec.IncomingRequest.OrtbRequest}, 2184 Account: config.Account{ 2185 ID: "testaccount", 2186 Events: config.Events{ 2187 Enabled: spec.EventsEnabled, 2188 }, 2189 DebugAllow: true, 2190 PriceFloors: config.AccountPriceFloors{Enabled: spec.AccountFloorsEnabled, EnforceDealFloors: spec.AccountEnforceDealFloors}, 2191 Privacy: spec.AccountPrivacy, 2192 Validations: spec.AccountConfigBidValidation, 2193 }, 2194 UserSyncs: mockIdFetcher(spec.IncomingRequest.Usersyncs), 2195 ImpExtInfoMap: impExtInfoMap, 2196 HookExecutor: &hookexecution.EmptyHookExecutor{}, 2197 TCF2Config: gdpr.NewTCF2Config(privacyConfig.GDPR.TCF2, config.AccountGDPR{}), 2198 Activities: activityControl, 2199 } 2200 2201 if spec.MultiBid != nil { 2202 auctionRequest.Account.DefaultBidLimit = spec.MultiBid.AccountMaxBid 2203 2204 requestExt := &openrtb_ext.ExtRequest{} 2205 err := jsonutil.UnmarshalValid(spec.IncomingRequest.OrtbRequest.Ext, requestExt) 2206 assert.NoError(t, err, "invalid request ext") 2207 validatedMultiBids, errs := openrtb_ext.ValidateAndBuildExtMultiBid(&requestExt.Prebid) 2208 for _, err := range errs { // same as in validateRequestExt(). 2209 auctionRequest.Warnings = append(auctionRequest.Warnings, &errortypes.Warning{ 2210 WarningCode: errortypes.MultiBidWarningCode, 2211 Message: err.Error(), 2212 }) 2213 } 2214 2215 requestExt.Prebid.MultiBid = validatedMultiBids 2216 updateReqExt, err := jsonutil.Marshal(requestExt) 2217 assert.NoError(t, err, "invalid request ext") 2218 auctionRequest.BidRequestWrapper.Ext = updateReqExt 2219 } 2220 2221 if spec.StartTime > 0 { 2222 auctionRequest.StartTime = time.Unix(0, spec.StartTime*1e+6) 2223 } 2224 if spec.RequestType != nil { 2225 auctionRequest.RequestType = *spec.RequestType 2226 } 2227 ctx := context.Background() 2228 2229 aucResponse, err := ex.HoldAuction(ctx, auctionRequest, debugLog) 2230 var bid *openrtb2.BidResponse 2231 var bidExt *openrtb_ext.ExtBidResponse 2232 if aucResponse != nil { 2233 bid = aucResponse.BidResponse 2234 bidExt = aucResponse.ExtBidResponse 2235 } 2236 if len(spec.Response.Error) > 0 && spec.Response.Bids == nil { 2237 if err.Error() != spec.Response.Error { 2238 t.Errorf("%s: Exchange returned different errors. Expected %s, got %s", filename, spec.Response.Error, err.Error()) 2239 } 2240 return 2241 } 2242 responseTimes := extractResponseTimes(t, filename, bid) 2243 for _, bidderName := range biddersInAuction { 2244 if _, ok := responseTimes[bidderName]; !ok { 2245 t.Errorf("%s: Response JSON missing expected ext.responsetimemillis.%s", filename, bidderName) 2246 } 2247 } 2248 if spec.Response.Bids != nil { 2249 diffOrtbResponses(t, filename, spec.Response.Bids, bid) 2250 if err == nil { 2251 if spec.Response.Error != "" { 2252 t.Errorf("%s: Exchange did not return expected error: %s", filename, spec.Response.Error) 2253 } 2254 } else { 2255 if err.Error() != spec.Response.Error { 2256 t.Errorf("%s: Exchange returned different errors. Expected %s, got %s", filename, spec.Response.Error, err.Error()) 2257 } 2258 } 2259 } 2260 if spec.DebugLog != nil { 2261 if spec.DebugLog.Enabled { 2262 if len(debugLog.Data.Response) == 0 { 2263 t.Errorf("%s: DebugLog response was not modified when it should have been", filename) 2264 } 2265 } else { 2266 if len(debugLog.Data.Response) != 0 { 2267 t.Errorf("%s: DebugLog response was modified when it shouldn't have been", filename) 2268 } 2269 } 2270 } 2271 if spec.IncomingRequest.OrtbRequest.Test == 1 { 2272 //compare debug info 2273 assert.JSONEq(t, string(bid.Ext), string(spec.Response.Ext), "Debug info modified") 2274 } 2275 2276 if spec.PassthroughFlag || (spec.MultiBid != nil && spec.MultiBid.AssertMultiBidWarnings) { 2277 expectedPassthough := "" 2278 actualPassthrough := "" 2279 actualBidRespExt := &openrtb_ext.ExtBidResponse{} 2280 if bid.Ext != nil { 2281 if err := jsonutil.UnmarshalValid(bid.Ext, actualBidRespExt); err != nil { 2282 assert.NoError(t, err, fmt.Sprintf("Error when unmarshalling: %s", err)) 2283 } 2284 if actualBidRespExt.Prebid != nil { 2285 actualPassthrough = string(actualBidRespExt.Prebid.Passthrough) 2286 } 2287 } 2288 expectedBidRespExt := &openrtb_ext.ExtBidResponse{} 2289 if spec.Response.Ext != nil { 2290 if err := jsonutil.UnmarshalValid(spec.Response.Ext, expectedBidRespExt); err != nil { 2291 assert.NoError(t, err, fmt.Sprintf("Error when unmarshalling: %s", err)) 2292 } 2293 if expectedBidRespExt.Prebid != nil { 2294 expectedPassthough = string(expectedBidRespExt.Prebid.Passthrough) 2295 } 2296 } 2297 2298 if spec.MultiBid != nil && spec.MultiBid.AssertMultiBidWarnings { 2299 assert.Equal(t, expectedBidRespExt.Warnings, actualBidRespExt.Warnings, "Expected same multi-bid warnings") 2300 } 2301 2302 if spec.PassthroughFlag { 2303 // special handling since JSONEq fails if either parameters is an empty string instead of json 2304 if expectedPassthough == "" || actualPassthrough == "" { 2305 assert.Equal(t, expectedPassthough, actualPassthrough, "Expected bid response extension is incorrect") 2306 } else { 2307 assert.JSONEq(t, expectedPassthough, actualPassthrough, "Expected bid response extension is incorrect") 2308 } 2309 } 2310 2311 } 2312 2313 if spec.FledgeEnabled { 2314 assert.JSONEq(t, string(spec.Response.Ext), string(bid.Ext), "ext mismatch") 2315 } 2316 2317 expectedBidRespExt := &openrtb_ext.ExtBidResponse{} 2318 if spec.Response.Ext != nil { 2319 if err := jsonutil.UnmarshalValid(spec.Response.Ext, expectedBidRespExt); err != nil { 2320 assert.NoError(t, err, fmt.Sprintf("Error when unmarshalling: %s", err)) 2321 } 2322 } 2323 if spec.HostConfigBidValidation.BannerCreativeMaxSize == config.ValidationEnforce || spec.HostConfigBidValidation.SecureMarkup == config.ValidationEnforce { 2324 actualBidRespExt := &openrtb_ext.ExtBidResponse{} 2325 if bid.Ext != nil { 2326 if err := jsonutil.UnmarshalValid(bid.Ext, actualBidRespExt); err != nil { 2327 assert.NoError(t, err, fmt.Sprintf("Error when unmarshalling: %s", err)) 2328 } 2329 } 2330 assert.Equal(t, expectedBidRespExt.Errors, actualBidRespExt.Errors, "Expected errors from response ext do not match") 2331 } 2332 if expectedBidRespExt.Prebid != nil { 2333 assert.ElementsMatch(t, expectedBidRespExt.Prebid.SeatNonBid, bidExt.Prebid.SeatNonBid, "Expected seatNonBids from response ext do not match") 2334 } 2335 } 2336 2337 func findBiddersInAuction(t *testing.T, context string, req *openrtb2.BidRequest) []string { 2338 if splitImps, err := splitImps(req.Imp); err != nil { 2339 t.Errorf("%s: Failed to parse Bidders from request: %v", context, err) 2340 return nil 2341 } else { 2342 bidders := make([]string, 0, len(splitImps)) 2343 for bidderName := range splitImps { 2344 bidders = append(bidders, bidderName) 2345 } 2346 return bidders 2347 } 2348 } 2349 2350 // extractResponseTimes validates the format of bid.ext.responsetimemillis, and then removes it. 2351 // This is done because the response time will change from run to run, so it's impossible to hardcode a value 2352 // into the JSON. The best we can do is make sure that the property exists. 2353 func extractResponseTimes(t *testing.T, context string, bid *openrtb2.BidResponse) map[string]int { 2354 if data, dataType, _, err := jsonparser.Get(bid.Ext, "responsetimemillis"); err != nil || dataType != jsonparser.Object { 2355 t.Errorf("%s: Exchange did not return ext.responsetimemillis object: %v", context, err) 2356 return nil 2357 } else { 2358 responseTimes := make(map[string]int) 2359 if err := jsonutil.UnmarshalValid(data, &responseTimes); err != nil { 2360 t.Errorf("%s: Failed to unmarshal ext.responsetimemillis into map[string]int: %v", context, err) 2361 return nil 2362 } 2363 2364 // Delete the response times so that they don't appear in the JSON, because they can't be tested reliably anyway. 2365 // If there's no other ext, just delete it altogether. 2366 bid.Ext = jsonparser.Delete(bid.Ext, "responsetimemillis") 2367 if jsonpatch.Equal(bid.Ext, []byte("{}")) { 2368 bid.Ext = nil 2369 } 2370 return responseTimes 2371 } 2372 } 2373 2374 func newExchangeForTests(t *testing.T, filename string, expectations map[string]*bidderSpec, aliases map[string]string, privacyConfig config.Privacy, bidIDGenerator BidIDGenerator, hostSChainFlag, floorsFlag bool, hostBidValidation config.Validations, server exchangeServer) Exchange { 2375 bidderAdapters := make(map[openrtb_ext.BidderName]AdaptedBidder, len(expectations)) 2376 bidderInfos := make(config.BidderInfos, len(expectations)) 2377 for _, bidderName := range openrtb_ext.CoreBidderNames() { 2378 if spec, ok := expectations[string(bidderName)]; ok { 2379 bidderAdapters[bidderName] = &validatingBidder{ 2380 t: t, 2381 fileName: filename, 2382 bidderName: string(bidderName), 2383 expectations: map[string]*bidderRequest{string(bidderName): spec.ExpectedRequest}, 2384 mockResponses: map[string]bidderResponse{string(bidderName): spec.MockResponse}, 2385 } 2386 bidderInfos[string(bidderName)] = config.BidderInfo{ModifyingVastXmlAllowed: spec.ModifyingVastXmlAllowed} 2387 } 2388 } 2389 2390 for alias, coreBidder := range aliases { 2391 if spec, ok := expectations[alias]; ok { 2392 if bidder, ok := bidderAdapters[openrtb_ext.BidderName(coreBidder)]; ok { 2393 bidder.(*validatingBidder).expectations[alias] = spec.ExpectedRequest 2394 bidder.(*validatingBidder).mockResponses[alias] = spec.MockResponse 2395 } else { 2396 bidderAdapters[openrtb_ext.BidderName(coreBidder)] = &validatingBidder{ 2397 t: t, 2398 fileName: filename, 2399 bidderName: coreBidder, 2400 expectations: map[string]*bidderRequest{alias: spec.ExpectedRequest}, 2401 mockResponses: map[string]bidderResponse{alias: spec.MockResponse}, 2402 } 2403 } 2404 } 2405 } 2406 2407 categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") 2408 if error != nil { 2409 t.Fatalf("Failed to create a category Fetcher: %v", error) 2410 } 2411 2412 gdprPermsBuilder := fakePermissionsBuilder{ 2413 permissions: &permissionsMock{ 2414 allowAllBidders: true, 2415 }, 2416 }.Builder 2417 2418 bidderToSyncerKey := map[string]string{} 2419 for _, bidderName := range openrtb_ext.CoreBidderNames() { 2420 bidderToSyncerKey[string(bidderName)] = string(bidderName) 2421 } 2422 2423 gdprDefaultValue := gdpr.SignalYes 2424 if privacyConfig.GDPR.DefaultValue == "0" { 2425 gdprDefaultValue = gdpr.SignalNo 2426 } 2427 2428 var hostSChainNode *openrtb2.SupplyChainNode 2429 if hostSChainFlag { 2430 hostSChainNode = &openrtb2.SupplyChainNode{ 2431 ASI: "pbshostcompany.com", SID: "00001", RID: "BidRequest", HP: openrtb2.Int8Ptr(1), 2432 } 2433 } 2434 2435 metricsEngine := metricsConf.NewMetricsEngine(&config.Configuration{}, openrtb_ext.CoreBidderNames(), nil, nil) 2436 requestSplitter := requestSplitter{ 2437 bidderToSyncerKey: bidderToSyncerKey, 2438 me: metricsEngine, 2439 privacyConfig: privacyConfig, 2440 gdprPermsBuilder: gdprPermsBuilder, 2441 hostSChainNode: hostSChainNode, 2442 bidderInfo: bidderInfos, 2443 } 2444 2445 return &exchange{ 2446 adapterMap: bidderAdapters, 2447 me: metricsEngine, 2448 cache: &wellBehavedCache{}, 2449 cacheTime: 0, 2450 currencyConverter: currency.NewRateConverter(&http.Client{}, "", time.Duration(0)), 2451 gdprDefaultValue: gdprDefaultValue, 2452 gdprPermsBuilder: gdprPermsBuilder, 2453 privacyConfig: privacyConfig, 2454 categoriesFetcher: categoriesFetcher, 2455 bidderInfo: bidderInfos, 2456 bidderToSyncerKey: bidderToSyncerKey, 2457 externalURL: "http://localhost", 2458 bidIDGenerator: bidIDGenerator, 2459 hostSChainNode: hostSChainNode, 2460 server: config.Server{ExternalUrl: server.ExternalUrl, GvlID: server.GvlID, DataCenter: server.DataCenter}, 2461 bidValidationEnforcement: hostBidValidation, 2462 requestSplitter: requestSplitter, 2463 priceFloorEnabled: floorsFlag, 2464 priceFloorFetcher: &mockPriceFloorFetcher{}, 2465 } 2466 } 2467 2468 type fakeBidIDGenerator struct { 2469 GenerateBidID bool `json:"generateBidID"` 2470 ReturnError bool `json:"returnError"` 2471 bidCount map[string]int 2472 } 2473 2474 func (f *fakeBidIDGenerator) Enabled() bool { 2475 return f.GenerateBidID 2476 } 2477 2478 func (f *fakeBidIDGenerator) New(bidder string) (string, error) { 2479 if f.ReturnError { 2480 return "", errors.New("Test error generating bid.ext.prebid.bidid") 2481 } 2482 2483 if f.bidCount == nil { 2484 f.bidCount = make(map[string]int) 2485 } 2486 2487 f.bidCount[bidder] += 1 2488 return fmt.Sprintf("bid-%v-%v", bidder, f.bidCount[bidder]), nil 2489 } 2490 2491 type fakeBooleanGenerator struct { 2492 value bool 2493 } 2494 2495 func (f *fakeBooleanGenerator) Generate() bool { 2496 return f.value 2497 } 2498 2499 func newExtRequest() openrtb_ext.ExtRequest { 2500 priceGran := openrtb_ext.PriceGranularity{ 2501 Precision: ptrutil.ToPtr(2), 2502 Ranges: []openrtb_ext.GranularityRange{ 2503 { 2504 Min: 0.0, 2505 Max: 20.0, 2506 Increment: 2.0, 2507 }, 2508 }, 2509 } 2510 2511 translateCategories := true 2512 brandCat := openrtb_ext.ExtIncludeBrandCategory{PrimaryAdServer: 1, WithCategory: true, TranslateCategories: &translateCategories} 2513 2514 reqExt := openrtb_ext.ExtRequestTargeting{ 2515 PriceGranularity: &priceGran, 2516 IncludeWinners: ptrutil.ToPtr(true), 2517 IncludeBrandCategory: &brandCat, 2518 } 2519 2520 return openrtb_ext.ExtRequest{ 2521 Prebid: openrtb_ext.ExtRequestPrebid{ 2522 Targeting: &reqExt, 2523 }, 2524 } 2525 } 2526 2527 func newExtRequestNoBrandCat() openrtb_ext.ExtRequest { 2528 priceGran := openrtb_ext.PriceGranularity{ 2529 Precision: ptrutil.ToPtr(2), 2530 Ranges: []openrtb_ext.GranularityRange{ 2531 { 2532 Min: 0.0, 2533 Max: 20.0, 2534 Increment: 2.0, 2535 }, 2536 }, 2537 } 2538 2539 brandCat := openrtb_ext.ExtIncludeBrandCategory{WithCategory: false} 2540 2541 reqExt := openrtb_ext.ExtRequestTargeting{ 2542 PriceGranularity: &priceGran, 2543 IncludeWinners: ptrutil.ToPtr(true), 2544 IncludeBrandCategory: &brandCat, 2545 } 2546 2547 return openrtb_ext.ExtRequest{ 2548 Prebid: openrtb_ext.ExtRequestPrebid{ 2549 Targeting: &reqExt, 2550 }, 2551 } 2552 } 2553 2554 func TestCategoryMapping(t *testing.T) { 2555 2556 categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") 2557 if error != nil { 2558 t.Errorf("Failed to create a category Fetcher: %v", error) 2559 } 2560 2561 requestExt := newExtRequest() 2562 2563 targData := &targetData{ 2564 priceGranularity: *requestExt.Prebid.Targeting.PriceGranularity, 2565 includeWinners: true, 2566 } 2567 2568 requestExt.Prebid.Targeting.DurationRangeSec = []int{15, 30, 50} 2569 2570 adapterBids := make(map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid) 2571 2572 cats1 := []string{"IAB1-3"} 2573 cats2 := []string{"IAB1-4"} 2574 cats3 := []string{"IAB1-1000"} 2575 cats4 := []string{"IAB1-2000"} 2576 bid1 := openrtb2.Bid{ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: cats1, W: 1, H: 1} 2577 bid2 := openrtb2.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 20.0000, Cat: cats2, W: 1, H: 1} 2578 bid3 := openrtb2.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 30.0000, Cat: cats3, W: 1, H: 1} 2579 bid4 := openrtb2.Bid{ID: "bid_id4", ImpID: "imp_id4", Price: 40.0000, Cat: cats4, W: 1, H: 1} 2580 2581 bid1_1 := entities.PbsOrtbBid{Bid: &bid1, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} 2582 bid1_2 := entities.PbsOrtbBid{Bid: &bid2, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 20.0000, OriginalBidCur: "USD", TargetBidderCode: ""} 2583 bid1_3 := entities.PbsOrtbBid{Bid: &bid3, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30, PrimaryCategory: "AdapterOverride"}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 30.0000, OriginalBidCur: "USD", TargetBidderCode: ""} 2584 bid1_4 := entities.PbsOrtbBid{Bid: &bid4, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 40.0000, OriginalBidCur: "USD", TargetBidderCode: ""} 2585 2586 innerBids := []*entities.PbsOrtbBid{ 2587 &bid1_1, 2588 &bid1_2, 2589 &bid1_3, 2590 &bid1_4, 2591 } 2592 2593 seatBid := entities.PbsOrtbSeatBid{Bids: innerBids, Currency: "USD"} 2594 bidderName1 := openrtb_ext.BidderName("appnexus") 2595 2596 adapterBids[bidderName1] = &seatBid 2597 2598 bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &nonBids{}) 2599 2600 assert.Equal(t, nil, err, "Category mapping error should be empty") 2601 assert.Equal(t, 1, len(rejections), "There should be 1 bid rejection message") 2602 assert.Equal(t, "bid rejected [bid ID: bid_id4] reason: Category mapping file for primary ad server: 'freewheel', publisher: '' not found", rejections[0], "Rejection message did not match expected") 2603 assert.Equal(t, "10.00_Electronics_30s", bidCategory["bid_id1"], "Category mapping doesn't match") 2604 assert.Equal(t, "20.00_Sports_50s", bidCategory["bid_id2"], "Category mapping doesn't match") 2605 assert.Equal(t, "20.00_AdapterOverride_30s", bidCategory["bid_id3"], "Category mapping override from adapter didn't take") 2606 assert.Equal(t, 3, len(adapterBids[bidderName1].Bids), "Bidders number doesn't match") 2607 assert.Equal(t, 3, len(bidCategory), "Bidders category mapping doesn't match") 2608 } 2609 2610 func TestCategoryMappingNoIncludeBrandCategory(t *testing.T) { 2611 2612 categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") 2613 if error != nil { 2614 t.Errorf("Failed to create a category Fetcher: %v", error) 2615 } 2616 2617 requestExt := newExtRequestNoBrandCat() 2618 2619 targData := &targetData{ 2620 priceGranularity: *requestExt.Prebid.Targeting.PriceGranularity, 2621 includeWinners: true, 2622 } 2623 requestExt.Prebid.Targeting.DurationRangeSec = []int{15, 30, 40, 50} 2624 2625 adapterBids := make(map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid) 2626 2627 cats1 := []string{"IAB1-3"} 2628 cats2 := []string{"IAB1-4"} 2629 cats3 := []string{"IAB1-1000"} 2630 cats4 := []string{"IAB1-2000"} 2631 bid1 := openrtb2.Bid{ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: cats1, W: 1, H: 1} 2632 bid2 := openrtb2.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 20.0000, Cat: cats2, W: 1, H: 1} 2633 bid3 := openrtb2.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 30.0000, Cat: cats3, W: 1, H: 1} 2634 bid4 := openrtb2.Bid{ID: "bid_id4", ImpID: "imp_id4", Price: 40.0000, Cat: cats4, W: 1, H: 1} 2635 2636 bid1_1 := entities.PbsOrtbBid{Bid: &bid1, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} 2637 bid1_2 := entities.PbsOrtbBid{Bid: &bid2, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 20.0000, OriginalBidCur: "USD", TargetBidderCode: ""} 2638 bid1_3 := entities.PbsOrtbBid{Bid: &bid3, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30, PrimaryCategory: "AdapterOverride"}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 30.0000, OriginalBidCur: "USD", TargetBidderCode: ""} 2639 bid1_4 := entities.PbsOrtbBid{Bid: &bid4, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 50}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 40.0000, OriginalBidCur: "USD", TargetBidderCode: ""} 2640 2641 innerBids := []*entities.PbsOrtbBid{ 2642 &bid1_1, 2643 &bid1_2, 2644 &bid1_3, 2645 &bid1_4, 2646 } 2647 2648 seatBid := entities.PbsOrtbSeatBid{Bids: innerBids, Currency: "USD"} 2649 bidderName1 := openrtb_ext.BidderName("appnexus") 2650 2651 adapterBids[bidderName1] = &seatBid 2652 2653 bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &nonBids{}) 2654 2655 assert.Equal(t, nil, err, "Category mapping error should be empty") 2656 assert.Empty(t, rejections, "There should be no bid rejection messages") 2657 assert.Equal(t, "10.00_30s", bidCategory["bid_id1"], "Category mapping doesn't match") 2658 assert.Equal(t, "20.00_40s", bidCategory["bid_id2"], "Category mapping doesn't match") 2659 assert.Equal(t, "20.00_30s", bidCategory["bid_id3"], "Category mapping doesn't match") 2660 assert.Equal(t, "20.00_50s", bidCategory["bid_id4"], "Category mapping doesn't match") 2661 assert.Equal(t, 4, len(adapterBids[bidderName1].Bids), "Bidders number doesn't match") 2662 assert.Equal(t, 4, len(bidCategory), "Bidders category mapping doesn't match") 2663 } 2664 2665 func TestCategoryMappingTranslateCategoriesNil(t *testing.T) { 2666 2667 categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") 2668 if error != nil { 2669 t.Errorf("Failed to create a category Fetcher: %v", error) 2670 } 2671 2672 requestExt := newExtRequestTranslateCategories(nil) 2673 2674 targData := &targetData{ 2675 priceGranularity: *requestExt.Prebid.Targeting.PriceGranularity, 2676 includeWinners: true, 2677 } 2678 2679 requestExt.Prebid.Targeting.DurationRangeSec = []int{15, 30, 50} 2680 2681 adapterBids := make(map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid) 2682 2683 cats1 := []string{"IAB1-3"} 2684 cats2 := []string{"IAB1-4"} 2685 cats3 := []string{"IAB1-1000"} 2686 bid1 := openrtb2.Bid{ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: cats1, W: 1, H: 1} 2687 bid2 := openrtb2.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 20.0000, Cat: cats2, W: 1, H: 1} 2688 bid3 := openrtb2.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 30.0000, Cat: cats3, W: 1, H: 1} 2689 2690 bid1_1 := entities.PbsOrtbBid{Bid: &bid1, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} 2691 bid1_2 := entities.PbsOrtbBid{Bid: &bid2, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 20.0000, OriginalBidCur: "USD", TargetBidderCode: ""} 2692 bid1_3 := entities.PbsOrtbBid{Bid: &bid3, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 30.0000, OriginalBidCur: "USD", TargetBidderCode: ""} 2693 2694 innerBids := []*entities.PbsOrtbBid{ 2695 &bid1_1, 2696 &bid1_2, 2697 &bid1_3, 2698 } 2699 2700 seatBid := entities.PbsOrtbSeatBid{Bids: innerBids, Currency: "USD"} 2701 bidderName1 := openrtb_ext.BidderName("appnexus") 2702 2703 adapterBids[bidderName1] = &seatBid 2704 2705 bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &nonBids{}) 2706 2707 assert.Equal(t, nil, err, "Category mapping error should be empty") 2708 assert.Equal(t, 1, len(rejections), "There should be 1 bid rejection message") 2709 assert.Equal(t, "bid rejected [bid ID: bid_id3] reason: Category mapping file for primary ad server: 'freewheel', publisher: '' not found", rejections[0], "Rejection message did not match expected") 2710 assert.Equal(t, "10.00_Electronics_30s", bidCategory["bid_id1"], "Category mapping doesn't match") 2711 assert.Equal(t, "20.00_Sports_50s", bidCategory["bid_id2"], "Category mapping doesn't match") 2712 assert.Equal(t, 2, len(adapterBids[bidderName1].Bids), "Bidders number doesn't match") 2713 assert.Equal(t, 2, len(bidCategory), "Bidders category mapping doesn't match") 2714 } 2715 2716 func newExtRequestTranslateCategories(translateCategories *bool) openrtb_ext.ExtRequest { 2717 priceGran := openrtb_ext.PriceGranularity{ 2718 Precision: ptrutil.ToPtr(2), 2719 Ranges: []openrtb_ext.GranularityRange{ 2720 { 2721 Min: 0.0, 2722 Max: 20.0, 2723 Increment: 2.0, 2724 }, 2725 }, 2726 } 2727 2728 brandCat := openrtb_ext.ExtIncludeBrandCategory{WithCategory: true, PrimaryAdServer: 1} 2729 if translateCategories != nil { 2730 brandCat.TranslateCategories = translateCategories 2731 } 2732 2733 reqExt := openrtb_ext.ExtRequestTargeting{ 2734 PriceGranularity: &priceGran, 2735 IncludeWinners: ptrutil.ToPtr(true), 2736 IncludeBrandCategory: &brandCat, 2737 } 2738 2739 return openrtb_ext.ExtRequest{ 2740 Prebid: openrtb_ext.ExtRequestPrebid{ 2741 Targeting: &reqExt, 2742 }, 2743 } 2744 } 2745 2746 func TestCategoryMappingTranslateCategoriesFalse(t *testing.T) { 2747 2748 categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") 2749 if error != nil { 2750 t.Errorf("Failed to create a category Fetcher: %v", error) 2751 } 2752 2753 translateCategories := false 2754 requestExt := newExtRequestTranslateCategories(&translateCategories) 2755 2756 targData := &targetData{ 2757 priceGranularity: *requestExt.Prebid.Targeting.PriceGranularity, 2758 includeWinners: true, 2759 } 2760 2761 requestExt.Prebid.Targeting.DurationRangeSec = []int{15, 30, 50} 2762 2763 adapterBids := make(map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid) 2764 2765 cats1 := []string{"IAB1-3"} 2766 cats2 := []string{"IAB1-4"} 2767 cats3 := []string{"IAB1-1000"} 2768 bid1 := openrtb2.Bid{ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: cats1, W: 1, H: 1} 2769 bid2 := openrtb2.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 20.0000, Cat: cats2, W: 1, H: 1} 2770 bid3 := openrtb2.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 30.0000, Cat: cats3, W: 1, H: 1} 2771 2772 bid1_1 := entities.PbsOrtbBid{Bid: &bid1, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} 2773 bid1_2 := entities.PbsOrtbBid{Bid: &bid2, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 20.0000, OriginalBidCur: "USD", TargetBidderCode: ""} 2774 bid1_3 := entities.PbsOrtbBid{Bid: &bid3, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 30.0000, OriginalBidCur: "USD", TargetBidderCode: ""} 2775 2776 innerBids := []*entities.PbsOrtbBid{ 2777 &bid1_1, 2778 &bid1_2, 2779 &bid1_3, 2780 } 2781 2782 seatBid := entities.PbsOrtbSeatBid{Bids: innerBids, Currency: "USD"} 2783 bidderName1 := openrtb_ext.BidderName("appnexus") 2784 2785 adapterBids[bidderName1] = &seatBid 2786 2787 bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &nonBids{}) 2788 2789 assert.Equal(t, nil, err, "Category mapping error should be empty") 2790 assert.Empty(t, rejections, "There should be no bid rejection messages") 2791 assert.Equal(t, "10.00_IAB1-3_30s", bidCategory["bid_id1"], "Category should not be translated") 2792 assert.Equal(t, "20.00_IAB1-4_50s", bidCategory["bid_id2"], "Category should not be translated") 2793 assert.Equal(t, "20.00_IAB1-1000_30s", bidCategory["bid_id3"], "Bid should not be rejected") 2794 assert.Equal(t, 3, len(adapterBids[bidderName1].Bids), "Bidders number doesn't match") 2795 assert.Equal(t, 3, len(bidCategory), "Bidders category mapping doesn't match") 2796 } 2797 2798 func TestCategoryDedupe(t *testing.T) { 2799 categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") 2800 if error != nil { 2801 t.Errorf("Failed to create a category Fetcher: %v", error) 2802 } 2803 requestExt := newExtRequest() 2804 targData := &targetData{ 2805 priceGranularity: *requestExt.Prebid.Targeting.PriceGranularity, 2806 includeWinners: true, 2807 } 2808 2809 // bid3 and bid5 will be same price, category, and duration so one of them should be removed based on the dedupe generator 2810 bid1 := openrtb2.Bid{ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: []string{"IAB1-3"}, W: 1, H: 1} 2811 bid2 := openrtb2.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 15.0000, Cat: []string{"IAB1-4"}, W: 1, H: 1} 2812 bid3 := openrtb2.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 20.0000, Cat: []string{"IAB1-3"}, W: 1, H: 1} 2813 bid4 := openrtb2.Bid{ID: "bid_id4", ImpID: "imp_id4", Price: 20.0000, Cat: []string{"IAB1-INVALID"}, W: 1, H: 1} 2814 bid5 := openrtb2.Bid{ID: "bid_id5", ImpID: "imp_id5", Price: 20.0000, Cat: []string{"IAB1-3"}, W: 1, H: 1} 2815 2816 bid1_1 := entities.PbsOrtbBid{Bid: &bid1, BidType: "video", BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, OriginalBidCPM: 10.0000, OriginalBidCur: "USD"} 2817 bid1_2 := entities.PbsOrtbBid{Bid: &bid2, BidType: "video", BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 50}, OriginalBidCPM: 15.0000, OriginalBidCur: "USD"} 2818 bid1_3 := entities.PbsOrtbBid{Bid: &bid3, BidType: "video", BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, OriginalBidCPM: 20.0000, OriginalBidCur: "USD"} 2819 bid1_4 := entities.PbsOrtbBid{Bid: &bid4, BidType: "video", BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, OriginalBidCPM: 20.0000, OriginalBidCur: "USD"} 2820 bid1_5 := entities.PbsOrtbBid{Bid: &bid5, BidType: "video", BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, OriginalBidCPM: 20.0000, OriginalBidCur: "USD"} 2821 2822 bidderName1 := openrtb_ext.BidderName("appnexus") 2823 2824 tests := []struct { 2825 name string 2826 dedupeGeneratorValue bool 2827 expectedBids []*entities.PbsOrtbBid 2828 expectedCategories map[string]string 2829 }{ 2830 { 2831 name: "bid_id5_selected_over_bid_id3", 2832 dedupeGeneratorValue: true, 2833 expectedBids: []*entities.PbsOrtbBid{&bid1_2, &bid1_5}, 2834 expectedCategories: map[string]string{ 2835 "bid_id2": "14.00_Sports_50s", 2836 "bid_id5": "20.00_Electronics_30s", 2837 }, 2838 }, 2839 { 2840 name: "bid_id3_selected_over_bid_id5", 2841 dedupeGeneratorValue: false, 2842 expectedBids: []*entities.PbsOrtbBid{&bid1_2, &bid1_3}, 2843 expectedCategories: map[string]string{ 2844 "bid_id2": "14.00_Sports_50s", 2845 "bid_id3": "20.00_Electronics_30s", 2846 }, 2847 }, 2848 } 2849 2850 for _, tt := range tests { 2851 t.Run(tt.name, func(t *testing.T) { 2852 adapterBids := map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ 2853 bidderName1: { 2854 Bids: []*entities.PbsOrtbBid{ 2855 &bid1_1, 2856 &bid1_2, 2857 &bid1_3, 2858 &bid1_4, 2859 &bid1_5, 2860 }, 2861 Currency: "USD", 2862 }, 2863 } 2864 deduplicateGenerator := fakeBooleanGenerator{value: tt.dedupeGeneratorValue} 2865 bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &deduplicateGenerator, &nonBids{}) 2866 2867 assert.Nil(t, err) 2868 assert.Equal(t, 3, len(rejections)) 2869 assert.Equal(t, adapterBids[bidderName1].Bids, tt.expectedBids) 2870 assert.Equal(t, bidCategory, tt.expectedCategories) 2871 }) 2872 } 2873 } 2874 2875 func TestNoCategoryDedupe(t *testing.T) { 2876 2877 categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") 2878 if error != nil { 2879 t.Errorf("Failed to create a category Fetcher: %v", error) 2880 } 2881 2882 requestExt := newExtRequestNoBrandCat() 2883 2884 targData := &targetData{ 2885 priceGranularity: *requestExt.Prebid.Targeting.PriceGranularity, 2886 includeWinners: true, 2887 } 2888 2889 adapterBids := make(map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid) 2890 2891 cats1 := []string{"IAB1-3"} 2892 cats2 := []string{"IAB1-4"} 2893 cats4 := []string{"IAB1-2000"} 2894 bid1 := openrtb2.Bid{ID: "bid_id1", ImpID: "imp_id1", Price: 14.0000, Cat: cats1, W: 1, H: 1} 2895 bid2 := openrtb2.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 14.0000, Cat: cats2, W: 1, H: 1} 2896 bid3 := openrtb2.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 20.0000, Cat: cats1, W: 1, H: 1} 2897 bid4 := openrtb2.Bid{ID: "bid_id4", ImpID: "imp_id4", Price: 20.0000, Cat: cats4, W: 1, H: 1} 2898 bid5 := openrtb2.Bid{ID: "bid_id5", ImpID: "imp_id5", Price: 10.0000, Cat: cats1, W: 1, H: 1} 2899 2900 bid1_1 := entities.PbsOrtbBid{Bid: &bid1, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 14.0000, OriginalBidCur: "USD", TargetBidderCode: ""} 2901 bid1_2 := entities.PbsOrtbBid{Bid: &bid2, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 14.0000, OriginalBidCur: "USD", TargetBidderCode: ""} 2902 bid1_3 := entities.PbsOrtbBid{Bid: &bid3, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 20.0000, OriginalBidCur: "USD", TargetBidderCode: ""} 2903 bid1_4 := entities.PbsOrtbBid{Bid: &bid4, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 20.0000, OriginalBidCur: "USD", TargetBidderCode: ""} 2904 bid1_5 := entities.PbsOrtbBid{Bid: &bid5, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} 2905 2906 selectedBids := make(map[string]int) 2907 expectedCategories := map[string]string{ 2908 "bid_id1": "14.00_30s", 2909 "bid_id2": "14.00_30s", 2910 "bid_id3": "20.00_30s", 2911 "bid_id4": "20.00_30s", 2912 "bid_id5": "10.00_30s", 2913 } 2914 2915 numIterations := 10 2916 2917 // Run the function many times, this should be enough for the 50% chance of which bid to remove to remove bid1 sometimes 2918 // and bid3 others. It's conceivably possible (but highly unlikely) that the same bid get chosen every single time, but 2919 // if you notice false fails from this test increase numIterations to make it even less likely to happen. 2920 for i := 0; i < numIterations; i++ { 2921 innerBids := []*entities.PbsOrtbBid{ 2922 &bid1_1, 2923 &bid1_2, 2924 &bid1_3, 2925 &bid1_4, 2926 &bid1_5, 2927 } 2928 2929 seatBid := entities.PbsOrtbSeatBid{Bids: innerBids, Currency: "USD"} 2930 bidderName1 := openrtb_ext.BidderName("appnexus") 2931 2932 adapterBids[bidderName1] = &seatBid 2933 2934 bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &nonBids{}) 2935 2936 assert.Equal(t, nil, err, "Category mapping error should be empty") 2937 assert.Equal(t, 2, len(rejections), "There should be 2 bid rejection messages") 2938 assert.Regexpf(t, regexp.MustCompile(`bid rejected \[bid ID: bid_id(1|2)\] reason: Bid was deduplicated`), rejections[0], "Rejection message did not match expected") 2939 assert.Regexpf(t, regexp.MustCompile(`bid rejected \[bid ID: bid_id(3|4)\] reason: Bid was deduplicated`), rejections[1], "Rejection message did not match expected") 2940 assert.Equal(t, 3, len(adapterBids[bidderName1].Bids), "Bidders number doesn't match") 2941 assert.Equal(t, 3, len(bidCategory), "Bidders category mapping doesn't match") 2942 2943 for bidId, bidCat := range bidCategory { 2944 assert.Equal(t, expectedCategories[bidId], bidCat, "Category mapping doesn't match") 2945 selectedBids[bidId]++ 2946 } 2947 } 2948 assert.Equal(t, numIterations, selectedBids["bid_id5"], "Bid 5 did not make it through every time") 2949 assert.NotEqual(t, 0, selectedBids["bid_id1"], "Bid 1 should be selected at least once") 2950 assert.NotEqual(t, 0, selectedBids["bid_id2"], "Bid 2 should be selected at least once") 2951 assert.NotEqual(t, 0, selectedBids["bid_id1"], "Bid 3 should be selected at least once") 2952 assert.NotEqual(t, 0, selectedBids["bid_id4"], "Bid 4 should be selected at least once") 2953 2954 } 2955 2956 func TestCategoryMappingBidderName(t *testing.T) { 2957 2958 categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") 2959 if error != nil { 2960 t.Errorf("Failed to create a category Fetcher: %v", error) 2961 } 2962 2963 requestExt := newExtRequest() 2964 requestExt.Prebid.Targeting.AppendBidderNames = true 2965 2966 targData := &targetData{ 2967 priceGranularity: *requestExt.Prebid.Targeting.PriceGranularity, 2968 includeWinners: true, 2969 } 2970 2971 requestExt.Prebid.Targeting.DurationRangeSec = []int{15, 30} 2972 2973 adapterBids := make(map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid) 2974 2975 cats1 := []string{"IAB1-1"} 2976 cats2 := []string{"IAB1-2"} 2977 bid1 := openrtb2.Bid{ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: cats1, W: 1, H: 1} 2978 bid2 := openrtb2.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 10.0000, Cat: cats2, W: 1, H: 1} 2979 2980 bid1_1 := entities.PbsOrtbBid{Bid: &bid1, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} 2981 bid1_2 := entities.PbsOrtbBid{Bid: &bid2, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} 2982 2983 innerBids1 := []*entities.PbsOrtbBid{ 2984 &bid1_1, 2985 } 2986 innerBids2 := []*entities.PbsOrtbBid{ 2987 &bid1_2, 2988 } 2989 2990 seatBid1 := entities.PbsOrtbSeatBid{Bids: innerBids1, Currency: "USD"} 2991 bidderName1 := openrtb_ext.BidderName("bidder1") 2992 2993 seatBid2 := entities.PbsOrtbSeatBid{Bids: innerBids2, Currency: "USD"} 2994 bidderName2 := openrtb_ext.BidderName("bidder2") 2995 2996 adapterBids[bidderName1] = &seatBid1 2997 adapterBids[bidderName2] = &seatBid2 2998 2999 bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &nonBids{}) 3000 3001 assert.NoError(t, err, "Category mapping error should be empty") 3002 assert.Empty(t, rejections, "There should be 0 bid rejection messages") 3003 assert.Equal(t, "10.00_VideoGames_30s_bidder1", bidCategory["bid_id1"], "Category mapping doesn't match") 3004 assert.Equal(t, "10.00_HomeDecor_30s_bidder2", bidCategory["bid_id2"], "Category mapping doesn't match") 3005 assert.Len(t, adapterBids[bidderName1].Bids, 1, "Bidders number doesn't match") 3006 assert.Len(t, adapterBids[bidderName2].Bids, 1, "Bidders number doesn't match") 3007 assert.Len(t, bidCategory, 2, "Bidders category mapping doesn't match") 3008 } 3009 3010 func TestCategoryMappingBidderNameNoCategories(t *testing.T) { 3011 3012 categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") 3013 if error != nil { 3014 t.Errorf("Failed to create a category Fetcher: %v", error) 3015 } 3016 3017 requestExt := newExtRequestNoBrandCat() 3018 requestExt.Prebid.Targeting.AppendBidderNames = true 3019 3020 targData := &targetData{ 3021 priceGranularity: *requestExt.Prebid.Targeting.PriceGranularity, 3022 includeWinners: true, 3023 } 3024 3025 requestExt.Prebid.Targeting.DurationRangeSec = []int{30, 10, 25, 5, 20, 50} 3026 3027 adapterBids := make(map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid) 3028 3029 cats1 := []string{"IAB1-1"} 3030 cats2 := []string{"IAB1-2"} 3031 bid1 := openrtb2.Bid{ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: cats1, W: 1, H: 1} 3032 bid2 := openrtb2.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 12.0000, Cat: cats2, W: 1, H: 1} 3033 3034 bid1_1 := entities.PbsOrtbBid{Bid: &bid1, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 17}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} 3035 bid1_2 := entities.PbsOrtbBid{Bid: &bid2, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 8}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 12.0000, OriginalBidCur: "USD", TargetBidderCode: ""} 3036 3037 innerBids1 := []*entities.PbsOrtbBid{ 3038 &bid1_1, 3039 } 3040 innerBids2 := []*entities.PbsOrtbBid{ 3041 &bid1_2, 3042 } 3043 3044 seatBid1 := entities.PbsOrtbSeatBid{Bids: innerBids1, Currency: "USD"} 3045 bidderName1 := openrtb_ext.BidderName("bidder1") 3046 3047 seatBid2 := entities.PbsOrtbSeatBid{Bids: innerBids2, Currency: "USD"} 3048 bidderName2 := openrtb_ext.BidderName("bidder2") 3049 3050 adapterBids[bidderName1] = &seatBid1 3051 adapterBids[bidderName2] = &seatBid2 3052 3053 bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &nonBids{}) 3054 3055 assert.NoError(t, err, "Category mapping error should be empty") 3056 assert.Empty(t, rejections, "There should be 0 bid rejection messages") 3057 assert.Equal(t, "10.00_20s_bidder1", bidCategory["bid_id1"], "Category mapping doesn't match") 3058 assert.Equal(t, "12.00_10s_bidder2", bidCategory["bid_id2"], "Category mapping doesn't match") 3059 assert.Len(t, adapterBids[bidderName1].Bids, 1, "Bidders number doesn't match") 3060 assert.Len(t, adapterBids[bidderName2].Bids, 1, "Bidders number doesn't match") 3061 assert.Len(t, bidCategory, 2, "Bidders category mapping doesn't match") 3062 } 3063 3064 func TestBidRejectionErrors(t *testing.T) { 3065 categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") 3066 if error != nil { 3067 t.Errorf("Failed to create a category Fetcher: %v", error) 3068 } 3069 3070 requestExt := newExtRequest() 3071 requestExt.Prebid.Targeting.DurationRangeSec = []int{15, 30, 50} 3072 3073 targData := &targetData{ 3074 priceGranularity: *requestExt.Prebid.Targeting.PriceGranularity, 3075 includeWinners: true, 3076 } 3077 3078 invalidReqExt := newExtRequest() 3079 invalidReqExt.Prebid.Targeting.DurationRangeSec = []int{15, 30, 50} 3080 invalidReqExt.Prebid.Targeting.IncludeBrandCategory.PrimaryAdServer = 2 3081 invalidReqExt.Prebid.Targeting.IncludeBrandCategory.Publisher = "some_publisher" 3082 3083 adapterBids := make(map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid) 3084 bidderName := openrtb_ext.BidderName("appnexus") 3085 3086 testCases := []struct { 3087 description string 3088 reqExt openrtb_ext.ExtRequest 3089 bids []*openrtb2.Bid 3090 duration int 3091 expectedRejections []string 3092 expectedCatDur string 3093 }{ 3094 { 3095 description: "Bid should be rejected due to not containing a category", 3096 reqExt: requestExt, 3097 bids: []*openrtb2.Bid{ 3098 {ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: []string{}, W: 1, H: 1}, 3099 }, 3100 duration: 30, 3101 expectedRejections: []string{ 3102 "bid rejected [bid ID: bid_id1] reason: Bid did not contain a category", 3103 }, 3104 }, 3105 { 3106 description: "Bid should be rejected due to missing category mapping file", 3107 reqExt: invalidReqExt, 3108 bids: []*openrtb2.Bid{ 3109 {ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: []string{"IAB1-1"}, W: 1, H: 1}, 3110 }, 3111 duration: 30, 3112 expectedRejections: []string{ 3113 "bid rejected [bid ID: bid_id1] reason: Category mapping file for primary ad server: 'dfp', publisher: 'some_publisher' not found", 3114 }, 3115 }, 3116 { 3117 description: "Bid should be rejected due to duration exceeding maximum", 3118 reqExt: requestExt, 3119 bids: []*openrtb2.Bid{ 3120 {ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: []string{"IAB1-1"}, W: 1, H: 1}, 3121 }, 3122 duration: 70, 3123 expectedRejections: []string{ 3124 "bid rejected [bid ID: bid_id1] reason: bid duration exceeds maximum allowed", 3125 }, 3126 }, 3127 { 3128 description: "Bid should be rejected due to duplicate bid", 3129 reqExt: requestExt, 3130 bids: []*openrtb2.Bid{ 3131 {ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: []string{"IAB1-1"}, W: 1, H: 1}, 3132 {ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: []string{"IAB1-1"}, W: 1, H: 1}, 3133 }, 3134 duration: 30, 3135 expectedRejections: []string{ 3136 "bid rejected [bid ID: bid_id1] reason: Bid was deduplicated", 3137 }, 3138 expectedCatDur: "10.00_VideoGames_30s", 3139 }, 3140 } 3141 3142 for _, test := range testCases { 3143 innerBids := []*entities.PbsOrtbBid{} 3144 for _, bid := range test.bids { 3145 currentBid := entities.PbsOrtbBid{ 3146 Bid: bid, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: test.duration}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} 3147 innerBids = append(innerBids, ¤tBid) 3148 } 3149 3150 seatBid := entities.PbsOrtbSeatBid{Bids: innerBids, Currency: "USD"} 3151 3152 adapterBids[bidderName] = &seatBid 3153 3154 bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, *test.reqExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &nonBids{}) 3155 3156 if len(test.expectedCatDur) > 0 { 3157 // Bid deduplication case 3158 assert.Equal(t, 1, len(adapterBids[bidderName].Bids), "Bidders number doesn't match") 3159 assert.Equal(t, 1, len(bidCategory), "Bidders category mapping doesn't match") 3160 assert.Equal(t, test.expectedCatDur, bidCategory["bid_id1"], "Bid category did not contain expected hb_pb_cat_dur") 3161 } else { 3162 assert.Empty(t, adapterBids[bidderName].Bids, "Bidders number doesn't match") 3163 assert.Empty(t, bidCategory, "Bidders category mapping doesn't match") 3164 } 3165 3166 assert.Empty(t, err, "Category mapping error should be empty") 3167 assert.Equal(t, test.expectedRejections, rejections, test.description) 3168 } 3169 } 3170 3171 func TestCategoryMappingTwoBiddersOneBidEachNoCategorySamePrice(t *testing.T) { 3172 3173 categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") 3174 if error != nil { 3175 t.Errorf("Failed to create a category Fetcher: %v", error) 3176 } 3177 3178 requestExt := newExtRequestTranslateCategories(nil) 3179 3180 targData := &targetData{ 3181 priceGranularity: *requestExt.Prebid.Targeting.PriceGranularity, 3182 includeWinners: true, 3183 } 3184 3185 requestExt.Prebid.Targeting.DurationRangeSec = []int{30} 3186 requestExt.Prebid.Targeting.IncludeBrandCategory.WithCategory = false 3187 3188 cats1 := []string{"IAB1-3"} 3189 cats2 := []string{"IAB1-4"} 3190 3191 bidApn1 := openrtb2.Bid{ID: "bid_idApn1", ImpID: "imp_idApn1", Price: 10.0000, Cat: cats1, W: 1, H: 1} 3192 bidApn2 := openrtb2.Bid{ID: "bid_idApn2", ImpID: "imp_idApn2", Price: 10.0000, Cat: cats2, W: 1, H: 1} 3193 3194 bid1_Apn1 := entities.PbsOrtbBid{Bid: &bidApn1, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} 3195 bid1_Apn2 := entities.PbsOrtbBid{Bid: &bidApn2, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} 3196 3197 innerBidsApn1 := []*entities.PbsOrtbBid{ 3198 &bid1_Apn1, 3199 } 3200 3201 innerBidsApn2 := []*entities.PbsOrtbBid{ 3202 &bid1_Apn2, 3203 } 3204 3205 for i := 1; i < 10; i++ { 3206 adapterBids := make(map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid) 3207 3208 seatBidApn1 := entities.PbsOrtbSeatBid{Bids: innerBidsApn1, Currency: "USD"} 3209 bidderNameApn1 := openrtb_ext.BidderName("appnexus1") 3210 3211 seatBidApn2 := entities.PbsOrtbSeatBid{Bids: innerBidsApn2, Currency: "USD"} 3212 bidderNameApn2 := openrtb_ext.BidderName("appnexus2") 3213 3214 adapterBids[bidderNameApn1] = &seatBidApn1 3215 adapterBids[bidderNameApn2] = &seatBidApn2 3216 3217 bidCategory, _, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &nonBids{}) 3218 3219 assert.NoError(t, err, "Category mapping error should be empty") 3220 assert.Len(t, rejections, 1, "There should be 1 bid rejection message") 3221 assert.Regexpf(t, regexp.MustCompile(`bid rejected \[bid ID: bid_idApn(1|2)\] reason: Bid was deduplicated`), rejections[0], "Rejection message did not match expected") 3222 assert.Len(t, bidCategory, 1, "Bidders category mapping should have only one element") 3223 3224 var resultBid string 3225 for bidId := range bidCategory { 3226 resultBid = bidId 3227 } 3228 3229 if resultBid == "bid_idApn1" { 3230 assert.Nil(t, seatBidApn2.Bids, "Appnexus_2 seat bid should not have any bids back") 3231 assert.Len(t, seatBidApn1.Bids, 1, "Appnexus_1 seat bid should have only one back") 3232 3233 } else { 3234 assert.Nil(t, seatBidApn1.Bids, "Appnexus_1 seat bid should not have any bids back") 3235 assert.Len(t, seatBidApn2.Bids, 1, "Appnexus_2 seat bid should have only one back") 3236 } 3237 } 3238 } 3239 3240 func TestCategoryMappingTwoBiddersManyBidsEachNoCategorySamePrice(t *testing.T) { 3241 // This test covers a very rare de-duplication case where bid needs to be removed from already processed bidder 3242 // This happens when current processing bidder has a bid that has same de-duplication key as a bid from already processed bidder 3243 // and already processed bid was selected to be removed 3244 3245 //In this test case bids bid_idApn1_1 and bid_idApn1_2 will be removed due to hardcoded "fakeRandomDeduplicateBidBooleanGenerator{true}" 3246 3247 // Also there are should be more than one bids in bidder to test how we remove single element from bids array. 3248 // In case there is just one bid to remove - we remove the entire bidder. 3249 3250 categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") 3251 if error != nil { 3252 t.Errorf("Failed to create a category Fetcher: %v", error) 3253 } 3254 3255 requestExt := newExtRequestTranslateCategories(nil) 3256 3257 targData := &targetData{ 3258 priceGranularity: *requestExt.Prebid.Targeting.PriceGranularity, 3259 includeWinners: true, 3260 } 3261 3262 requestExt.Prebid.Targeting.DurationRangeSec = []int{30} 3263 requestExt.Prebid.Targeting.IncludeBrandCategory.WithCategory = false 3264 3265 cats1 := []string{"IAB1-3"} 3266 cats2 := []string{"IAB1-4"} 3267 3268 bidApn1_1 := openrtb2.Bid{ID: "bid_idApn1_1", ImpID: "imp_idApn1_1", Price: 10.0000, Cat: cats1, W: 1, H: 1} 3269 bidApn1_2 := openrtb2.Bid{ID: "bid_idApn1_2", ImpID: "imp_idApn1_2", Price: 20.0000, Cat: cats1, W: 1, H: 1} 3270 3271 bidApn2_1 := openrtb2.Bid{ID: "bid_idApn2_1", ImpID: "imp_idApn2_1", Price: 10.0000, Cat: cats2, W: 1, H: 1} 3272 bidApn2_2 := openrtb2.Bid{ID: "bid_idApn2_2", ImpID: "imp_idApn2_2", Price: 20.0000, Cat: cats2, W: 1, H: 1} 3273 3274 bid1_Apn1_1 := entities.PbsOrtbBid{Bid: &bidApn1_1, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} 3275 bid1_Apn1_2 := entities.PbsOrtbBid{Bid: &bidApn1_2, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 20.0000, OriginalBidCur: "USD", TargetBidderCode: ""} 3276 3277 bid1_Apn2_1 := entities.PbsOrtbBid{Bid: &bidApn2_1, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} 3278 bid1_Apn2_2 := entities.PbsOrtbBid{Bid: &bidApn2_2, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 20.0000, OriginalBidCur: "USD", TargetBidderCode: ""} 3279 3280 innerBidsApn1 := []*entities.PbsOrtbBid{ 3281 &bid1_Apn1_1, 3282 &bid1_Apn1_2, 3283 } 3284 3285 innerBidsApn2 := []*entities.PbsOrtbBid{ 3286 &bid1_Apn2_1, 3287 &bid1_Apn2_2, 3288 } 3289 3290 adapterBids := make(map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid) 3291 3292 seatBidApn1 := entities.PbsOrtbSeatBid{Bids: innerBidsApn1, Currency: "USD"} 3293 bidderNameApn1 := openrtb_ext.BidderName("appnexus1") 3294 3295 seatBidApn2 := entities.PbsOrtbSeatBid{Bids: innerBidsApn2, Currency: "USD"} 3296 bidderNameApn2 := openrtb_ext.BidderName("appnexus2") 3297 3298 adapterBids[bidderNameApn1] = &seatBidApn1 3299 adapterBids[bidderNameApn2] = &seatBidApn2 3300 3301 _, adapterBids, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &fakeBooleanGenerator{value: true}, &nonBids{}) 3302 3303 assert.NoError(t, err, "Category mapping error should be empty") 3304 3305 //Total number of bids from all bidders in this case should be 2 3306 bidsFromFirstBidder := adapterBids[bidderNameApn1] 3307 bidsFromSecondBidder := adapterBids[bidderNameApn2] 3308 3309 totalNumberOfbids := 0 3310 3311 //due to random map order we need to identify what bidder was first 3312 firstBidderIndicator := true 3313 3314 if bidsFromFirstBidder.Bids != nil { 3315 totalNumberOfbids += len(bidsFromFirstBidder.Bids) 3316 } 3317 3318 if bidsFromSecondBidder.Bids != nil { 3319 firstBidderIndicator = false 3320 totalNumberOfbids += len(bidsFromSecondBidder.Bids) 3321 } 3322 3323 assert.Equal(t, 2, totalNumberOfbids, "2 bids total should be returned") 3324 assert.Len(t, rejections, 2, "2 bids should be de-duplicated") 3325 3326 if firstBidderIndicator { 3327 assert.Len(t, adapterBids[bidderNameApn1].Bids, 2) 3328 assert.Len(t, adapterBids[bidderNameApn2].Bids, 0) 3329 3330 assert.Equal(t, "bid_idApn1_1", adapterBids[bidderNameApn1].Bids[0].Bid.ID, "Incorrect expected bid 1 id") 3331 assert.Equal(t, "bid_idApn1_2", adapterBids[bidderNameApn1].Bids[1].Bid.ID, "Incorrect expected bid 2 id") 3332 3333 assert.Equal(t, "bid rejected [bid ID: bid_idApn2_1] reason: Bid was deduplicated", rejections[0], "Incorrect rejected bid 1") 3334 assert.Equal(t, "bid rejected [bid ID: bid_idApn2_2] reason: Bid was deduplicated", rejections[1], "Incorrect rejected bid 2") 3335 3336 } else { 3337 assert.Len(t, adapterBids[bidderNameApn1].Bids, 0) 3338 assert.Len(t, adapterBids[bidderNameApn2].Bids, 2) 3339 3340 assert.Equal(t, "bid_idApn2_1", adapterBids[bidderNameApn2].Bids[0].Bid.ID, "Incorrect expected bid 1 id") 3341 assert.Equal(t, "bid_idApn2_2", adapterBids[bidderNameApn2].Bids[1].Bid.ID, "Incorrect expected bid 2 id") 3342 3343 assert.Equal(t, "bid rejected [bid ID: bid_idApn1_1] reason: Bid was deduplicated", rejections[0], "Incorrect rejected bid 1") 3344 assert.Equal(t, "bid rejected [bid ID: bid_idApn1_2] reason: Bid was deduplicated", rejections[1], "Incorrect rejected bid 2") 3345 3346 } 3347 } 3348 3349 func TestRemoveBidById(t *testing.T) { 3350 cats1 := []string{"IAB1-3"} 3351 3352 bidApn1_1 := openrtb2.Bid{ID: "bid_idApn1_1", ImpID: "imp_idApn1_1", Price: 10.0000, Cat: cats1, W: 1, H: 1} 3353 bidApn1_2 := openrtb2.Bid{ID: "bid_idApn1_2", ImpID: "imp_idApn1_2", Price: 20.0000, Cat: cats1, W: 1, H: 1} 3354 bidApn1_3 := openrtb2.Bid{ID: "bid_idApn1_3", ImpID: "imp_idApn1_3", Price: 10.0000, Cat: cats1, W: 1, H: 1} 3355 3356 bid1_Apn1_1 := entities.PbsOrtbBid{Bid: &bidApn1_1, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} 3357 bid1_Apn1_2 := entities.PbsOrtbBid{Bid: &bidApn1_2, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 20.0000, OriginalBidCur: "USD", TargetBidderCode: ""} 3358 bid1_Apn1_3 := entities.PbsOrtbBid{Bid: &bidApn1_3, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} 3359 3360 type aTest struct { 3361 desc string 3362 inBidName string 3363 outBids []*entities.PbsOrtbBid 3364 } 3365 testCases := []aTest{ 3366 { 3367 desc: "remove element from the middle", 3368 inBidName: "bid_idApn1_2", 3369 outBids: []*entities.PbsOrtbBid{&bid1_Apn1_1, &bid1_Apn1_3}, 3370 }, 3371 { 3372 desc: "remove element from the end", 3373 inBidName: "bid_idApn1_3", 3374 outBids: []*entities.PbsOrtbBid{&bid1_Apn1_1, &bid1_Apn1_2}, 3375 }, 3376 { 3377 desc: "remove element from the beginning", 3378 inBidName: "bid_idApn1_1", 3379 outBids: []*entities.PbsOrtbBid{&bid1_Apn1_2, &bid1_Apn1_3}, 3380 }, 3381 { 3382 desc: "remove element that doesn't exist", 3383 inBidName: "bid_idApn", 3384 outBids: []*entities.PbsOrtbBid{&bid1_Apn1_1, &bid1_Apn1_2, &bid1_Apn1_3}, 3385 }, 3386 } 3387 for _, test := range testCases { 3388 3389 innerBidsApn1 := []*entities.PbsOrtbBid{ 3390 &bid1_Apn1_1, 3391 &bid1_Apn1_2, 3392 &bid1_Apn1_3, 3393 } 3394 3395 seatBidApn1 := &entities.PbsOrtbSeatBid{Bids: innerBidsApn1, Currency: "USD"} 3396 3397 removeBidById(seatBidApn1, test.inBidName) 3398 assert.Len(t, seatBidApn1.Bids, len(test.outBids), test.desc) 3399 assert.ElementsMatch(t, seatBidApn1.Bids, test.outBids, "Incorrect bids in response") 3400 } 3401 3402 } 3403 3404 func TestUpdateRejections(t *testing.T) { 3405 rejections := []string{} 3406 3407 rejections = updateRejections(rejections, "bid_id1", "some reason 1") 3408 rejections = updateRejections(rejections, "bid_id2", "some reason 2") 3409 3410 assert.Equal(t, 2, len(rejections), "Rejections should contain 2 rejection messages") 3411 assert.Containsf(t, rejections, "bid rejected [bid ID: bid_id1] reason: some reason 1", "Rejection message did not match expected") 3412 assert.Containsf(t, rejections, "bid rejected [bid ID: bid_id2] reason: some reason 2", "Rejection message did not match expected") 3413 } 3414 3415 func TestApplyDealSupport(t *testing.T) { 3416 type testInput struct { 3417 dealPriority int 3418 impExt json.RawMessage 3419 targ map[string]string 3420 bidderName openrtb_ext.BidderName 3421 } 3422 3423 type testOutput struct { 3424 hbPbCatDur string 3425 dealErr string 3426 dealTierSatisfied bool 3427 } 3428 3429 testCases := []struct { 3430 description string 3431 in testInput 3432 expected testOutput 3433 }{ 3434 { 3435 description: "hb_pb_cat_dur should be modified", 3436 in: testInput{ 3437 dealPriority: 5, 3438 impExt: json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}}}`), 3439 targ: map[string]string{ 3440 "hb_pb_cat_dur": "12.00_movies_30s", 3441 }, 3442 bidderName: openrtb_ext.BidderName("appnexus"), 3443 }, 3444 expected: testOutput{ 3445 hbPbCatDur: "tier5_movies_30s", 3446 dealErr: "", 3447 dealTierSatisfied: true, 3448 }, 3449 }, 3450 { 3451 description: "hb_pb_cat_dur should be modified even with a mixed case bidder in the impExt", 3452 in: testInput{ 3453 dealPriority: 5, 3454 impExt: json.RawMessage(`{"prebid": {"bidder": {"APPnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}}}`), 3455 targ: map[string]string{ 3456 "hb_pb_cat_dur": "12.00_movies_30s", 3457 }, 3458 bidderName: openrtb_ext.BidderName("appnexus"), 3459 }, 3460 expected: testOutput{ 3461 hbPbCatDur: "tier5_movies_30s", 3462 dealErr: "", 3463 dealTierSatisfied: true, 3464 }, 3465 }, 3466 { 3467 description: "hb_pb_cat_dur should be modified even with a mixed case bidder in the winningBidsByBidder map", 3468 in: testInput{ 3469 dealPriority: 5, 3470 impExt: json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}}}`), 3471 targ: map[string]string{ 3472 "hb_pb_cat_dur": "12.00_movies_30s", 3473 }, 3474 bidderName: openrtb_ext.BidderName("APPnexus"), 3475 }, 3476 expected: testOutput{ 3477 hbPbCatDur: "tier5_movies_30s", 3478 dealErr: "", 3479 dealTierSatisfied: true, 3480 }, 3481 }, 3482 { 3483 description: "hb_pb_cat_dur should not be modified due to unknown bidder in the winningBidsByBidder map", 3484 in: testInput{ 3485 dealPriority: 9, 3486 impExt: json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 10, "prefix": "tier"}, "placementId": 10433394}}}}`), 3487 targ: map[string]string{ 3488 "hb_pb_cat_dur": "12.00_medicine_30s", 3489 }, 3490 bidderName: openrtb_ext.BidderName("unknown"), 3491 }, 3492 expected: testOutput{ 3493 hbPbCatDur: "12.00_medicine_30s", 3494 dealErr: "", 3495 dealTierSatisfied: false, 3496 }, 3497 }, 3498 { 3499 description: "hb_pb_cat_dur should not be modified due to priority not exceeding min", 3500 in: testInput{ 3501 dealPriority: 9, 3502 impExt: json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 10, "prefix": "tier"}, "placementId": 10433394}}}}`), 3503 targ: map[string]string{ 3504 "hb_pb_cat_dur": "12.00_medicine_30s", 3505 }, 3506 bidderName: openrtb_ext.BidderName("appnexus"), 3507 }, 3508 expected: testOutput{ 3509 hbPbCatDur: "12.00_medicine_30s", 3510 dealErr: "", 3511 dealTierSatisfied: false, 3512 }, 3513 }, 3514 { 3515 description: "hb_pb_cat_dur should not be modified due to invalid config", 3516 in: testInput{ 3517 dealPriority: 5, 3518 impExt: json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 5, "prefix": ""}, "placementId": 10433394}}}}`), 3519 targ: map[string]string{ 3520 "hb_pb_cat_dur": "12.00_games_30s", 3521 }, 3522 bidderName: openrtb_ext.BidderName("appnexus"), 3523 }, 3524 expected: testOutput{ 3525 hbPbCatDur: "12.00_games_30s", 3526 dealErr: "dealTier configuration invalid for bidder 'appnexus', imp ID 'imp_id1'", 3527 dealTierSatisfied: false, 3528 }, 3529 }, 3530 { 3531 description: "hb_pb_cat_dur should not be modified due to deal priority of 0", 3532 in: testInput{ 3533 dealPriority: 0, 3534 impExt: json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}}}`), 3535 targ: map[string]string{ 3536 "hb_pb_cat_dur": "12.00_auto_30s", 3537 }, 3538 bidderName: openrtb_ext.BidderName("appnexus"), 3539 }, 3540 expected: testOutput{ 3541 hbPbCatDur: "12.00_auto_30s", 3542 dealErr: "", 3543 dealTierSatisfied: false, 3544 }, 3545 }, 3546 } 3547 3548 for _, test := range testCases { 3549 bidRequest := &openrtb2.BidRequest{ 3550 ID: "some-request-id", 3551 Imp: []openrtb2.Imp{ 3552 { 3553 ID: "imp_id1", 3554 Ext: test.in.impExt, 3555 }, 3556 }, 3557 } 3558 3559 bid := entities.PbsOrtbBid{Bid: &openrtb2.Bid{ID: "123456"}, BidMeta: nil, BidType: "video", BidTargets: map[string]string{}, BidVideo: &openrtb_ext.ExtBidPrebidVideo{}, BidEvents: nil, BidFloors: nil, DealPriority: test.in.dealPriority, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 0, OriginalBidCur: "USD", TargetBidderCode: ""} 3560 bidCategory := map[string]string{ 3561 bid.Bid.ID: test.in.targ["hb_pb_cat_dur"], 3562 } 3563 3564 auc := &auction{ 3565 allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ 3566 "imp_id1": { 3567 test.in.bidderName: {&bid}, 3568 }, 3569 }, 3570 } 3571 3572 dealErrs := applyDealSupport(bidRequest, auc, bidCategory, nil) 3573 3574 assert.Equal(t, test.expected.hbPbCatDur, bidCategory[auc.allBidsByBidder["imp_id1"][test.in.bidderName][0].Bid.ID], test.description) 3575 assert.Equal(t, test.expected.dealTierSatisfied, auc.allBidsByBidder["imp_id1"][test.in.bidderName][0].DealTierSatisfied, "expected.dealTierSatisfied=%v when %v", test.expected.dealTierSatisfied, test.description) 3576 if len(test.expected.dealErr) > 0 { 3577 assert.Containsf(t, dealErrs, errors.New(test.expected.dealErr), "Expected error message not found in deal errors") 3578 } 3579 } 3580 } 3581 3582 func TestApplyDealSupportMultiBid(t *testing.T) { 3583 type args struct { 3584 bidRequest *openrtb2.BidRequest 3585 auc *auction 3586 bidCategory map[string]string 3587 multiBid map[string]openrtb_ext.ExtMultiBid 3588 } 3589 type want struct { 3590 errs []error 3591 expectedHbPbCatDur map[string]map[string][]string 3592 expectedDealTierSatisfied map[string]map[string][]bool 3593 } 3594 tests := []struct { 3595 name string 3596 args args 3597 want want 3598 }{ 3599 { 3600 name: "multibid disabled, hb_pb_cat_dur should be modified only for first bid", 3601 args: args{ 3602 bidRequest: &openrtb2.BidRequest{ 3603 ID: "some-request-id", 3604 Imp: []openrtb2.Imp{ 3605 { 3606 ID: "imp_id1", 3607 Ext: json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}}}`), 3608 }, 3609 { 3610 ID: "imp_id1", 3611 Ext: json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}}}`), 3612 }, 3613 }, 3614 }, 3615 auc: &auction{ 3616 allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ 3617 "imp_id1": { 3618 openrtb_ext.BidderName("appnexus"): { 3619 &entities.PbsOrtbBid{Bid: &openrtb2.Bid{ID: "123456"}, BidMeta: nil, BidType: "video", BidTargets: map[string]string{}, BidVideo: &openrtb_ext.ExtBidPrebidVideo{}, BidEvents: nil, BidFloors: nil, DealPriority: 5, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 0, OriginalBidCur: "USD", TargetBidderCode: ""}, 3620 &entities.PbsOrtbBid{Bid: &openrtb2.Bid{ID: "789101"}, BidMeta: nil, BidType: "video", BidTargets: map[string]string{}, BidVideo: &openrtb_ext.ExtBidPrebidVideo{}, BidEvents: nil, BidFloors: nil, DealPriority: 5, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 0, OriginalBidCur: "USD", TargetBidderCode: ""}, 3621 }, 3622 }, 3623 }, 3624 }, 3625 bidCategory: map[string]string{ 3626 "123456": "12.00_movies_30s", 3627 "789101": "12.00_movies_30s", 3628 }, 3629 multiBid: nil, 3630 }, 3631 want: want{ 3632 errs: []error{}, 3633 expectedHbPbCatDur: map[string]map[string][]string{ 3634 "imp_id1": { 3635 "appnexus": []string{"tier5_movies_30s", "12.00_movies_30s"}, 3636 }, 3637 }, 3638 expectedDealTierSatisfied: map[string]map[string][]bool{ 3639 "imp_id1": { 3640 "appnexus": []bool{true, false}, 3641 }, 3642 }, 3643 }, 3644 }, 3645 { 3646 name: "multibid enabled, hb_pb_cat_dur should be modified for all winning bids", 3647 args: args{ 3648 bidRequest: &openrtb2.BidRequest{ 3649 ID: "some-request-id", 3650 Imp: []openrtb2.Imp{ 3651 { 3652 ID: "imp_id1", 3653 Ext: json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}}}`), 3654 }, 3655 { 3656 ID: "imp_id1", 3657 Ext: json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}}}`), 3658 }, 3659 }, 3660 }, 3661 auc: &auction{ 3662 allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ 3663 "imp_id1": { 3664 openrtb_ext.BidderName("appnexus"): { 3665 &entities.PbsOrtbBid{Bid: &openrtb2.Bid{ID: "123456"}, BidMeta: nil, BidType: "video", BidTargets: map[string]string{}, BidVideo: &openrtb_ext.ExtBidPrebidVideo{}, BidEvents: nil, BidFloors: nil, DealPriority: 5, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 0, OriginalBidCur: "USD", TargetBidderCode: ""}, 3666 &entities.PbsOrtbBid{Bid: &openrtb2.Bid{ID: "789101"}, BidMeta: nil, BidType: "video", BidTargets: map[string]string{}, BidVideo: &openrtb_ext.ExtBidPrebidVideo{}, BidEvents: nil, BidFloors: nil, DealPriority: 5, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 0, OriginalBidCur: "USD", TargetBidderCode: ""}, 3667 }, 3668 }, 3669 }, 3670 }, 3671 bidCategory: map[string]string{ 3672 "123456": "12.00_movies_30s", 3673 "789101": "12.00_movies_30s", 3674 }, 3675 multiBid: map[string]openrtb_ext.ExtMultiBid{ 3676 "appnexus": { 3677 TargetBidderCodePrefix: "appN", 3678 MaxBids: ptrutil.ToPtr(2), 3679 }, 3680 }, 3681 }, 3682 want: want{ 3683 errs: []error{}, 3684 expectedHbPbCatDur: map[string]map[string][]string{ 3685 "imp_id1": { 3686 "appnexus": []string{"tier5_movies_30s", "tier5_movies_30s"}, 3687 }, 3688 }, 3689 expectedDealTierSatisfied: map[string]map[string][]bool{ 3690 "imp_id1": { 3691 "appnexus": []bool{true, true}, 3692 }, 3693 }, 3694 }, 3695 }, 3696 { 3697 name: "multibid enabled but TargetBidderCodePrefix not defined, hb_pb_cat_dur should be modified only for first bid", 3698 args: args{ 3699 bidRequest: &openrtb2.BidRequest{ 3700 ID: "some-request-id", 3701 Imp: []openrtb2.Imp{ 3702 { 3703 ID: "imp_id1", 3704 Ext: json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}}}`), 3705 }, 3706 { 3707 ID: "imp_id1", 3708 Ext: json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}}}`), 3709 }, 3710 }, 3711 }, 3712 auc: &auction{ 3713 allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ 3714 "imp_id1": { 3715 openrtb_ext.BidderName("appnexus"): { 3716 &entities.PbsOrtbBid{Bid: &openrtb2.Bid{ID: "123456"}, BidMeta: nil, BidType: "video", BidTargets: map[string]string{}, BidVideo: &openrtb_ext.ExtBidPrebidVideo{}, BidEvents: nil, BidFloors: nil, DealPriority: 5, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 0, OriginalBidCur: "USD", TargetBidderCode: ""}, 3717 &entities.PbsOrtbBid{Bid: &openrtb2.Bid{ID: "789101"}, BidMeta: nil, BidType: "video", BidTargets: map[string]string{}, BidVideo: &openrtb_ext.ExtBidPrebidVideo{}, BidEvents: nil, BidFloors: nil, DealPriority: 5, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 0, OriginalBidCur: "USD", TargetBidderCode: ""}, 3718 }, 3719 }, 3720 }, 3721 }, 3722 bidCategory: map[string]string{ 3723 "123456": "12.00_movies_30s", 3724 "789101": "12.00_movies_30s", 3725 }, 3726 multiBid: map[string]openrtb_ext.ExtMultiBid{ 3727 "appnexus": { 3728 MaxBids: ptrutil.ToPtr(2), 3729 }, 3730 }, 3731 }, 3732 want: want{ 3733 errs: []error{}, 3734 expectedHbPbCatDur: map[string]map[string][]string{ 3735 "imp_id1": { 3736 "appnexus": []string{"tier5_movies_30s", "12.00_movies_30s"}, 3737 }, 3738 }, 3739 expectedDealTierSatisfied: map[string]map[string][]bool{ 3740 "imp_id1": { 3741 "appnexus": []bool{true, false}, 3742 }, 3743 }, 3744 }, 3745 }, 3746 } 3747 for _, tt := range tests { 3748 t.Run(tt.name, func(t *testing.T) { 3749 errs := applyDealSupport(tt.args.bidRequest, tt.args.auc, tt.args.bidCategory, tt.args.multiBid) 3750 assert.Equal(t, tt.want.errs, errs) 3751 3752 for impID, topBidsPerImp := range tt.args.auc.allBidsByBidder { 3753 for bidder, topBidsPerBidder := range topBidsPerImp { 3754 for i, topBid := range topBidsPerBidder { 3755 assert.Equal(t, tt.want.expectedHbPbCatDur[impID][bidder.String()][i], tt.args.bidCategory[topBid.Bid.ID], tt.name) 3756 assert.Equal(t, tt.want.expectedDealTierSatisfied[impID][bidder.String()][i], topBid.DealTierSatisfied, tt.name) 3757 } 3758 } 3759 } 3760 }) 3761 } 3762 } 3763 3764 func TestGetDealTiers(t *testing.T) { 3765 testCases := []struct { 3766 description string 3767 request openrtb2.BidRequest 3768 expected map[string]openrtb_ext.DealTierBidderMap 3769 }{ 3770 { 3771 description: "None", 3772 request: openrtb2.BidRequest{ 3773 Imp: []openrtb2.Imp{}, 3774 }, 3775 expected: map[string]openrtb_ext.DealTierBidderMap{}, 3776 }, 3777 { 3778 description: "One", 3779 request: openrtb2.BidRequest{ 3780 Imp: []openrtb2.Imp{ 3781 {ID: "imp1", Ext: json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}}}}}`)}, 3782 }, 3783 }, 3784 expected: map[string]openrtb_ext.DealTierBidderMap{ 3785 "imp1": {openrtb_ext.BidderAppnexus: {Prefix: "tier", MinDealTier: 5}}, 3786 }, 3787 }, 3788 { 3789 description: "Many", 3790 request: openrtb2.BidRequest{ 3791 Imp: []openrtb2.Imp{ 3792 {ID: "imp1", Ext: json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier1"}}}}}`)}, 3793 {ID: "imp2", Ext: json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 8, "prefix": "tier2"}}}}}`)}, 3794 }, 3795 }, 3796 expected: map[string]openrtb_ext.DealTierBidderMap{ 3797 "imp1": {openrtb_ext.BidderAppnexus: {Prefix: "tier1", MinDealTier: 5}}, 3798 "imp2": {openrtb_ext.BidderAppnexus: {Prefix: "tier2", MinDealTier: 8}}, 3799 }, 3800 }, 3801 { 3802 description: "Many - Skips Malformed", 3803 request: openrtb2.BidRequest{ 3804 Imp: []openrtb2.Imp{ 3805 {ID: "imp1", Ext: json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier1"}}}}}`)}, 3806 {ID: "imp2", Ext: json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": "wrong type"}}}}`)}, 3807 }, 3808 }, 3809 expected: map[string]openrtb_ext.DealTierBidderMap{ 3810 "imp1": {openrtb_ext.BidderAppnexus: {Prefix: "tier1", MinDealTier: 5}}, 3811 }, 3812 }, 3813 } 3814 3815 for _, test := range testCases { 3816 result := getDealTiers(&test.request) 3817 assert.Equal(t, test.expected, result, test.description) 3818 } 3819 } 3820 3821 func TestValidateDealTier(t *testing.T) { 3822 testCases := []struct { 3823 description string 3824 dealTier openrtb_ext.DealTier 3825 expectedResult bool 3826 }{ 3827 { 3828 description: "Valid", 3829 dealTier: openrtb_ext.DealTier{Prefix: "prefix", MinDealTier: 5}, 3830 expectedResult: true, 3831 }, 3832 { 3833 description: "Invalid - Empty", 3834 dealTier: openrtb_ext.DealTier{}, 3835 expectedResult: false, 3836 }, 3837 { 3838 description: "Invalid - Empty Prefix", 3839 dealTier: openrtb_ext.DealTier{MinDealTier: 5}, 3840 expectedResult: false, 3841 }, 3842 { 3843 description: "Invalid - Empty Deal Tier", 3844 dealTier: openrtb_ext.DealTier{Prefix: "prefix"}, 3845 expectedResult: false, 3846 }, 3847 } 3848 3849 for _, test := range testCases { 3850 assert.Equal(t, test.expectedResult, validateDealTier(test.dealTier), test.description) 3851 } 3852 } 3853 3854 func TestUpdateHbPbCatDur(t *testing.T) { 3855 testCases := []struct { 3856 description string 3857 targ map[string]string 3858 dealTier openrtb_ext.DealTier 3859 dealPriority int 3860 expectedHbPbCatDur string 3861 expectedDealTierSatisfied bool 3862 }{ 3863 { 3864 description: "hb_pb_cat_dur should be updated with prefix and tier", 3865 targ: map[string]string{ 3866 "hb_pb": "12.00", 3867 "hb_pb_cat_dur": "12.00_movies_30s", 3868 }, 3869 dealTier: openrtb_ext.DealTier{ 3870 Prefix: "tier", 3871 MinDealTier: 5, 3872 }, 3873 dealPriority: 5, 3874 expectedHbPbCatDur: "tier5_movies_30s", 3875 expectedDealTierSatisfied: true, 3876 }, 3877 { 3878 description: "hb_pb_cat_dur should not be updated due to bid priority", 3879 targ: map[string]string{ 3880 "hb_pb": "12.00", 3881 "hb_pb_cat_dur": "12.00_auto_30s", 3882 }, 3883 dealTier: openrtb_ext.DealTier{ 3884 Prefix: "tier", 3885 MinDealTier: 10, 3886 }, 3887 dealPriority: 6, 3888 expectedHbPbCatDur: "12.00_auto_30s", 3889 expectedDealTierSatisfied: false, 3890 }, 3891 { 3892 description: "hb_pb_cat_dur should be updated with prefix and tier", 3893 targ: map[string]string{ 3894 "hb_pb": "12.00", 3895 "hb_pb_cat_dur": "12.00_medicine_30s", 3896 }, 3897 dealTier: openrtb_ext.DealTier{ 3898 Prefix: "tier", 3899 MinDealTier: 1, 3900 }, 3901 dealPriority: 7, 3902 expectedHbPbCatDur: "tier7_medicine_30s", 3903 expectedDealTierSatisfied: true, 3904 }, 3905 } 3906 3907 for _, test := range testCases { 3908 bid := entities.PbsOrtbBid{Bid: &openrtb2.Bid{ID: "123456"}, BidMeta: nil, BidType: "video", BidTargets: map[string]string{}, BidVideo: &openrtb_ext.ExtBidPrebidVideo{}, BidEvents: nil, BidFloors: nil, DealPriority: test.dealPriority, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 0, OriginalBidCur: "USD", TargetBidderCode: ""} 3909 bidCategory := map[string]string{ 3910 bid.Bid.ID: test.targ["hb_pb_cat_dur"], 3911 } 3912 3913 updateHbPbCatDur(&bid, test.dealTier, bidCategory) 3914 3915 assert.Equal(t, test.expectedHbPbCatDur, bidCategory[bid.Bid.ID], test.description) 3916 assert.Equal(t, test.expectedDealTierSatisfied, bid.DealTierSatisfied, test.description) 3917 } 3918 } 3919 3920 func TestMakeBidExtJSON(t *testing.T) { 3921 3922 type aTest struct { 3923 description string 3924 ext json.RawMessage 3925 extBidPrebid openrtb_ext.ExtBidPrebid 3926 impExtInfo map[string]ImpExtInfo 3927 origbidcpm float64 3928 origbidcur string 3929 expectedBidExt string 3930 expectedErrMessage string 3931 } 3932 3933 testCases := []aTest{ 3934 { 3935 description: "Valid extension, non empty extBidPrebid, valid imp ext info, meta from adapter", 3936 ext: json.RawMessage(`{"video":{"h":100}}`), 3937 extBidPrebid: openrtb_ext.ExtBidPrebid{Type: openrtb_ext.BidType("video"), Meta: &openrtb_ext.ExtBidPrebidMeta{BrandName: "foo"}, Passthrough: nil}, 3938 impExtInfo: map[string]ImpExtInfo{"test_imp_id": {true, []byte(`{"video":{"h":480,"mimes":["video/mp4"]}}`), json.RawMessage(`{"imp_passthrough_val": 1}`)}}, 3939 origbidcpm: 10.0000, 3940 origbidcur: "USD", 3941 expectedBidExt: `{"prebid":{"meta": {"brandName": "foo","adaptercode": "adapter"}, "passthrough":{"imp_passthrough_val":1}, "type":"video"}, "storedrequestattributes":{"h":480,"mimes":["video/mp4"]},"video":{"h":100}, "origbidcpm": 10, "origbidcur": "USD"}`, 3942 expectedErrMessage: "", 3943 }, 3944 { 3945 description: "Valid extension, non empty extBidPrebid, valid imp ext info, meta from response, imp passthrough is nil", 3946 ext: json.RawMessage(`{"video":{"h":100},"prebid":{"meta": {"brandName": "foo"}}}`), 3947 extBidPrebid: openrtb_ext.ExtBidPrebid{Type: openrtb_ext.BidType("video")}, 3948 impExtInfo: map[string]ImpExtInfo{"test_imp_id": {true, []byte(`{"video":{"h":480,"mimes":["video/mp4"]}}`), nil}}, 3949 origbidcpm: 10.0000, 3950 origbidcur: "USD", 3951 expectedBidExt: `{"prebid":{"meta": {"brandName": "foo","adaptercode": "adapter"}, "type":"video"},"storedrequestattributes":{"h":480,"mimes":["video/mp4"]},"video":{"h":100}, "origbidcpm": 10, "origbidcur": "USD"}`, 3952 expectedErrMessage: "", 3953 }, 3954 { 3955 description: "Empty extension, non empty extBidPrebid and valid imp ext info", 3956 ext: nil, 3957 extBidPrebid: openrtb_ext.ExtBidPrebid{Type: openrtb_ext.BidType("video")}, 3958 impExtInfo: map[string]ImpExtInfo{"test_imp_id": {true, []byte(`{"video":{"h":480,"mimes":["video/mp4"]}}`), json.RawMessage(`{"imp_passthrough_val": 1}`)}}, 3959 origbidcpm: 0, 3960 expectedBidExt: `{"origbidcpm": 0,"prebid":{"meta":{"adaptercode": "adapter"},"passthrough":{"imp_passthrough_val":1}, "type":"video"},"storedrequestattributes":{"h":480,"mimes":["video/mp4"]}}`, 3961 expectedErrMessage: "", 3962 }, 3963 { 3964 description: "Valid extension, non empty extBidPrebid and imp ext info not found", 3965 ext: json.RawMessage(`{"video":{"h":100}}`), 3966 extBidPrebid: openrtb_ext.ExtBidPrebid{Type: openrtb_ext.BidType("video")}, 3967 impExtInfo: map[string]ImpExtInfo{"another_imp_id": {true, []byte(`{"video":{"h":480,"mimes":["video/mp4"]}}`), json.RawMessage(`{"imp_passthrough_val": 1}`)}}, 3968 origbidcpm: 10.0000, 3969 origbidcur: "USD", 3970 expectedBidExt: `{"prebid":{"meta":{"adaptercode": "adapter"},"type":"video"},"video":{"h":100}, "origbidcpm": 10, "origbidcur": "USD"}`, 3971 expectedErrMessage: "", 3972 }, 3973 { 3974 description: "Valid extension, empty extBidPrebid and valid imp ext info", 3975 ext: json.RawMessage(`{"video":{"h":100}}`), 3976 extBidPrebid: openrtb_ext.ExtBidPrebid{}, 3977 origbidcpm: 10.0000, 3978 origbidcur: "USD", 3979 impExtInfo: map[string]ImpExtInfo{"test_imp_id": {true, []byte(`{"video":{"h":480,"mimes":["video/mp4"]}}`), json.RawMessage(`{"imp_passthrough_val": 1}`)}}, 3980 expectedBidExt: `{"prebid":{"meta":{"adaptercode": "adapter"},"passthrough":{"imp_passthrough_val":1}},"storedrequestattributes":{"h":480,"mimes":["video/mp4"]},"video":{"h":100}, "origbidcpm": 10, "origbidcur": "USD"}`, 3981 expectedErrMessage: "", 3982 }, 3983 { 3984 description: "Valid extension, non empty extBidPrebid and empty imp ext info", 3985 ext: json.RawMessage(`{"video":{"h":100}}`), 3986 extBidPrebid: openrtb_ext.ExtBidPrebid{Type: openrtb_ext.BidType("video")}, 3987 origbidcpm: 10.0000, 3988 origbidcur: "USD", 3989 impExtInfo: nil, 3990 expectedBidExt: `{"prebid":{"meta":{"adaptercode": "adapter"},"type":"video"},"video":{"h":100}, "origbidcpm": 10, "origbidcur": "USD"}`, 3991 expectedErrMessage: "", 3992 }, 3993 { 3994 description: "Valid extension, non empty extBidPrebid and valid imp ext info without video attr", 3995 ext: json.RawMessage(`{"video":{"h":100}}`), 3996 extBidPrebid: openrtb_ext.ExtBidPrebid{Type: openrtb_ext.BidType("video")}, 3997 origbidcpm: 10.0000, 3998 origbidcur: "USD", 3999 impExtInfo: map[string]ImpExtInfo{"test_imp_id": {true, []byte(`{"banner":{"h":480}}`), json.RawMessage(`{"imp_passthrough_val": 1}`)}}, 4000 expectedBidExt: `{"prebid":{"meta":{"adaptercode": "adapter"},"passthrough":{"imp_passthrough_val":1}, "type":"video"},"video":{"h":100}, "origbidcpm": 10, "origbidcur": "USD"}`, 4001 expectedErrMessage: "", 4002 }, 4003 { 4004 description: "Valid extension with prebid, non empty extBidPrebid and valid imp ext info without video attr", 4005 ext: json.RawMessage(`{"prebid":{"targeting":100}}`), 4006 extBidPrebid: openrtb_ext.ExtBidPrebid{Type: openrtb_ext.BidType("video")}, 4007 origbidcpm: 10.0000, 4008 origbidcur: "USD", 4009 impExtInfo: map[string]ImpExtInfo{"test_imp_id": {true, []byte(`{"banner":{"h":480}}`), json.RawMessage(`{"imp_passthrough_val": 1}`)}}, 4010 expectedBidExt: `{"prebid":{"meta":{"adaptercode": "adapter"},"passthrough":{"imp_passthrough_val":1}, "type":"video"}, "origbidcpm": 10, "origbidcur": "USD"}`, 4011 expectedErrMessage: "", 4012 }, 4013 { 4014 description: "Valid extension with prebid, non empty extBidPrebid and valid imp ext info with video attr", 4015 ext: json.RawMessage(`{"prebid":{"targeting":100}}`), 4016 extBidPrebid: openrtb_ext.ExtBidPrebid{Type: openrtb_ext.BidType("video")}, 4017 origbidcpm: 10.0000, 4018 origbidcur: "USD", 4019 impExtInfo: map[string]ImpExtInfo{"test_imp_id": {true, []byte(`{"video":{"h":480,"mimes":["video/mp4"]}}`), json.RawMessage(`{"imp_passthrough_val": 1}`)}}, 4020 expectedBidExt: `{"prebid":{"meta":{"adaptercode": "adapter"},"passthrough":{"imp_passthrough_val":1}, "type":"video"}, "storedrequestattributes":{"h":480,"mimes":["video/mp4"]}, "origbidcpm": 10, "origbidcur": "USD"}`, 4021 expectedErrMessage: "", 4022 }, 4023 { 4024 description: "Meta - Defined By Bid - Nil Extension", 4025 ext: nil, 4026 extBidPrebid: openrtb_ext.ExtBidPrebid{Type: openrtb_ext.BidType("banner"), Meta: &openrtb_ext.ExtBidPrebidMeta{BrandName: "foo"}}, 4027 impExtInfo: map[string]ImpExtInfo{}, 4028 origbidcpm: 0, 4029 origbidcur: "USD", 4030 expectedBidExt: `{"origbidcpm": 0,"prebid":{"meta":{"brandName":"foo","adaptercode": "adapter"},"type":"banner"}, "origbidcur": "USD"}`, 4031 expectedErrMessage: "", 4032 }, 4033 { 4034 description: "Meta - Defined By Bid - Empty Extension", 4035 ext: json.RawMessage(`{}`), 4036 extBidPrebid: openrtb_ext.ExtBidPrebid{Type: openrtb_ext.BidType("banner"), Meta: &openrtb_ext.ExtBidPrebidMeta{BrandName: "foo"}}, 4037 impExtInfo: nil, 4038 origbidcpm: 0, 4039 origbidcur: "USD", 4040 expectedBidExt: `{"origbidcpm": 0,"prebid":{"meta":{"brandName":"foo","adaptercode": "adapter"},"type":"banner"}, "origbidcur": "USD"}`, 4041 expectedErrMessage: "", 4042 }, 4043 { 4044 description: "Meta - Defined By Bid - Existing Extension Overwritten", 4045 ext: json.RawMessage(`{"prebid":{"meta":{"brandName":"notfoo", "brandId": 42}}}`), 4046 extBidPrebid: openrtb_ext.ExtBidPrebid{Type: openrtb_ext.BidType("banner"), Meta: &openrtb_ext.ExtBidPrebidMeta{BrandName: "foo"}}, 4047 impExtInfo: nil, 4048 origbidcpm: 10.0000, 4049 origbidcur: "USD", 4050 expectedBidExt: `{"prebid":{"meta":{"brandName":"foo","adaptercode": "adapter"},"type":"banner"}, "origbidcpm": 10, "origbidcur": "USD"}`, 4051 expectedErrMessage: "", 4052 }, 4053 { 4054 description: "Meta - Not Defined By Bid - Persists From Bid Ext", 4055 ext: json.RawMessage(`{"prebid":{"meta":{"brandName":"foo"}}}`), 4056 extBidPrebid: openrtb_ext.ExtBidPrebid{Type: openrtb_ext.BidType("banner")}, 4057 impExtInfo: nil, 4058 origbidcpm: 10.0000, 4059 origbidcur: "USD", 4060 expectedBidExt: `{"prebid":{"meta":{"brandName":"foo","adaptercode": "adapter"},"type":"banner"}, "origbidcpm": 10, "origbidcur": "USD"}`, 4061 expectedErrMessage: "", 4062 }, 4063 { 4064 description: "Meta - Not Defined By Bid - Persists From Bid Ext - Invalid Fields Ignored", 4065 ext: json.RawMessage(`{"prebid":{"meta":{"brandName":"foo","unknown":"value"}}}`), 4066 extBidPrebid: openrtb_ext.ExtBidPrebid{Type: openrtb_ext.BidType("banner")}, 4067 impExtInfo: nil, 4068 origbidcpm: -1, 4069 origbidcur: "USD", 4070 expectedBidExt: `{"prebid":{"meta":{"brandName":"foo","adaptercode":"adapter"},"type":"banner"}, "origbidcur": "USD"}`, 4071 expectedErrMessage: "", 4072 }, 4073 { 4074 description: "Meta - Not Defined", 4075 ext: nil, 4076 extBidPrebid: openrtb_ext.ExtBidPrebid{Type: openrtb_ext.BidType("banner")}, 4077 impExtInfo: nil, 4078 origbidcpm: 0, 4079 origbidcur: "USD", 4080 expectedBidExt: `{"origbidcpm": 0,"prebid":{"type":"banner","meta":{"adaptercode":"adapter"}}, "origbidcur": "USD"}`, 4081 expectedErrMessage: "", 4082 }, 4083 //Error cases 4084 { 4085 description: "Invalid extension, valid extBidPrebid and valid imp ext info", 4086 ext: json.RawMessage(`{invalid json}`), 4087 extBidPrebid: openrtb_ext.ExtBidPrebid{Type: openrtb_ext.BidType("video")}, 4088 expectedBidExt: ``, 4089 expectedErrMessage: "expects \" or n, but found i", 4090 }, 4091 { 4092 description: "Meta - Invalid", 4093 ext: json.RawMessage(`{"prebid":{"meta":{"brandId":"foo"}}}`), // brandId should be an int, but is a string in this test case 4094 extBidPrebid: openrtb_ext.ExtBidPrebid{Type: openrtb_ext.BidType("banner")}, 4095 impExtInfo: nil, 4096 expectedErrMessage: "error validating response from server, cannot unmarshal openrtb_ext.ExtBidPrebidMeta.BrandID: unexpected character: \xff", 4097 }, 4098 } 4099 4100 for _, test := range testCases { 4101 t.Run(test.description, func(t *testing.T) { 4102 var adapter openrtb_ext.BidderName = "adapter" 4103 result, err := makeBidExtJSON(test.ext, &test.extBidPrebid, test.impExtInfo, "test_imp_id", test.origbidcpm, test.origbidcur, adapter) 4104 4105 if test.expectedErrMessage == "" { 4106 assert.JSONEq(t, test.expectedBidExt, string(result), "Incorrect result") 4107 assert.NoError(t, err, "Error should not be returned") 4108 } else { 4109 assert.Contains(t, err.Error(), test.expectedErrMessage, "incorrect error message") 4110 } 4111 }) 4112 } 4113 } 4114 4115 func TestStoredAuctionResponses(t *testing.T) { 4116 categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") 4117 if error != nil { 4118 t.Errorf("Failed to create a category Fetcher: %v", error) 4119 } 4120 4121 e := new(exchange) 4122 e.cache = &wellBehavedCache{} 4123 e.me = &metricsConf.NilMetricsEngine{} 4124 e.categoriesFetcher = categoriesFetcher 4125 e.bidIDGenerator = &fakeBidIDGenerator{GenerateBidID: false, ReturnError: false} 4126 e.currencyConverter = currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) 4127 e.gdprPermsBuilder = fakePermissionsBuilder{ 4128 permissions: &permissionsMock{ 4129 allowAllBidders: true, 4130 }, 4131 }.Builder 4132 4133 // Define mock incoming bid requeset 4134 mockBidRequest := &openrtb2.BidRequest{ 4135 ID: "request-id", 4136 Imp: []openrtb2.Imp{{ 4137 ID: "impression-id", 4138 Video: &openrtb2.Video{W: ptrutil.ToPtr[int64](400), H: ptrutil.ToPtr[int64](300)}, 4139 }}, 4140 } 4141 4142 expectedBidResponse := &openrtb2.BidResponse{ 4143 ID: "request-id", 4144 SeatBid: []openrtb2.SeatBid{ 4145 { 4146 Bid: []openrtb2.Bid{ 4147 {ID: "bid_id", ImpID: "impression-id", Ext: json.RawMessage(`{"origbidcpm":0,"prebid":{"meta":{},"type":"video"}}`)}, 4148 }, 4149 Seat: "appnexus", 4150 }, 4151 }, 4152 } 4153 4154 testCases := []struct { 4155 desc string 4156 storedAuctionResp map[string]json.RawMessage 4157 errorExpected bool 4158 }{ 4159 { 4160 desc: "Single imp with valid stored response", 4161 storedAuctionResp: map[string]json.RawMessage{ 4162 "impression-id": json.RawMessage(`[{"bid": [{"id": "bid_id", "ext": {"prebid": {"type": "video"}}}],"seat": "appnexus"}]`), 4163 }, 4164 errorExpected: false, 4165 }, 4166 { 4167 desc: "Single imp with invalid stored response", 4168 storedAuctionResp: map[string]json.RawMessage{ 4169 "impression-id": json.RawMessage(`[}]`), 4170 }, 4171 errorExpected: true, 4172 }, 4173 } 4174 4175 for _, test := range testCases { 4176 4177 auctionRequest := &AuctionRequest{ 4178 BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: mockBidRequest}, 4179 Account: config.Account{}, 4180 UserSyncs: &emptyUsersync{}, 4181 StoredAuctionResponses: test.storedAuctionResp, 4182 HookExecutor: &hookexecution.EmptyHookExecutor{}, 4183 TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), 4184 } 4185 // Run test 4186 outBidResponse, err := e.HoldAuction(context.Background(), auctionRequest, &DebugLog{}) 4187 if test.errorExpected { 4188 assert.Error(t, err, "Error should be returned") 4189 } else { 4190 assert.NoErrorf(t, err, "%s. HoldAuction error: %v \n", test.desc, err) 4191 outBidResponse.Ext = nil 4192 assert.Equal(t, expectedBidResponse, outBidResponse.BidResponse, "Incorrect stored auction response") 4193 } 4194 4195 } 4196 } 4197 4198 func TestBuildStoredAuctionResponses(t *testing.T) { 4199 4200 type testIn struct { 4201 StoredAuctionResponses map[string]json.RawMessage 4202 } 4203 type testResults struct { 4204 adapterBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid 4205 fledge *openrtb_ext.Fledge 4206 liveAdapters []openrtb_ext.BidderName 4207 } 4208 4209 testCases := []struct { 4210 desc string 4211 in testIn 4212 expected testResults 4213 errorMessage string 4214 }{ 4215 { 4216 desc: "Single imp with single stored response bid", 4217 in: testIn{ 4218 StoredAuctionResponses: map[string]json.RawMessage{ 4219 "impression-id": json.RawMessage(`[{"bid": [{"id": "bid_id", "ext": {"prebid": {"type": "native"}}}],"seat": "appnexus"}]`), 4220 }, 4221 }, 4222 expected: testResults{ 4223 adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ 4224 openrtb_ext.BidderName("appnexus"): { 4225 Bids: []*entities.PbsOrtbBid{ 4226 { 4227 Bid: &openrtb2.Bid{ID: "bid_id", ImpID: "impression-id", Ext: []byte(`{"prebid": {"type": "native"}}`)}, 4228 BidType: openrtb_ext.BidTypeNative, 4229 }, 4230 }, 4231 }, 4232 }, 4233 liveAdapters: []openrtb_ext.BidderName{openrtb_ext.BidderName("appnexus")}, 4234 }, 4235 }, 4236 { 4237 desc: "Single imp with single stored response bid with incorrect bid type", 4238 in: testIn{ 4239 StoredAuctionResponses: map[string]json.RawMessage{ 4240 "impression-id": json.RawMessage(`[{"bid": [{"id": "bid_id", "ext": {"prebid": {"type": "incorrect"}}}],"seat": "appnexus"}]`), 4241 }, 4242 }, 4243 expected: testResults{ 4244 adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ 4245 openrtb_ext.BidderName("appnexus"): { 4246 Bids: []*entities.PbsOrtbBid{ 4247 { 4248 Bid: &openrtb2.Bid{ID: "bid_id", ImpID: "impression-id", Ext: []byte(`{"prebid": {"type": "native"}}`)}, 4249 BidType: openrtb_ext.BidTypeNative, 4250 }, 4251 }, 4252 }, 4253 }, 4254 liveAdapters: []openrtb_ext.BidderName{openrtb_ext.BidderName("appnexus")}, 4255 }, 4256 errorMessage: "Failed to parse bid mediatype for impression \"impression-id\", invalid BidType: incorrect", 4257 }, 4258 { 4259 desc: "Single imp with multiple bids in stored response one bidder", 4260 in: testIn{ 4261 StoredAuctionResponses: map[string]json.RawMessage{ 4262 "impression-id": json.RawMessage(`[{"bid": [{"id": "bid_id1", "ext": {"prebid": {"type": "native"}}}, {"id": "bid_id2", "ext": {"prebid": {"type": "video"}}}],"seat": "appnexus"}]`), 4263 }, 4264 }, 4265 expected: testResults{ 4266 adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ 4267 openrtb_ext.BidderName("appnexus"): { 4268 Bids: []*entities.PbsOrtbBid{ 4269 {Bid: &openrtb2.Bid{ID: "bid_id1", ImpID: "impression-id", Ext: []byte(`{"prebid": {"type": "native"}}`)}, BidType: openrtb_ext.BidTypeNative}, 4270 {Bid: &openrtb2.Bid{ID: "bid_id2", ImpID: "impression-id", Ext: []byte(`{"prebid": {"type": "video"}}`)}, BidType: openrtb_ext.BidTypeVideo}, 4271 }, 4272 }, 4273 }, 4274 liveAdapters: []openrtb_ext.BidderName{openrtb_ext.BidderName("appnexus")}, 4275 }, 4276 }, 4277 { 4278 desc: "Single imp with multiple bids in stored response two bidders", 4279 in: testIn{ 4280 StoredAuctionResponses: map[string]json.RawMessage{ 4281 "impression-id": json.RawMessage(`[{"bid": [{"id": "apn_id1", "ext": {"prebid": {"type": "native"}}}, {"id": "apn_id2", "ext": {"prebid": {"type": "native"}}}],"seat": "appnexus"}, {"bid": [{"id": "rubicon_id1", "ext": {"prebid": {"type": "banner"}}}, {"id": "rubicon_id2", "ext": {"prebid": {"type": "banner"}}}],"seat": "rubicon"}]`), 4282 }, 4283 }, 4284 expected: testResults{ 4285 adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ 4286 openrtb_ext.BidderName("appnexus"): { 4287 Bids: []*entities.PbsOrtbBid{ 4288 {Bid: &openrtb2.Bid{ID: "apn_id1", ImpID: "impression-id", Ext: []byte(`{"prebid": {"type": "native"}}`)}, BidType: openrtb_ext.BidTypeNative}, 4289 {Bid: &openrtb2.Bid{ID: "apn_id2", ImpID: "impression-id", Ext: []byte(`{"prebid": {"type": "native"}}`)}, BidType: openrtb_ext.BidTypeNative}, 4290 }, 4291 }, 4292 openrtb_ext.BidderName("rubicon"): { 4293 Bids: []*entities.PbsOrtbBid{ 4294 {Bid: &openrtb2.Bid{ID: "rubicon_id1", ImpID: "impression-id", Ext: []byte(`{"prebid": {"type": "banner"}}`)}, BidType: openrtb_ext.BidTypeBanner}, 4295 {Bid: &openrtb2.Bid{ID: "rubicon_id2", ImpID: "impression-id", Ext: []byte(`{"prebid": {"type": "banner"}}`)}, BidType: openrtb_ext.BidTypeBanner}, 4296 }, 4297 }, 4298 }, 4299 liveAdapters: []openrtb_ext.BidderName{openrtb_ext.BidderName("appnexus"), openrtb_ext.BidderName("rubicon")}, 4300 }, 4301 }, 4302 { 4303 desc: "Two imps with two bids in stored response two bidders, different bids number", 4304 in: testIn{ 4305 StoredAuctionResponses: map[string]json.RawMessage{ 4306 "impression-id1": json.RawMessage(`[{"bid": [{"id": "apn_id1", "ext": {"prebid": {"type": "native"}}}, {"id": "apn_id2", "ext": {"prebid": {"type": "native"}}}],"seat": "appnexus"}]`), 4307 "impression-id2": json.RawMessage(`[{"bid": [{"id": "apn_id1", "ext": {"prebid": {"type": "native"}}}, {"id": "apn_id2", "ext": {"prebid": {"type": "native"}}}],"seat": "appnexus"}, {"bid": [{"id": "rubicon_id1", "ext": {"prebid": {"type": "native"}}}, {"id": "rubicon_id2", "ext": {"prebid": {"type": "native"}}}],"seat": "rubicon"}]`), 4308 }, 4309 }, 4310 expected: testResults{ 4311 adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ 4312 openrtb_ext.BidderName("appnexus"): { 4313 Bids: []*entities.PbsOrtbBid{ 4314 {Bid: &openrtb2.Bid{ID: "apn_id1", ImpID: "impression-id1", Ext: []byte(`{"prebid": {"type": "native"}}`)}, BidType: openrtb_ext.BidTypeNative}, 4315 {Bid: &openrtb2.Bid{ID: "apn_id2", ImpID: "impression-id1", Ext: []byte(`{"prebid": {"type": "native"}}`)}, BidType: openrtb_ext.BidTypeNative}, 4316 {Bid: &openrtb2.Bid{ID: "apn_id1", ImpID: "impression-id2", Ext: []byte(`{"prebid": {"type": "native"}}`)}, BidType: openrtb_ext.BidTypeNative}, 4317 {Bid: &openrtb2.Bid{ID: "apn_id2", ImpID: "impression-id2", Ext: []byte(`{"prebid": {"type": "native"}}`)}, BidType: openrtb_ext.BidTypeNative}, 4318 }, 4319 }, 4320 openrtb_ext.BidderName("rubicon"): { 4321 Bids: []*entities.PbsOrtbBid{ 4322 {Bid: &openrtb2.Bid{ID: "rubicon_id1", ImpID: "impression-id2", Ext: []byte(`{"prebid": {"type": "native"}}`)}, BidType: openrtb_ext.BidTypeNative}, 4323 {Bid: &openrtb2.Bid{ID: "rubicon_id2", ImpID: "impression-id2", Ext: []byte(`{"prebid": {"type": "native"}}`)}, BidType: openrtb_ext.BidTypeNative}, 4324 }, 4325 }, 4326 }, 4327 liveAdapters: []openrtb_ext.BidderName{openrtb_ext.BidderName("appnexus"), openrtb_ext.BidderName("rubicon")}, 4328 }, 4329 }, 4330 { 4331 desc: "Two imps with two bids in stored response two bidders", 4332 in: testIn{ 4333 StoredAuctionResponses: map[string]json.RawMessage{ 4334 "impression-id1": json.RawMessage(`[{"bid": [{"id": "apn_id1", "ext": {"prebid": {"type": "native"}}}, {"id": "apn_id2", "ext": {"prebid": {"type": "native"}}}],"seat": "appnexus"}, {"bid": [{"id": "rubicon_id1", "ext": {"prebid": {"type": "native"}}}, {"id": "rubicon_id2", "ext": {"prebid": {"type": "native"}}}],"seat": "rubicon"}]`), 4335 "impression-id2": json.RawMessage(`[{"bid": [{"id": "apn_id1", "ext": {"prebid": {"type": "native"}}}, {"id": "apn_id2", "ext": {"prebid": {"type": "native"}}}],"seat": "appnexus"}, {"bid": [{"id": "rubicon_id1", "ext": {"prebid": {"type": "native"}}}, {"id": "rubicon_id2", "ext": {"prebid": {"type": "native"}}}],"seat": "rubicon"}]`), 4336 }, 4337 }, 4338 expected: testResults{ 4339 adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ 4340 openrtb_ext.BidderName("appnexus"): { 4341 Bids: []*entities.PbsOrtbBid{ 4342 {Bid: &openrtb2.Bid{ID: "apn_id1", ImpID: "impression-id1", Ext: []byte(`{"prebid": {"type": "native"}}`)}, BidType: openrtb_ext.BidTypeNative}, 4343 {Bid: &openrtb2.Bid{ID: "apn_id2", ImpID: "impression-id1", Ext: []byte(`{"prebid": {"type": "native"}}`)}, BidType: openrtb_ext.BidTypeNative}, 4344 {Bid: &openrtb2.Bid{ID: "apn_id1", ImpID: "impression-id2", Ext: []byte(`{"prebid": {"type": "native"}}`)}, BidType: openrtb_ext.BidTypeNative}, 4345 {Bid: &openrtb2.Bid{ID: "apn_id2", ImpID: "impression-id2", Ext: []byte(`{"prebid": {"type": "native"}}`)}, BidType: openrtb_ext.BidTypeNative}, 4346 }, 4347 }, 4348 openrtb_ext.BidderName("rubicon"): { 4349 Bids: []*entities.PbsOrtbBid{ 4350 {Bid: &openrtb2.Bid{ID: "rubicon_id1", ImpID: "impression-id1", Ext: []byte(`{"prebid": {"type": "native"}}`)}, BidType: openrtb_ext.BidTypeNative}, 4351 {Bid: &openrtb2.Bid{ID: "rubicon_id2", ImpID: "impression-id1", Ext: []byte(`{"prebid": {"type": "native"}}`)}, BidType: openrtb_ext.BidTypeNative}, 4352 {Bid: &openrtb2.Bid{ID: "rubicon_id1", ImpID: "impression-id2", Ext: []byte(`{"prebid": {"type": "native"}}`)}, BidType: openrtb_ext.BidTypeNative}, 4353 {Bid: &openrtb2.Bid{ID: "rubicon_id2", ImpID: "impression-id2", Ext: []byte(`{"prebid": {"type": "native"}}`)}, BidType: openrtb_ext.BidTypeNative}, 4354 }, 4355 }, 4356 }, 4357 liveAdapters: []openrtb_ext.BidderName{openrtb_ext.BidderName("appnexus"), openrtb_ext.BidderName("rubicon")}, 4358 }, 4359 }, 4360 { 4361 desc: "Fledge in stored response bid", 4362 in: testIn{ 4363 StoredAuctionResponses: map[string]json.RawMessage{ 4364 "impression-id": json.RawMessage(`[{"bid": [],"seat": "openx", "ext": {"prebid": {"fledge": {"auctionconfigs": [{"impid": "1", "bidder": "openx", "adapter": "openx", "config": [1,2,3]}]}}}}]`), 4365 }, 4366 }, 4367 expected: testResults{ 4368 adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ 4369 openrtb_ext.BidderName("openx"): { 4370 Bids: []*entities.PbsOrtbBid{}, 4371 }, 4372 }, 4373 liveAdapters: []openrtb_ext.BidderName{openrtb_ext.BidderName("openx")}, 4374 fledge: &openrtb_ext.Fledge{ 4375 AuctionConfigs: []*openrtb_ext.FledgeAuctionConfig{ 4376 { 4377 ImpId: "impression-id", 4378 Bidder: "openx", 4379 Adapter: "openx", 4380 Config: json.RawMessage("[1,2,3]"), 4381 }, 4382 }, 4383 }, 4384 }, 4385 }, 4386 { 4387 desc: "Single imp with single stored response bid with bid.mtype", 4388 in: testIn{ 4389 StoredAuctionResponses: map[string]json.RawMessage{ 4390 "impression-id": json.RawMessage(`[{"bid": [{"id": "bid_id", "mtype": 2, "ext": {"prebid": {"type": "native"}}}],"seat": "appnexus"}]`), 4391 }, 4392 }, 4393 expected: testResults{ 4394 adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ 4395 openrtb_ext.BidderName("appnexus"): { 4396 Bids: []*entities.PbsOrtbBid{ 4397 { 4398 Bid: &openrtb2.Bid{ID: "bid_id", ImpID: "impression-id", MType: 2, Ext: []byte(`{"prebid": {"type": "native"}}`)}, 4399 BidType: openrtb_ext.BidTypeVideo, 4400 }, 4401 }, 4402 }, 4403 }, 4404 liveAdapters: []openrtb_ext.BidderName{openrtb_ext.BidderName("appnexus")}, 4405 }, 4406 }, 4407 { 4408 desc: "Multiple imps with multiple stored response bid with bid.mtype and different types", 4409 in: testIn{ 4410 StoredAuctionResponses: map[string]json.RawMessage{ 4411 "impression-id1": json.RawMessage(`[{"bid": [{"id": "bid_id", "mtype": 1, "ext": {"prebid": {"type": "native"}}}],"seat": "appnexus"}]`), 4412 "impression-id2": json.RawMessage(`[{"bid": [{"id": "bid_id", "mtype": 2, "ext": {"prebid": {"type": "native"}}}],"seat": "appnexus"}]`), 4413 "impression-id3": json.RawMessage(`[{"bid": [{"id": "bid_id", "mtype": 3, "ext": {"prebid": {"type": "native"}}}],"seat": "appnexus"}]`), 4414 "impression-id4": json.RawMessage(`[{"bid": [{"id": "bid_id", "mtype": 4, "ext": {"prebid": {"type": "native"}}}],"seat": "appnexus"}]`), 4415 }, 4416 }, 4417 expected: testResults{ 4418 adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ 4419 openrtb_ext.BidderName("appnexus"): { 4420 Bids: []*entities.PbsOrtbBid{ 4421 { 4422 Bid: &openrtb2.Bid{ID: "bid_id", ImpID: "impression-id1", MType: 1, Ext: []byte(`{"prebid": {"type": "native"}}`)}, 4423 BidType: openrtb_ext.BidTypeBanner, 4424 }, 4425 { 4426 Bid: &openrtb2.Bid{ID: "bid_id", ImpID: "impression-id2", MType: 2, Ext: []byte(`{"prebid": {"type": "native"}}`)}, 4427 BidType: openrtb_ext.BidTypeVideo, 4428 }, 4429 { 4430 Bid: &openrtb2.Bid{ID: "bid_id", ImpID: "impression-id3", MType: 3, Ext: []byte(`{"prebid": {"type": "native"}}`)}, 4431 BidType: openrtb_ext.BidTypeAudio, 4432 }, 4433 { 4434 Bid: &openrtb2.Bid{ID: "bid_id", ImpID: "impression-id4", MType: 4, Ext: []byte(`{"prebid": {"type": "native"}}`)}, 4435 BidType: openrtb_ext.BidTypeNative, 4436 }, 4437 }, 4438 }, 4439 }, 4440 liveAdapters: []openrtb_ext.BidderName{openrtb_ext.BidderName("appnexus")}, 4441 }, 4442 }, 4443 { 4444 desc: "Single imp with single stored response bid with incorrect bid.mtype", 4445 in: testIn{ 4446 StoredAuctionResponses: map[string]json.RawMessage{ 4447 "impression-id": json.RawMessage(`[{"bid": [{"id": "bid_id", "mtype": 10, "ext": {"prebid": {"type": "native"}}}],"seat": "appnexus"}]`), 4448 }, 4449 }, 4450 expected: testResults{ 4451 adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ 4452 openrtb_ext.BidderName("appnexus"): { 4453 Bids: []*entities.PbsOrtbBid{ 4454 { 4455 Bid: &openrtb2.Bid{ID: "bid_id", ImpID: "impression-id", MType: 2, Ext: []byte(`{"prebid": {"type": "native"}}`)}, 4456 BidType: openrtb_ext.BidTypeVideo, 4457 }, 4458 }, 4459 }, 4460 }, 4461 liveAdapters: []openrtb_ext.BidderName{openrtb_ext.BidderName("appnexus")}, 4462 }, 4463 errorMessage: "Failed to parse bid mType for impression \"impression-id\"", 4464 }, 4465 } 4466 for _, test := range testCases { 4467 4468 bids, fledge, adapters, err := buildStoredAuctionResponse(test.in.StoredAuctionResponses) 4469 if len(test.errorMessage) > 0 { 4470 assert.Equal(t, test.errorMessage, err.Error(), " incorrect expected error") 4471 } else { 4472 assert.NoErrorf(t, err, "%s. HoldAuction error: %v \n", test.desc, err) 4473 4474 assert.ElementsMatch(t, test.expected.liveAdapters, adapters, "Incorrect adapter list") 4475 assert.Equal(t, fledge, test.expected.fledge, "Incorrect FLEDGE response") 4476 4477 for _, bidderName := range test.expected.liveAdapters { 4478 assert.ElementsMatch(t, test.expected.adapterBids[bidderName].Bids, bids[bidderName].Bids, "Incorrect bids") 4479 } 4480 } 4481 } 4482 } 4483 4484 func TestAuctionDebugEnabled(t *testing.T) { 4485 categoriesFetcher, err := newCategoryFetcher("./test/category-mapping") 4486 assert.NoError(t, err, "error should be nil") 4487 e := new(exchange) 4488 e.cache = &wellBehavedCache{} 4489 e.me = &metricsConf.NilMetricsEngine{} 4490 e.categoriesFetcher = categoriesFetcher 4491 e.bidIDGenerator = &fakeBidIDGenerator{GenerateBidID: false, ReturnError: false} 4492 e.currencyConverter = currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) 4493 e.gdprPermsBuilder = fakePermissionsBuilder{ 4494 permissions: &permissionsMock{ 4495 allowAllBidders: true, 4496 }, 4497 }.Builder 4498 e.requestSplitter = requestSplitter{ 4499 me: e.me, 4500 gdprPermsBuilder: e.gdprPermsBuilder, 4501 } 4502 4503 ctx := context.Background() 4504 4505 bidRequest := &openrtb2.BidRequest{ 4506 ID: "some-request-id", 4507 Test: 1, 4508 } 4509 4510 auctionRequest := &AuctionRequest{ 4511 BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: bidRequest}, 4512 Account: config.Account{DebugAllow: false}, 4513 UserSyncs: &emptyUsersync{}, 4514 StartTime: time.Now(), 4515 RequestType: metrics.ReqTypeORTB2Web, 4516 HookExecutor: &hookexecution.EmptyHookExecutor{}, 4517 TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), 4518 } 4519 4520 debugLog := &DebugLog{DebugOverride: true, DebugEnabledOrOverridden: true} 4521 resp, err := e.HoldAuction(ctx, auctionRequest, debugLog) 4522 4523 assert.NoError(t, err, "error should be nil") 4524 4525 expectedResolvedRequest := `{"id":"some-request-id","imp":null,"test":1}` 4526 actualResolvedRequest, _, _, err := jsonparser.Get(resp.Ext, "debug", "resolvedrequest") 4527 assert.NoError(t, err, "error should be nil") 4528 assert.NotNil(t, actualResolvedRequest, "actualResolvedRequest should not be nil") 4529 assert.JSONEq(t, expectedResolvedRequest, string(actualResolvedRequest), "Resolved request is incorrect") 4530 4531 } 4532 4533 func TestPassExperimentConfigsToHoldAuction(t *testing.T) { 4534 noBidServer := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(204) } 4535 server := httptest.NewServer(http.HandlerFunc(noBidServer)) 4536 defer server.Close() 4537 4538 cfg := &config.Configuration{} 4539 4540 biddersInfo, err := config.LoadBidderInfoFromDisk("../static/bidder-info") 4541 if err != nil { 4542 t.Fatal(err) 4543 } 4544 biddersInfo["appnexus"] = config.BidderInfo{ 4545 Endpoint: "test.com", 4546 Capabilities: &config.CapabilitiesInfo{ 4547 Site: &config.PlatformInfo{ 4548 MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeBanner, openrtb_ext.BidTypeVideo}, 4549 }, 4550 }, 4551 Experiment: config.BidderInfoExperiment{AdsCert: config.BidderAdsCert{Enabled: true}}} 4552 4553 signer := MockSigner{} 4554 4555 adapters, adaptersErr := BuildAdapters(server.Client(), cfg, biddersInfo, &metricsConf.NilMetricsEngine{}) 4556 if adaptersErr != nil { 4557 t.Fatalf("Error intializing adapters: %v", adaptersErr) 4558 } 4559 4560 currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) 4561 4562 gdprPermsBuilder := fakePermissionsBuilder{ 4563 permissions: &permissionsMock{ 4564 allowAllBidders: true, 4565 }, 4566 }.Builder 4567 4568 e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &signer, macros.NewStringIndexBasedReplacer(), nil).(*exchange) 4569 4570 // Define mock incoming bid requeset 4571 mockBidRequest := &openrtb2.BidRequest{ 4572 ID: "some-request-id", 4573 Imp: []openrtb2.Imp{{ 4574 ID: "some-impression-id", 4575 Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}, {W: 300, H: 600}}}, 4576 Ext: json.RawMessage(`{"prebid":{"bidder":{"appnexus":{"placementId":1}}}}`), 4577 }}, 4578 Site: &openrtb2.Site{Page: "prebid.org", Ext: json.RawMessage(`{"amp":0}`)}, 4579 Ext: json.RawMessage(`{"prebid":{"experiment":{"adscert":{"enabled": true}}}}`), 4580 } 4581 4582 auctionRequest := &AuctionRequest{ 4583 BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: mockBidRequest}, 4584 Account: config.Account{}, 4585 UserSyncs: &emptyUsersync{}, 4586 HookExecutor: &hookexecution.EmptyHookExecutor{}, 4587 TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), 4588 } 4589 4590 debugLog := DebugLog{} 4591 _, err = e.HoldAuction(context.Background(), auctionRequest, &debugLog) 4592 4593 assert.NoError(t, err, "unexpected error occured") 4594 assert.Equal(t, "test.com", signer.data, "incorrect signer data") 4595 } 4596 4597 func TestCallSignHeader(t *testing.T) { 4598 type aTest struct { 4599 description string 4600 experiment openrtb_ext.Experiment 4601 bidderInfo config.BidderInfo 4602 expectedResult bool 4603 } 4604 var nilExperiment openrtb_ext.Experiment 4605 4606 testCases := []aTest{ 4607 { 4608 description: "both experiment.adsCert enabled for request and for bidder ", 4609 experiment: openrtb_ext.Experiment{AdsCert: &openrtb_ext.AdsCert{Enabled: true}}, 4610 bidderInfo: config.BidderInfo{Experiment: config.BidderInfoExperiment{AdsCert: config.BidderAdsCert{Enabled: true}}}, 4611 expectedResult: true, 4612 }, 4613 { 4614 description: "experiment is not defined in request, bidder config adsCert enabled", 4615 experiment: nilExperiment, 4616 bidderInfo: config.BidderInfo{Experiment: config.BidderInfoExperiment{AdsCert: config.BidderAdsCert{Enabled: true}}}, 4617 expectedResult: false, 4618 }, 4619 { 4620 description: "experiment.adsCert is not defined in request, bidder config adsCert enabled", 4621 experiment: openrtb_ext.Experiment{AdsCert: nil}, 4622 bidderInfo: config.BidderInfo{Experiment: config.BidderInfoExperiment{AdsCert: config.BidderAdsCert{Enabled: true}}}, 4623 expectedResult: false, 4624 }, 4625 { 4626 description: "experiment.adsCert is disabled in request, bidder config adsCert enabled", 4627 experiment: openrtb_ext.Experiment{AdsCert: &openrtb_ext.AdsCert{Enabled: false}}, 4628 bidderInfo: config.BidderInfo{Experiment: config.BidderInfoExperiment{AdsCert: config.BidderAdsCert{Enabled: true}}}, 4629 expectedResult: false, 4630 }, 4631 { 4632 description: "experiment.adsCert is enabled in request, bidder config adsCert disabled", 4633 experiment: openrtb_ext.Experiment{AdsCert: &openrtb_ext.AdsCert{Enabled: true}}, 4634 bidderInfo: config.BidderInfo{Experiment: config.BidderInfoExperiment{AdsCert: config.BidderAdsCert{Enabled: false}}}, 4635 expectedResult: false, 4636 }, 4637 { 4638 description: "experiment.adsCert is disabled in request, bidder config adsCert disabled", 4639 experiment: openrtb_ext.Experiment{AdsCert: &openrtb_ext.AdsCert{Enabled: false}}, 4640 bidderInfo: config.BidderInfo{Experiment: config.BidderInfoExperiment{AdsCert: config.BidderAdsCert{Enabled: false}}}, 4641 expectedResult: false, 4642 }, 4643 } 4644 for _, test := range testCases { 4645 result := isAdsCertEnabled(&test.experiment, test.bidderInfo) 4646 assert.Equal(t, test.expectedResult, result, "incorrect result returned") 4647 } 4648 4649 } 4650 4651 func TestValidateBannerCreativeSize(t *testing.T) { 4652 exchange := exchange{bidValidationEnforcement: config.Validations{MaxCreativeWidth: 100, MaxCreativeHeight: 100}, 4653 me: metricsConf.NewMetricsEngine(&config.Configuration{}, openrtb_ext.CoreBidderNames(), nil, nil), 4654 } 4655 testCases := []struct { 4656 description string 4657 givenBid *entities.PbsOrtbBid 4658 givenBidResponseExt *openrtb_ext.ExtBidResponse 4659 givenBidderName string 4660 givenPubID string 4661 expectedBannerCreativeValid bool 4662 }{ 4663 { 4664 description: "The dimensions are invalid, both values bigger than the max", 4665 givenBid: &entities.PbsOrtbBid{Bid: &openrtb2.Bid{W: 200, H: 200}}, 4666 givenBidResponseExt: &openrtb_ext.ExtBidResponse{Errors: make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage)}, 4667 givenBidderName: "bidder", 4668 givenPubID: "1", 4669 expectedBannerCreativeValid: false, 4670 }, 4671 { 4672 description: "The width is invalid, height is valid, the dimensions as a whole are invalid", 4673 givenBid: &entities.PbsOrtbBid{Bid: &openrtb2.Bid{W: 200, H: 50}}, 4674 givenBidResponseExt: &openrtb_ext.ExtBidResponse{Errors: make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage)}, 4675 givenBidderName: "bidder", 4676 givenPubID: "1", 4677 expectedBannerCreativeValid: false, 4678 }, 4679 { 4680 description: "The width is valid, height is invalid, the dimensions as a whole are invalid", 4681 givenBid: &entities.PbsOrtbBid{Bid: &openrtb2.Bid{W: 50, H: 200}}, 4682 givenBidResponseExt: &openrtb_ext.ExtBidResponse{Errors: make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage)}, 4683 givenBidderName: "bidder", 4684 givenPubID: "1", 4685 expectedBannerCreativeValid: false, 4686 }, 4687 { 4688 description: "Both width and height are valid, the dimensions are valid", 4689 givenBid: &entities.PbsOrtbBid{Bid: &openrtb2.Bid{W: 50, H: 50}}, 4690 givenBidResponseExt: &openrtb_ext.ExtBidResponse{Errors: make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage)}, 4691 givenBidderName: "bidder", 4692 givenPubID: "1", 4693 expectedBannerCreativeValid: true, 4694 }, 4695 } 4696 for _, test := range testCases { 4697 acutalBannerCreativeValid := exchange.validateBannerCreativeSize(test.givenBid, test.givenBidResponseExt, openrtb_ext.BidderName(test.givenBidderName), test.givenPubID, "enforce") 4698 assert.Equal(t, test.expectedBannerCreativeValid, acutalBannerCreativeValid) 4699 } 4700 } 4701 4702 func TestValidateBidAdM(t *testing.T) { 4703 exchange := exchange{bidValidationEnforcement: config.Validations{MaxCreativeWidth: 100, MaxCreativeHeight: 100}, 4704 me: metricsConf.NewMetricsEngine(&config.Configuration{}, openrtb_ext.CoreBidderNames(), nil, nil), 4705 } 4706 testCases := []struct { 4707 description string 4708 givenBid *entities.PbsOrtbBid 4709 givenBidResponseExt *openrtb_ext.ExtBidResponse 4710 givenBidderName string 4711 givenPubID string 4712 expectedBidAdMValid bool 4713 }{ 4714 { 4715 description: "The adm of the bid contains insecure string and no secure string, adm is invalid", 4716 givenBid: &entities.PbsOrtbBid{Bid: &openrtb2.Bid{AdM: "http://domain.com/invalid"}}, 4717 givenBidResponseExt: &openrtb_ext.ExtBidResponse{Errors: make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage)}, 4718 givenBidderName: "bidder", 4719 givenPubID: "1", 4720 expectedBidAdMValid: false, 4721 }, 4722 { 4723 description: "The adm has both an insecure and secure string defined and therefore the adm is valid", 4724 givenBid: &entities.PbsOrtbBid{Bid: &openrtb2.Bid{AdM: "http://www.foo.com https://www.bar.com"}}, 4725 givenBidResponseExt: &openrtb_ext.ExtBidResponse{Errors: make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage)}, 4726 givenBidderName: "bidder", 4727 givenPubID: "1", 4728 expectedBidAdMValid: true, 4729 }, 4730 { 4731 description: "The adm has both an insecure and secure string defined and therefore the adm is valid", 4732 givenBid: &entities.PbsOrtbBid{Bid: &openrtb2.Bid{AdM: "http%3A//www.foo.com https%3A//www.bar.com"}}, 4733 givenBidResponseExt: &openrtb_ext.ExtBidResponse{Errors: make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage)}, 4734 givenBidderName: "bidder", 4735 givenPubID: "1", 4736 expectedBidAdMValid: true, 4737 }, 4738 { 4739 description: "The adm of the bid are valid with a secure string", 4740 givenBid: &entities.PbsOrtbBid{Bid: &openrtb2.Bid{AdM: "https://domain.com/valid"}}, 4741 givenBidResponseExt: &openrtb_ext.ExtBidResponse{Errors: make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage)}, 4742 givenBidderName: "bidder", 4743 givenPubID: "1", 4744 expectedBidAdMValid: true, 4745 }, 4746 } 4747 for _, test := range testCases { 4748 actualBidAdMValid := exchange.validateBidAdM(test.givenBid, test.givenBidResponseExt, openrtb_ext.BidderName(test.givenBidderName), test.givenPubID, "enforce") 4749 assert.Equal(t, test.expectedBidAdMValid, actualBidAdMValid) 4750 4751 } 4752 } 4753 4754 func TestMakeBidWithValidation(t *testing.T) { 4755 sampleAd := "<?xml version=\"1.0\" encoding=\"UTF-8\"?><VAST ...></VAST>" 4756 sampleOpenrtbBid := &openrtb2.Bid{ID: "some-bid-id", AdM: sampleAd} 4757 4758 testCases := []struct { 4759 name string 4760 givenBidRequestExt json.RawMessage 4761 givenValidations config.Validations 4762 givenBids []*entities.PbsOrtbBid 4763 givenSeat openrtb_ext.BidderName 4764 expectedNumOfBids int 4765 expectedNonBids *nonBids 4766 expectedNumDebugErrors int 4767 expectedNumDebugWarnings int 4768 }{ 4769 { 4770 name: "One_of_two_bids_is_invalid_based_on_DSA_object_presence", 4771 givenBidRequestExt: json.RawMessage(`{"dsa": {"dsarequired": 2}}`), 4772 givenValidations: config.Validations{}, 4773 givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{Ext: json.RawMessage(`{"dsa": {"adrender":1}}`)}}, {Bid: &openrtb2.Bid{}}}, 4774 givenSeat: "pubmatic", 4775 expectedNumOfBids: 1, 4776 expectedNonBids: &nonBids{ 4777 seatNonBidsMap: map[string][]openrtb_ext.NonBid{ 4778 "pubmatic": { 4779 { 4780 StatusCode: 300, 4781 Ext: openrtb_ext.NonBidExt{ 4782 Prebid: openrtb_ext.ExtResponseNonBidPrebid{ 4783 Bid: openrtb_ext.NonBidObject{}, 4784 }, 4785 }, 4786 }, 4787 }, 4788 }, 4789 }, 4790 expectedNumDebugWarnings: 1, 4791 }, 4792 { 4793 name: "Creative_size_validation_enforced,_one_of_two_bids_has_invalid_dimensions", 4794 givenValidations: config.Validations{BannerCreativeMaxSize: config.ValidationEnforce, MaxCreativeWidth: 100, MaxCreativeHeight: 100}, 4795 givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{W: 200, H: 200}, BidType: openrtb_ext.BidTypeBanner}, {Bid: &openrtb2.Bid{W: 50, H: 50}, BidType: openrtb_ext.BidTypeBanner}}, 4796 givenSeat: "pubmatic", 4797 expectedNumOfBids: 1, 4798 expectedNonBids: &nonBids{ 4799 seatNonBidsMap: map[string][]openrtb_ext.NonBid{ 4800 "pubmatic": { 4801 { 4802 StatusCode: 351, 4803 Ext: openrtb_ext.NonBidExt{ 4804 Prebid: openrtb_ext.ExtResponseNonBidPrebid{ 4805 Bid: openrtb_ext.NonBidObject{ 4806 W: 200, 4807 H: 200, 4808 }, 4809 }, 4810 }, 4811 }, 4812 }, 4813 }, 4814 }, 4815 expectedNumDebugErrors: 1, 4816 }, 4817 { 4818 name: "Creative_size_validation_warned,_one_of_two_bids_has_invalid_dimensions", 4819 givenValidations: config.Validations{BannerCreativeMaxSize: config.ValidationWarn, MaxCreativeWidth: 100, MaxCreativeHeight: 100}, 4820 givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{W: 200, H: 200}, BidType: openrtb_ext.BidTypeBanner}, {Bid: &openrtb2.Bid{W: 50, H: 50}, BidType: openrtb_ext.BidTypeBanner}}, 4821 givenSeat: "pubmatic", 4822 expectedNumOfBids: 2, 4823 expectedNonBids: &nonBids{}, 4824 expectedNumDebugErrors: 1, 4825 }, 4826 { 4827 name: "AdM_validation_enforced,_one_of_two_bids_has_invalid_AdM", 4828 givenValidations: config.Validations{SecureMarkup: config.ValidationEnforce}, 4829 givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{AdM: "http://domain.com/invalid", ImpID: "1"}, BidType: openrtb_ext.BidTypeBanner}, {Bid: &openrtb2.Bid{AdM: "https://domain.com/valid", ImpID: "2"}, BidType: openrtb_ext.BidTypeBanner}}, 4830 givenSeat: "pubmatic", 4831 expectedNumOfBids: 1, 4832 expectedNonBids: &nonBids{ 4833 seatNonBidsMap: map[string][]openrtb_ext.NonBid{ 4834 "pubmatic": { 4835 { 4836 ImpId: "1", 4837 StatusCode: 352, 4838 }, 4839 }, 4840 }, 4841 }, 4842 expectedNumDebugErrors: 1, 4843 }, 4844 { 4845 name: "AdM_validation_warned,_one_of_two_bids_has_invalid_AdM", 4846 givenValidations: config.Validations{SecureMarkup: config.ValidationWarn}, 4847 givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{AdM: "http://domain.com/invalid", ImpID: "1"}, BidType: openrtb_ext.BidTypeBanner}, {Bid: &openrtb2.Bid{AdM: "https://domain.com/valid", ImpID: "2"}, BidType: openrtb_ext.BidTypeBanner}}, 4848 givenSeat: "pubmatic", 4849 expectedNumOfBids: 2, 4850 expectedNonBids: &nonBids{}, 4851 expectedNumDebugErrors: 1, 4852 }, 4853 { 4854 name: "Adm_validation_skipped,_creative_size_validation_enforced,_one_of_two_bids_has_invalid_AdM", 4855 givenValidations: config.Validations{SecureMarkup: config.ValidationSkip, BannerCreativeMaxSize: config.ValidationEnforce}, 4856 givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{AdM: "http://domain.com/invalid"}, BidType: openrtb_ext.BidTypeBanner}, {Bid: &openrtb2.Bid{AdM: "https://domain.com/valid"}, BidType: openrtb_ext.BidTypeBanner}}, 4857 givenSeat: "pubmatic", 4858 expectedNumOfBids: 2, 4859 expectedNonBids: &nonBids{}, 4860 }, 4861 { 4862 name: "Creative_size_validation_skipped,_Adm_Validation_enforced,_one_of_two_bids_has_invalid_dimensions", 4863 givenValidations: config.Validations{BannerCreativeMaxSize: config.ValidationSkip, MaxCreativeWidth: 100, MaxCreativeHeight: 100}, 4864 givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{W: 200, H: 200}, BidType: openrtb_ext.BidTypeBanner}, {Bid: &openrtb2.Bid{W: 50, H: 50}, BidType: openrtb_ext.BidTypeBanner}}, 4865 givenSeat: "pubmatic", 4866 expectedNumOfBids: 2, 4867 expectedNonBids: &nonBids{}, 4868 }, 4869 } 4870 4871 // Test set up 4872 sampleAuction := &auction{cacheIds: map[*openrtb2.Bid]string{sampleOpenrtbBid: "CACHE_UUID_1234"}} 4873 4874 noBidHandler := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(204) } 4875 server := httptest.NewServer(http.HandlerFunc(noBidHandler)) 4876 defer server.Close() 4877 4878 bidderImpl := &goodSingleBidder{ 4879 httpRequest: &adapters.RequestData{ 4880 Method: "POST", 4881 Uri: server.URL, 4882 Body: []byte("{\"key\":\"val\"}"), 4883 Headers: http.Header{}, 4884 }, 4885 bidResponse: &adapters.BidderResponse{}, 4886 } 4887 e := new(exchange) 4888 e.adapterMap = map[openrtb_ext.BidderName]AdaptedBidder{ 4889 openrtb_ext.BidderAppnexus: AdaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderAppnexus, nil, ""), 4890 } 4891 e.cache = &wellBehavedCache{} 4892 e.me = &metricsConf.NilMetricsEngine{} 4893 4894 e.currencyConverter = currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) 4895 4896 ImpExtInfoMap := make(map[string]ImpExtInfo) 4897 ImpExtInfoMap["1"] = ImpExtInfo{} 4898 ImpExtInfoMap["2"] = ImpExtInfo{} 4899 4900 //Run tests 4901 for _, test := range testCases { 4902 t.Run(test.name, func(t *testing.T) { 4903 bidExtResponse := &openrtb_ext.ExtBidResponse{ 4904 Errors: make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage), 4905 Warnings: make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage), 4906 } 4907 bidRequest := &openrtb_ext.RequestWrapper{ 4908 BidRequest: &openrtb2.BidRequest{ 4909 Regs: &openrtb2.Regs{ 4910 Ext: test.givenBidRequestExt, 4911 }, 4912 }, 4913 } 4914 e.bidValidationEnforcement = test.givenValidations 4915 sampleBids := test.givenBids 4916 nonBids := &nonBids{} 4917 resultingBids, resultingErrs := e.makeBid(sampleBids, sampleAuction, true, ImpExtInfoMap, bidRequest, bidExtResponse, test.givenSeat, "", nonBids) 4918 4919 assert.Equal(t, 0, len(resultingErrs)) 4920 assert.Equal(t, test.expectedNumOfBids, len(resultingBids)) 4921 assert.Equal(t, test.expectedNonBids, nonBids) 4922 assert.Equal(t, test.expectedNumDebugErrors, len(bidExtResponse.Errors)) 4923 assert.Equal(t, test.expectedNumDebugWarnings, len(bidExtResponse.Warnings)) 4924 }) 4925 } 4926 } 4927 4928 func TestSetBidValidationStatus(t *testing.T) { 4929 testCases := []struct { 4930 description string 4931 givenHost config.Validations 4932 givenAccount config.Validations 4933 expected config.Validations 4934 }{ 4935 { 4936 description: "Host configuration is different than account, account setting should be preferred (enforce)", 4937 givenHost: config.Validations{BannerCreativeMaxSize: config.ValidationSkip, SecureMarkup: config.ValidationSkip}, 4938 givenAccount: config.Validations{BannerCreativeMaxSize: config.ValidationEnforce, SecureMarkup: config.ValidationEnforce}, 4939 expected: config.Validations{BannerCreativeMaxSize: config.ValidationEnforce, SecureMarkup: config.ValidationSkip}, 4940 }, 4941 { 4942 description: "Host configuration is different than account, account setting should be preferred (warn)", 4943 givenHost: config.Validations{BannerCreativeMaxSize: config.ValidationEnforce, SecureMarkup: config.ValidationEnforce}, 4944 givenAccount: config.Validations{BannerCreativeMaxSize: config.ValidationWarn, SecureMarkup: config.ValidationWarn}, 4945 expected: config.Validations{BannerCreativeMaxSize: config.ValidationWarn, SecureMarkup: config.ValidationEnforce}, 4946 }, 4947 { 4948 description: "Host configuration is different than account, account setting should be preferred (skip)", 4949 givenHost: config.Validations{BannerCreativeMaxSize: config.ValidationWarn, SecureMarkup: config.ValidationWarn}, 4950 givenAccount: config.Validations{BannerCreativeMaxSize: config.ValidationSkip, SecureMarkup: config.ValidationSkip}, 4951 expected: config.Validations{BannerCreativeMaxSize: config.ValidationSkip, SecureMarkup: config.ValidationWarn}, 4952 }, 4953 { 4954 description: "No account confiugration given, host confg should be preferred", 4955 givenHost: config.Validations{BannerCreativeMaxSize: config.ValidationSkip, SecureMarkup: config.ValidationSkip}, 4956 givenAccount: config.Validations{}, 4957 expected: config.Validations{BannerCreativeMaxSize: config.ValidationSkip, SecureMarkup: config.ValidationSkip}, 4958 }, 4959 } 4960 for _, test := range testCases { 4961 test.givenHost.SetBannerCreativeMaxSize(test.givenAccount) 4962 assert.Equal(t, test.expected, test.givenHost) 4963 } 4964 } 4965 4966 /* 4967 TestOverrideConfigAlternateBidderCodesWithRequestValues makes sure that the correct alternabiddercodes list is forwarded to the adapters and only the approved bids are returned in auction response. 4968 4969 1. request.ext.prebid.alternatebiddercodes has priority over the content of config.Account.Alternatebiddercodes. 4970 4971 2. request is updated with config.Account.Alternatebiddercodes values if request.ext.prebid.alternatebiddercodes is empty or not specified. 4972 4973 3. request.ext.prebid.alternatebiddercodes is given priority over config.Account.Alternatebiddercodes if both are specified. 4974 */ 4975 func TestOverrideConfigAlternateBidderCodesWithRequestValues(t *testing.T) { 4976 type testIn struct { 4977 config config.Configuration 4978 requestExt json.RawMessage 4979 } 4980 type testResults struct { 4981 expectedSeats []string 4982 } 4983 4984 testCases := []struct { 4985 desc string 4986 in testIn 4987 expected testResults 4988 }{ 4989 { 4990 desc: "alternatebiddercode defined neither in config nor in the request", 4991 in: testIn{ 4992 config: config.Configuration{}, 4993 }, 4994 expected: testResults{ 4995 expectedSeats: []string{"pubmatic"}, 4996 }, 4997 }, 4998 { 4999 desc: "alternatebiddercode defined in config and not in request", 5000 in: testIn{ 5001 config: config.Configuration{ 5002 AccountDefaults: config.Account{ 5003 AlternateBidderCodes: &openrtb_ext.ExtAlternateBidderCodes{ 5004 Enabled: true, 5005 Bidders: map[string]openrtb_ext.ExtAdapterAlternateBidderCodes{ 5006 "pubmatic": { 5007 Enabled: true, 5008 AllowedBidderCodes: []string{"groupm"}, 5009 }, 5010 }, 5011 }, 5012 }, 5013 }, 5014 requestExt: json.RawMessage(`{}`), 5015 }, 5016 expected: testResults{ 5017 expectedSeats: []string{"pubmatic", "groupm"}, 5018 }, 5019 }, 5020 { 5021 desc: "alternatebiddercode defined in request and not in config", 5022 in: testIn{ 5023 requestExt: json.RawMessage(`{"prebid": {"alternatebiddercodes": {"enabled": true, "bidders": {"pubmatic": {"enabled": true, "allowedbiddercodes": ["appnexus"]}}}}}`), 5024 }, 5025 expected: testResults{ 5026 expectedSeats: []string{"pubmatic", "appnexus"}, 5027 }, 5028 }, 5029 { 5030 desc: "alternatebiddercode defined in both config and in request", 5031 in: testIn{ 5032 config: config.Configuration{ 5033 AccountDefaults: config.Account{ 5034 AlternateBidderCodes: &openrtb_ext.ExtAlternateBidderCodes{ 5035 Enabled: true, 5036 Bidders: map[string]openrtb_ext.ExtAdapterAlternateBidderCodes{ 5037 "pubmatic": { 5038 Enabled: true, 5039 AllowedBidderCodes: []string{"groupm"}, 5040 }, 5041 }, 5042 }, 5043 }, 5044 }, 5045 requestExt: json.RawMessage(`{"prebid": {"alternatebiddercodes": {"enabled": true, "bidders": {"pubmatic": {"enabled": true, "allowedbiddercodes": ["ix"]}}}}}`), 5046 }, 5047 expected: testResults{ 5048 expectedSeats: []string{"pubmatic", "ix"}, 5049 }, 5050 }, 5051 } 5052 5053 // Init an exchange to run an auction from 5054 noBidServer := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(204) } 5055 mockPubMaticBidService := httptest.NewServer(http.HandlerFunc(noBidServer)) 5056 defer mockPubMaticBidService.Close() 5057 5058 categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") 5059 if error != nil { 5060 t.Errorf("Failed to create a category Fetcher: %v", error) 5061 } 5062 5063 mockBidderRequestResponse := &goodSingleBidder{ 5064 httpRequest: &adapters.RequestData{ 5065 Method: "POST", 5066 Uri: mockPubMaticBidService.URL, 5067 Body: []byte("{\"key\":\"val\"}"), 5068 Headers: http.Header{}, 5069 }, 5070 bidResponse: &adapters.BidderResponse{ 5071 Bids: []*adapters.TypedBid{ 5072 {Bid: &openrtb2.Bid{ID: "1"}, Seat: ""}, 5073 {Bid: &openrtb2.Bid{ID: "2"}, Seat: "pubmatic"}, 5074 {Bid: &openrtb2.Bid{ID: "3"}, Seat: "appnexus"}, 5075 {Bid: &openrtb2.Bid{ID: "4"}, Seat: "groupm"}, 5076 {Bid: &openrtb2.Bid{ID: "5"}, Seat: "ix"}, 5077 }, 5078 Currency: "USD", 5079 }, 5080 } 5081 5082 e := new(exchange) 5083 e.cache = &wellBehavedCache{} 5084 e.me = &metricsConf.NilMetricsEngine{} 5085 e.gdprPermsBuilder = fakePermissionsBuilder{ 5086 permissions: &permissionsMock{ 5087 allowAllBidders: true, 5088 }, 5089 }.Builder 5090 e.currencyConverter = currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) 5091 e.categoriesFetcher = categoriesFetcher 5092 e.bidIDGenerator = &fakeBidIDGenerator{GenerateBidID: false, ReturnError: false} 5093 e.requestSplitter = requestSplitter{ 5094 me: e.me, 5095 gdprPermsBuilder: e.gdprPermsBuilder, 5096 } 5097 5098 // Define mock incoming bid requeset 5099 mockBidRequest := &openrtb2.BidRequest{ 5100 ID: "some-request-id", 5101 Imp: []openrtb2.Imp{{ 5102 ID: "some-impression-id", 5103 Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}, {W: 300, H: 600}}}, 5104 Ext: json.RawMessage(`{"prebid":{"bidder":{"pubmatic": {"publisherId": 1}}}}`), 5105 }}, 5106 Site: &openrtb2.Site{Page: "prebid.org", Ext: json.RawMessage(`{"amp":0}`)}, 5107 } 5108 5109 // Run tests 5110 for _, test := range testCases { 5111 e.adapterMap = map[openrtb_ext.BidderName]AdaptedBidder{ 5112 openrtb_ext.BidderPubmatic: AdaptBidder(mockBidderRequestResponse, mockPubMaticBidService.Client(), &test.in.config, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderPubmatic, nil, ""), 5113 } 5114 5115 mockBidRequest.Ext = test.in.requestExt 5116 5117 auctionRequest := &AuctionRequest{ 5118 BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: mockBidRequest}, 5119 Account: test.in.config.AccountDefaults, 5120 UserSyncs: &emptyUsersync{}, 5121 HookExecutor: &hookexecution.EmptyHookExecutor{}, 5122 TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), 5123 } 5124 5125 // Run test 5126 outBidResponse, err := e.HoldAuction(context.Background(), auctionRequest, &DebugLog{}) 5127 5128 // Assertions 5129 assert.NoErrorf(t, err, "%s. HoldAuction error: %v \n", test.desc, err) 5130 assert.NotNil(t, outBidResponse) 5131 assert.False(t, auctionRequest.BidderResponseStartTime.IsZero()) 5132 5133 // So 2 seatBids are expected as, 5134 // the default "" and "pubmatic" bids will be in one seat and the extra-bids "groupm"/"appnexus"/"ix" in another seat. 5135 assert.Len(t, outBidResponse.SeatBid, len(test.expected.expectedSeats), "%s. seatbid count miss-match\n", test.desc) 5136 5137 for i, seatBid := range outBidResponse.SeatBid { 5138 assert.Contains(t, test.expected.expectedSeats, seatBid.Seat, "%s. unexpected seatbid\n", test.desc) 5139 5140 if seatBid.Seat == string(openrtb_ext.BidderPubmatic) { 5141 assert.Len(t, outBidResponse.SeatBid[i].Bid, 2, "%s. unexpected bid count\n", test.desc) 5142 } else { 5143 assert.Len(t, outBidResponse.SeatBid[i].Bid, 1, "%s. unexpected bid count\n", test.desc) 5144 } 5145 } 5146 } 5147 } 5148 5149 func TestGetAllBids(t *testing.T) { 5150 noBidServer := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) } 5151 server := httptest.NewServer(http.HandlerFunc(noBidServer)) 5152 defer server.Close() 5153 5154 type testIn struct { 5155 bidderRequests []BidderRequest 5156 bidAdjustments map[string]float64 5157 conversions currency.Conversions 5158 accountDebugAllowed bool 5159 globalPrivacyControlHeader string 5160 headerDebugAllowed bool 5161 alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes 5162 experiment *openrtb_ext.Experiment 5163 hookExecutor hookexecution.StageExecutor 5164 pbsRequestStartTime time.Time 5165 bidAdjustmentRules map[string][]openrtb_ext.Adjustment 5166 tmaxAdjustments *TmaxAdjustmentsPreprocessed 5167 adapterMap map[openrtb_ext.BidderName]AdaptedBidder 5168 } 5169 type testResults struct { 5170 adapterBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid 5171 adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra 5172 extraRespInfo extraAuctionResponseInfo 5173 } 5174 testCases := []struct { 5175 desc string 5176 in testIn 5177 expected testResults 5178 }{ 5179 { 5180 desc: "alternateBidderCodes-config-absent: pubmatic bidder returns bids with 'pubmatic' and 'groupm' seats", 5181 in: testIn{ 5182 bidderRequests: []BidderRequest{ 5183 { 5184 BidderName: "pubmatic", 5185 BidderCoreName: "pubmatic", 5186 BidRequest: &openrtb2.BidRequest{ 5187 ID: "some-request-id", 5188 Imp: []openrtb2.Imp{{ 5189 ID: "some-impression-id", 5190 }}, 5191 }, 5192 }, 5193 }, 5194 conversions: ¤cy.ConstantRates{}, 5195 hookExecutor: hookexecution.EmptyHookExecutor{}, 5196 pbsRequestStartTime: time.Now(), 5197 adapterMap: map[openrtb_ext.BidderName]AdaptedBidder{ 5198 openrtb_ext.BidderPubmatic: AdaptBidder(&goodSingleBidder{ 5199 httpRequest: &adapters.RequestData{ 5200 Method: "POST", 5201 Uri: server.URL, 5202 }, 5203 bidResponse: &adapters.BidderResponse{ 5204 Bids: []*adapters.TypedBid{ 5205 {Bid: &openrtb2.Bid{ID: "1"}, Seat: "pubmatic"}, 5206 {Bid: &openrtb2.Bid{ID: "2"}, Seat: "groupm"}, 5207 }, 5208 }, 5209 }, server.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderPubmatic, nil, ""), 5210 }, 5211 }, 5212 expected: testResults{ 5213 extraRespInfo: extraAuctionResponseInfo{ 5214 bidsFound: true, 5215 }, 5216 adapterExtra: map[openrtb_ext.BidderName]*seatResponseExtra{ 5217 "pubmatic": { 5218 Warnings: []openrtb_ext.ExtBidderMessage{ 5219 { 5220 Code: errortypes.AlternateBidderCodeWarningCode, 5221 Message: `alternateBidderCodes disabled for "pubmatic", rejecting bids for "groupm"`, 5222 }, 5223 }, 5224 }, 5225 }, 5226 adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ 5227 "pubmatic": { 5228 Bids: []*entities.PbsOrtbBid{ 5229 { 5230 Bid: &openrtb2.Bid{ 5231 ID: "1", 5232 }, 5233 OriginalBidCur: "USD", 5234 AdapterCode: openrtb_ext.BidderPubmatic, 5235 }, 5236 }, 5237 Currency: "USD", 5238 Seat: "pubmatic", 5239 HttpCalls: []*openrtb_ext.ExtHttpCall{}, 5240 }, 5241 }, 5242 }, 5243 }, 5244 { 5245 desc: "alternateBidderCodes-enabled: pubmatic bidder returns bids with 'pubmatic' and 'groupm' seats", 5246 in: testIn{ 5247 bidderRequests: []BidderRequest{ 5248 { 5249 BidderName: "pubmatic", 5250 BidderCoreName: "pubmatic", 5251 BidRequest: &openrtb2.BidRequest{ 5252 ID: "some-request-id", 5253 Imp: []openrtb2.Imp{{ 5254 ID: "some-impression-id", 5255 }}, 5256 }, 5257 }, 5258 }, 5259 conversions: ¤cy.ConstantRates{}, 5260 alternateBidderCodes: openrtb_ext.ExtAlternateBidderCodes{ 5261 Enabled: true, 5262 Bidders: map[string]openrtb_ext.ExtAdapterAlternateBidderCodes{ 5263 "pubmatic": { 5264 Enabled: true, 5265 AllowedBidderCodes: []string{"groupm"}, 5266 }, 5267 }, 5268 }, 5269 hookExecutor: hookexecution.EmptyHookExecutor{}, 5270 pbsRequestStartTime: time.Now(), 5271 adapterMap: map[openrtb_ext.BidderName]AdaptedBidder{ 5272 openrtb_ext.BidderPubmatic: AdaptBidder(&goodSingleBidder{ 5273 httpRequest: &adapters.RequestData{ 5274 Method: "POST", 5275 Uri: server.URL, 5276 }, 5277 bidResponse: &adapters.BidderResponse{ 5278 Bids: []*adapters.TypedBid{ 5279 {Bid: &openrtb2.Bid{ID: "1"}, Seat: "pubmatic"}, 5280 {Bid: &openrtb2.Bid{ID: "2"}, Seat: "groupm"}, 5281 }, 5282 }, 5283 }, server.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderPubmatic, nil, ""), 5284 }, 5285 }, 5286 expected: testResults{ 5287 extraRespInfo: extraAuctionResponseInfo{ 5288 bidsFound: true, 5289 }, 5290 adapterExtra: map[openrtb_ext.BidderName]*seatResponseExtra{ 5291 "pubmatic": { 5292 Warnings: []openrtb_ext.ExtBidderMessage{}, 5293 }, 5294 }, 5295 adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ 5296 "pubmatic": { 5297 Bids: []*entities.PbsOrtbBid{ 5298 { 5299 Bid: &openrtb2.Bid{ 5300 ID: "1", 5301 }, 5302 OriginalBidCur: "USD", 5303 AdapterCode: openrtb_ext.BidderPubmatic, 5304 }, 5305 }, 5306 Currency: "USD", 5307 Seat: "pubmatic", 5308 HttpCalls: []*openrtb_ext.ExtHttpCall{}, 5309 }, 5310 "groupm": { 5311 Bids: []*entities.PbsOrtbBid{ 5312 { 5313 Bid: &openrtb2.Bid{ 5314 ID: "2", 5315 }, 5316 OriginalBidCur: "USD", 5317 AdapterCode: openrtb_ext.BidderPubmatic, 5318 }, 5319 }, 5320 Currency: "USD", 5321 Seat: "groupm", 5322 HttpCalls: []*openrtb_ext.ExtHttpCall{}, 5323 }, 5324 }, 5325 }, 5326 }, 5327 { 5328 desc: "alternateBidderCodes-enabled: pubmatic bidder returns bids with only 'groupm' seat", 5329 in: testIn{ 5330 bidderRequests: []BidderRequest{ 5331 { 5332 BidderName: "pubmatic", 5333 BidderCoreName: "pubmatic", 5334 BidRequest: &openrtb2.BidRequest{ 5335 ID: "some-request-id", 5336 Imp: []openrtb2.Imp{{ 5337 ID: "some-impression-id", 5338 }}, 5339 }, 5340 }, 5341 }, 5342 conversions: ¤cy.ConstantRates{}, 5343 alternateBidderCodes: openrtb_ext.ExtAlternateBidderCodes{ 5344 Enabled: true, 5345 Bidders: map[string]openrtb_ext.ExtAdapterAlternateBidderCodes{ 5346 "pubmatic": { 5347 Enabled: true, 5348 AllowedBidderCodes: []string{"groupm"}, 5349 }, 5350 }, 5351 }, 5352 hookExecutor: hookexecution.EmptyHookExecutor{}, 5353 pbsRequestStartTime: time.Now(), 5354 adapterMap: map[openrtb_ext.BidderName]AdaptedBidder{ 5355 openrtb_ext.BidderPubmatic: AdaptBidder(&goodSingleBidder{ 5356 httpRequest: &adapters.RequestData{ 5357 Method: "POST", 5358 Uri: server.URL, 5359 }, 5360 bidResponse: &adapters.BidderResponse{ 5361 Bids: []*adapters.TypedBid{ 5362 {Bid: &openrtb2.Bid{ID: "2"}, Seat: "groupm"}, 5363 }, 5364 }, 5365 }, server.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderPubmatic, nil, ""), 5366 }, 5367 }, 5368 expected: testResults{ 5369 extraRespInfo: extraAuctionResponseInfo{ 5370 bidsFound: true, 5371 }, 5372 adapterExtra: map[openrtb_ext.BidderName]*seatResponseExtra{ 5373 "pubmatic": { 5374 Warnings: []openrtb_ext.ExtBidderMessage{}, 5375 }, 5376 }, 5377 adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ 5378 "groupm": { 5379 Bids: []*entities.PbsOrtbBid{ 5380 { 5381 Bid: &openrtb2.Bid{ 5382 ID: "2", 5383 }, 5384 OriginalBidCur: "USD", 5385 AdapterCode: openrtb_ext.BidderPubmatic, 5386 }, 5387 }, 5388 Currency: "USD", 5389 Seat: "groupm", 5390 HttpCalls: []*openrtb_ext.ExtHttpCall{}, 5391 }, 5392 }, 5393 }, 5394 }, 5395 { 5396 desc: "bidder responded with empty bid", 5397 in: testIn{ 5398 bidderRequests: []BidderRequest{ 5399 { 5400 BidderName: "pubmatic", 5401 BidderCoreName: "pubmatic", 5402 BidRequest: &openrtb2.BidRequest{ 5403 ID: "some-request-id", 5404 Imp: []openrtb2.Imp{{ 5405 ID: "some-impression-id", 5406 }}, 5407 }, 5408 }, 5409 }, 5410 conversions: ¤cy.ConstantRates{}, 5411 alternateBidderCodes: openrtb_ext.ExtAlternateBidderCodes{ 5412 Enabled: true, 5413 Bidders: map[string]openrtb_ext.ExtAdapterAlternateBidderCodes{ 5414 "pubmatic": { 5415 Enabled: true, 5416 AllowedBidderCodes: []string{"groupm"}, 5417 }, 5418 }, 5419 }, 5420 hookExecutor: hookexecution.EmptyHookExecutor{}, 5421 pbsRequestStartTime: time.Now(), 5422 adapterMap: map[openrtb_ext.BidderName]AdaptedBidder{ 5423 openrtb_ext.BidderPubmatic: AdaptBidder(&goodSingleBidder{ 5424 httpRequest: &adapters.RequestData{ 5425 Method: "POST", 5426 Uri: server.URL, 5427 }, 5428 bidResponse: &adapters.BidderResponse{}, 5429 }, server.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderPubmatic, nil, ""), 5430 }, 5431 }, 5432 expected: testResults{ 5433 extraRespInfo: extraAuctionResponseInfo{ 5434 bidsFound: false, 5435 }, 5436 adapterExtra: map[openrtb_ext.BidderName]*seatResponseExtra{ 5437 "pubmatic": { 5438 Warnings: []openrtb_ext.ExtBidderMessage{}, 5439 }, 5440 }, 5441 adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{}, 5442 }, 5443 }, 5444 } 5445 for _, test := range testCases { 5446 t.Run(test.desc, func(t *testing.T) { 5447 e := exchange{ 5448 cache: &wellBehavedCache{}, 5449 me: &metricsConf.NilMetricsEngine{}, 5450 gdprPermsBuilder: fakePermissionsBuilder{ 5451 permissions: &permissionsMock{ 5452 allowAllBidders: true, 5453 }, 5454 }.Builder, 5455 adapterMap: test.in.adapterMap, 5456 } 5457 5458 adapterBids, adapterExtra, extraRespInfo := e.getAllBids(context.Background(), test.in.bidderRequests, test.in.bidAdjustments, 5459 test.in.conversions, test.in.accountDebugAllowed, test.in.globalPrivacyControlHeader, test.in.headerDebugAllowed, test.in.alternateBidderCodes, test.in.experiment, 5460 test.in.hookExecutor, test.in.pbsRequestStartTime, test.in.bidAdjustmentRules, test.in.tmaxAdjustments, false) 5461 5462 assert.Equalf(t, test.expected.extraRespInfo.bidsFound, extraRespInfo.bidsFound, "extraRespInfo.bidsFound mismatch") 5463 assert.Equalf(t, test.expected.adapterBids, adapterBids, "adapterBids mismatch") 5464 assert.Equalf(t, len(test.expected.adapterExtra), len(adapterExtra), "adapterExtra length mismatch") 5465 for adapter, extra := range test.expected.adapterExtra { 5466 assert.Equalf(t, extra.Warnings, adapterExtra[adapter].Warnings, "adapterExtra.Warnings mismatch for adapter [%s]", adapter) 5467 } 5468 }) 5469 } 5470 } 5471 5472 type MockSigner struct { 5473 data string 5474 } 5475 5476 func (ms *MockSigner) Sign(destinationURL string, body []byte) (string, error) { 5477 ms.data = destinationURL 5478 return "mock data", nil 5479 } 5480 5481 type exchangeSpec struct { 5482 GDPREnabled bool `json:"gdpr_enabled"` 5483 FloorsEnabled bool `json:"floors_enabled"` 5484 IncomingRequest exchangeRequest `json:"incomingRequest"` 5485 OutgoingRequests map[string]*bidderSpec `json:"outgoingRequests"` 5486 Response exchangeResponse `json:"response,omitempty"` 5487 EnforceCCPA bool `json:"enforceCcpa"` 5488 EnforceLMT bool `json:"enforceLmt"` 5489 AssumeGDPRApplies bool `json:"assume_gdpr_applies"` 5490 DebugLog *DebugLog `json:"debuglog,omitempty"` 5491 EventsEnabled bool `json:"events_enabled,omitempty"` 5492 StartTime int64 `json:"start_time_ms,omitempty"` 5493 BidIDGenerator *fakeBidIDGenerator `json:"bidIDGenerator,omitempty"` 5494 RequestType *metrics.RequestType `json:"requestType,omitempty"` 5495 PassthroughFlag bool `json:"passthrough_flag,omitempty"` 5496 HostSChainFlag bool `json:"host_schain_flag,omitempty"` 5497 HostConfigBidValidation config.Validations `json:"host_bid_validations"` 5498 AccountConfigBidValidation config.Validations `json:"account_bid_validations"` 5499 AccountFloorsEnabled bool `json:"account_floors_enabled"` 5500 AccountEnforceDealFloors bool `json:"account_enforce_deal_floors"` 5501 FledgeEnabled bool `json:"fledge_enabled,omitempty"` 5502 MultiBid *multiBidSpec `json:"multiBid,omitempty"` 5503 Server exchangeServer `json:"server,omitempty"` 5504 AccountPrivacy config.AccountPrivacy `json:"accountPrivacy,omitempty"` 5505 } 5506 5507 type multiBidSpec struct { 5508 AccountMaxBid int `json:"default_bid_limit"` 5509 AssertMultiBidWarnings bool `json:"assert_multi_bid_warnings"` 5510 } 5511 5512 type exchangeRequest struct { 5513 OrtbRequest openrtb2.BidRequest `json:"ortbRequest"` 5514 Usersyncs map[string]string `json:"usersyncs"` 5515 } 5516 5517 type exchangeResponse struct { 5518 Bids *openrtb2.BidResponse `json:"bids"` 5519 Error string `json:"error,omitempty"` 5520 Ext json.RawMessage `json:"ext,omitempty"` 5521 } 5522 5523 type exchangeServer struct { 5524 ExternalUrl string `json:"externalURL"` 5525 GvlID int `json:"gvlID"` 5526 DataCenter string `json:"dataCenter"` 5527 } 5528 5529 type bidderSpec struct { 5530 ExpectedRequest *bidderRequest `json:"expectRequest"` 5531 MockResponse bidderResponse `json:"mockResponse"` 5532 ModifyingVastXmlAllowed bool `json:"modifyingVastXmlAllowed,omitempty"` 5533 } 5534 5535 type bidderRequest struct { 5536 OrtbRequest openrtb2.BidRequest `json:"ortbRequest"` 5537 BidAdjustments map[string]float64 `json:"bidAdjustments"` 5538 } 5539 5540 type bidderResponse struct { 5541 SeatBids []*bidderSeatBid `json:"pbsSeatBids,omitempty"` 5542 Errors []string `json:"errors,omitempty"` 5543 HttpCalls []*openrtb_ext.ExtHttpCall `json:"httpCalls,omitempty"` 5544 } 5545 5546 // bidderSeatBid is basically a subset of entities.PbsOrtbSeatBid from exchange/bidder.go. 5547 // The only real reason I'm not reusing that type is because I don't want people to think that the 5548 // JSON property tags on those types are contracts in prod. 5549 type bidderSeatBid struct { 5550 Bids []bidderBid `json:"pbsBids,omitempty"` 5551 Seat string `json:"seat"` 5552 Currency string `json:"currency"` 5553 FledgeAuctionConfigs []*openrtb_ext.FledgeAuctionConfig `json:"fledgeAuctionConfigs,omitempty"` 5554 } 5555 5556 // bidderBid is basically a subset of entities.PbsOrtbBid from exchange/bidder.go. 5557 // See the comment on bidderSeatBid for more info. 5558 type bidderBid struct { 5559 Bid *openrtb2.Bid `json:"ortbBid,omitempty"` 5560 Type string `json:"bidType,omitempty"` 5561 Meta *openrtb_ext.ExtBidPrebidMeta `json:"bidMeta,omitempty"` 5562 } 5563 5564 type mockIdFetcher map[string]string 5565 5566 func (f mockIdFetcher) GetUID(key string) (uid string, exists bool, notExpired bool) { 5567 uid, exists = f[string(key)] 5568 return 5569 } 5570 5571 func (f mockIdFetcher) HasAnyLiveSyncs() bool { 5572 return len(f) > 0 5573 } 5574 5575 type validatingBidder struct { 5576 t *testing.T 5577 fileName string 5578 bidderName string 5579 5580 // These are maps because they may contain aliases. They should _at least_ contain an entry for bidderName. 5581 expectations map[string]*bidderRequest 5582 mockResponses map[string]bidderResponse 5583 } 5584 5585 func (b *validatingBidder) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestOptions bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, executor hookexecution.StageExecutor, ruleToAdjustments openrtb_ext.AdjustmentsByDealID) (seatBids []*entities.PbsOrtbSeatBid, extaInfo extraBidderRespInfo, errs []error) { 5586 if expectedRequest, ok := b.expectations[string(bidderRequest.BidderName)]; ok { 5587 if expectedRequest != nil { 5588 if !reflect.DeepEqual(expectedRequest.BidAdjustments, bidRequestOptions.bidAdjustments) { 5589 b.t.Errorf("%s: Bidder %s got wrong bid adjustment. Expected %v, got %v", b.fileName, bidderRequest.BidderName, expectedRequest.BidAdjustments, bidRequestOptions.bidAdjustments) 5590 } 5591 diffOrtbRequests(b.t, fmt.Sprintf("Request to %s in %s", string(bidderRequest.BidderName), b.fileName), &expectedRequest.OrtbRequest, bidderRequest.BidRequest) 5592 } 5593 } else { 5594 b.t.Errorf("%s: Bidder %s got unexpected request for alias %s. No input assertions.", b.fileName, b.bidderName, bidderRequest.BidderName) 5595 } 5596 5597 if mockResponse, ok := b.mockResponses[string(bidderRequest.BidderName)]; ok { 5598 if len(mockResponse.SeatBids) != 0 { 5599 for _, mockSeatBid := range mockResponse.SeatBids { 5600 var bids []*entities.PbsOrtbBid 5601 5602 if len(mockSeatBid.Bids) != 0 { 5603 bids = make([]*entities.PbsOrtbBid, len(mockSeatBid.Bids)) 5604 for i := 0; i < len(bids); i++ { 5605 bids[i] = &entities.PbsOrtbBid{ 5606 OriginalBidCPM: mockSeatBid.Bids[i].Bid.Price, 5607 Bid: mockSeatBid.Bids[i].Bid, 5608 BidType: openrtb_ext.BidType(mockSeatBid.Bids[i].Type), 5609 BidMeta: mockSeatBid.Bids[i].Meta, 5610 } 5611 } 5612 } 5613 5614 seatBids = append(seatBids, &entities.PbsOrtbSeatBid{ 5615 Bids: bids, 5616 HttpCalls: mockResponse.HttpCalls, 5617 Seat: mockSeatBid.Seat, 5618 Currency: mockSeatBid.Currency, 5619 FledgeAuctionConfigs: mockSeatBid.FledgeAuctionConfigs, 5620 }) 5621 } 5622 } else { 5623 seatBids = []*entities.PbsOrtbSeatBid{{ 5624 Bids: nil, 5625 HttpCalls: mockResponse.HttpCalls, 5626 Seat: string(bidderRequest.BidderName), 5627 }} 5628 } 5629 5630 for _, err := range mockResponse.Errors { 5631 errs = append(errs, errors.New(err)) 5632 } 5633 } else { 5634 b.t.Errorf("%s: Bidder %s got unexpected request for alias %s. No mock responses.", b.fileName, b.bidderName, bidderRequest.BidderName) 5635 } 5636 5637 return 5638 } 5639 5640 type capturingRequestBidder struct { 5641 req *openrtb2.BidRequest 5642 } 5643 5644 func (b *capturingRequestBidder) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestOptions bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, executor hookexecution.StageExecutor, ruleToAdjustments openrtb_ext.AdjustmentsByDealID) (seatBid []*entities.PbsOrtbSeatBid, errs []error) { 5645 b.req = bidderRequest.BidRequest 5646 return []*entities.PbsOrtbSeatBid{{}}, nil 5647 } 5648 5649 func diffOrtbRequests(t *testing.T, description string, expected *openrtb2.BidRequest, actual *openrtb2.BidRequest) { 5650 t.Helper() 5651 actualJSON, err := jsonutil.Marshal(actual) 5652 if err != nil { 5653 t.Fatalf("%s failed to marshal actual BidRequest into JSON. %v", description, err) 5654 } 5655 5656 expectedJSON, err := jsonutil.Marshal(expected) 5657 if err != nil { 5658 t.Fatalf("%s failed to marshal expected BidRequest into JSON. %v", description, err) 5659 } 5660 5661 assert.JSONEq(t, string(expectedJSON), string(actualJSON), description) 5662 } 5663 5664 func diffOrtbResponses(t *testing.T, description string, expected *openrtb2.BidResponse, actual *openrtb2.BidResponse) { 5665 t.Helper() 5666 // The OpenRTB spec is wonky here. Since "bidresponse.seatbid" is an array, order technically matters to any JSON diff or 5667 // deep equals method. However, for all intents and purposes it really *doesn't* matter. ...so this nasty logic makes compares 5668 // the seatbids in an order-independent way. 5669 // 5670 // Note that the same thing is technically true of the "seatbid[i].bid" array... but since none of our exchange code relies on 5671 // this implementation detail, I'm cutting a corner and ignoring it here. 5672 actualSeats := mapifySeatBids(t, description, actual.SeatBid) 5673 expectedSeats := mapifySeatBids(t, description, expected.SeatBid) 5674 actualJSON, err := jsonutil.Marshal(actualSeats) 5675 if err != nil { 5676 t.Fatalf("%s failed to marshal actual BidResponse into JSON. %v", description, err) 5677 } 5678 5679 expectedJSON, err := jsonutil.Marshal(expectedSeats) 5680 if err != nil { 5681 t.Fatalf("%s failed to marshal expected BidResponse into JSON. %v", description, err) 5682 } 5683 assert.JSONEq(t, string(expectedJSON), string(actualJSON), description) 5684 } 5685 5686 func mapifySeatBids(t *testing.T, context string, seatBids []openrtb2.SeatBid) map[string]*openrtb2.SeatBid { 5687 seatMap := make(map[string]*openrtb2.SeatBid, len(seatBids)) 5688 for i := 0; i < len(seatBids); i++ { 5689 seatName := seatBids[i].Seat 5690 if _, ok := seatMap[seatName]; ok { 5691 t.Fatalf("%s: Contains duplicate Seat: %s", context, seatName) 5692 } else { 5693 // The sequence of extra bids for same seat from different bidder is not guaranteed as we randomize the list of adapters 5694 // This is w.r.t changes at exchange.go#561 (club bids from different bidders for same extra-bid) 5695 sort.Slice(seatBids[i].Bid, func(x, y int) bool { 5696 return isNewWinningBid(&seatBids[i].Bid[x], &seatBids[i].Bid[y], true) 5697 }) 5698 seatMap[seatName] = &seatBids[i] 5699 } 5700 } 5701 5702 return seatMap 5703 } 5704 5705 func mockHandler(statusCode int, getBody string, postBody string) http.Handler { 5706 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 5707 w.WriteHeader(statusCode) 5708 if r.Method == "GET" { 5709 w.Write([]byte(getBody)) 5710 } else { 5711 w.Write([]byte(postBody)) 5712 } 5713 }) 5714 } 5715 5716 func mockSlowHandler(delay time.Duration, statusCode int, body string) http.Handler { 5717 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 5718 time.Sleep(delay) 5719 5720 w.WriteHeader(statusCode) 5721 w.Write([]byte(body)) 5722 }) 5723 } 5724 5725 type wellBehavedCache struct{} 5726 5727 func (c *wellBehavedCache) GetExtCacheData() (scheme string, host string, path string) { 5728 return "https", "www.pbcserver.com", "/pbcache/endpoint" 5729 } 5730 5731 func (c *wellBehavedCache) PutJson(ctx context.Context, values []pbc.Cacheable) ([]string, []error) { 5732 ids := make([]string, len(values)) 5733 for i := 0; i < len(values); i++ { 5734 ids[i] = strconv.Itoa(i) 5735 } 5736 return ids, nil 5737 } 5738 5739 type emptyUsersync struct{} 5740 5741 func (e *emptyUsersync) GetUID(key string) (uid string, exists bool, notExpired bool) { 5742 return "", false, false 5743 } 5744 5745 func (e *emptyUsersync) HasAnyLiveSyncs() bool { 5746 return false 5747 } 5748 5749 type panicingAdapter struct{} 5750 5751 func (panicingAdapter) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestMetadata bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, executor hookexecution.StageExecutor, ruleToAdjustments openrtb_ext.AdjustmentsByDealID) (posb []*entities.PbsOrtbSeatBid, extraInfo extraBidderRespInfo, errs []error) { 5752 panic("Panic! Panic! The world is ending!") 5753 } 5754 5755 func blankAdapterConfig(bidderList []openrtb_ext.BidderName) map[string]config.Adapter { 5756 adapters := make(map[string]config.Adapter) 5757 for _, b := range bidderList { 5758 adapters[strings.ToLower(string(b))] = config.Adapter{} 5759 } 5760 5761 // Audience Network requires additional config to be built. 5762 adapters["audiencenetwork"] = config.Adapter{PlatformID: "anyID", AppSecret: "anySecret"} 5763 5764 return adapters 5765 } 5766 5767 type nilCategoryFetcher struct{} 5768 5769 func (nilCategoryFetcher) FetchCategories(ctx context.Context, primaryAdServer, publisherId, iabCategory string) (string, error) { 5770 return "", nil 5771 } 5772 5773 type mockBidder struct { 5774 mock.Mock 5775 lastExtraRequestInfo *adapters.ExtraRequestInfo 5776 } 5777 5778 func (m *mockBidder) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { 5779 m.lastExtraRequestInfo = reqInfo 5780 5781 args := m.Called(request, reqInfo) 5782 return args.Get(0).([]*adapters.RequestData), args.Get(1).([]error) 5783 } 5784 5785 func (m *mockBidder) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { 5786 args := m.Called(internalRequest, externalRequest, response) 5787 return args.Get(0).(*adapters.BidderResponse), args.Get(1).([]error) 5788 } 5789 5790 func parseRequestAliases(r openrtb2.BidRequest) (map[string]string, error) { 5791 if len(r.Ext) == 0 { 5792 return nil, nil 5793 } 5794 5795 ext := struct { 5796 Prebid struct { 5797 Aliases map[string]string `json:"aliases"` 5798 } `json:"prebid"` 5799 }{} 5800 5801 if err := jsonutil.Unmarshal(r.Ext, &ext); err != nil { 5802 return nil, err 5803 } 5804 5805 return ext.Prebid.Aliases, nil 5806 } 5807 5808 func getInfoFromImp(req *openrtb_ext.RequestWrapper) (json.RawMessage, string, error) { 5809 bidRequest := req.BidRequest 5810 imp := bidRequest.Imp[0] 5811 impID := imp.ID 5812 5813 var bidderExts map[string]json.RawMessage 5814 if err := jsonutil.UnmarshalValid(imp.Ext, &bidderExts); err != nil { 5815 return nil, "", err 5816 } 5817 5818 var extPrebid openrtb_ext.ExtImpPrebid 5819 if bidderExts[openrtb_ext.PrebidExtKey] != nil { 5820 if err := jsonutil.UnmarshalValid(bidderExts[openrtb_ext.PrebidExtKey], &extPrebid); err != nil { 5821 return nil, "", err 5822 } 5823 } 5824 return extPrebid.Passthrough, impID, nil 5825 } 5826 5827 func TestModulesCanBeExecutedForMultipleBiddersSimultaneously(t *testing.T) { 5828 noBidServer := func(w http.ResponseWriter, r *http.Request) { 5829 w.WriteHeader(204) 5830 } 5831 server := httptest.NewServer(http.HandlerFunc(noBidServer)) 5832 defer server.Close() 5833 5834 reqBdy := []byte(`{"key":"val"}`) 5835 5836 bidderImplAppnexus := &goodSingleBidder{ 5837 httpRequest: &adapters.RequestData{Method: http.MethodPost, Uri: server.URL, Body: reqBdy, Headers: http.Header{}}, 5838 bidResponse: &adapters.BidderResponse{}, 5839 } 5840 bidderImplTelaria := &goodSingleBidder{ 5841 httpRequest: &adapters.RequestData{Method: http.MethodPost, Uri: server.URL, Body: reqBdy, Headers: http.Header{}}, 5842 bidResponse: &adapters.BidderResponse{}, 5843 } 5844 bidderImpl33Across := &goodSingleBidder{ 5845 httpRequest: &adapters.RequestData{Method: http.MethodPost, Uri: server.URL, Body: reqBdy, Headers: http.Header{}}, 5846 bidResponse: &adapters.BidderResponse{}, 5847 } 5848 bidderImplAax := &goodSingleBidder{ 5849 httpRequest: &adapters.RequestData{Method: http.MethodPost, Uri: server.URL, Body: reqBdy, Headers: http.Header{}}, 5850 bidResponse: &adapters.BidderResponse{}, 5851 } 5852 5853 e := new(exchange) 5854 e.me = &metricsConf.NilMetricsEngine{} 5855 e.currencyConverter = currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) 5856 e.requestSplitter = requestSplitter{ 5857 me: e.me, 5858 gdprPermsBuilder: e.gdprPermsBuilder, 5859 } 5860 5861 bidRequest := &openrtb2.BidRequest{ 5862 ID: "some-request-id", 5863 Imp: []openrtb2.Imp{{ 5864 ID: "some-impression-id", 5865 Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}, {W: 300, H: 600}}}, 5866 Ext: json.RawMessage( 5867 `{"prebid":{"bidder":{"telaria": {"placementId": 1}, "appnexus": {"placementid": 2}, "33across": {"placementId": 3}, "aax": {"placementid": 4}}}}`, 5868 ), 5869 }}, 5870 Site: &openrtb2.Site{Page: "prebid.org", Ext: json.RawMessage(`{"amp":0}`)}, 5871 Device: &openrtb2.Device{UA: "curl/7.54.0", IP: "::1"}, 5872 AT: 1, 5873 TMax: 500, 5874 } 5875 5876 exec := hookexecution.NewHookExecutor(TestApplyHookMutationsBuilder{}, "/openrtb2/auction", &metricsConfig.NilMetricsEngine{}) 5877 5878 auctionRequest := &AuctionRequest{ 5879 BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: bidRequest}, 5880 Account: config.Account{DebugAllow: true}, 5881 UserSyncs: &emptyUsersync{}, 5882 StartTime: time.Now(), 5883 HookExecutor: exec, 5884 TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), 5885 } 5886 5887 e.adapterMap = map[openrtb_ext.BidderName]AdaptedBidder{ 5888 openrtb_ext.BidderAppnexus: AdaptBidder(bidderImplAppnexus, server.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderAppnexus, &config.DebugInfo{}, ""), 5889 openrtb_ext.BidderTelaria: AdaptBidder(bidderImplTelaria, server.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderTelaria, &config.DebugInfo{}, ""), 5890 openrtb_ext.Bidder33Across: AdaptBidder(bidderImpl33Across, server.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.Bidder33Across, &config.DebugInfo{}, ""), 5891 openrtb_ext.BidderAax: AdaptBidder(bidderImplAax, server.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderAax, &config.DebugInfo{}, ""), 5892 } 5893 // Run test 5894 _, err := e.HoldAuction(context.Background(), auctionRequest, &DebugLog{}) 5895 // Assert no HoldAuction err 5896 assert.NoErrorf(t, err, "ex.HoldAuction returned an err") 5897 assert.False(t, auctionRequest.BidderResponseStartTime.IsZero()) 5898 5899 // check stage outcomes 5900 assert.Equal(t, len(exec.GetOutcomes()), len(e.adapterMap), "stage outcomes append operation failed") 5901 //check that all modules were applied and logged 5902 for _, sto := range exec.GetOutcomes() { 5903 assert.Equal(t, 2, len(sto.Groups), "not all groups were executed") 5904 for _, group := range sto.Groups { 5905 assert.Equal(t, 5, len(group.InvocationResults), "not all module hooks were applied") 5906 for _, r := range group.InvocationResults { 5907 assert.Equal(t, "success", string(r.Status), fmt.Sprintf("Module %s hook %s completed unsuccessfully", r.HookID.ModuleCode, r.HookID.HookImplCode)) 5908 } 5909 } 5910 } 5911 } 5912 5913 type TestApplyHookMutationsBuilder struct { 5914 hooks.EmptyPlanBuilder 5915 } 5916 5917 func (e TestApplyHookMutationsBuilder) PlanForBidderRequestStage(_ string, _ *config.Account) hooks.Plan[hookstage.BidderRequest] { 5918 return hooks.Plan[hookstage.BidderRequest]{ 5919 hooks.Group[hookstage.BidderRequest]{ 5920 Timeout: 100 * time.Millisecond, 5921 Hooks: []hooks.HookWrapper[hookstage.BidderRequest]{ 5922 {Module: "foobar1", Code: "foo1", Hook: mockUpdateBidRequestHook{}}, 5923 {Module: "foobar2", Code: "foo2", Hook: mockUpdateBidRequestHook{}}, 5924 {Module: "foobar3", Code: "foo3", Hook: mockUpdateBidRequestHook{}}, 5925 {Module: "foobar4", Code: "foo4", Hook: mockUpdateBidRequestHook{}}, 5926 {Module: "foobar5", Code: "foo5", Hook: mockUpdateBidRequestHook{}}, 5927 }, 5928 }, 5929 hooks.Group[hookstage.BidderRequest]{ 5930 Timeout: 100 * time.Millisecond, 5931 Hooks: []hooks.HookWrapper[hookstage.BidderRequest]{ 5932 {Module: "foobar6", Code: "foo6", Hook: mockUpdateBidRequestHook{}}, 5933 {Module: "foobar7", Code: "foo7", Hook: mockUpdateBidRequestHook{}}, 5934 {Module: "foobar8", Code: "foo8", Hook: mockUpdateBidRequestHook{}}, 5935 {Module: "foobar9", Code: "foo9", Hook: mockUpdateBidRequestHook{}}, 5936 {Module: "foobar10", Code: "foo10", Hook: mockUpdateBidRequestHook{}}, 5937 }, 5938 }, 5939 } 5940 } 5941 5942 type mockUpdateBidRequestHook struct{} 5943 5944 func (e mockUpdateBidRequestHook) HandleBidderRequestHook(_ context.Context, mctx hookstage.ModuleInvocationContext, _ hookstage.BidderRequestPayload) (hookstage.HookResult[hookstage.BidderRequestPayload], error) { 5945 time.Sleep(50 * time.Millisecond) 5946 c := hookstage.ChangeSet[hookstage.BidderRequestPayload]{} 5947 c.AddMutation( 5948 func(payload hookstage.BidderRequestPayload) (hookstage.BidderRequestPayload, error) { 5949 site := ptrutil.Clone(payload.Request.Site) 5950 site.Name = "test" 5951 payload.Request.Site = site 5952 return payload, nil 5953 }, hookstage.MutationUpdate, "bidRequest", "site.name", 5954 ).AddMutation( 5955 func(payload hookstage.BidderRequestPayload) (hookstage.BidderRequestPayload, error) { 5956 site := ptrutil.Clone(payload.Request.Site) 5957 site.Domain = "test.com" 5958 payload.Request.Site = site 5959 return payload, nil 5960 }, hookstage.MutationUpdate, "bidRequest", "site.domain", 5961 ) 5962 5963 mctx.ModuleContext = map[string]interface{}{"some-ctx": "some-ctx"} 5964 5965 return hookstage.HookResult[hookstage.BidderRequestPayload]{ChangeSet: c, ModuleContext: mctx.ModuleContext}, nil 5966 } 5967 5968 func TestNilAuctionRequest(t *testing.T) { 5969 ex := &exchange{} 5970 response, err := ex.HoldAuction(context.Background(), nil, &DebugLog{}) 5971 assert.Nil(t, response) 5972 assert.Nil(t, err) 5973 } 5974 5975 func TestSelectNewDuration(t *testing.T) { 5976 type testInput struct { 5977 dur int 5978 durRanges []int 5979 } 5980 type testOutput struct { 5981 dur int 5982 err error 5983 } 5984 testCases := []struct { 5985 desc string 5986 in testInput 5987 expected testOutput 5988 }{ 5989 { 5990 desc: "nil duration range array, don't expect error", 5991 in: testInput{ 5992 dur: 1, 5993 durRanges: nil, 5994 }, 5995 expected: testOutput{1, nil}, 5996 }, 5997 { 5998 desc: "empty duration range array, don't expect error", 5999 in: testInput{ 6000 dur: 1, 6001 durRanges: []int{}, 6002 }, 6003 expected: testOutput{1, nil}, 6004 }, 6005 { 6006 desc: "all duration range array elements less than duration, expect error", 6007 in: testInput{ 6008 dur: 5, 6009 durRanges: []int{-1, 0, 1, 2, 3}, 6010 }, 6011 expected: testOutput{5, errors.New("bid duration exceeds maximum allowed")}, 6012 }, 6013 { 6014 desc: "all duration range array elements greater than duration, expect smallest element in durRanges and nil error", 6015 in: testInput{ 6016 dur: 5, 6017 durRanges: []int{9, math.MaxInt32, 8}, 6018 }, 6019 expected: testOutput{8, nil}, 6020 }, 6021 { 6022 desc: "some array elements greater than duration, expect the value greater than dur that is closest in value.", 6023 in: testInput{ 6024 dur: 5, 6025 durRanges: []int{math.MaxInt32, -3, 7, 2}, 6026 }, 6027 expected: testOutput{7, nil}, 6028 }, 6029 { 6030 desc: "an entry in the duration range array is equal to duration, expect its value in return.", 6031 in: testInput{ 6032 dur: 5, 6033 durRanges: []int{-3, math.MaxInt32, 5, 7}, 6034 }, 6035 expected: testOutput{5, nil}, 6036 }, 6037 } 6038 for _, tc := range testCases { 6039 newDur, err := findDurationRange(tc.in.dur, tc.in.durRanges) 6040 6041 assert.Equal(t, tc.expected.dur, newDur, tc.desc) 6042 assert.Equal(t, tc.expected.err, err, tc.desc) 6043 } 6044 } 6045 6046 func TestSetSeatNonBid(t *testing.T) { 6047 type args struct { 6048 bidResponseExt *openrtb_ext.ExtBidResponse 6049 seatNonBids nonBids 6050 } 6051 tests := []struct { 6052 name string 6053 args args 6054 want *openrtb_ext.ExtBidResponse 6055 }{ 6056 { 6057 name: "empty-seatNonBidsMap", 6058 args: args{seatNonBids: nonBids{}, bidResponseExt: nil}, 6059 want: nil, 6060 }, 6061 { 6062 name: "nil-bidResponseExt", 6063 args: args{seatNonBids: nonBids{seatNonBidsMap: map[string][]openrtb_ext.NonBid{"key": nil}}, bidResponseExt: nil}, 6064 want: &openrtb_ext.ExtBidResponse{ 6065 Prebid: &openrtb_ext.ExtResponsePrebid{ 6066 SeatNonBid: []openrtb_ext.SeatNonBid{{ 6067 Seat: "key", 6068 }}, 6069 }, 6070 }, 6071 }, 6072 } 6073 for _, tt := range tests { 6074 t.Run(tt.name, func(t *testing.T) { 6075 if got := setSeatNonBid(tt.args.bidResponseExt, tt.args.seatNonBids); !reflect.DeepEqual(got, tt.want) { 6076 t.Errorf("setSeatNonBid() = %v, want %v", got, tt.want) 6077 } 6078 }) 6079 } 6080 } 6081 6082 func TestBuildMultiBidMap(t *testing.T) { 6083 type testCase struct { 6084 desc string 6085 inPrebid *openrtb_ext.ExtRequestPrebid 6086 expected map[string]openrtb_ext.ExtMultiBid 6087 } 6088 testGroups := []struct { 6089 groupDesc string 6090 tests []testCase 6091 }{ 6092 { 6093 groupDesc: "Nil or empty tests", 6094 tests: []testCase{ 6095 { 6096 desc: "prebid nil, expect nil map", 6097 inPrebid: nil, 6098 expected: nil, 6099 }, 6100 { 6101 desc: "prebid.MultiBid nil, expect nil map", 6102 inPrebid: &openrtb_ext.ExtRequestPrebid{}, 6103 expected: nil, 6104 }, 6105 { 6106 desc: "not-nil prebid.MultiBid is empty, expect empty map", 6107 inPrebid: &openrtb_ext.ExtRequestPrebid{ 6108 MultiBid: []*openrtb_ext.ExtMultiBid{}, 6109 }, 6110 expected: map[string]openrtb_ext.ExtMultiBid{}, 6111 }, 6112 }, 6113 }, 6114 { 6115 groupDesc: "prebid.MultiBid.Bidder tests", 6116 tests: []testCase{ 6117 { 6118 desc: "Lowercase prebid.MultiBid.Bidder is found in the BidderName list, entry is mapped", 6119 inPrebid: &openrtb_ext.ExtRequestPrebid{ 6120 MultiBid: []*openrtb_ext.ExtMultiBid{ 6121 {Bidder: "appnexus"}, 6122 }, 6123 }, 6124 expected: map[string]openrtb_ext.ExtMultiBid{ 6125 "appnexus": {Bidder: "appnexus"}, 6126 }, 6127 }, 6128 { 6129 desc: "Uppercase prebid.MultiBid.Bidder is found in the BidderName list, entry is mapped", 6130 inPrebid: &openrtb_ext.ExtRequestPrebid{ 6131 MultiBid: []*openrtb_ext.ExtMultiBid{ 6132 {Bidder: "APPNEXUS"}, 6133 }, 6134 }, 6135 expected: map[string]openrtb_ext.ExtMultiBid{ 6136 "appnexus": {Bidder: "APPNEXUS"}, 6137 }, 6138 }, 6139 { 6140 desc: "Lowercase prebid.MultiBid.Bidder is not found in the BidderName list, expect empty map", 6141 inPrebid: &openrtb_ext.ExtRequestPrebid{ 6142 MultiBid: []*openrtb_ext.ExtMultiBid{ 6143 {Bidder: "unknown"}, 6144 }, 6145 }, 6146 expected: map[string]openrtb_ext.ExtMultiBid{}, 6147 }, 6148 { 6149 desc: "Mixed-case prebid.MultiBid.Bidder is not found in the BidderName list, expect empty map", 6150 inPrebid: &openrtb_ext.ExtRequestPrebid{ 6151 MultiBid: []*openrtb_ext.ExtMultiBid{ 6152 {Bidder: "UnknownBidder"}, 6153 }, 6154 }, 6155 expected: map[string]openrtb_ext.ExtMultiBid{}, 6156 }, 6157 { 6158 desc: "Different-cased prebid.MultiBid.Bidder entries that refer to the same adapter are found in the BidderName list are mapped once", 6159 inPrebid: &openrtb_ext.ExtRequestPrebid{ 6160 MultiBid: []*openrtb_ext.ExtMultiBid{ 6161 {Bidder: "AppNexus"}, 6162 {Bidder: "appnexus"}, 6163 }, 6164 }, 6165 expected: map[string]openrtb_ext.ExtMultiBid{ 6166 "appnexus": {Bidder: "appnexus"}, 6167 }, 6168 }, 6169 }, 6170 }, 6171 { 6172 groupDesc: "prebid.MultiBid.Bidders tests", 6173 tests: []testCase{ 6174 { 6175 desc: "Lowercase prebid.MultiBid.Bidder is found in the BidderName list, entry is mapped", 6176 inPrebid: &openrtb_ext.ExtRequestPrebid{ 6177 MultiBid: []*openrtb_ext.ExtMultiBid{ 6178 {Bidders: []string{"appnexus"}}, 6179 }, 6180 }, 6181 expected: map[string]openrtb_ext.ExtMultiBid{ 6182 "appnexus": { 6183 Bidders: []string{"appnexus"}, 6184 }, 6185 }, 6186 }, 6187 { 6188 desc: "Lowercase prebid.MultiBid.Bidder is not found in the BidderName list, expect empty map", 6189 inPrebid: &openrtb_ext.ExtRequestPrebid{ 6190 MultiBid: []*openrtb_ext.ExtMultiBid{ 6191 {Bidders: []string{"unknown"}}, 6192 }, 6193 }, 6194 expected: map[string]openrtb_ext.ExtMultiBid{}, 6195 }, 6196 { 6197 desc: "Mixed-case prebid.MultiBid.Bidder is not found in the BidderName list, expect empty map", 6198 inPrebid: &openrtb_ext.ExtRequestPrebid{ 6199 MultiBid: []*openrtb_ext.ExtMultiBid{ 6200 {Bidders: []string{"UnknownBidder"}}, 6201 }, 6202 }, 6203 expected: map[string]openrtb_ext.ExtMultiBid{}, 6204 }, 6205 { 6206 desc: "Different-cased prebid.MultiBid.Bidder entries that refer to the same adapter are found in the BidderName list are mapped once", 6207 inPrebid: &openrtb_ext.ExtRequestPrebid{ 6208 MultiBid: []*openrtb_ext.ExtMultiBid{ 6209 {Bidders: []string{"AppNexus", "appnexus", "UnknownBidder"}}, 6210 }, 6211 }, 6212 expected: map[string]openrtb_ext.ExtMultiBid{ 6213 "appnexus": { 6214 Bidders: []string{"AppNexus", "appnexus", "UnknownBidder"}, 6215 }, 6216 }, 6217 }, 6218 }, 6219 }, 6220 { 6221 groupDesc: "prebid.MultiBid.Bidder and prebid.MultiBid.Bidders entries in tests", 6222 tests: []testCase{ 6223 { 6224 desc: "prebid.MultiBid.Bidder found, ignore entries in prebid.MultiBid.Bidders, even if its unknown", 6225 inPrebid: &openrtb_ext.ExtRequestPrebid{ 6226 MultiBid: []*openrtb_ext.ExtMultiBid{ 6227 { 6228 Bidder: "UnknownBidder", 6229 Bidders: []string{"appnexus", "rubicon", "pubmatic"}, 6230 }, 6231 }, 6232 }, 6233 expected: map[string]openrtb_ext.ExtMultiBid{}, 6234 }, 6235 { 6236 desc: "prebid.MultiBid.Bidder found in one entry, prebid.MultiBid.Bidders in another. Add all to map", 6237 inPrebid: &openrtb_ext.ExtRequestPrebid{ 6238 MultiBid: []*openrtb_ext.ExtMultiBid{ 6239 { 6240 Bidder: "pubmatic", 6241 Bidders: []string{"appnexus", "rubicon", "UnknownBidder"}, 6242 }, 6243 { 6244 Bidders: []string{"UnknownBidder", "appnexus", "rubicon"}, 6245 }, 6246 }, 6247 }, 6248 expected: map[string]openrtb_ext.ExtMultiBid{ 6249 "pubmatic": { 6250 Bidder: "pubmatic", 6251 Bidders: []string{"appnexus", "rubicon", "UnknownBidder"}, 6252 }, 6253 "appnexus": { 6254 Bidders: []string{"UnknownBidder", "appnexus", "rubicon"}, 6255 }, 6256 "rubicon": { 6257 Bidders: []string{"UnknownBidder", "appnexus", "rubicon"}, 6258 }, 6259 }, 6260 }, 6261 }, 6262 }, 6263 } 6264 for _, group := range testGroups { 6265 for _, tc := range group.tests { 6266 t.Run(group.groupDesc+tc.desc, func(t *testing.T) { 6267 multiBidMap := buildMultiBidMap(tc.inPrebid) 6268 assert.Equal(t, tc.expected, multiBidMap, tc.desc) 6269 }) 6270 } 6271 } 6272 } 6273 6274 func TestBidsToUpdate(t *testing.T) { 6275 type testInput struct { 6276 multiBid map[string]openrtb_ext.ExtMultiBid 6277 bidder string 6278 } 6279 testCases := []struct { 6280 desc string 6281 in testInput 6282 expected int 6283 }{ 6284 { 6285 desc: "Empty multibid map. Expect openrtb_ext.DefaultBidLimit", 6286 in: testInput{}, 6287 expected: openrtb_ext.DefaultBidLimit, 6288 }, 6289 { 6290 desc: "Empty bidder. Expect openrtb_ext.DefaultBidLimit", 6291 in: testInput{ 6292 multiBid: map[string]openrtb_ext.ExtMultiBid{ 6293 "appnexus": { 6294 Bidder: "appnexus", 6295 MaxBids: ptrutil.ToPtr(2), 6296 }, 6297 }, 6298 }, 6299 expected: openrtb_ext.DefaultBidLimit, 6300 }, 6301 { 6302 desc: "bidder finds a match in multibid map but TargetBidderCodePrefix is empty. Expect openrtb_ext.DefaultBidLimit", 6303 in: testInput{ 6304 multiBid: map[string]openrtb_ext.ExtMultiBid{ 6305 "appnexus": { 6306 Bidder: "appnexus", 6307 MaxBids: ptrutil.ToPtr(2), 6308 }, 6309 }, 6310 bidder: "appnexus", 6311 }, 6312 expected: openrtb_ext.DefaultBidLimit, 6313 }, 6314 { 6315 desc: "multibid element with non-empty TargetBidderCodePrefix matches bidder. Expect MaxBids value", 6316 in: testInput{ 6317 multiBid: map[string]openrtb_ext.ExtMultiBid{ 6318 "appnexus": { 6319 Bidder: "appnexus", 6320 MaxBids: ptrutil.ToPtr(2), 6321 TargetBidderCodePrefix: "aPrefix", 6322 }, 6323 }, 6324 bidder: "appnexus", 6325 }, 6326 expected: 2, 6327 }, 6328 } 6329 for _, tc := range testCases { 6330 t.Run(tc.desc, func(t *testing.T) { 6331 assert.Equal(t, tc.expected, bidsToUpdate(tc.in.multiBid, tc.in.bidder), tc.desc) 6332 }) 6333 } 6334 }