github.com/prebid/prebid-server@v0.275.0/exchange/bidder_test.go (about) 1 package exchange 2 3 import ( 4 "bytes" 5 "context" 6 "crypto/tls" 7 "encoding/json" 8 "errors" 9 "fmt" 10 "io" 11 "net/http" 12 "net/http/httptest" 13 "net/http/httptrace" 14 "sort" 15 "strings" 16 "testing" 17 "time" 18 19 "github.com/golang/glog" 20 "github.com/prebid/openrtb/v19/adcom1" 21 nativeRequests "github.com/prebid/openrtb/v19/native1/request" 22 nativeResponse "github.com/prebid/openrtb/v19/native1/response" 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/hooks/hookexecution" 31 "github.com/prebid/prebid-server/metrics" 32 metricsConfig "github.com/prebid/prebid-server/metrics/config" 33 "github.com/prebid/prebid-server/openrtb_ext" 34 "github.com/prebid/prebid-server/version" 35 "github.com/stretchr/testify/assert" 36 "github.com/stretchr/testify/mock" 37 ) 38 39 // TestSingleBidder makes sure that the following things work if the Bidder needs only one request. 40 // 41 // 1. The Bidder implementation is called with the arguments we expect. 42 // 2. The returned values are correct for a non-test bid. 43 func TestSingleBidder(t *testing.T) { 44 type aTest struct { 45 debugInfo *config.DebugInfo 46 httpCallsLen int 47 } 48 49 testCases := []*aTest{ 50 {&config.DebugInfo{Allow: false}, 0}, 51 {&config.DebugInfo{Allow: true}, 1}, 52 } 53 54 respStatus := 200 55 respBody := "{\"bid\":false}" 56 server := httptest.NewServer(mockHandler(respStatus, "getBody", respBody)) 57 defer server.Close() 58 59 requestHeaders := http.Header{} 60 requestHeaders.Add("Content-Type", "application/json") 61 62 bidAdjustments := map[string]float64{"test": 2.0} 63 firstInitialPrice := 3.0 64 secondInitialPrice := 4.0 65 66 bidderImpl := &goodSingleBidder{ 67 httpRequest: &adapters.RequestData{ 68 Method: "POST", 69 Uri: server.URL, 70 Body: []byte("{\"key\":\"val\"}"), 71 Headers: http.Header{}, 72 }, 73 bidResponse: nil, 74 } 75 76 ctx := context.Background() 77 78 for _, test := range testCases { 79 mockBidderResponse := &adapters.BidderResponse{ 80 Bids: []*adapters.TypedBid{ 81 { 82 Bid: &openrtb2.Bid{ 83 Price: firstInitialPrice, 84 }, 85 BidType: openrtb_ext.BidTypeBanner, 86 DealPriority: 4, 87 }, 88 { 89 Bid: &openrtb2.Bid{ 90 Price: secondInitialPrice, 91 }, 92 BidType: openrtb_ext.BidTypeVideo, 93 DealPriority: 5, 94 }, 95 }, 96 } 97 bidderImpl.bidResponse = mockBidderResponse 98 99 bidder := AdaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderAppnexus, test.debugInfo, "") 100 currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) 101 102 bidderReq := BidderRequest{ 103 BidRequest: &openrtb2.BidRequest{Imp: []openrtb2.Imp{{ID: "impId"}}}, 104 BidderName: "test", 105 } 106 bidReqOptions := bidRequestOptions{ 107 accountDebugAllowed: true, 108 headerDebugAllowed: false, 109 addCallSignHeader: false, 110 bidAdjustments: bidAdjustments, 111 } 112 extraInfo := &adapters.ExtraRequestInfo{} 113 seatBids, extraBidderRespInfo, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), extraInfo, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil) 114 115 assert.Len(t, seatBids, 1) 116 assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero()) 117 seatBid := seatBids[0] 118 119 // Make sure the goodSingleBidder was called with the expected arguments. 120 if bidderImpl.httpResponse == nil { 121 t.Errorf("The Bidder should be called with the server's response.") 122 } 123 if bidderImpl.httpResponse.StatusCode != respStatus { 124 t.Errorf("Bad response status. Expected %d, got %d", respStatus, bidderImpl.httpResponse.StatusCode) 125 } 126 if string(bidderImpl.httpResponse.Body) != respBody { 127 t.Errorf("Bad response body. Expected %s, got %s", respBody, string(bidderImpl.httpResponse.Body)) 128 } 129 130 // Make sure the returned values are what we expect 131 if len(errortypes.FatalOnly(errs)) != 0 { 132 t.Errorf("bidder.Bid returned %d errors. Expected 0", len(errs)) 133 } 134 135 if !test.debugInfo.Allow && len(errortypes.WarningOnly(errs)) != 1 { 136 t.Errorf("bidder.Bid returned %d warnings. Expected 1", len(errs)) 137 } 138 if len(seatBid.Bids) != len(mockBidderResponse.Bids) { 139 t.Fatalf("Expected %d bids. Got %d", len(mockBidderResponse.Bids), len(seatBid.Bids)) 140 } 141 for index, typedBid := range mockBidderResponse.Bids { 142 if typedBid.Bid != seatBid.Bids[index].Bid { 143 t.Errorf("Bid %d did not point to the same bid returned by the Bidder.", index) 144 } 145 if typedBid.BidType != seatBid.Bids[index].BidType { 146 t.Errorf("Bid %d did not have the right type. Expected %s, got %s", index, typedBid.BidType, seatBid.Bids[index].BidType) 147 } 148 if typedBid.DealPriority != seatBid.Bids[index].DealPriority { 149 t.Errorf("Bid %d did not have the right deal priority. Expected %s, got %s", index, typedBid.BidType, seatBid.Bids[index].BidType) 150 } 151 } 152 bidAdjustment := bidAdjustments["test"] 153 if mockBidderResponse.Bids[0].Bid.Price != bidAdjustment*firstInitialPrice { 154 t.Errorf("Bid[0].Price was not adjusted properly. Expected %f, got %f", bidAdjustment*firstInitialPrice, mockBidderResponse.Bids[0].Bid.Price) 155 } 156 if mockBidderResponse.Bids[1].Bid.Price != bidAdjustment*secondInitialPrice { 157 t.Errorf("Bid[1].Price was not adjusted properly. Expected %f, got %f", bidAdjustment*secondInitialPrice, mockBidderResponse.Bids[1].Bid.Price) 158 } 159 if len(seatBid.HttpCalls) != test.httpCallsLen { 160 t.Errorf("The bidder shouldn't log HttpCalls when request.test == 0. Found %d", len(seatBid.HttpCalls)) 161 } 162 for index, bid := range seatBid.Bids { 163 assert.NotEqual(t, mockBidderResponse.Bids[index].Bid.Price, bid.OriginalBidCPM, "The bid price was adjusted, so the originally bid CPM should be different") 164 } 165 } 166 } 167 168 func TestSingleBidderGzip(t *testing.T) { 169 type aTest struct { 170 debugInfo *config.DebugInfo 171 httpCallsLen int 172 } 173 174 testCases := []*aTest{ 175 {&config.DebugInfo{Allow: false}, 0}, 176 {&config.DebugInfo{Allow: true}, 1}, 177 } 178 179 respStatus := 200 180 respBody := "{\"bid\":false}" 181 server := httptest.NewServer(mockHandler(respStatus, "getBody", respBody)) 182 defer server.Close() 183 184 requestHeaders := http.Header{} 185 requestHeaders.Add("Content-Type", "application/json") 186 187 bidAdjustments := map[string]float64{"test": 2.0} 188 firstInitialPrice := 3.0 189 secondInitialPrice := 4.0 190 191 bidderImpl := &goodSingleBidder{ 192 httpRequest: &adapters.RequestData{ 193 Method: "POST", 194 Uri: server.URL, 195 Body: []byte(`{"key":"val"}`), 196 Headers: http.Header{}, 197 }, 198 bidResponse: nil, 199 } 200 201 ctx := context.Background() 202 203 for _, test := range testCases { 204 mockBidderResponse := &adapters.BidderResponse{ 205 Bids: []*adapters.TypedBid{ 206 { 207 Bid: &openrtb2.Bid{ 208 Price: firstInitialPrice, 209 }, 210 BidType: openrtb_ext.BidTypeBanner, 211 DealPriority: 4, 212 }, 213 { 214 Bid: &openrtb2.Bid{ 215 Price: secondInitialPrice, 216 }, 217 BidType: openrtb_ext.BidTypeVideo, 218 DealPriority: 5, 219 }, 220 }, 221 } 222 bidderImpl.bidResponse = mockBidderResponse 223 224 bidder := AdaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderAppnexus, test.debugInfo, "GZIP") 225 currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) 226 227 bidderReq := BidderRequest{ 228 BidRequest: &openrtb2.BidRequest{Imp: []openrtb2.Imp{{ID: "impId"}}}, 229 BidderName: "test", 230 } 231 bidReqOptions := bidRequestOptions{ 232 accountDebugAllowed: true, 233 headerDebugAllowed: false, 234 addCallSignHeader: false, 235 bidAdjustments: bidAdjustments, 236 } 237 extraInfo := &adapters.ExtraRequestInfo{} 238 seatBids, extraBidderRespInfo, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), extraInfo, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil) 239 assert.Len(t, seatBids, 1) 240 assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero()) 241 seatBid := seatBids[0] 242 243 // Make sure the goodSingleBidder was called with the expected arguments. 244 if bidderImpl.httpResponse == nil { 245 t.Errorf("The Bidder should be called with the server's response.") 246 } 247 if bidderImpl.httpResponse.StatusCode != respStatus { 248 t.Errorf("Bad response status. Expected %d, got %d", respStatus, bidderImpl.httpResponse.StatusCode) 249 } 250 if string(bidderImpl.httpResponse.Body) != respBody { 251 t.Errorf("Bad response body. Expected %s, got %s", respBody, string(bidderImpl.httpResponse.Body)) 252 } 253 254 // Make sure the returned values are what we expect 255 if len(errortypes.FatalOnly(errs)) != 0 { 256 t.Errorf("bidder.Bid returned %d errors. Expected 0", len(errs)) 257 } 258 259 if !test.debugInfo.Allow && len(errortypes.WarningOnly(errs)) != 1 { 260 t.Errorf("bidder.Bid returned %d warnings. Expected 1", len(errs)) 261 } 262 if len(seatBid.Bids) != len(mockBidderResponse.Bids) { 263 t.Fatalf("Expected %d bids. Got %d", len(mockBidderResponse.Bids), len(seatBid.Bids)) 264 } 265 for index, typedBid := range mockBidderResponse.Bids { 266 if typedBid.Bid != seatBid.Bids[index].Bid { 267 t.Errorf("Bid %d did not point to the same bid returned by the Bidder.", index) 268 } 269 if typedBid.BidType != seatBid.Bids[index].BidType { 270 t.Errorf("Bid %d did not have the right type. Expected %s, got %s", index, typedBid.BidType, seatBid.Bids[index].BidType) 271 } 272 if typedBid.DealPriority != seatBid.Bids[index].DealPriority { 273 t.Errorf("Bid %d did not have the right deal priority. Expected %s, got %s", index, typedBid.BidType, seatBid.Bids[index].BidType) 274 } 275 } 276 bidAdjustment := bidAdjustments["test"] 277 if mockBidderResponse.Bids[0].Bid.Price != bidAdjustment*firstInitialPrice { 278 t.Errorf("Bid[0].Price was not adjusted properly. Expected %f, got %f", bidAdjustment*firstInitialPrice, mockBidderResponse.Bids[0].Bid.Price) 279 } 280 if mockBidderResponse.Bids[1].Bid.Price != bidAdjustment*secondInitialPrice { 281 t.Errorf("Bid[1].Price was not adjusted properly. Expected %f, got %f", bidAdjustment*secondInitialPrice, mockBidderResponse.Bids[1].Bid.Price) 282 } 283 if len(seatBid.HttpCalls) != test.httpCallsLen { 284 t.Errorf("The bidder shouldn't log HttpCalls when request.test == 0. Found %d", len(seatBid.HttpCalls)) 285 } 286 if test.debugInfo.Allow && len(seatBid.HttpCalls) > 0 { 287 assert.Equalf(t, "gzip", seatBid.HttpCalls[0].RequestHeaders["Content-Encoding"][0], "Mismatched headers") 288 assert.Equalf(t, "{\"key\":\"val\"}", seatBid.HttpCalls[0].RequestBody, "Mismatched request bodies") 289 } 290 for index, bid := range seatBid.Bids { 291 assert.NotEqual(t, mockBidderResponse.Bids[index].Bid.Price, bid.OriginalBidCPM, "The bid price was adjusted, so the originally bid CPM should be different") 292 } 293 } 294 } 295 296 func TestRequestBidRemovesSensitiveHeaders(t *testing.T) { 297 server := httptest.NewServer(mockHandler(200, "getBody", "responseJson")) 298 defer server.Close() 299 300 oldVer := version.Ver 301 version.Ver = "test-version" 302 defer func() { 303 version.Ver = oldVer 304 }() 305 306 requestHeaders := http.Header{} 307 requestHeaders.Add("Content-Type", "application/json") 308 requestHeaders.Add("Authorization", "anySecret") 309 310 bidderImpl := &goodSingleBidder{ 311 httpRequest: &adapters.RequestData{ 312 Method: "POST", 313 Uri: server.URL, 314 Body: []byte("requestJson"), 315 Headers: requestHeaders, 316 }, 317 bidResponse: &adapters.BidderResponse{ 318 Bids: []*adapters.TypedBid{}, 319 }, 320 } 321 322 debugInfo := &config.DebugInfo{Allow: true} 323 ctx := context.Background() 324 325 bidder := AdaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderAppnexus, debugInfo, "") 326 currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) 327 328 bidderReq := BidderRequest{ 329 BidRequest: &openrtb2.BidRequest{Imp: []openrtb2.Imp{{ID: "impId"}}}, 330 BidderName: "test", 331 } 332 bidAdjustments := map[string]float64{"test": 1} 333 bidReqOptions := bidRequestOptions{ 334 accountDebugAllowed: true, 335 headerDebugAllowed: false, 336 addCallSignHeader: false, 337 bidAdjustments: bidAdjustments, 338 } 339 extraInfo := &adapters.ExtraRequestInfo{} 340 seatBids, extraBidderRespInfo, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), extraInfo, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil) 341 expectedHttpCalls := []*openrtb_ext.ExtHttpCall{ 342 { 343 Uri: server.URL, 344 RequestBody: "requestJson", 345 RequestHeaders: map[string][]string{"Content-Type": {"application/json"}, "X-Prebid": {"pbs-go/test-version"}}, 346 ResponseBody: "responseJson", 347 Status: 200, 348 }, 349 } 350 351 assert.Empty(t, errs) 352 assert.Len(t, seatBids, 1) 353 assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero()) 354 assert.ElementsMatch(t, seatBids[0].HttpCalls, expectedHttpCalls) 355 } 356 357 func TestSetGPCHeader(t *testing.T) { 358 server := httptest.NewServer(mockHandler(200, "getBody", "responseJson")) 359 defer server.Close() 360 361 requestHeaders := http.Header{} 362 requestHeaders.Add("Content-Type", "application/json") 363 364 bidderImpl := &goodSingleBidder{ 365 httpRequest: &adapters.RequestData{ 366 Method: "POST", 367 Uri: server.URL, 368 Body: []byte("requestJson"), 369 Headers: requestHeaders, 370 }, 371 bidResponse: &adapters.BidderResponse{ 372 Bids: []*adapters.TypedBid{}, 373 }, 374 } 375 376 debugInfo := &config.DebugInfo{Allow: true} 377 ctx := context.Background() 378 379 bidder := AdaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderAppnexus, debugInfo, "") 380 currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) 381 bidderReq := BidderRequest{ 382 BidRequest: &openrtb2.BidRequest{Imp: []openrtb2.Imp{{ID: "impId"}}}, 383 BidderName: "test", 384 } 385 bidAdjustments := map[string]float64{"test": 1} 386 bidReqOptions := bidRequestOptions{ 387 accountDebugAllowed: true, 388 headerDebugAllowed: false, 389 addCallSignHeader: false, 390 bidAdjustments: bidAdjustments, 391 } 392 extraInfo := &adapters.ExtraRequestInfo{GlobalPrivacyControlHeader: "1"} 393 seatBids, extraBidderRespInfo, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), extraInfo, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil) 394 395 expectedHttpCall := []*openrtb_ext.ExtHttpCall{ 396 { 397 Uri: server.URL, 398 RequestBody: "requestJson", 399 RequestHeaders: map[string][]string{"Content-Type": {"application/json"}, "X-Prebid": {"pbs-go/unknown"}, "Sec-Gpc": {"1"}}, 400 ResponseBody: "responseJson", 401 Status: 200, 402 }, 403 } 404 405 assert.Empty(t, errs) 406 assert.Len(t, seatBids, 1) 407 assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero()) 408 assert.ElementsMatch(t, seatBids[0].HttpCalls, expectedHttpCall) 409 } 410 411 func TestSetGPCHeaderNil(t *testing.T) { 412 server := httptest.NewServer(mockHandler(200, "getBody", "responseJson")) 413 defer server.Close() 414 415 bidderImpl := &goodSingleBidder{ 416 httpRequest: &adapters.RequestData{ 417 Method: "POST", 418 Uri: server.URL, 419 Body: []byte("requestJson"), 420 Headers: nil, 421 }, 422 bidResponse: &adapters.BidderResponse{ 423 Bids: []*adapters.TypedBid{}, 424 }, 425 } 426 427 debugInfo := &config.DebugInfo{Allow: true} 428 ctx := context.Background() 429 430 bidder := AdaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderAppnexus, debugInfo, "") 431 currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) 432 433 bidderReq := BidderRequest{ 434 BidRequest: &openrtb2.BidRequest{Imp: []openrtb2.Imp{{ID: "impId"}}}, 435 BidderName: "test", 436 } 437 bidAdjustments := map[string]float64{"test": 1} 438 bidReqOptions := bidRequestOptions{ 439 accountDebugAllowed: true, 440 headerDebugAllowed: false, 441 addCallSignHeader: false, 442 bidAdjustments: bidAdjustments, 443 } 444 extraInfo := &adapters.ExtraRequestInfo{GlobalPrivacyControlHeader: "1"} 445 seatBids, extraBidderRespInfo, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), extraInfo, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil) 446 447 expectedHttpCall := []*openrtb_ext.ExtHttpCall{ 448 { 449 Uri: server.URL, 450 RequestBody: "requestJson", 451 RequestHeaders: map[string][]string{"X-Prebid": {"pbs-go/unknown"}, "Sec-Gpc": {"1"}}, 452 ResponseBody: "responseJson", 453 Status: 200, 454 }, 455 } 456 457 assert.Empty(t, errs) 458 assert.Len(t, seatBids, 1) 459 assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero()) 460 assert.ElementsMatch(t, seatBids[0].HttpCalls, expectedHttpCall) 461 } 462 463 // TestMultiBidder makes sure all the requests get sent, and the responses processed. 464 // Because this is done in parallel, it should be run under the race detector. 465 func TestMultiBidder(t *testing.T) { 466 respStatus := 200 467 getRespBody := "{\"wasPost\":false}" 468 postRespBody := "{\"wasPost\":true}" 469 server := httptest.NewServer(mockHandler(respStatus, getRespBody, postRespBody)) 470 defer server.Close() 471 472 requestHeaders := http.Header{} 473 requestHeaders.Add("Content-Type", "application/json") 474 475 mockBidderResponse := &adapters.BidderResponse{ 476 Bids: []*adapters.TypedBid{ 477 { 478 Bid: &openrtb2.Bid{}, 479 BidType: openrtb_ext.BidTypeBanner, 480 }, 481 { 482 Bid: &openrtb2.Bid{}, 483 BidType: openrtb_ext.BidTypeVideo, 484 }, 485 }, 486 } 487 488 bidderImpl := &mixedMultiBidder{ 489 httpRequests: []*adapters.RequestData{{ 490 Method: "POST", 491 Uri: server.URL, 492 Body: []byte("{\"key\":\"val\"}"), 493 Headers: http.Header{}, 494 }, 495 { 496 Method: "GET", 497 Uri: server.URL, 498 Body: []byte("{\"key\":\"val2\"}"), 499 Headers: http.Header{}, 500 }}, 501 bidResponse: mockBidderResponse, 502 } 503 bidder := AdaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderAppnexus, nil, "") 504 currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) 505 bidderReq := BidderRequest{ 506 BidRequest: &openrtb2.BidRequest{Imp: []openrtb2.Imp{{ID: "impId"}}}, 507 BidderName: "test", 508 } 509 bidAdjustments := map[string]float64{"test": 1.0} 510 bidReqOptions := bidRequestOptions{ 511 accountDebugAllowed: true, 512 headerDebugAllowed: true, 513 addCallSignHeader: false, 514 bidAdjustments: bidAdjustments, 515 } 516 seatBids, extraBidderRespInfo, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil) 517 518 if len(seatBids) != 1 { 519 t.Fatalf("SeatBid should exist, because bids exist.") 520 } 521 522 if len(errs) != 1+len(bidderImpl.httpRequests) { 523 t.Errorf("Expected %d errors. Got %d", 1+len(bidderImpl.httpRequests), len(errs)) 524 } 525 if len(seatBids[0].Bids) != len(bidderImpl.httpResponses)*len(mockBidderResponse.Bids) { 526 t.Errorf("Expected %d bids. Got %d", len(bidderImpl.httpResponses)*len(mockBidderResponse.Bids), len(seatBids[0].Bids)) 527 } 528 assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero()) 529 530 } 531 532 // TestBidderTimeout makes sure that things work smoothly if the context expires before the Bidder 533 // manages to complete its task. 534 func TestBidderTimeout(t *testing.T) { 535 // Fixes #369 (hopefully): Define a context which has already expired 536 ctx, cancelFunc := context.WithDeadline(context.Background(), time.Now().Add(-7*time.Hour)) 537 cancelFunc() 538 <-ctx.Done() 539 540 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 541 w.WriteHeader(200) 542 if r.Method == "GET" { 543 w.Write([]byte("getBody")) 544 } else { 545 w.Write([]byte("postBody")) 546 } 547 }) 548 549 server := httptest.NewServer(handler) 550 defer server.Close() 551 552 bidder := &bidderAdapter{ 553 Bidder: &mixedMultiBidder{}, 554 BidderName: openrtb_ext.BidderAppnexus, 555 Client: server.Client(), 556 me: &metricsConfig.NilMetricsEngine{}, 557 } 558 tmaxAdjustments := &TmaxAdjustmentsPreprocessed{} 559 callInfo := bidder.doRequest(ctx, &adapters.RequestData{ 560 Method: "POST", 561 Uri: server.URL, 562 }, time.Now(), tmaxAdjustments) 563 if callInfo.err == nil { 564 t.Errorf("The bidder should report an error if the context has expired already.") 565 } 566 if callInfo.response != nil { 567 t.Errorf("There should be no response if the request never completed.") 568 } 569 } 570 571 // TestInvalidRequest makes sure that bidderAdapter.doRequest returns errors on bad requests. 572 func TestInvalidRequest(t *testing.T) { 573 server := httptest.NewServer(mockHandler(200, "getBody", "postBody")) 574 bidder := &bidderAdapter{ 575 Bidder: &mixedMultiBidder{}, 576 Client: server.Client(), 577 } 578 tmaxAdjustments := &TmaxAdjustmentsPreprocessed{} 579 callInfo := bidder.doRequest(context.Background(), &adapters.RequestData{ 580 Method: "\"", // force http.NewRequest() to fail 581 }, time.Now(), tmaxAdjustments) 582 if callInfo.err == nil { 583 t.Errorf("bidderAdapter.doRequest should return an error if the request data is malformed.") 584 } 585 } 586 587 // TestConnectionClose makes sure that bidderAdapter.doRequest returns errors if the connection closes unexpectedly. 588 func TestConnectionClose(t *testing.T) { 589 var server *httptest.Server 590 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 591 server.CloseClientConnections() 592 }) 593 server = httptest.NewServer(handler) 594 595 bidder := &bidderAdapter{ 596 Bidder: &mixedMultiBidder{}, 597 Client: server.Client(), 598 BidderName: openrtb_ext.BidderAppnexus, 599 me: &metricsConfig.NilMetricsEngine{}, 600 } 601 tmaxAdjustments := &TmaxAdjustmentsPreprocessed{} 602 callInfo := bidder.doRequest(context.Background(), &adapters.RequestData{ 603 Method: "POST", 604 Uri: server.URL, 605 }, time.Now(), tmaxAdjustments) 606 if callInfo.err == nil { 607 t.Errorf("bidderAdapter.doRequest should return an error if the connection closes unexpectedly.") 608 } 609 } 610 611 type bid struct { 612 currency string 613 price float64 614 originalBidCur string 615 } 616 617 // TestMultiCurrencies rate converter is set / active. 618 func TestMultiCurrencies(t *testing.T) { 619 // Setup: 620 respStatus := 200 621 getRespBody := "{\"wasPost\":false}" 622 postRespBody := "{\"wasPost\":true}" 623 624 testCases := []struct { 625 bids []bid 626 rates currency.Rates 627 expectedBids []bid 628 expectedBadCurrencyErrors []error 629 description string 630 }{ 631 { 632 bids: []bid{ 633 {currency: "USD", price: 1.1}, 634 {currency: "USD", price: 1.2}, 635 {currency: "USD", price: 1.3}, 636 }, 637 rates: currency.Rates{ 638 Conversions: map[string]map[string]float64{ 639 "GBP": { 640 "USD": 1.3050530256, 641 }, 642 "EUR": { 643 "USD": 1.1435678764, 644 }, 645 }, 646 }, 647 expectedBids: []bid{ 648 {currency: "USD", price: 1.1, originalBidCur: "USD"}, 649 {currency: "USD", price: 1.2, originalBidCur: "USD"}, 650 {currency: "USD", price: 1.3, originalBidCur: "USD"}, 651 }, 652 expectedBadCurrencyErrors: []error{}, 653 description: "Case 1 - Bidder respond with the same currency (default one) on all HTTP responses", 654 }, 655 { 656 bids: []bid{ 657 {currency: "", price: 1.1}, 658 {currency: "", price: 1.2}, 659 {currency: "", price: 1.3}, 660 }, 661 rates: currency.Rates{ 662 Conversions: map[string]map[string]float64{ 663 "GBP": { 664 "USD": 1.3050530256, 665 }, 666 "EUR": { 667 "USD": 1.1435678764, 668 }, 669 }, 670 }, 671 expectedBids: []bid{ 672 {currency: "USD", price: 1.1, originalBidCur: "USD"}, 673 {currency: "USD", price: 1.2, originalBidCur: "USD"}, 674 {currency: "USD", price: 1.3, originalBidCur: "USD"}, 675 }, 676 expectedBadCurrencyErrors: []error{}, 677 description: "Case 2 - Bidder respond with no currency on all HTTP responses", 678 }, 679 { 680 bids: []bid{ 681 {currency: "EUR", price: 1.1}, 682 {currency: "EUR", price: 1.2}, 683 {currency: "EUR", price: 1.3}, 684 }, 685 rates: currency.Rates{ 686 Conversions: map[string]map[string]float64{ 687 "GBP": { 688 "USD": 1.3050530256, 689 }, 690 "EUR": { 691 "USD": 1.1435678764, 692 }, 693 }, 694 }, 695 expectedBids: []bid{ 696 {currency: "USD", price: 1.1 * 1.1435678764, originalBidCur: "EUR"}, 697 {currency: "USD", price: 1.2 * 1.1435678764, originalBidCur: "EUR"}, 698 {currency: "USD", price: 1.3 * 1.1435678764, originalBidCur: "EUR"}, 699 }, 700 expectedBadCurrencyErrors: []error{}, 701 description: "Case 3 - Bidder respond with the same non default currency on all HTTP responses", 702 }, 703 { 704 bids: []bid{ 705 {currency: "USD", price: 1.1}, 706 {currency: "EUR", price: 1.2}, 707 {currency: "GBP", price: 1.3}, 708 }, 709 rates: currency.Rates{ 710 Conversions: map[string]map[string]float64{ 711 "GBP": { 712 "USD": 1.3050530256, 713 }, 714 "EUR": { 715 "USD": 1.1435678764, 716 }, 717 }, 718 }, 719 expectedBids: []bid{ 720 {currency: "USD", price: 1.1, originalBidCur: "USD"}, 721 {currency: "USD", price: 1.2 * 1.1435678764, originalBidCur: "EUR"}, 722 {currency: "USD", price: 1.3 * 1.3050530256, originalBidCur: "GBP"}, 723 }, 724 expectedBadCurrencyErrors: []error{}, 725 description: "Case 4 - Bidder respond with a mix of currencies on all HTTP responses", 726 }, 727 { 728 bids: []bid{ 729 {currency: "", price: 1.1}, 730 {currency: "EUR", price: 1.2}, 731 {currency: "GBP", price: 1.3}, 732 }, 733 rates: currency.Rates{ 734 Conversions: map[string]map[string]float64{ 735 "GBP": { 736 "USD": 1.3050530256, 737 }, 738 "EUR": { 739 "USD": 1.1435678764, 740 }, 741 }, 742 }, 743 expectedBids: []bid{ 744 {currency: "USD", price: 1.1, originalBidCur: "USD"}, 745 {currency: "USD", price: 1.2 * 1.1435678764, originalBidCur: "EUR"}, 746 {currency: "USD", price: 1.3 * 1.3050530256, originalBidCur: "GBP"}, 747 }, 748 expectedBadCurrencyErrors: []error{}, 749 description: "Case 5 - Bidder respond with a mix of currencies and no currency on all HTTP responses", 750 }, 751 { 752 bids: []bid{ 753 {currency: "JPY", price: 1.1}, 754 {currency: "EUR", price: 1.2}, 755 {currency: "GBP", price: 1.3}, 756 }, 757 rates: currency.Rates{ 758 Conversions: map[string]map[string]float64{ 759 "GBP": { 760 "USD": 1.3050530256, 761 }, 762 "EUR": { 763 "USD": 1.1435678764, 764 }, 765 }, 766 }, 767 expectedBids: []bid{ 768 {currency: "USD", price: 1.2 * 1.1435678764, originalBidCur: "EUR"}, 769 {currency: "USD", price: 1.3 * 1.3050530256, originalBidCur: "GBP"}, 770 }, 771 expectedBadCurrencyErrors: []error{ 772 currency.ConversionNotFoundError{FromCur: "JPY", ToCur: "USD"}, 773 }, 774 description: "Case 6 - Bidder respond with a mix of currencies and one unknown on all HTTP responses", 775 }, 776 { 777 bids: []bid{ 778 {currency: "JPY", price: 1.1}, 779 {currency: "BZD", price: 1.2}, 780 {currency: "DKK", price: 1.3}, 781 }, 782 rates: currency.Rates{ 783 Conversions: map[string]map[string]float64{ 784 "GBP": { 785 "USD": 1.3050530256, 786 }, 787 "EUR": { 788 "USD": 1.1435678764, 789 }, 790 }, 791 }, 792 expectedBids: []bid{}, 793 expectedBadCurrencyErrors: []error{ 794 currency.ConversionNotFoundError{FromCur: "JPY", ToCur: "USD"}, 795 currency.ConversionNotFoundError{FromCur: "BZD", ToCur: "USD"}, 796 currency.ConversionNotFoundError{FromCur: "DKK", ToCur: "USD"}, 797 }, 798 description: "Case 7 - Bidder respond with currencies not having any rate on all HTTP responses", 799 }, 800 { 801 bids: []bid{ 802 {currency: "AAA", price: 1.1}, 803 {currency: "BBB", price: 1.2}, 804 {currency: "CCC", price: 1.3}, 805 }, 806 rates: currency.Rates{ 807 Conversions: map[string]map[string]float64{ 808 "GBP": { 809 "USD": 1.3050530256, 810 }, 811 "EUR": { 812 "USD": 1.1435678764, 813 }, 814 }, 815 }, 816 expectedBids: []bid{}, 817 expectedBadCurrencyErrors: []error{ 818 errors.New("currency: tag is not a recognized currency"), 819 errors.New("currency: tag is not a recognized currency"), 820 errors.New("currency: tag is not a recognized currency"), 821 }, 822 description: "Case 8 - Bidder respond with not existing currencies", 823 }, 824 } 825 826 server := httptest.NewServer(mockHandler(respStatus, getRespBody, postRespBody)) 827 defer server.Close() 828 829 for _, tc := range testCases { 830 mockBidderResponses := make([]*adapters.BidderResponse, len(tc.bids)) 831 bidderImpl := &goodMultiHTTPCallsBidder{ 832 bidResponses: mockBidderResponses, 833 } 834 bidderImpl.httpRequest = make([]*adapters.RequestData, len(tc.bids)) 835 836 for i, bid := range tc.bids { 837 mockBidderResponses[i] = &adapters.BidderResponse{ 838 Bids: []*adapters.TypedBid{ 839 { 840 Bid: &openrtb2.Bid{ 841 Price: bid.price, 842 }, 843 BidType: openrtb_ext.BidTypeBanner, 844 }, 845 }, 846 Currency: bid.currency, 847 } 848 849 bidderImpl.httpRequest[i] = &adapters.RequestData{ 850 Method: "POST", 851 Uri: server.URL, 852 Body: []byte("{\"key\":\"val\"}"), 853 Headers: http.Header{}, 854 } 855 } 856 857 mockedHTTPServer := httptest.NewServer(http.HandlerFunc( 858 func(rw http.ResponseWriter, req *http.Request) { 859 b, err := json.Marshal(tc.rates) 860 if err == nil { 861 rw.WriteHeader(http.StatusOK) 862 rw.Write(b) 863 } else { 864 rw.WriteHeader(http.StatusInternalServerError) 865 } 866 }), 867 ) 868 869 // Execute: 870 bidder := AdaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderAppnexus, nil, "") 871 currencyConverter := currency.NewRateConverter( 872 &http.Client{}, 873 mockedHTTPServer.URL, 874 time.Duration(24)*time.Hour, 875 ) 876 time.Sleep(time.Duration(500) * time.Millisecond) 877 currencyConverter.Run() 878 879 bidderReq := BidderRequest{ 880 BidRequest: &openrtb2.BidRequest{Imp: []openrtb2.Imp{{ID: "impId"}}}, 881 BidderName: openrtb_ext.BidderAppnexus, 882 } 883 bidAdjustments := map[string]float64{string(openrtb_ext.BidderAppnexus): 1} 884 seatBids, extraBidderRespInfo, errs := bidder.requestBid( 885 context.Background(), 886 bidderReq, 887 currencyConverter.Rates(), 888 &adapters.ExtraRequestInfo{}, 889 &adscert.NilSigner{}, 890 bidRequestOptions{ 891 accountDebugAllowed: true, 892 headerDebugAllowed: true, 893 addCallSignHeader: false, 894 bidAdjustments: bidAdjustments, 895 }, 896 openrtb_ext.ExtAlternateBidderCodes{}, 897 &hookexecution.EmptyHookExecutor{}, 898 nil, 899 ) 900 assert.Len(t, seatBids, 1) 901 seatBid := seatBids[0] 902 assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero()) 903 904 // Verify: 905 resultLightBids := make([]bid, len(seatBid.Bids)) 906 for i, b := range seatBid.Bids { 907 resultLightBids[i] = bid{ 908 price: b.Bid.Price, 909 currency: seatBid.Currency, 910 originalBidCur: b.OriginalBidCur, 911 } 912 } 913 assert.ElementsMatch(t, tc.expectedBids, resultLightBids, tc.description) 914 assert.ElementsMatch(t, tc.expectedBadCurrencyErrors, errs, tc.description) 915 } 916 } 917 918 // TestMultiCurrencies_RateConverterNotSet no rate converter is set / active. 919 func TestMultiCurrencies_RateConverterNotSet(t *testing.T) { 920 // Setup: 921 respStatus := 200 922 getRespBody := "{\"wasPost\":false}" 923 postRespBody := "{\"wasPost\":true}" 924 925 testCases := []struct { 926 bidCurrency []string 927 expectedBidsCount uint 928 expectedBadCurrencyErrors []error 929 description string 930 }{ 931 { 932 bidCurrency: []string{"USD", "USD", "USD"}, 933 expectedBidsCount: 3, 934 expectedBadCurrencyErrors: []error{}, 935 description: "Case 1 - Bidder respond with the same currency (default one) on all HTTP responses", 936 }, 937 { 938 bidCurrency: []string{"EUR", "EUR", "EUR"}, 939 expectedBidsCount: 0, 940 expectedBadCurrencyErrors: []error{ 941 currency.ConversionNotFoundError{FromCur: "EUR", ToCur: "USD"}, 942 currency.ConversionNotFoundError{FromCur: "EUR", ToCur: "USD"}, 943 currency.ConversionNotFoundError{FromCur: "EUR", ToCur: "USD"}, 944 }, 945 description: "Case 2 - Bidder respond with the same currency (not default one) on all HTTP responses", 946 }, 947 { 948 bidCurrency: []string{"", "", ""}, 949 expectedBidsCount: 3, 950 expectedBadCurrencyErrors: []error{}, 951 description: "Case 3 - Bidder responds with currency not set on all HTTP responses", 952 }, 953 { 954 bidCurrency: []string{"", "USD", ""}, 955 expectedBidsCount: 3, 956 expectedBadCurrencyErrors: []error{}, 957 description: "Case 4 - Bidder responds with a mix of not set and default currency in HTTP responses", 958 }, 959 { 960 bidCurrency: []string{"USD", "USD", ""}, 961 expectedBidsCount: 3, 962 expectedBadCurrencyErrors: []error{}, 963 description: "Case 5 - Bidder responds with a mix of not set and default currency in HTTP responses", 964 }, 965 { 966 bidCurrency: []string{"", "", "USD"}, 967 expectedBidsCount: 3, 968 expectedBadCurrencyErrors: []error{}, 969 description: "Case 6 - Bidder responds with a mix of not set and default currency in HTTP responses", 970 }, 971 { 972 bidCurrency: []string{"EUR", "", "USD"}, 973 expectedBidsCount: 2, 974 expectedBadCurrencyErrors: []error{ 975 currency.ConversionNotFoundError{FromCur: "EUR", ToCur: "USD"}, 976 }, 977 description: "Case 7 - Bidder responds with a mix of not set, non default currency and default currency in HTTP responses", 978 }, 979 { 980 bidCurrency: []string{"GBP", "", "USD"}, 981 expectedBidsCount: 2, 982 expectedBadCurrencyErrors: []error{ 983 currency.ConversionNotFoundError{FromCur: "GBP", ToCur: "USD"}, 984 }, 985 description: "Case 8 - Bidder responds with a mix of not set, non default currency and default currency in HTTP responses", 986 }, 987 { 988 bidCurrency: []string{"GBP", "", ""}, 989 expectedBidsCount: 2, 990 expectedBadCurrencyErrors: []error{ 991 currency.ConversionNotFoundError{FromCur: "GBP", ToCur: "USD"}, 992 }, 993 description: "Case 9 - Bidder responds with a mix of not set and empty currencies (default currency) in HTTP responses", 994 }, 995 // Bidder respond with not existing currencies 996 { 997 bidCurrency: []string{"AAA", "BBB", "CCC"}, 998 expectedBidsCount: 0, 999 expectedBadCurrencyErrors: []error{ 1000 errors.New("currency: tag is not a recognized currency"), 1001 errors.New("currency: tag is not a recognized currency"), 1002 errors.New("currency: tag is not a recognized currency"), 1003 }, 1004 description: "Case 10 - Bidder respond with not existing currencies", 1005 }, 1006 } 1007 1008 server := httptest.NewServer(mockHandler(respStatus, getRespBody, postRespBody)) 1009 defer server.Close() 1010 1011 for _, tc := range testCases { 1012 mockBidderResponses := make([]*adapters.BidderResponse, len(tc.bidCurrency)) 1013 bidderImpl := &goodMultiHTTPCallsBidder{ 1014 bidResponses: mockBidderResponses, 1015 } 1016 bidderImpl.httpRequest = make([]*adapters.RequestData, len(tc.bidCurrency)) 1017 1018 for i, cur := range tc.bidCurrency { 1019 mockBidderResponses[i] = &adapters.BidderResponse{ 1020 Bids: []*adapters.TypedBid{ 1021 { 1022 Bid: &openrtb2.Bid{}, 1023 BidType: openrtb_ext.BidTypeBanner, 1024 }, 1025 }, 1026 Currency: cur, 1027 } 1028 1029 bidderImpl.httpRequest[i] = &adapters.RequestData{ 1030 Method: "POST", 1031 Uri: server.URL, 1032 Body: []byte("{\"key\":\"val\"}"), 1033 Headers: http.Header{}, 1034 } 1035 } 1036 1037 // Execute: 1038 bidder := AdaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderAppnexus, nil, "") 1039 currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) 1040 bidderReq := BidderRequest{ 1041 BidRequest: &openrtb2.BidRequest{Imp: []openrtb2.Imp{{ID: "impId"}}}, 1042 BidderName: "test", 1043 } 1044 bidAdjustments := map[string]float64{"test": 1} 1045 seatBids, extraBidderRespInfo, errs := bidder.requestBid( 1046 context.Background(), 1047 bidderReq, 1048 currencyConverter.Rates(), 1049 &adapters.ExtraRequestInfo{}, 1050 &adscert.NilSigner{}, 1051 bidRequestOptions{ 1052 accountDebugAllowed: true, 1053 headerDebugAllowed: true, 1054 addCallSignHeader: false, 1055 bidAdjustments: bidAdjustments, 1056 }, 1057 openrtb_ext.ExtAlternateBidderCodes{}, 1058 &hookexecution.EmptyHookExecutor{}, 1059 nil, 1060 ) 1061 assert.Len(t, seatBids, 1) 1062 seatBid := seatBids[0] 1063 1064 // Verify: 1065 assert.Equal(t, false, (seatBid == nil && tc.expectedBidsCount != 0), tc.description) 1066 assert.Equal(t, tc.expectedBidsCount, uint(len(seatBid.Bids)), tc.description) 1067 assert.ElementsMatch(t, tc.expectedBadCurrencyErrors, errs, tc.description) 1068 assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero()) 1069 } 1070 } 1071 1072 // TestMultiCurrencies_RequestCurrencyPick tests request currencies pick. 1073 func TestMultiCurrencies_RequestCurrencyPick(t *testing.T) { 1074 // Setup: 1075 respStatus := 200 1076 getRespBody := "{\"wasPost\":false}" 1077 postRespBody := "{\"wasPost\":true}" 1078 1079 testCases := []struct { 1080 bidRequestCurrencies []string 1081 bidResponsesCurrency string 1082 expectedPickedCurrency string 1083 expectedError bool 1084 rates currency.Rates 1085 description string 1086 }{ 1087 { 1088 bidRequestCurrencies: []string{"EUR", "USD", "JPY"}, 1089 bidResponsesCurrency: "EUR", 1090 expectedPickedCurrency: "EUR", 1091 expectedError: false, 1092 rates: currency.Rates{ 1093 Conversions: map[string]map[string]float64{ 1094 "JPY": { 1095 "USD": 0.0089, 1096 }, 1097 "GBP": { 1098 "USD": 1.3050530256, 1099 }, 1100 "EUR": { 1101 "USD": 1.1435678764, 1102 }, 1103 }, 1104 }, 1105 description: "Case 1 - Allowed currencies in bid request are known, first one is picked", 1106 }, 1107 { 1108 bidRequestCurrencies: []string{"JPY"}, 1109 bidResponsesCurrency: "JPY", 1110 expectedPickedCurrency: "JPY", 1111 expectedError: false, 1112 rates: currency.Rates{ 1113 Conversions: map[string]map[string]float64{ 1114 "JPY": { 1115 "USD": 0.0089, 1116 }, 1117 }, 1118 }, 1119 description: "Case 2 - There is only one allowed currencies in bid request, it's a known one, it's picked", 1120 }, 1121 { 1122 bidRequestCurrencies: []string{"CNY", "USD", "EUR", "JPY"}, 1123 bidResponsesCurrency: "USD", 1124 expectedPickedCurrency: "USD", 1125 expectedError: false, 1126 rates: currency.Rates{ 1127 Conversions: map[string]map[string]float64{ 1128 "JPY": { 1129 "USD": 0.0089, 1130 }, 1131 "GBP": { 1132 "USD": 1.3050530256, 1133 }, 1134 "EUR": { 1135 "USD": 1.1435678764, 1136 }, 1137 }, 1138 }, 1139 description: "Case 3 - First allowed currencies in bid request is not known but the others are, second one is picked", 1140 }, 1141 { 1142 bidRequestCurrencies: []string{"CNY", "EUR", "JPY"}, 1143 bidResponsesCurrency: "", 1144 expectedPickedCurrency: "", 1145 expectedError: true, 1146 rates: currency.Rates{ 1147 Conversions: map[string]map[string]float64{}, 1148 }, 1149 description: "Case 4 - None allowed currencies in bid request are known, an error is returned", 1150 }, 1151 { 1152 bidRequestCurrencies: []string{"CNY", "EUR", "JPY", "USD"}, 1153 bidResponsesCurrency: "USD", 1154 expectedPickedCurrency: "USD", 1155 expectedError: false, 1156 rates: currency.Rates{ 1157 Conversions: map[string]map[string]float64{}, 1158 }, 1159 description: "Case 5 - None allowed currencies in bid request are known but the default one (`USD`), no rates are set but default currency will be picked", 1160 }, 1161 { 1162 bidRequestCurrencies: nil, 1163 bidResponsesCurrency: "USD", 1164 expectedPickedCurrency: "USD", 1165 expectedError: false, 1166 rates: currency.Rates{ 1167 Conversions: map[string]map[string]float64{}, 1168 }, 1169 description: "Case 6 - No allowed currencies specified in bid request, default one is picked: `USD`", 1170 }, 1171 } 1172 1173 server := httptest.NewServer(mockHandler(respStatus, getRespBody, postRespBody)) 1174 defer server.Close() 1175 1176 for _, tc := range testCases { 1177 1178 mockedHTTPServer := httptest.NewServer(http.HandlerFunc( 1179 func(rw http.ResponseWriter, req *http.Request) { 1180 b, err := json.Marshal(tc.rates) 1181 if err == nil { 1182 rw.WriteHeader(http.StatusOK) 1183 rw.Write(b) 1184 } else { 1185 rw.WriteHeader(http.StatusInternalServerError) 1186 } 1187 }), 1188 ) 1189 1190 mockBidderResponses := []*adapters.BidderResponse{ 1191 { 1192 Bids: []*adapters.TypedBid{ 1193 { 1194 Bid: &openrtb2.Bid{}, 1195 BidType: openrtb_ext.BidTypeBanner, 1196 }, 1197 }, 1198 Currency: tc.bidResponsesCurrency, 1199 }, 1200 } 1201 bidderImpl := &goodMultiHTTPCallsBidder{ 1202 bidResponses: mockBidderResponses, 1203 } 1204 bidderImpl.httpRequest = []*adapters.RequestData{ 1205 { 1206 Method: "POST", 1207 Uri: server.URL, 1208 Body: []byte("{\"key\":\"val\"}"), 1209 Headers: http.Header{}, 1210 }, 1211 } 1212 1213 // Execute: 1214 bidder := AdaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderAppnexus, nil, "") 1215 currencyConverter := currency.NewRateConverter( 1216 &http.Client{}, 1217 mockedHTTPServer.URL, 1218 time.Duration(24)*time.Hour, 1219 ) 1220 bidderReq := BidderRequest{ 1221 BidRequest: &openrtb2.BidRequest{Cur: tc.bidRequestCurrencies, Imp: []openrtb2.Imp{{ID: "impId"}}}, 1222 BidderName: "test", 1223 } 1224 bidAdjustments := map[string]float64{"test": 1} 1225 seatBids, extraBidderRespInfo, errs := bidder.requestBid( 1226 context.Background(), 1227 bidderReq, 1228 currencyConverter.Rates(), 1229 &adapters.ExtraRequestInfo{}, 1230 &adscert.NilSigner{}, 1231 bidRequestOptions{ 1232 accountDebugAllowed: true, 1233 headerDebugAllowed: false, 1234 addCallSignHeader: false, 1235 bidAdjustments: bidAdjustments, 1236 }, 1237 openrtb_ext.ExtAlternateBidderCodes{}, 1238 &hookexecution.EmptyHookExecutor{}, 1239 nil, 1240 ) 1241 assert.Len(t, seatBids, 1) 1242 seatBid := seatBids[0] 1243 1244 // Verify: 1245 if tc.expectedError { 1246 assert.NotNil(t, errs, tc.description) 1247 } else { 1248 assert.Nil(t, errs, tc.description) 1249 assert.Equal(t, tc.expectedPickedCurrency, seatBid.Currency, tc.description) 1250 assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero()) 1251 } 1252 } 1253 } 1254 1255 func TestMakeExt(t *testing.T) { 1256 testCases := []struct { 1257 description string 1258 given *httpCallInfo 1259 expected *openrtb_ext.ExtHttpCall 1260 }{ 1261 { 1262 description: "Nil", 1263 given: nil, 1264 expected: &openrtb_ext.ExtHttpCall{}, 1265 }, 1266 { 1267 description: "Empty", 1268 given: &httpCallInfo{ 1269 err: nil, 1270 response: nil, 1271 request: nil, 1272 }, 1273 expected: &openrtb_ext.ExtHttpCall{}, 1274 }, 1275 { 1276 description: "Request & Response - No Error", 1277 given: &httpCallInfo{ 1278 err: nil, 1279 request: &adapters.RequestData{ 1280 Uri: "requestUri", 1281 Body: []byte("requestBody"), 1282 Headers: makeHeader(map[string][]string{"Key1": {"value1", "value2"}}), 1283 }, 1284 response: &adapters.ResponseData{ 1285 Body: []byte("responseBody"), 1286 StatusCode: 999, 1287 }, 1288 }, 1289 expected: &openrtb_ext.ExtHttpCall{ 1290 Uri: "requestUri", 1291 RequestBody: "requestBody", 1292 RequestHeaders: map[string][]string{"Key1": {"value1", "value2"}}, 1293 ResponseBody: "responseBody", 1294 Status: 999, 1295 }, 1296 }, 1297 { 1298 description: "Request & Response - No Error with Authorization removal", 1299 given: &httpCallInfo{ 1300 err: nil, 1301 request: &adapters.RequestData{ 1302 Uri: "requestUri", 1303 Body: []byte("requestBody"), 1304 Headers: makeHeader(map[string][]string{"Key1": {"value1", "value2"}, "Authorization": {"secret"}}), 1305 }, 1306 response: &adapters.ResponseData{ 1307 Body: []byte("responseBody"), 1308 StatusCode: 999, 1309 }, 1310 }, 1311 expected: &openrtb_ext.ExtHttpCall{ 1312 Uri: "requestUri", 1313 RequestBody: "requestBody", 1314 RequestHeaders: map[string][]string{"Key1": {"value1", "value2"}}, 1315 ResponseBody: "responseBody", 1316 Status: 999, 1317 }, 1318 }, 1319 { 1320 description: "Request & Response - No Error with nil header", 1321 given: &httpCallInfo{ 1322 err: nil, 1323 request: &adapters.RequestData{ 1324 Uri: "requestUri", 1325 Body: []byte("requestBody"), 1326 Headers: nil, 1327 }, 1328 response: &adapters.ResponseData{ 1329 Body: []byte("responseBody"), 1330 StatusCode: 999, 1331 }, 1332 }, 1333 expected: &openrtb_ext.ExtHttpCall{ 1334 Uri: "requestUri", 1335 RequestBody: "requestBody", 1336 RequestHeaders: nil, 1337 ResponseBody: "responseBody", 1338 Status: 999, 1339 }, 1340 }, 1341 { 1342 description: "Request & Response - Error", 1343 given: &httpCallInfo{ 1344 err: errors.New("error"), 1345 request: &adapters.RequestData{ 1346 Uri: "requestUri", 1347 Body: []byte("requestBody"), 1348 Headers: makeHeader(map[string][]string{"Key1": {"value1", "value2"}}), 1349 }, 1350 response: &adapters.ResponseData{ 1351 Body: []byte("responseBody"), 1352 StatusCode: 999, 1353 }, 1354 }, 1355 expected: &openrtb_ext.ExtHttpCall{ 1356 Uri: "requestUri", 1357 RequestBody: "requestBody", 1358 RequestHeaders: map[string][]string{"Key1": {"value1", "value2"}}, 1359 }, 1360 }, 1361 { 1362 description: "Request Only", 1363 given: &httpCallInfo{ 1364 err: nil, 1365 request: &adapters.RequestData{ 1366 Uri: "requestUri", 1367 Body: []byte("requestBody"), 1368 Headers: makeHeader(map[string][]string{"Key1": {"value1", "value2"}}), 1369 }, 1370 response: nil, 1371 }, 1372 expected: &openrtb_ext.ExtHttpCall{ 1373 Uri: "requestUri", 1374 RequestBody: "requestBody", 1375 RequestHeaders: map[string][]string{"Key1": {"value1", "value2"}}, 1376 }, 1377 }, { 1378 description: "Response Only", 1379 given: &httpCallInfo{ 1380 err: nil, 1381 response: &adapters.ResponseData{ 1382 Body: []byte("responseBody"), 1383 StatusCode: 999, 1384 }, 1385 }, 1386 expected: &openrtb_ext.ExtHttpCall{}, 1387 }, 1388 } 1389 1390 for _, test := range testCases { 1391 result := makeExt(test.given) 1392 assert.Equal(t, test.expected, result, test.description) 1393 } 1394 } 1395 1396 func TestFilterHeader(t *testing.T) { 1397 testCases := []struct { 1398 description string 1399 given http.Header 1400 expected http.Header 1401 }{ 1402 { 1403 description: "Nil", 1404 given: nil, 1405 expected: nil, 1406 }, 1407 { 1408 description: "Empty", 1409 given: http.Header{}, 1410 expected: http.Header{}, 1411 }, 1412 { 1413 description: "One", 1414 given: makeHeader(map[string][]string{"Key1": {"value1"}}), 1415 expected: makeHeader(map[string][]string{"Key1": {"value1"}}), 1416 }, 1417 { 1418 description: "Many", 1419 given: makeHeader(map[string][]string{"Key1": {"value1"}, "Key2": {"value2a", "value2b"}}), 1420 expected: makeHeader(map[string][]string{"Key1": {"value1"}, "Key2": {"value2a", "value2b"}}), 1421 }, 1422 { 1423 description: "Authorization Header Omitted", 1424 given: makeHeader(map[string][]string{"authorization": {"secret"}}), 1425 expected: http.Header{}, 1426 }, 1427 { 1428 description: "Authorization Header Omitted - Case Insensitive", 1429 given: makeHeader(map[string][]string{"AuThOrIzAtIoN": {"secret"}}), 1430 expected: http.Header{}, 1431 }, 1432 { 1433 description: "Authorization Header Omitted + Other Keys", 1434 given: makeHeader(map[string][]string{"authorization": {"secret"}, "Key1": {"value1"}}), 1435 expected: makeHeader(map[string][]string{"Key1": {"value1"}}), 1436 }, 1437 } 1438 1439 for _, test := range testCases { 1440 result := filterHeader(test.given) 1441 assert.Equal(t, test.expected, result, test.description) 1442 } 1443 } 1444 1445 func makeHeader(v map[string][]string) http.Header { 1446 h := http.Header{} 1447 for key, values := range v { 1448 for _, value := range values { 1449 h.Add(key, value) 1450 } 1451 } 1452 return h 1453 } 1454 1455 func TestMobileNativeTypes(t *testing.T) { 1456 respBody := "{\"bid\":false}" 1457 respStatus := 200 1458 server := httptest.NewServer(mockHandler(respStatus, "getBody", respBody)) 1459 defer server.Close() 1460 1461 reqBody := "{\"key\":\"val\"}" 1462 reqURL := server.URL 1463 1464 testCases := []struct { 1465 mockBidderRequest *openrtb2.BidRequest 1466 mockBidderResponse *adapters.BidderResponse 1467 expectedValue string 1468 description string 1469 }{ 1470 { 1471 mockBidderRequest: &openrtb2.BidRequest{ 1472 Imp: []openrtb2.Imp{ 1473 { 1474 ID: "some-imp-id", 1475 Native: &openrtb2.Native{ 1476 Request: "{\"ver\":\"1.1\",\"context\":1,\"contextsubtype\":11,\"plcmttype\":4,\"plcmtcnt\":1,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":500}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":1,\"hmin\":1}},{\"id\":3,\"required\":0,\"data\":{\"type\":1,\"len\":200}},{\"id\":4,\"required\":0,\"data\":{\"type\":2,\"len\":15000}},{\"id\":5,\"required\":0,\"data\":{\"type\":6,\"len\":40}}]}", 1477 }, 1478 }, 1479 }, 1480 App: &openrtb2.App{}, 1481 }, 1482 mockBidderResponse: &adapters.BidderResponse{ 1483 Bids: []*adapters.TypedBid{ 1484 { 1485 Bid: &openrtb2.Bid{ 1486 ImpID: "some-imp-id", 1487 AdM: "{\"assets\":[{\"id\":2,\"img\":{\"url\":\"http://vcdn.adnxs.com/p/creative-image/f8/7f/0f/13/f87f0f13-230c-4f05-8087-db9216e393de.jpg\",\"w\":989,\"h\":742,\"ext\":{\"appnexus\":{\"prevent_crop\":0}}}},{\"id\":1,\"title\":{\"text\":\"This is a Prebid Native Creative\"}},{\"id\":3,\"data\":{\"value\":\"Prebid.org\"}},{\"id\":4,\"data\":{\"value\":\"This is a Prebid Native Creative. There are many like it, but this one is mine.\"}}],\"link\":{\"url\":\"http://some-url.com\"},\"imptrackers\":[\"http://someimptracker.com\"],\"jstracker\":\"some-js-tracker\"}", 1488 Price: 10, 1489 }, 1490 BidType: openrtb_ext.BidTypeNative, 1491 }, 1492 }, 1493 }, 1494 expectedValue: "{\"assets\":[{\"id\":2,\"img\":{\"type\":3,\"url\":\"http://vcdn.adnxs.com/p/creative-image/f8/7f/0f/13/f87f0f13-230c-4f05-8087-db9216e393de.jpg\",\"w\":989,\"h\":742,\"ext\":{\"appnexus\":{\"prevent_crop\":0}}}},{\"id\":1,\"title\":{\"text\":\"This is a Prebid Native Creative\"}},{\"id\":3,\"data\":{\"type\":1,\"value\":\"Prebid.org\"}},{\"id\":4,\"data\":{\"type\":2,\"value\":\"This is a Prebid Native Creative. There are many like it, but this one is mine.\"}}],\"link\":{\"url\":\"http://some-url.com\"},\"imptrackers\":[\"http://someimptracker.com\"],\"jstracker\":\"some-js-tracker\"}", 1495 description: "Checks types in response", 1496 }, 1497 { 1498 mockBidderRequest: &openrtb2.BidRequest{ 1499 Imp: []openrtb2.Imp{ 1500 { 1501 ID: "some-imp-id", 1502 Native: &openrtb2.Native{ 1503 Request: "{\"ver\":\"1.1\",\"context\":1,\"contextsubtype\":11,\"plcmttype\":4,\"plcmtcnt\":1,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":500}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":1,\"hmin\":1}},{\"id\":3,\"required\":0,\"data\":{\"type\":1,\"len\":200}},{\"id\":4,\"required\":0,\"data\":{\"type\":2,\"len\":15000}},{\"id\":5,\"required\":0,\"data\":{\"type\":6,\"len\":40}}]}", 1504 }, 1505 }, 1506 }, 1507 App: &openrtb2.App{}, 1508 }, 1509 mockBidderResponse: &adapters.BidderResponse{ 1510 Bids: []*adapters.TypedBid{ 1511 { 1512 Bid: &openrtb2.Bid{ 1513 ImpID: "some-imp-id", 1514 AdM: "{\"some-diff-markup\":\"creative\"}", 1515 Price: 10, 1516 }, 1517 BidType: openrtb_ext.BidTypeNative, 1518 }, 1519 }, 1520 }, 1521 expectedValue: "{\"some-diff-markup\":\"creative\"}", 1522 description: "Non IAB compliant markup", 1523 }, 1524 } 1525 1526 for _, tc := range testCases { 1527 bidderImpl := &goodSingleBidder{ 1528 httpRequest: &adapters.RequestData{ 1529 Method: "POST", 1530 Uri: reqURL, 1531 Body: []byte(reqBody), 1532 Headers: http.Header{}, 1533 }, 1534 bidResponse: tc.mockBidderResponse, 1535 } 1536 bidder := AdaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderAppnexus, nil, "") 1537 currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) 1538 1539 bidderReq := BidderRequest{ 1540 BidRequest: tc.mockBidderRequest, 1541 BidderName: "test", 1542 } 1543 bidAdjustments := map[string]float64{"test": 1.0} 1544 seatBids, extraBidderRespInfo, _ := bidder.requestBid( 1545 context.Background(), 1546 bidderReq, 1547 currencyConverter.Rates(), 1548 &adapters.ExtraRequestInfo{}, 1549 &adscert.NilSigner{}, 1550 bidRequestOptions{ 1551 accountDebugAllowed: true, 1552 headerDebugAllowed: true, 1553 addCallSignHeader: false, 1554 bidAdjustments: bidAdjustments, 1555 }, 1556 openrtb_ext.ExtAlternateBidderCodes{}, 1557 &hookexecution.EmptyHookExecutor{}, 1558 nil, 1559 ) 1560 assert.Len(t, seatBids, 1) 1561 assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero()) 1562 var actualValue string 1563 for _, bid := range seatBids[0].Bids { 1564 actualValue = bid.Bid.AdM 1565 assert.JSONEq(t, tc.expectedValue, actualValue, tc.description) 1566 } 1567 } 1568 } 1569 1570 func TestRequestBidsStoredBidResponses(t *testing.T) { 1571 respBody := "{\"bid\":false}" 1572 respStatus := 200 1573 server := httptest.NewServer(mockHandler(respStatus, "getBody", respBody)) 1574 defer server.Close() 1575 1576 bidRespId1 := json.RawMessage(`{"id": "resp_id1", "seatbid": [{"bid": [{"id": "bid_id1"}], "seat": "testBidder1"}], "bidid": "123", "cur": "USD"}`) 1577 bidRespId2 := json.RawMessage(`{"id": "resp_id2", "seatbid": [{"bid": [{"id": "bid_id2_1", "impid": "bid1impid1"},{"id": "bid_id2_2", "impid": "bid2impid2"}], "seat": "testBidder2"}], "bidid": "124", "cur": "USD"}`) 1578 1579 testCases := []struct { 1580 description string 1581 mockBidderRequest *openrtb2.BidRequest 1582 bidderStoredResponses map[string]json.RawMessage 1583 impReplaceImpId map[string]bool 1584 expectedBidIds []string 1585 expectedImpIds []string 1586 }{ 1587 { 1588 description: "Single imp with stored bid response, replace impid is true", 1589 mockBidderRequest: &openrtb2.BidRequest{ 1590 Imp: nil, 1591 App: &openrtb2.App{}, 1592 }, 1593 bidderStoredResponses: map[string]json.RawMessage{ 1594 "bidResponseId1": bidRespId1, 1595 }, 1596 impReplaceImpId: map[string]bool{ 1597 "bidResponseId1": true, 1598 }, 1599 expectedBidIds: []string{"bid_id1"}, 1600 expectedImpIds: []string{"bidResponseId1"}, 1601 }, 1602 { 1603 description: "Single imp with multiple stored bid responses, replace impid is true", 1604 mockBidderRequest: &openrtb2.BidRequest{ 1605 Imp: nil, 1606 App: &openrtb2.App{}, 1607 }, 1608 bidderStoredResponses: map[string]json.RawMessage{ 1609 "bidResponseId2": bidRespId2, 1610 }, 1611 impReplaceImpId: map[string]bool{ 1612 "bidResponseId2": true, 1613 }, 1614 expectedBidIds: []string{"bid_id2_1", "bid_id2_2"}, 1615 expectedImpIds: []string{"bidResponseId2", "bidResponseId2"}, 1616 }, 1617 { 1618 description: "Single imp with multiple stored bid responses, replace impid is false", 1619 mockBidderRequest: &openrtb2.BidRequest{ 1620 Imp: nil, 1621 App: &openrtb2.App{}, 1622 }, 1623 bidderStoredResponses: map[string]json.RawMessage{ 1624 "bidResponseId2": bidRespId2, 1625 }, 1626 impReplaceImpId: map[string]bool{ 1627 "bidResponseId2": false, 1628 }, 1629 expectedBidIds: []string{"bid_id2_1", "bid_id2_2"}, 1630 expectedImpIds: []string{"bid1impid1", "bid2impid2"}, 1631 }, 1632 { 1633 description: "Two imp with multiple stored bid responses, replace impid is true and false", 1634 mockBidderRequest: &openrtb2.BidRequest{ 1635 Imp: nil, 1636 App: &openrtb2.App{}, 1637 }, 1638 bidderStoredResponses: map[string]json.RawMessage{ 1639 "bidResponseId1": bidRespId1, 1640 "bidResponseId2": bidRespId2, 1641 }, 1642 impReplaceImpId: map[string]bool{ 1643 "bidResponseId1": true, 1644 "bidResponseId2": false, 1645 }, 1646 expectedBidIds: []string{"bid_id2_1", "bid_id2_2", "bid_id1"}, 1647 expectedImpIds: []string{"bid1impid1", "bid2impid2", "bidResponseId1"}, 1648 }, 1649 } 1650 1651 for _, tc := range testCases { 1652 1653 bidderImpl := &goodSingleBidderWithStoredBidResp{} 1654 bidder := AdaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderAppnexus, nil, "") 1655 currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) 1656 1657 bidderReq := BidderRequest{ 1658 BidRequest: tc.mockBidderRequest, 1659 BidderName: openrtb_ext.BidderAppnexus, 1660 BidderStoredResponses: tc.bidderStoredResponses, 1661 ImpReplaceImpId: tc.impReplaceImpId, 1662 } 1663 bidAdjustments := map[string]float64{string(openrtb_ext.BidderAppnexus): 1.0} 1664 seatBids, extraBidderRespInfo, _ := bidder.requestBid( 1665 context.Background(), 1666 bidderReq, 1667 currencyConverter.Rates(), 1668 &adapters.ExtraRequestInfo{}, 1669 &adscert.NilSigner{}, 1670 bidRequestOptions{ 1671 accountDebugAllowed: true, 1672 headerDebugAllowed: true, 1673 addCallSignHeader: false, 1674 bidAdjustments: bidAdjustments, 1675 }, 1676 openrtb_ext.ExtAlternateBidderCodes{}, 1677 &hookexecution.EmptyHookExecutor{}, 1678 nil, 1679 ) 1680 assert.Len(t, seatBids, 1) 1681 assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero()) 1682 1683 assert.Len(t, seatBids[0].Bids, len(tc.expectedBidIds), "Incorrect bids number for test case ", tc.description) 1684 for _, bid := range seatBids[0].Bids { 1685 assert.Contains(t, tc.expectedBidIds, bid.Bid.ID, tc.description) 1686 assert.Contains(t, tc.expectedImpIds, bid.Bid.ImpID, tc.description) 1687 } 1688 } 1689 1690 } 1691 1692 // TestFledge verifies that fledge responses from bidders are collected. 1693 func TestFledge(t *testing.T) { 1694 respStatus := 200 1695 respBody := "{\"bid\":false}" 1696 server := httptest.NewServer(mockHandler(respStatus, "getBody", respBody)) 1697 defer server.Close() 1698 1699 fledgeAuctionConfig1 := &openrtb_ext.FledgeAuctionConfig{ 1700 ImpId: "imp-id-1", 1701 Config: json.RawMessage("[1,2,3]"), 1702 Bidder: "openx", 1703 } 1704 fledgeAuctionConfig2 := &openrtb_ext.FledgeAuctionConfig{ 1705 ImpId: "imp-id-2", 1706 Config: json.RawMessage("[3,2,1]"), 1707 Bidder: "openx", 1708 } 1709 1710 testCases := []struct { 1711 mockBidderResponse []*adapters.BidderResponse 1712 expectedFledge []*openrtb_ext.FledgeAuctionConfig 1713 description string 1714 }{ 1715 { 1716 mockBidderResponse: []*adapters.BidderResponse{ 1717 { 1718 Bids: []*adapters.TypedBid{}, 1719 FledgeAuctionConfigs: []*openrtb_ext.FledgeAuctionConfig{fledgeAuctionConfig1, fledgeAuctionConfig2}, 1720 }, 1721 nil, 1722 }, 1723 expectedFledge: []*openrtb_ext.FledgeAuctionConfig{fledgeAuctionConfig1, fledgeAuctionConfig2}, 1724 description: "Collects FLEDGE auction configs from single bidder response", 1725 }, 1726 { 1727 mockBidderResponse: []*adapters.BidderResponse{ 1728 { 1729 Bids: []*adapters.TypedBid{}, 1730 FledgeAuctionConfigs: []*openrtb_ext.FledgeAuctionConfig{fledgeAuctionConfig2}, 1731 }, 1732 { 1733 Bids: []*adapters.TypedBid{}, 1734 FledgeAuctionConfigs: []*openrtb_ext.FledgeAuctionConfig{fledgeAuctionConfig1}, 1735 }, 1736 }, 1737 expectedFledge: []*openrtb_ext.FledgeAuctionConfig{fledgeAuctionConfig1, fledgeAuctionConfig2}, 1738 description: "Collects FLEDGE auction configs from multiple bidder response", 1739 }, 1740 } 1741 1742 for _, tc := range testCases { 1743 bidderImpl := &goodMultiHTTPCallsBidder{ 1744 httpRequest: []*adapters.RequestData{ 1745 { 1746 Method: "POST", 1747 Uri: server.URL, 1748 Body: []byte("{\"key\":\"val1\"}"), 1749 Headers: http.Header{}, 1750 }, 1751 { 1752 Method: "POST", 1753 Uri: server.URL, 1754 Body: []byte("{\"key\":\"val2\"}"), 1755 Headers: http.Header{}, 1756 }, 1757 }, 1758 bidResponses: tc.mockBidderResponse, 1759 } 1760 bidder := AdaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderOpenx, nil, "") 1761 currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) 1762 1763 bidderReq := BidderRequest{ 1764 BidRequest: &openrtb2.BidRequest{ 1765 Imp: []openrtb2.Imp{ 1766 { 1767 ID: "imp-id-1", 1768 Banner: &openrtb2.Banner{}, 1769 Ext: json.RawMessage(`{"ae": 1}`), 1770 }, 1771 { 1772 ID: "imp-id-2", 1773 Banner: &openrtb2.Banner{}, 1774 Ext: json.RawMessage(`{"ae": 1}`), 1775 }, 1776 }, 1777 }, 1778 BidderName: "openx", 1779 } 1780 seatBids, extraBidderRespInfo, _ := bidder.requestBid( 1781 context.Background(), 1782 bidderReq, 1783 currencyConverter.Rates(), 1784 &adapters.ExtraRequestInfo{}, 1785 &adscert.NilSigner{}, 1786 bidRequestOptions{ 1787 accountDebugAllowed: true, 1788 headerDebugAllowed: true, 1789 addCallSignHeader: false, 1790 bidAdjustments: map[string]float64{"test": 1.0}, 1791 }, 1792 openrtb_ext.ExtAlternateBidderCodes{}, 1793 &hookexecution.EmptyHookExecutor{}, 1794 nil, 1795 ) 1796 assert.Len(t, seatBids, 1) 1797 assert.NotNil(t, seatBids[0].FledgeAuctionConfigs) 1798 assert.Len(t, seatBids[0].FledgeAuctionConfigs, len(tc.expectedFledge)) 1799 1800 assert.ElementsMatch(t, seatBids[0].FledgeAuctionConfigs, tc.expectedFledge) 1801 assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero()) 1802 } 1803 } 1804 1805 func TestErrorReporting(t *testing.T) { 1806 bidder := AdaptBidder(&bidRejector{}, nil, &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderAppnexus, nil, "") 1807 currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) 1808 bidderReq := BidderRequest{ 1809 BidRequest: &openrtb2.BidRequest{Imp: []openrtb2.Imp{{ID: "impId"}}}, 1810 BidderName: "test", 1811 } 1812 bidAdjustments := map[string]float64{"test": 1.0} 1813 bidReqOptions := bidRequestOptions{ 1814 accountDebugAllowed: true, 1815 headerDebugAllowed: false, 1816 addCallSignHeader: false, 1817 bidAdjustments: bidAdjustments, 1818 } 1819 bids, extraBidderRespInfo, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil) 1820 if bids != nil { 1821 t.Errorf("There should be no seatbid if no http requests are returned.") 1822 } 1823 if len(errs) != 1 { 1824 t.Fatalf("Expected 1 error. got %d", len(errs)) 1825 } 1826 if errs[0].Error() != "Invalid params on BidRequest." { 1827 t.Errorf(`Error message was mutated. Expected "%s", Got "%s"`, "Invalid params on BidRequest.", errs[0].Error()) 1828 } 1829 assert.True(t, extraBidderRespInfo.respProcessingStartTime.IsZero()) 1830 } 1831 1832 func TestSetAssetTypes(t *testing.T) { 1833 testCases := []struct { 1834 respAsset nativeResponse.Asset 1835 nativeReq nativeRequests.Request 1836 expectedErr string 1837 desc string 1838 }{ 1839 { 1840 respAsset: nativeResponse.Asset{ 1841 ID: openrtb2.Int64Ptr(1), 1842 Img: &nativeResponse.Image{ 1843 URL: "http://some-url", 1844 }, 1845 }, 1846 nativeReq: nativeRequests.Request{ 1847 Assets: []nativeRequests.Asset{ 1848 { 1849 ID: 1, 1850 Img: &nativeRequests.Image{ 1851 Type: 2, 1852 }, 1853 }, 1854 { 1855 ID: 2, 1856 Data: &nativeRequests.Data{ 1857 Type: 4, 1858 }, 1859 }, 1860 }, 1861 }, 1862 expectedErr: "", 1863 desc: "Matching image asset exists in the request and asset type is set correctly", 1864 }, 1865 { 1866 respAsset: nativeResponse.Asset{ 1867 ID: openrtb2.Int64Ptr(2), 1868 Data: &nativeResponse.Data{ 1869 Label: "some label", 1870 }, 1871 }, 1872 nativeReq: nativeRequests.Request{ 1873 Assets: []nativeRequests.Asset{ 1874 { 1875 ID: 1, 1876 Img: &nativeRequests.Image{ 1877 Type: 2, 1878 }, 1879 }, 1880 { 1881 ID: 2, 1882 Data: &nativeRequests.Data{ 1883 Type: 4, 1884 }, 1885 }, 1886 }, 1887 }, 1888 expectedErr: "", 1889 desc: "Matching data asset exists in the request and asset type is set correctly", 1890 }, 1891 { 1892 respAsset: nativeResponse.Asset{ 1893 ID: openrtb2.Int64Ptr(1), 1894 Img: &nativeResponse.Image{ 1895 URL: "http://some-url", 1896 }, 1897 }, 1898 nativeReq: nativeRequests.Request{ 1899 Assets: []nativeRequests.Asset{ 1900 { 1901 ID: 2, 1902 Img: &nativeRequests.Image{ 1903 Type: 2, 1904 }, 1905 }, 1906 }, 1907 }, 1908 expectedErr: "Unable to find asset with ID:1 in the request", 1909 desc: "Matching image asset with the same ID doesn't exist in the request", 1910 }, 1911 { 1912 respAsset: nativeResponse.Asset{ 1913 ID: openrtb2.Int64Ptr(2), 1914 Data: &nativeResponse.Data{ 1915 Label: "some label", 1916 }, 1917 }, 1918 nativeReq: nativeRequests.Request{ 1919 Assets: []nativeRequests.Asset{ 1920 { 1921 ID: 2, 1922 Img: &nativeRequests.Image{ 1923 Type: 2, 1924 }, 1925 }, 1926 }, 1927 }, 1928 expectedErr: "Response has a Data asset with ID:2 present that doesn't exist in the request", 1929 desc: "Assets with same ID in the req and resp are of different types", 1930 }, 1931 { 1932 respAsset: nativeResponse.Asset{ 1933 ID: openrtb2.Int64Ptr(1), 1934 Img: &nativeResponse.Image{ 1935 URL: "http://some-url", 1936 }, 1937 }, 1938 nativeReq: nativeRequests.Request{ 1939 Assets: []nativeRequests.Asset{ 1940 { 1941 ID: 1, 1942 Data: &nativeRequests.Data{ 1943 Type: 2, 1944 }, 1945 }, 1946 }, 1947 }, 1948 expectedErr: "Response has an Image asset with ID:1 present that doesn't exist in the request", 1949 desc: "Assets with same ID in the req and resp are of different types", 1950 }, 1951 { 1952 respAsset: nativeResponse.Asset{ 1953 Img: &nativeResponse.Image{ 1954 URL: "http://some-url", 1955 }, 1956 }, 1957 nativeReq: nativeRequests.Request{ 1958 Assets: []nativeRequests.Asset{ 1959 { 1960 ID: 1, 1961 Img: &nativeRequests.Image{ 1962 Type: 2, 1963 }, 1964 }, 1965 }, 1966 }, 1967 expectedErr: "Response Image asset doesn't have an ID", 1968 desc: "Response Image without an ID", 1969 }, 1970 { 1971 respAsset: nativeResponse.Asset{ 1972 Data: &nativeResponse.Data{ 1973 Label: "some label", 1974 }, 1975 }, 1976 nativeReq: nativeRequests.Request{ 1977 Assets: []nativeRequests.Asset{ 1978 { 1979 ID: 1, 1980 Data: &nativeRequests.Data{ 1981 Type: 2, 1982 }, 1983 }, 1984 }, 1985 }, 1986 expectedErr: "Response Data asset doesn't have an ID", 1987 desc: "Response Data asset without an ID", 1988 }, 1989 } 1990 1991 for _, test := range testCases { 1992 err := setAssetTypes(test.respAsset, test.nativeReq) 1993 if len(test.expectedErr) != 0 { 1994 assert.EqualError(t, err, test.expectedErr, "Test Case: %s", test.desc) 1995 continue 1996 } else { 1997 assert.NoError(t, err, "Test Case: %s", test.desc) 1998 } 1999 2000 for _, asset := range test.nativeReq.Assets { 2001 if asset.Img != nil && test.respAsset.Img != nil { 2002 assert.Equal(t, asset.Img.Type, test.respAsset.Img.Type, "Asset type not set correctly. Test Case: %s", test.desc) 2003 } 2004 if asset.Data != nil && test.respAsset.Data != nil { 2005 assert.Equal(t, asset.Data.Type, test.respAsset.Data.Type, "Asset type not set correctly. Test Case: %s", test.desc) 2006 } 2007 } 2008 } 2009 } 2010 2011 func TestCallRecordAdapterConnections(t *testing.T) { 2012 // Setup mock server 2013 respStatus := 200 2014 respBody := "{\"bid\":false}" 2015 server := httptest.NewServer(mockHandler(respStatus, "getBody", respBody)) 2016 defer server.Close() 2017 2018 // declare requestBid parameters 2019 bidAdjustments := map[string]float64{string(openrtb_ext.BidderAppnexus): 2.0} 2020 2021 bidderImpl := &goodSingleBidder{ 2022 httpRequest: &adapters.RequestData{ 2023 Method: "POST", 2024 Uri: server.URL, 2025 Body: []byte("{\"key\":\"val\"}"), 2026 Headers: http.Header{}, 2027 }, 2028 bidResponse: &adapters.BidderResponse{}, 2029 } 2030 2031 // setup a mock mockMetricEngine engine and its expectation 2032 mockMetricEngine := &metrics.MetricsEngineMock{} 2033 expectedAdapterName := openrtb_ext.BidderAppnexus 2034 compareConnWaitTime := func(dur time.Duration) bool { return dur.Nanoseconds() > 0 } 2035 2036 mockMetricEngine.On("RecordAdapterConnections", expectedAdapterName, false, mock.MatchedBy(compareConnWaitTime)).Once() 2037 mockMetricEngine.On("RecordOverheadTime", metrics.PreBidder, mock.Anything).Once() 2038 mockMetricEngine.On("RecordBidderServerResponseTime", mock.Anything).Once() 2039 2040 // Run requestBid using an http.Client with a mock handler 2041 bidder := AdaptBidder(bidderImpl, server.Client(), &config.Configuration{}, mockMetricEngine, openrtb_ext.BidderAppnexus, nil, "") 2042 currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) 2043 2044 bidderReq := BidderRequest{ 2045 BidRequest: &openrtb2.BidRequest{Imp: []openrtb2.Imp{{ID: "impId"}}}, 2046 BidderName: openrtb_ext.BidderAppnexus, 2047 } 2048 bidReqOptions := bidRequestOptions{ 2049 accountDebugAllowed: true, 2050 headerDebugAllowed: true, 2051 addCallSignHeader: false, 2052 bidAdjustments: bidAdjustments, 2053 } 2054 _, _, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{PbsEntryPoint: metrics.ReqTypeORTB2Web}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil) 2055 2056 // Assert no errors 2057 assert.Equal(t, 0, len(errs), "bidder.requestBid returned errors %v \n", errs) 2058 2059 // Assert RecordAdapterConnections() was called with the parameters we expected 2060 mockMetricEngine.AssertExpectations(t) 2061 } 2062 2063 type DNSDoneTripper struct{} 2064 2065 func (DNSDoneTripper) RoundTrip(req *http.Request) (*http.Response, error) { 2066 // Access the httptrace.ClientTrace 2067 trace := httptrace.ContextClientTrace(req.Context()) 2068 // Call the DNSDone method on the client trace 2069 trace.DNSDone(httptrace.DNSDoneInfo{}) 2070 2071 resp := &http.Response{ 2072 StatusCode: 200, 2073 Body: io.NopCloser(strings.NewReader("postBody")), 2074 } 2075 2076 return resp, nil 2077 } 2078 2079 type TLSHandshakeTripper struct{} 2080 2081 func (TLSHandshakeTripper) RoundTrip(req *http.Request) (*http.Response, error) { 2082 // Access the httptrace.ClientTrace 2083 trace := httptrace.ContextClientTrace(req.Context()) 2084 // Call the TLSHandshakeDone method on the client trace 2085 trace.TLSHandshakeDone(tls.ConnectionState{}, nil) 2086 2087 resp := &http.Response{ 2088 StatusCode: 200, 2089 Body: io.NopCloser(strings.NewReader("postBody")), 2090 } 2091 2092 return resp, nil 2093 } 2094 2095 func TestCallRecordDNSTime(t *testing.T) { 2096 // setup a mock metrics engine and its expectation 2097 metricsMock := &metrics.MetricsEngineMock{} 2098 metricsMock.Mock.On("RecordDNSTime", mock.Anything).Return() 2099 metricsMock.On("RecordOverheadTime", metrics.PreBidder, mock.Anything).Once() 2100 metricsMock.On("RecordBidderServerResponseTime", mock.Anything).Once() 2101 2102 // Instantiate the bidder that will send the request. We'll make sure to use an 2103 // http.Client that runs our mock RoundTripper so DNSDone(httptrace.DNSDoneInfo{}) 2104 // gets called 2105 bidder := &bidderAdapter{ 2106 Bidder: &mixedMultiBidder{}, 2107 Client: &http.Client{Transport: DNSDoneTripper{}}, 2108 me: metricsMock, 2109 } 2110 tmaxAdjustments := &TmaxAdjustmentsPreprocessed{} 2111 2112 // Run test 2113 bidder.doRequest(context.Background(), &adapters.RequestData{Method: "POST", Uri: "http://www.example.com/"}, time.Now(), tmaxAdjustments) 2114 2115 // Tried one or another, none seem to work without panicking 2116 metricsMock.AssertExpectations(t) 2117 } 2118 2119 func TestCallRecordTLSHandshakeTime(t *testing.T) { 2120 // setup a mock metrics engine and its expectation 2121 metricsMock := &metrics.MetricsEngineMock{} 2122 metricsMock.Mock.On("RecordTLSHandshakeTime", mock.Anything).Return() 2123 metricsMock.On("RecordOverheadTime", metrics.PreBidder, mock.Anything).Once() 2124 metricsMock.On("RecordBidderServerResponseTime", mock.Anything).Once() 2125 2126 // Instantiate the bidder that will send the request. We'll make sure to use an 2127 // http.Client that runs our mock RoundTripper so DNSDone(httptrace.DNSDoneInfo{}) 2128 // gets called 2129 bidder := &bidderAdapter{ 2130 Bidder: &mixedMultiBidder{}, 2131 Client: &http.Client{Transport: TLSHandshakeTripper{}}, 2132 me: metricsMock, 2133 } 2134 tmaxAdjustments := &TmaxAdjustmentsPreprocessed{} 2135 2136 // Run test 2137 bidder.doRequest(context.Background(), &adapters.RequestData{Method: "POST", Uri: "http://www.example.com/"}, time.Now(), tmaxAdjustments) 2138 2139 // Tried one or another, none seem to work without panicking 2140 metricsMock.AssertExpectations(t) 2141 } 2142 2143 func TestTimeoutNotificationOff(t *testing.T) { 2144 respBody := "{\"bid\":false}" 2145 respStatus := 200 2146 server := httptest.NewServer(mockHandler(respStatus, "getBody", respBody)) 2147 defer server.Close() 2148 2149 bidderImpl := ¬ifyingBidder{ 2150 notifyRequest: adapters.RequestData{ 2151 Method: "GET", 2152 Uri: server.URL + "/notify/me", 2153 Body: nil, 2154 Headers: http.Header{}, 2155 }, 2156 } 2157 bidder := &bidderAdapter{ 2158 Bidder: bidderImpl, 2159 Client: server.Client(), 2160 config: bidderAdapterConfig{Debug: config.Debug{}}, 2161 me: &metricsConfig.NilMetricsEngine{}, 2162 } 2163 if tb, ok := bidder.Bidder.(adapters.TimeoutBidder); !ok { 2164 t.Error("Failed to cast bidder to a TimeoutBidder") 2165 } else { 2166 bidder.doTimeoutNotification(tb, &adapters.RequestData{}, glog.Warningf) 2167 } 2168 } 2169 2170 func TestTimeoutNotificationOn(t *testing.T) { 2171 // Expire context immediately to force timeout handler. 2172 ctx, cancelFunc := context.WithDeadline(context.Background(), time.Now()) 2173 cancelFunc() 2174 2175 // Notification logic is hardcoded for 200ms. We need to wait for a little longer than that. 2176 server := httptest.NewServer(mockSlowHandler(205*time.Millisecond, 200, `{"bid":false}`)) 2177 defer server.Close() 2178 2179 bidder := ¬ifyingBidder{ 2180 notifyRequest: adapters.RequestData{ 2181 Method: "GET", 2182 Uri: server.URL + "/notify/me", 2183 Body: nil, 2184 Headers: http.Header{}, 2185 }, 2186 } 2187 2188 // Wrap with BidderInfo to mimic exchange.go flow. 2189 bidderWrappedWithInfo := wrapWithBidderInfo(bidder) 2190 2191 bidderAdapter := &bidderAdapter{ 2192 Bidder: bidderWrappedWithInfo, 2193 Client: server.Client(), 2194 config: bidderAdapterConfig{ 2195 Debug: config.Debug{ 2196 TimeoutNotification: config.TimeoutNotification{ 2197 Log: true, 2198 SamplingRate: 1.0, 2199 }, 2200 }, 2201 }, 2202 me: &metricsConfig.NilMetricsEngine{}, 2203 } 2204 2205 // Unwrap To Mimic exchange.go Casting Code 2206 var coreBidder adapters.Bidder = bidderAdapter.Bidder 2207 if b, ok := coreBidder.(*adapters.InfoAwareBidder); ok { 2208 coreBidder = b.Bidder 2209 } 2210 if _, ok := coreBidder.(adapters.TimeoutBidder); !ok { 2211 t.Fatal("Failed to cast bidder to a TimeoutBidder") 2212 } 2213 2214 bidRequest := adapters.RequestData{ 2215 Method: "POST", 2216 Uri: server.URL, 2217 Body: []byte(`{"id":"this-id","app":{"publisher":{"id":"pub-id"}}}`), 2218 } 2219 2220 var loggerBuffer bytes.Buffer 2221 logger := func(msg string, args ...interface{}) { 2222 loggerBuffer.WriteString(fmt.Sprintf(fmt.Sprintln(msg), args...)) 2223 } 2224 tmaxAdjustments := &TmaxAdjustmentsPreprocessed{} 2225 bidderAdapter.doRequestImpl(ctx, &bidRequest, logger, time.Now(), tmaxAdjustments) 2226 2227 // Wait a little longer than the 205ms mock server sleep. 2228 time.Sleep(210 * time.Millisecond) 2229 2230 logExpected := "TimeoutNotification: error:(context deadline exceeded) body:\n" 2231 logActual := loggerBuffer.String() 2232 assert.EqualValues(t, logExpected, logActual) 2233 } 2234 2235 func TestParseDebugInfoTrue(t *testing.T) { 2236 debugInfo := &config.DebugInfo{Allow: true} 2237 resDebugInfo := parseDebugInfo(debugInfo) 2238 assert.True(t, resDebugInfo, "Debug Allow value should be true") 2239 } 2240 2241 func TestParseDebugInfoFalse(t *testing.T) { 2242 debugInfo := &config.DebugInfo{Allow: false} 2243 resDebugInfo := parseDebugInfo(debugInfo) 2244 assert.False(t, resDebugInfo, "Debug Allow value should be false") 2245 } 2246 2247 func TestParseDebugInfoIsNil(t *testing.T) { 2248 resDebugInfo := parseDebugInfo(nil) 2249 assert.True(t, resDebugInfo, "Debug Allow value should be true") 2250 } 2251 2252 func TestPrepareStoredResponse(t *testing.T) { 2253 result := prepareStoredResponse("imp_id1", json.RawMessage(`{"id": "resp_id1"}`)) 2254 assert.Equal(t, []byte(ImpIdReqBody+"imp_id1"), result.request.Body, "incorrect request body") 2255 assert.Equal(t, []byte(`{"id": "resp_id1"}`), result.response.Body, "incorrect response body") 2256 } 2257 2258 func TestRequestBidsWithAdsCertsSigner(t *testing.T) { 2259 respStatus := 200 2260 respBody := `{"bid":false}` 2261 server := httptest.NewServer(mockHandler(respStatus, "getBody", respBody)) 2262 defer server.Close() 2263 2264 requestHeaders := http.Header{} 2265 requestHeaders.Add("Content-Type", "application/json") 2266 2267 bidderImpl := &goodSingleBidder{ 2268 httpRequest: &adapters.RequestData{ 2269 Method: "POST", 2270 Uri: server.URL, 2271 Body: []byte(`{"key":"val"}`), 2272 Headers: http.Header{}, 2273 }, 2274 bidResponse: nil, 2275 } 2276 bidderImpl.bidResponse = &adapters.BidderResponse{ 2277 Bids: []*adapters.TypedBid{ 2278 { 2279 Bid: &openrtb2.Bid{ 2280 ID: "bidId", 2281 }, 2282 BidType: openrtb_ext.BidTypeBanner, 2283 DealPriority: 4, 2284 }, 2285 }, 2286 } 2287 2288 bidder := AdaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderAppnexus, &config.DebugInfo{Allow: false}, "") 2289 currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) 2290 2291 bidderReq := BidderRequest{ 2292 BidRequest: &openrtb2.BidRequest{Imp: []openrtb2.Imp{{ID: "impId"}}}, 2293 BidderName: "test", 2294 } 2295 ctx := context.Background() 2296 bidAdjustments := map[string]float64{string(openrtb_ext.BidderAppnexus): 2.0} 2297 bidReqOptions := bidRequestOptions{ 2298 accountDebugAllowed: false, 2299 headerDebugAllowed: false, 2300 addCallSignHeader: true, 2301 bidAdjustments: bidAdjustments, 2302 } 2303 _, _, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &MockSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil) 2304 2305 assert.Empty(t, errs, "no errors should be returned") 2306 } 2307 2308 func wrapWithBidderInfo(bidder adapters.Bidder) adapters.Bidder { 2309 bidderInfo := config.BidderInfo{ 2310 Disabled: false, 2311 Capabilities: &config.CapabilitiesInfo{ 2312 App: &config.PlatformInfo{ 2313 MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeBanner}, 2314 }, 2315 }, 2316 } 2317 return adapters.BuildInfoAwareBidder(bidder, bidderInfo) 2318 } 2319 2320 type goodSingleBidder struct { 2321 bidRequest *openrtb2.BidRequest 2322 httpRequest *adapters.RequestData 2323 httpResponse *adapters.ResponseData 2324 bidResponse *adapters.BidderResponse 2325 hasStoredBidResponses bool 2326 } 2327 2328 func (bidder *goodSingleBidder) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { 2329 bidder.bidRequest = request 2330 return []*adapters.RequestData{bidder.httpRequest}, nil 2331 } 2332 2333 func (bidder *goodSingleBidder) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { 2334 bidder.httpResponse = response 2335 return bidder.bidResponse, nil 2336 } 2337 2338 type goodSingleBidderWithStoredBidResp struct { 2339 } 2340 2341 func (bidder *goodSingleBidderWithStoredBidResp) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { 2342 return nil, nil 2343 } 2344 2345 func (bidder *goodSingleBidderWithStoredBidResp) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { 2346 var bidResp openrtb2.BidResponse 2347 if err := json.Unmarshal(response.Body, &bidResp); err != nil { 2348 return nil, []error{err} 2349 } 2350 bidResponse := adapters.NewBidderResponseWithBidsCapacity(5) 2351 2352 for _, sb := range bidResp.SeatBid { 2353 for i := range sb.Bid { 2354 bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ 2355 Bid: &sb.Bid[i], 2356 BidType: openrtb_ext.BidTypeVideo, 2357 }) 2358 } 2359 } 2360 return bidResponse, nil 2361 } 2362 2363 type goodMultiHTTPCallsBidder struct { 2364 bidRequest *openrtb2.BidRequest 2365 httpRequest []*adapters.RequestData 2366 httpResponses []*adapters.ResponseData 2367 bidResponses []*adapters.BidderResponse 2368 bidResponseNumber int 2369 } 2370 2371 func (bidder *goodMultiHTTPCallsBidder) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { 2372 bidder.bidRequest = request 2373 response := make([]*adapters.RequestData, len(bidder.httpRequest)) 2374 2375 for i, r := range bidder.httpRequest { 2376 response[i] = r 2377 } 2378 return response, nil 2379 } 2380 2381 func (bidder *goodMultiHTTPCallsBidder) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { 2382 br := bidder.bidResponses[bidder.bidResponseNumber] 2383 bidder.bidResponseNumber++ 2384 bidder.httpResponses = append(bidder.httpResponses, response) 2385 2386 return br, nil 2387 } 2388 2389 type mixedMultiBidder struct { 2390 bidRequest *openrtb2.BidRequest 2391 httpRequests []*adapters.RequestData 2392 httpResponses []*adapters.ResponseData 2393 bidResponse *adapters.BidderResponse 2394 } 2395 2396 func (bidder *mixedMultiBidder) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { 2397 bidder.bidRequest = request 2398 return bidder.httpRequests, []error{errors.New("The requests weren't ideal.")} 2399 } 2400 2401 func (bidder *mixedMultiBidder) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { 2402 bidder.httpResponses = append(bidder.httpResponses, response) 2403 return bidder.bidResponse, []error{errors.New("The bidResponse weren't ideal.")} 2404 } 2405 2406 type bidRejector struct { 2407 httpResponse *adapters.ResponseData 2408 } 2409 2410 func (bidder *bidRejector) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { 2411 return nil, []error{errors.New("Invalid params on BidRequest.")} 2412 } 2413 2414 func (bidder *bidRejector) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { 2415 bidder.httpResponse = response 2416 return nil, []error{errors.New("Can't make a response.")} 2417 } 2418 2419 type notifyingBidder struct { 2420 requests []*adapters.RequestData 2421 notifyRequest adapters.RequestData 2422 } 2423 2424 func (bidder *notifyingBidder) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { 2425 return bidder.requests, nil 2426 } 2427 2428 func (bidder *notifyingBidder) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { 2429 return nil, nil 2430 } 2431 2432 func (bidder *notifyingBidder) MakeTimeoutNotification(req *adapters.RequestData) (*adapters.RequestData, []error) { 2433 return &bidder.notifyRequest, nil 2434 } 2435 2436 func TestExtraBid(t *testing.T) { 2437 respStatus := 200 2438 respBody := "{\"bid\":false}" 2439 server := httptest.NewServer(mockHandler(respStatus, "getBody", respBody)) 2440 defer server.Close() 2441 2442 requestHeaders := http.Header{} 2443 requestHeaders.Add("Content-Type", "application/json") 2444 2445 bidderImpl := &goodSingleBidder{ 2446 httpRequest: &adapters.RequestData{ 2447 Method: "POST", 2448 Uri: server.URL, 2449 Body: []byte("{\"key\":\"val\"}"), 2450 Headers: http.Header{}, 2451 }, 2452 bidResponse: &adapters.BidderResponse{ 2453 Bids: []*adapters.TypedBid{ 2454 { 2455 Bid: &openrtb2.Bid{ 2456 ID: "pubmaticImp1", 2457 }, 2458 BidType: openrtb_ext.BidTypeBanner, 2459 DealPriority: 4, 2460 Seat: "pubmatic", 2461 }, 2462 { 2463 Bid: &openrtb2.Bid{ 2464 ID: "groupmImp1", 2465 }, 2466 BidType: openrtb_ext.BidTypeVideo, 2467 DealPriority: 5, 2468 Seat: "groupm", 2469 }, 2470 }, 2471 }, 2472 } 2473 2474 wantSeatBids := []*entities.PbsOrtbSeatBid{ 2475 { 2476 HttpCalls: []*openrtb_ext.ExtHttpCall{}, 2477 Bids: []*entities.PbsOrtbBid{{ 2478 Bid: &openrtb2.Bid{ID: "groupmImp1"}, 2479 DealPriority: 5, 2480 BidType: openrtb_ext.BidTypeVideo, 2481 OriginalBidCur: "USD", 2482 BidMeta: &openrtb_ext.ExtBidPrebidMeta{AdapterCode: string(openrtb_ext.BidderPubmatic)}, 2483 }}, 2484 Seat: "groupm", 2485 Currency: "USD", 2486 }, 2487 { 2488 HttpCalls: []*openrtb_ext.ExtHttpCall{}, 2489 Bids: []*entities.PbsOrtbBid{{ 2490 Bid: &openrtb2.Bid{ID: "pubmaticImp1"}, 2491 DealPriority: 4, 2492 BidType: openrtb_ext.BidTypeBanner, 2493 OriginalBidCur: "USD", 2494 BidMeta: &openrtb_ext.ExtBidPrebidMeta{AdapterCode: string(openrtb_ext.BidderPubmatic)}, 2495 }}, 2496 Seat: string(openrtb_ext.BidderPubmatic), 2497 Currency: "USD", 2498 }, 2499 } 2500 2501 bidder := AdaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderAppnexus, &config.DebugInfo{}, "") 2502 currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) 2503 2504 bidderReq := BidderRequest{ 2505 BidRequest: &openrtb2.BidRequest{Imp: []openrtb2.Imp{{ID: "impId"}}}, 2506 BidderName: openrtb_ext.BidderPubmatic, 2507 } 2508 2509 bidAdjustments := map[string]float64{string(openrtb_ext.BidderAppnexus): 2.0} 2510 bidReqOptions := bidRequestOptions{ 2511 accountDebugAllowed: false, 2512 headerDebugAllowed: false, 2513 addCallSignHeader: true, 2514 bidAdjustments: bidAdjustments, 2515 } 2516 2517 seatBids, extraBidderRespInfo, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &MockSigner{}, bidReqOptions, 2518 openrtb_ext.ExtAlternateBidderCodes{ 2519 Enabled: true, 2520 Bidders: map[string]openrtb_ext.ExtAdapterAlternateBidderCodes{ 2521 string(openrtb_ext.BidderPubmatic): { 2522 Enabled: true, 2523 AllowedBidderCodes: []string{"groupm"}, 2524 }, 2525 }, 2526 }, 2527 &hookexecution.EmptyHookExecutor{}, 2528 nil) 2529 assert.Nil(t, errs) 2530 assert.Len(t, seatBids, 2) 2531 sort.Slice(seatBids, func(i, j int) bool { 2532 return len(seatBids[i].Seat) < len(seatBids[j].Seat) 2533 }) 2534 assert.Equal(t, wantSeatBids, seatBids) 2535 assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero()) 2536 } 2537 2538 func TestExtraBidWithAlternateBidderCodeDisabled(t *testing.T) { 2539 respStatus := 200 2540 respBody := "{\"bid\":false}" 2541 server := httptest.NewServer(mockHandler(respStatus, "getBody", respBody)) 2542 defer server.Close() 2543 2544 requestHeaders := http.Header{} 2545 requestHeaders.Add("Content-Type", "application/json") 2546 2547 bidderImpl := &goodSingleBidder{ 2548 httpRequest: &adapters.RequestData{ 2549 Method: "POST", 2550 Uri: server.URL, 2551 Body: []byte("{\"key\":\"val\"}"), 2552 Headers: http.Header{}, 2553 }, 2554 bidResponse: &adapters.BidderResponse{ 2555 Bids: []*adapters.TypedBid{ 2556 { 2557 Bid: &openrtb2.Bid{ 2558 ID: "pubmaticImp1", 2559 }, 2560 BidType: openrtb_ext.BidTypeBanner, 2561 DealPriority: 4, 2562 Seat: "pubmatic", 2563 }, 2564 { 2565 Bid: &openrtb2.Bid{ 2566 ID: "groupmImp1", 2567 }, 2568 BidType: openrtb_ext.BidTypeVideo, 2569 DealPriority: 5, 2570 Seat: "groupm-rejected", 2571 }, 2572 { 2573 Bid: &openrtb2.Bid{ 2574 ID: "groupmImp2", 2575 }, 2576 BidType: openrtb_ext.BidTypeVideo, 2577 DealPriority: 5, 2578 Seat: "groupm-allowed", 2579 }, 2580 }, 2581 }, 2582 } 2583 2584 wantSeatBids := []*entities.PbsOrtbSeatBid{ 2585 { 2586 HttpCalls: []*openrtb_ext.ExtHttpCall{}, 2587 Bids: []*entities.PbsOrtbBid{{ 2588 Bid: &openrtb2.Bid{ID: "groupmImp2"}, 2589 DealPriority: 5, 2590 BidType: openrtb_ext.BidTypeVideo, 2591 OriginalBidCur: "USD", 2592 BidMeta: &openrtb_ext.ExtBidPrebidMeta{AdapterCode: string(openrtb_ext.BidderPubmatic)}, 2593 }}, 2594 Seat: "groupm-allowed", 2595 Currency: "USD", 2596 }, 2597 { 2598 HttpCalls: []*openrtb_ext.ExtHttpCall{}, 2599 Bids: []*entities.PbsOrtbBid{{ 2600 Bid: &openrtb2.Bid{ID: "pubmaticImp1"}, 2601 DealPriority: 4, 2602 BidType: openrtb_ext.BidTypeBanner, 2603 OriginalBidCur: "USD", 2604 BidMeta: &openrtb_ext.ExtBidPrebidMeta{AdapterCode: string(openrtb_ext.BidderPubmatic)}, 2605 }}, 2606 Seat: string(openrtb_ext.BidderPubmatic), 2607 Currency: "USD", 2608 }, 2609 } 2610 wantErrs := []error{ 2611 &errortypes.Warning{ 2612 WarningCode: errortypes.AlternateBidderCodeWarningCode, 2613 Message: `invalid biddercode "groupm-rejected" sent by adapter "pubmatic"`, 2614 }, 2615 } 2616 2617 bidder := AdaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderAppnexus, &config.DebugInfo{}, "") 2618 currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) 2619 2620 bidderReq := BidderRequest{ 2621 BidRequest: &openrtb2.BidRequest{Imp: []openrtb2.Imp{{ID: "impId"}}}, 2622 BidderName: openrtb_ext.BidderPubmatic, 2623 } 2624 bidAdjustments := map[string]float64{string(openrtb_ext.BidderAppnexus): 2.0} 2625 bidReqOptions := bidRequestOptions{ 2626 accountDebugAllowed: false, 2627 headerDebugAllowed: false, 2628 addCallSignHeader: true, 2629 bidAdjustments: bidAdjustments, 2630 } 2631 2632 seatBids, extraBidderRespInfo, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &MockSigner{}, bidReqOptions, 2633 openrtb_ext.ExtAlternateBidderCodes{ 2634 Enabled: true, 2635 Bidders: map[string]openrtb_ext.ExtAdapterAlternateBidderCodes{ 2636 string(openrtb_ext.BidderPubmatic): { 2637 Enabled: true, 2638 AllowedBidderCodes: []string{"groupm-allowed"}, 2639 }, 2640 }, 2641 }, 2642 &hookexecution.EmptyHookExecutor{}, 2643 nil) 2644 assert.Equal(t, wantErrs, errs) 2645 assert.Len(t, seatBids, 2) 2646 assert.ElementsMatch(t, wantSeatBids, seatBids) 2647 assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero()) 2648 } 2649 2650 func TestExtraBidWithBidAdjustments(t *testing.T) { 2651 respStatus := 200 2652 respBody := "{\"bid\":false}" 2653 server := httptest.NewServer(mockHandler(respStatus, "getBody", respBody)) 2654 defer server.Close() 2655 2656 requestHeaders := http.Header{} 2657 requestHeaders.Add("Content-Type", "application/json") 2658 2659 bidderImpl := &goodSingleBidder{ 2660 httpRequest: &adapters.RequestData{ 2661 Method: "POST", 2662 Uri: server.URL, 2663 Body: []byte("{\"key\":\"val\"}"), 2664 Headers: http.Header{}, 2665 }, 2666 bidResponse: &adapters.BidderResponse{ 2667 Bids: []*adapters.TypedBid{ 2668 { 2669 Bid: &openrtb2.Bid{ 2670 ID: "pubmaticImp1", 2671 Price: 3, 2672 }, 2673 BidType: openrtb_ext.BidTypeBanner, 2674 DealPriority: 4, 2675 Seat: "pubmatic", 2676 }, 2677 { 2678 Bid: &openrtb2.Bid{ 2679 ID: "groupmImp1", 2680 Price: 7, 2681 }, 2682 BidType: openrtb_ext.BidTypeVideo, 2683 DealPriority: 5, 2684 Seat: "groupm", 2685 }, 2686 }, 2687 }, 2688 } 2689 2690 wantSeatBids := []*entities.PbsOrtbSeatBid{ 2691 { 2692 HttpCalls: []*openrtb_ext.ExtHttpCall{}, 2693 Bids: []*entities.PbsOrtbBid{{ 2694 Bid: &openrtb2.Bid{ 2695 ID: "groupmImp1", 2696 Price: 21, 2697 }, 2698 DealPriority: 5, 2699 BidType: openrtb_ext.BidTypeVideo, 2700 OriginalBidCPM: 7, 2701 OriginalBidCur: "USD", 2702 BidMeta: &openrtb_ext.ExtBidPrebidMeta{AdapterCode: string(openrtb_ext.BidderPubmatic)}, 2703 }}, 2704 Seat: "groupm", 2705 Currency: "USD", 2706 }, 2707 { 2708 HttpCalls: []*openrtb_ext.ExtHttpCall{}, 2709 Bids: []*entities.PbsOrtbBid{{ 2710 Bid: &openrtb2.Bid{ 2711 ID: "pubmaticImp1", 2712 Price: 6, 2713 }, 2714 DealPriority: 4, 2715 BidType: openrtb_ext.BidTypeBanner, 2716 OriginalBidCur: "USD", 2717 OriginalBidCPM: 3, 2718 BidMeta: &openrtb_ext.ExtBidPrebidMeta{AdapterCode: string(openrtb_ext.BidderPubmatic)}, 2719 }}, 2720 Seat: string(openrtb_ext.BidderPubmatic), 2721 Currency: "USD", 2722 }, 2723 } 2724 2725 bidder := AdaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderAppnexus, &config.DebugInfo{}, "") 2726 currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) 2727 2728 bidderReq := BidderRequest{ 2729 BidRequest: &openrtb2.BidRequest{Imp: []openrtb2.Imp{{ID: "impId"}}}, 2730 BidderName: openrtb_ext.BidderPubmatic, 2731 } 2732 bidAdjustments := map[string]float64{ 2733 string(openrtb_ext.BidderPubmatic): 2, 2734 "groupm": 3, 2735 } 2736 2737 bidReqOptions := bidRequestOptions{ 2738 accountDebugAllowed: false, 2739 headerDebugAllowed: false, 2740 addCallSignHeader: true, 2741 bidAdjustments: bidAdjustments, 2742 } 2743 2744 seatBids, extraBidderRespInfo, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &MockSigner{}, bidReqOptions, 2745 openrtb_ext.ExtAlternateBidderCodes{ 2746 Enabled: true, 2747 Bidders: map[string]openrtb_ext.ExtAdapterAlternateBidderCodes{ 2748 string(openrtb_ext.BidderPubmatic): { 2749 Enabled: true, 2750 AllowedBidderCodes: []string{"groupm"}, 2751 }, 2752 }, 2753 }, 2754 &hookexecution.EmptyHookExecutor{}, 2755 nil) 2756 assert.Nil(t, errs) 2757 assert.Len(t, seatBids, 2) 2758 sort.Slice(seatBids, func(i, j int) bool { 2759 return len(seatBids[i].Seat) < len(seatBids[j].Seat) 2760 }) 2761 assert.Equal(t, wantSeatBids, seatBids) 2762 assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero()) 2763 } 2764 2765 func TestExtraBidWithBidAdjustmentsUsingAdapterCode(t *testing.T) { 2766 respStatus := 200 2767 respBody := "{\"bid\":false}" 2768 server := httptest.NewServer(mockHandler(respStatus, "getBody", respBody)) 2769 defer server.Close() 2770 2771 requestHeaders := http.Header{} 2772 requestHeaders.Add("Content-Type", "application/json") 2773 2774 bidderImpl := &goodSingleBidder{ 2775 httpRequest: &adapters.RequestData{ 2776 Method: "POST", 2777 Uri: server.URL, 2778 Body: []byte("{\"key\":\"val\"}"), 2779 Headers: http.Header{}, 2780 }, 2781 bidResponse: &adapters.BidderResponse{ 2782 Bids: []*adapters.TypedBid{ 2783 { 2784 Bid: &openrtb2.Bid{ 2785 ID: "pubmaticImp1", 2786 Price: 3, 2787 }, 2788 BidType: openrtb_ext.BidTypeBanner, 2789 DealPriority: 4, 2790 Seat: "pubmatic", 2791 }, 2792 { 2793 Bid: &openrtb2.Bid{ 2794 ID: "groupmImp1", 2795 Price: 7, 2796 }, 2797 BidType: openrtb_ext.BidTypeVideo, 2798 DealPriority: 5, 2799 Seat: "groupm", 2800 }, 2801 }, 2802 }, 2803 } 2804 2805 wantSeatBids := []*entities.PbsOrtbSeatBid{ 2806 { 2807 HttpCalls: []*openrtb_ext.ExtHttpCall{}, 2808 Bids: []*entities.PbsOrtbBid{{ 2809 Bid: &openrtb2.Bid{ 2810 ID: "groupmImp1", 2811 Price: 14, 2812 }, 2813 DealPriority: 5, 2814 BidType: openrtb_ext.BidTypeVideo, 2815 OriginalBidCPM: 7, 2816 OriginalBidCur: "USD", 2817 BidMeta: &openrtb_ext.ExtBidPrebidMeta{AdapterCode: string(openrtb_ext.BidderPubmatic)}, 2818 }}, 2819 Seat: "groupm", 2820 Currency: "USD", 2821 }, 2822 { 2823 HttpCalls: []*openrtb_ext.ExtHttpCall{}, 2824 Bids: []*entities.PbsOrtbBid{{ 2825 Bid: &openrtb2.Bid{ 2826 ID: "pubmaticImp1", 2827 Price: 6, 2828 }, 2829 DealPriority: 4, 2830 BidType: openrtb_ext.BidTypeBanner, 2831 OriginalBidCur: "USD", 2832 OriginalBidCPM: 3, 2833 BidMeta: &openrtb_ext.ExtBidPrebidMeta{AdapterCode: string(openrtb_ext.BidderPubmatic)}, 2834 }}, 2835 Seat: string(openrtb_ext.BidderPubmatic), 2836 Currency: "USD", 2837 }, 2838 } 2839 2840 bidder := AdaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderAppnexus, &config.DebugInfo{}, "") 2841 currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) 2842 2843 bidderReq := BidderRequest{ 2844 BidRequest: &openrtb2.BidRequest{Imp: []openrtb2.Imp{{ID: "impId"}}}, 2845 BidderName: openrtb_ext.BidderPubmatic, 2846 } 2847 bidAdjustments := map[string]float64{ 2848 string(openrtb_ext.BidderPubmatic): 2, 2849 } 2850 2851 bidReqOptions := bidRequestOptions{ 2852 accountDebugAllowed: false, 2853 headerDebugAllowed: false, 2854 addCallSignHeader: true, 2855 bidAdjustments: bidAdjustments, 2856 } 2857 2858 seatBids, extraBidderRespInfo, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &MockSigner{}, bidReqOptions, 2859 openrtb_ext.ExtAlternateBidderCodes{ 2860 Enabled: true, 2861 Bidders: map[string]openrtb_ext.ExtAdapterAlternateBidderCodes{ 2862 string(openrtb_ext.BidderPubmatic): { 2863 Enabled: true, 2864 AllowedBidderCodes: []string{"groupm"}, 2865 }, 2866 }, 2867 }, 2868 &hookexecution.EmptyHookExecutor{}, 2869 nil) 2870 assert.Nil(t, errs) 2871 assert.Len(t, seatBids, 2) 2872 sort.Slice(seatBids, func(i, j int) bool { 2873 return len(seatBids[i].Seat) < len(seatBids[j].Seat) 2874 }) 2875 assert.Equal(t, wantSeatBids, seatBids) 2876 assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero()) 2877 } 2878 2879 func TestExtraBidWithMultiCurrencies(t *testing.T) { 2880 respStatus := 200 2881 respBody := "{\"bid\":false}" 2882 server := httptest.NewServer(mockHandler(respStatus, "getBody", respBody)) 2883 defer server.Close() 2884 2885 requestHeaders := http.Header{} 2886 requestHeaders.Add("Content-Type", "application/json") 2887 2888 bidderImpl := &goodSingleBidder{ 2889 httpRequest: &adapters.RequestData{ 2890 Method: "POST", 2891 Uri: server.URL, 2892 Body: []byte("{\"key\":\"val\"}"), 2893 Headers: http.Header{}, 2894 }, 2895 bidResponse: &adapters.BidderResponse{ 2896 Bids: []*adapters.TypedBid{ 2897 { 2898 Bid: &openrtb2.Bid{ 2899 ID: "pubmaticImp1", 2900 Price: 3, 2901 }, 2902 BidType: openrtb_ext.BidTypeBanner, 2903 DealPriority: 4, 2904 Seat: "pubmatic", 2905 }, 2906 { 2907 Bid: &openrtb2.Bid{ 2908 ID: "groupmImp1", 2909 Price: 7, 2910 }, 2911 BidType: openrtb_ext.BidTypeVideo, 2912 DealPriority: 5, 2913 Seat: "groupm", 2914 }, 2915 }, 2916 }, 2917 } 2918 2919 wantSeatBids := []*entities.PbsOrtbSeatBid{ 2920 { 2921 HttpCalls: []*openrtb_ext.ExtHttpCall{}, 2922 Bids: []*entities.PbsOrtbBid{{ 2923 Bid: &openrtb2.Bid{ 2924 ID: "groupmImp1", 2925 Price: 571.5994430039375, 2926 }, 2927 DealPriority: 5, 2928 BidType: openrtb_ext.BidTypeVideo, 2929 OriginalBidCPM: 7, 2930 OriginalBidCur: "USD", 2931 BidMeta: &openrtb_ext.ExtBidPrebidMeta{AdapterCode: string(openrtb_ext.BidderPubmatic)}, 2932 }}, 2933 Seat: "groupm", 2934 Currency: "INR", 2935 }, 2936 { 2937 HttpCalls: []*openrtb_ext.ExtHttpCall{}, 2938 Bids: []*entities.PbsOrtbBid{{ 2939 Bid: &openrtb2.Bid{ 2940 ID: "pubmaticImp1", 2941 Price: 244.97118985883034, 2942 }, 2943 DealPriority: 4, 2944 BidType: openrtb_ext.BidTypeBanner, 2945 OriginalBidCPM: 3, 2946 OriginalBidCur: "USD", 2947 BidMeta: &openrtb_ext.ExtBidPrebidMeta{AdapterCode: string(openrtb_ext.BidderPubmatic)}, 2948 }}, 2949 Seat: string(openrtb_ext.BidderPubmatic), 2950 Currency: "INR", 2951 }, 2952 } 2953 2954 mockedHTTPServer := httptest.NewServer(http.HandlerFunc( 2955 func(rw http.ResponseWriter, req *http.Request) { 2956 rw.Write([]byte(`{"dataAsOf":"2022-11-24T00:00:00.000Z","generatedAt":"2022-11-24T15:00:46.363Z","conversions":{"USD":{"USD":1,"INR":81.65706328627678}}}`)) 2957 rw.WriteHeader(http.StatusOK) 2958 }), 2959 ) 2960 2961 // Execute: 2962 bidder := AdaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderAppnexus, nil, "") 2963 currencyConverter := currency.NewRateConverter( 2964 &http.Client{}, 2965 mockedHTTPServer.URL, 2966 time.Duration(24)*time.Hour, 2967 ) 2968 time.Sleep(time.Duration(500) * time.Millisecond) 2969 currencyConverter.Run() 2970 2971 bidderReq := BidderRequest{ 2972 BidRequest: &openrtb2.BidRequest{Imp: []openrtb2.Imp{{ID: "impId"}}, Cur: []string{"INR"}}, 2973 BidderName: openrtb_ext.BidderPubmatic, 2974 } 2975 2976 bidAdjustments := map[string]float64{string(openrtb_ext.BidderAppnexus): 2.0} 2977 bidReqOptions := bidRequestOptions{ 2978 accountDebugAllowed: false, 2979 headerDebugAllowed: false, 2980 addCallSignHeader: true, 2981 bidAdjustments: bidAdjustments, 2982 } 2983 2984 seatBids, extraBidderRespInfo, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &MockSigner{}, bidReqOptions, 2985 openrtb_ext.ExtAlternateBidderCodes{ 2986 Enabled: true, 2987 Bidders: map[string]openrtb_ext.ExtAdapterAlternateBidderCodes{ 2988 string(openrtb_ext.BidderPubmatic): { 2989 Enabled: true, 2990 AllowedBidderCodes: []string{"groupm"}, 2991 }, 2992 }, 2993 }, 2994 &hookexecution.EmptyHookExecutor{}, 2995 nil) 2996 assert.Nil(t, errs) 2997 assert.Len(t, seatBids, 2) 2998 sort.Slice(seatBids, func(i, j int) bool { 2999 return len(seatBids[i].Seat) < len(seatBids[j].Seat) 3000 }) 3001 assert.Equal(t, wantSeatBids, seatBids) 3002 assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero()) 3003 } 3004 3005 func TestGetBidType(t *testing.T) { 3006 testCases := []struct { 3007 name string 3008 givenBidType openrtb_ext.BidType 3009 givenImpId string 3010 givenImp []openrtb2.Imp 3011 expected string 3012 }{ 3013 { 3014 name: "VideoInstream", 3015 givenImp: []openrtb2.Imp{ 3016 { 3017 ID: "imp-id", 3018 Video: &openrtb2.Video{ 3019 Plcmt: adcom1.VideoPlcmtInstream, 3020 }, 3021 }, 3022 }, 3023 givenBidType: openrtb_ext.BidTypeVideo, 3024 givenImpId: "imp-id", 3025 expected: "video-instream", 3026 }, 3027 { 3028 name: "VideoOutstream", 3029 givenImp: []openrtb2.Imp{ 3030 { 3031 ID: "imp-id", 3032 Video: &openrtb2.Video{ 3033 Plcmt: adcom1.VideoPlcmtAccompanyingContent, 3034 }, 3035 }, 3036 }, 3037 givenBidType: openrtb_ext.BidTypeVideo, 3038 givenImpId: "imp-id", 3039 expected: "video-outstream", 3040 }, 3041 { 3042 name: "NonVideoBidType", 3043 givenImp: []openrtb2.Imp{}, 3044 givenBidType: openrtb_ext.BidTypeBanner, 3045 givenImpId: "imp-id", 3046 expected: string(openrtb_ext.BidTypeBanner), 3047 }, 3048 { 3049 name: "VideoBidTypeImpVideoIsNil", 3050 givenImp: []openrtb2.Imp{ 3051 { 3052 ID: "imp-id", 3053 }, 3054 }, 3055 givenBidType: openrtb_ext.BidTypeVideo, 3056 givenImpId: "imp-id", 3057 expected: "video-instream", 3058 }, 3059 } 3060 3061 for _, test := range testCases { 3062 t.Run(test.name, func(t *testing.T) { 3063 actual := getBidTypeForAdjustments(test.givenBidType, test.givenImpId, test.givenImp) 3064 assert.Equal(t, test.expected, actual, "Bid type doesn't match") 3065 }) 3066 } 3067 } 3068 3069 type mockBidderTmaxCtx struct { 3070 startTime, deadline, now time.Time 3071 ok bool 3072 } 3073 3074 func (m *mockBidderTmaxCtx) Deadline() (deadline time.Time, _ bool) { 3075 return m.deadline, m.ok 3076 } 3077 func (m *mockBidderTmaxCtx) RemainingDurationMS(deadline time.Time) int64 { 3078 return deadline.Sub(m.startTime).Milliseconds() 3079 } 3080 3081 func (m *mockBidderTmaxCtx) Until(t time.Time) time.Duration { 3082 return t.Sub(m.now) 3083 } 3084 3085 func TestUpdateBidderTmax(t *testing.T) { 3086 respStatus := 200 3087 respBody := "{\"bid\":false}" 3088 server := httptest.NewServer(mockHandler(respStatus, "getBody", respBody)) 3089 defer server.Close() 3090 3091 requestHeaders := http.Header{} 3092 requestHeaders.Add("Content-Type", "application/json") 3093 3094 currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) 3095 var requestTmax int64 = 700 3096 3097 bidderReq := BidderRequest{ 3098 BidRequest: &openrtb2.BidRequest{Imp: []openrtb2.Imp{{ID: "impId"}}, TMax: requestTmax}, 3099 BidderName: "test", 3100 } 3101 extraInfo := &adapters.ExtraRequestInfo{} 3102 3103 tests := []struct { 3104 description string 3105 requestTmax int64 3106 tmaxAdjustments *TmaxAdjustmentsPreprocessed 3107 assertFn func(actualTmax int64) bool 3108 }{ 3109 { 3110 description: "tmax-is-not-enabled", 3111 requestTmax: requestTmax, 3112 tmaxAdjustments: &TmaxAdjustmentsPreprocessed{IsEnforced: false}, 3113 assertFn: func(actualTmax int64) bool { 3114 return requestTmax == actualTmax 3115 }, 3116 }, 3117 { 3118 description: "updates-bidder-tmax", 3119 requestTmax: requestTmax, 3120 tmaxAdjustments: &TmaxAdjustmentsPreprocessed{IsEnforced: true, BidderResponseDurationMin: 100, BidderNetworkLatencyBuffer: 50, PBSResponsePreparationDuration: 50}, 3121 assertFn: func(actualTmax int64) bool { 3122 return requestTmax > actualTmax 3123 }, 3124 }, 3125 } 3126 for _, test := range tests { 3127 t.Run(test.description, func(t *testing.T) { 3128 bidderImpl := &goodSingleBidder{ 3129 httpRequest: &adapters.RequestData{ 3130 Method: "POST", 3131 Uri: server.URL, 3132 Body: []byte("{\"key\":\"val\"}"), 3133 Headers: http.Header{}, 3134 }, 3135 bidResponse: &adapters.BidderResponse{}, 3136 } 3137 3138 now := time.Now() 3139 ctx, cancel := context.WithDeadline(context.Background(), now.Add(500*time.Millisecond)) 3140 defer cancel() 3141 bidReqOptions := bidRequestOptions{bidderRequestStartTime: now, tmaxAdjustments: test.tmaxAdjustments} 3142 bidder := AdaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderAppnexus, &config.DebugInfo{Allow: false}, "") 3143 _, _, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), extraInfo, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil) 3144 assert.Empty(t, errs) 3145 assert.True(t, test.assertFn(bidderImpl.bidRequest.TMax)) 3146 }) 3147 } 3148 } 3149 3150 func TestHasShorterDurationThanTmax(t *testing.T) { 3151 var requestTmaxMS int64 = 700 3152 requestTmaxNS := requestTmaxMS * int64(time.Millisecond) 3153 startTime := time.Date(2023, 5, 30, 1, 0, 0, 0, time.UTC) 3154 now := time.Date(2023, 5, 30, 1, 0, 0, int(200*time.Millisecond), time.UTC) 3155 deadline := time.Date(2023, 5, 30, 1, 0, 0, int(requestTmaxNS), time.UTC) 3156 ctx := &mockBidderTmaxCtx{startTime: startTime, deadline: deadline, now: now, ok: true} 3157 3158 tests := []struct { 3159 description string 3160 ctx bidderTmaxContext 3161 requestTmax int64 3162 tmaxAdjustments TmaxAdjustmentsPreprocessed 3163 expected bool 3164 }{ 3165 { 3166 description: "tmax-disabled", 3167 ctx: ctx, 3168 requestTmax: requestTmaxMS, 3169 tmaxAdjustments: TmaxAdjustmentsPreprocessed{IsEnforced: false}, 3170 expected: false, 3171 }, 3172 { 3173 description: "remaing-duration-greater-than-bidder-response-min", 3174 ctx: ctx, 3175 requestTmax: requestTmaxMS, 3176 tmaxAdjustments: TmaxAdjustmentsPreprocessed{IsEnforced: true, PBSResponsePreparationDuration: 50, BidderNetworkLatencyBuffer: 10, BidderResponseDurationMin: 40}, 3177 expected: false, 3178 }, 3179 { 3180 description: "remaing-duration-less-than-bidder-response-min", 3181 ctx: ctx, 3182 requestTmax: requestTmaxMS, 3183 tmaxAdjustments: TmaxAdjustmentsPreprocessed{IsEnforced: true, PBSResponsePreparationDuration: 100, BidderNetworkLatencyBuffer: 10, BidderResponseDurationMin: 500}, 3184 expected: true, 3185 }, 3186 } 3187 3188 for _, test := range tests { 3189 t.Run(test.description, func(t *testing.T) { 3190 assert.Equal(t, test.expected, hasShorterDurationThanTmax(test.ctx, test.tmaxAdjustments)) 3191 }) 3192 } 3193 } 3194 3195 func TestDoRequestImplWithTmax(t *testing.T) { 3196 respStatus := 200 3197 respBody := "{\"bid\":false}" 3198 server := httptest.NewServer(mockHandler(respStatus, "getBody", respBody)) 3199 defer server.Close() 3200 requestStartTime := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC) 3201 3202 bidRequest := adapters.RequestData{ 3203 Method: "POST", 3204 Uri: server.URL, 3205 Body: []byte(`{"id":"this-id","app":{"publisher":{"id":"pub-id"}}}`), 3206 } 3207 3208 bidderAdapter := bidderAdapter{ 3209 me: &metricsConfig.NilMetricsEngine{}, 3210 Client: server.Client(), 3211 } 3212 logger := func(msg string, args ...interface{}) {} 3213 3214 tests := []struct { 3215 ctxDeadline time.Time 3216 description string 3217 tmaxAdjustments *TmaxAdjustmentsPreprocessed 3218 assertFn func(err error) 3219 }{ 3220 { 3221 ctxDeadline: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC), 3222 description: "returns-tmax-timeout-error", 3223 tmaxAdjustments: &TmaxAdjustmentsPreprocessed{IsEnforced: true, PBSResponsePreparationDuration: 100, BidderNetworkLatencyBuffer: 10, BidderResponseDurationMin: 5000}, 3224 assertFn: func(err error) { assert.Equal(t, errTmaxTimeout, err) }, 3225 }, 3226 { 3227 ctxDeadline: time.Now().Add(5 * time.Second), 3228 description: "remaining-duration-greater-than-tmax-min", 3229 tmaxAdjustments: &TmaxAdjustmentsPreprocessed{IsEnforced: true, PBSResponsePreparationDuration: 100, BidderNetworkLatencyBuffer: 10, BidderResponseDurationMin: 100}, 3230 assertFn: func(err error) { assert.Nil(t, err) }, 3231 }, 3232 { 3233 description: "tmax-disabled", 3234 tmaxAdjustments: &TmaxAdjustmentsPreprocessed{IsEnforced: false}, 3235 assertFn: func(err error) { assert.Nil(t, err) }, 3236 }, 3237 { 3238 description: "tmax-BidderResponseDurationMin-not-set", 3239 tmaxAdjustments: &TmaxAdjustmentsPreprocessed{IsEnforced: true, BidderResponseDurationMin: 0}, 3240 assertFn: func(err error) { assert.Nil(t, err) }, 3241 }, 3242 { 3243 description: "tmax-is-nil", 3244 tmaxAdjustments: nil, 3245 assertFn: func(err error) { assert.Nil(t, err) }, 3246 }, 3247 } 3248 for _, test := range tests { 3249 var ( 3250 ctx context.Context 3251 cancelFn context.CancelFunc 3252 ) 3253 3254 if test.ctxDeadline.IsZero() { 3255 ctx = context.Background() 3256 } else { 3257 ctx, cancelFn = context.WithDeadline(context.Background(), test.ctxDeadline) 3258 defer cancelFn() 3259 } 3260 3261 httpCallInfo := bidderAdapter.doRequestImpl(ctx, &bidRequest, logger, requestStartTime, test.tmaxAdjustments) 3262 test.assertFn(httpCallInfo.err) 3263 } 3264 } 3265 3266 func TestDoRequestImplWithTmaxTimeout(t *testing.T) { 3267 respStatus := 200 3268 respBody := "{\"bid\":false}" 3269 server := httptest.NewServer(mockHandler(respStatus, "getBody", respBody)) 3270 defer server.Close() 3271 requestStartTime := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC) 3272 3273 bidRequest := adapters.RequestData{ 3274 Method: "POST", 3275 Uri: server.URL, 3276 Body: []byte(`{"id":"this-id","app":{"publisher":{"id":"pub-id"}}}`), 3277 } 3278 3279 metricsMock := &metrics.MetricsEngineMock{} 3280 metricsMock.On("RecordOverheadTime", metrics.PreBidder, mock.Anything).Once() 3281 metricsMock.On("RecordTMaxTimeout").Once() 3282 3283 bidderAdapter := bidderAdapter{ 3284 me: metricsMock, 3285 Client: server.Client(), 3286 } 3287 logger := func(msg string, args ...interface{}) {} 3288 3289 tests := []struct { 3290 ctxDeadline time.Time 3291 description string 3292 tmaxAdjustments *TmaxAdjustmentsPreprocessed 3293 assertFn func(err error) 3294 }{ 3295 { 3296 ctxDeadline: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC), 3297 description: "returns-tmax-timeout-error", 3298 tmaxAdjustments: &TmaxAdjustmentsPreprocessed{IsEnforced: true, PBSResponsePreparationDuration: 100, BidderNetworkLatencyBuffer: 10, BidderResponseDurationMin: 5000}, 3299 assertFn: func(err error) { assert.Equal(t, errTmaxTimeout, err) }, 3300 }, 3301 } 3302 for _, test := range tests { 3303 var ( 3304 ctx context.Context 3305 cancelFn context.CancelFunc 3306 ) 3307 3308 if test.ctxDeadline.IsZero() { 3309 ctx = context.Background() 3310 } else { 3311 ctx, cancelFn = context.WithDeadline(context.Background(), test.ctxDeadline) 3312 defer cancelFn() 3313 } 3314 3315 httpCallInfo := bidderAdapter.doRequestImpl(ctx, &bidRequest, logger, requestStartTime, test.tmaxAdjustments) 3316 test.assertFn(httpCallInfo.err) 3317 } 3318 }