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