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