Repository: prebid/prebid-server
Pull request #3308 (2023-11-20 12:44:13)

Huaweiads: Remove empty image assets from response
bind the number of main image objects from huaweiads instead of all main image objects in the request

github-actions[bot]: ## Code coverage summary 
Note: 
- Prebid team doesn't anticipate tests covering code paths that might result in marshal and unmarshal errors 
- Coverage summary encompasses all commits leading up to the latest one, db2064a481686296746ecd57c1ddc39a5278df63 
#### huaweiads 
Refer [here](https://htmlpreview.github.io/?https://github.com/prebid/prebid-server/coverage-preview/.github/preview/6930269101_1700484301/huaweiads.html) for heat map coverage report 
``` 
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:267:		MakeRequests				93.3%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:325:		getFinalEndPoint			85.7%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:355:		MakeBids				91.7%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:382:		Builder					100.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:395:		getExtraInfo				94.1%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:430:		getHeaders				88.9%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:446:		getReqJson				90.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:461:		getReqAdslot30				100.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:483:		checkAndExtractOpenrtbFormat		100.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:511:		getBannerFormat				100.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:527:		getNativeFormat				97.4%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:593:		filterPopularSizes			100.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:615:		getVideoFormat				100.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:628:		convertAdtypeStringToInteger		60.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:652:		getReqAppInfo				93.8%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:686:		getFinalPkgName				93.8%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:721:		getClientTime				75.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:741:		getReqDeviceInfo			96.6%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:784:		getCountryCode				100.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:797:		convertCountryCode			75.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:818:		getCountryCodeFromMCC			100.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:835:		getDeviceIDFromUserExt			100.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:880:		getReqNetWorkInfo			85.7%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:916:		getReqRegsInfo				100.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:925:		getReqGeoInfo				100.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:937:		getReqConsentInfo			80.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:947:		unmarshalExtImpHuaweiAds		88.2%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:974:		checkRespStatusCode			77.8%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:997:		checkHuaweiAdsResponseRetcode		71.4%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:1015:	convertHuaweiAdsRespToBidderResp	90.2%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:1084:	getNurl					66.7%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:1097:	handleHuaweiAdsContent			87.5%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:1117:	extractAdmBanner			60.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:1139:	extractAdmNative			86.4%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:1262:	getDecodeValue				80.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:1273:	jsonEncode				100.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:1282:	extractAdmPicture			90.9%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:1345:	getClickUrl				100.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:1362:	getDspImpClickTrackings			100.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:1376:	getStrings				87.5%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:1391:	getDuration				100.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:1398:	extractAdmVideo				85.4%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:1561:	getVastImpClickErrorTrackingUrls	100.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:1581:	getVastEventTrackingUrls		100.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:1601:	computeHmacSha256			100.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:1608:	getDigestAuthorization			100.0%
total:										(statements)				90.3%

 ``` 

github-actions[bot]: ## Code coverage summary 
Note: 
- Prebid team doesn't anticipate tests covering code paths that might result in marshal and unmarshal errors 
- Coverage summary encompasses all commits leading up to the latest one, 932600ec359516fb1394fba3bb7d0ce84d808e98 
#### huaweiads 
Refer [here](https://htmlpreview.github.io/?https://github.com/prebid/prebid-server/coverage-preview/.github/preview/6930284709_1700484386/huaweiads.html) for heat map coverage report 
``` 
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:267:		MakeRequests				93.3%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:325:		getFinalEndPoint			85.7%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:355:		MakeBids				91.7%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:382:		Builder					100.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:395:		getExtraInfo				94.1%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:430:		getHeaders				88.9%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:446:		getReqJson				90.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:461:		getReqAdslot30				100.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:483:		checkAndExtractOpenrtbFormat		100.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:511:		getBannerFormat				100.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:527:		getNativeFormat				97.4%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:593:		filterPopularSizes			100.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:615:		getVideoFormat				100.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:628:		convertAdtypeStringToInteger		60.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:652:		getReqAppInfo				93.8%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:686:		getFinalPkgName				93.8%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:721:		getClientTime				75.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:741:		getReqDeviceInfo			96.6%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:784:		getCountryCode				100.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:797:		convertCountryCode			75.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:818:		getCountryCodeFromMCC			100.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:835:		getDeviceIDFromUserExt			100.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:880:		getReqNetWorkInfo			85.7%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:916:		getReqRegsInfo				100.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:925:		getReqGeoInfo				100.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:937:		getReqConsentInfo			80.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:947:		unmarshalExtImpHuaweiAds		88.2%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:974:		checkRespStatusCode			77.8%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:997:		checkHuaweiAdsResponseRetcode		71.4%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:1015:	convertHuaweiAdsRespToBidderResp	90.2%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:1084:	getNurl					66.7%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:1097:	handleHuaweiAdsContent			87.5%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:1117:	extractAdmBanner			60.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:1139:	extractAdmNative			86.4%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:1262:	getDecodeValue				80.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:1273:	jsonEncode				100.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:1282:	extractAdmPicture			90.9%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:1345:	getClickUrl				100.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:1362:	getDspImpClickTrackings			100.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:1376:	getStrings				87.5%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:1391:	getDuration				100.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:1398:	extractAdmVideo				85.4%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:1561:	getVastImpClickErrorTrackingUrls	100.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:1581:	getVastEventTrackingUrls		100.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:1601:	computeHmacSha256			100.0%
github.com/prebid/prebid-server/v2/adapters/huaweiads/huaweiads.go:1608:	getDigestAuthorization			100.0%
total:										(statements)				90.3%

 ``` 


Patch:
diff --git a/adapters/huaweiads/huaweiads.go b/adapters/huaweiads/huaweiads.go
index ac4e2446..0d4f43e1 100644
--- a/adapters/huaweiads/huaweiads.go
+++ b/adapters/huaweiads/huaweiads.go
@@ -535,7 +535,7 @@ func getNativeFormat(adslot30 *adslot30, openRTBImp *openrtb2.Imp) error {
 	}
 
 	//popular size for native ads
-	popularSizes := []map[string]int64{{"w": 225, "h": 150}, {"w": 300, "h": 250}, {"w": 320, "h": 250}, {"w": 640, "h": 360}, {"w": 720, "h": 1280}, {"w": 1080, "h": 170}, {"w": 1080, "h": 430}, {"w": 1080, "h": 432}, {"w": 1080, "h": 504}, {"w": 1080, "h": 607}, {"w": 1080, "h": 1620}, {"w": 1200, "h": 627}, {"w": 1280, "h": 720}, {"w": 1312, "h": 768}, {"w": 1920, "h": 1080}}
+	popularSizes := []map[string]int64{{"w": 225, "h": 150}, {"w": 300, "h": 250}, {"w": 320, "h": 250}, {"w": 640, "h": 360}, {"w": 1080, "h": 170}, {"w": 1080, "h": 430}, {"w": 1080, "h": 432}, {"w": 1080, "h": 504}, {"w": 1080, "h": 607}, {"w": 1080, "h": 1620}, {"w": 1200, "h": 627}, {"w": 1280, "h": 720}, {"w": 1312, "h": 768}, {"w": 1920, "h": 1080}}
 
 	// only compute the main image number, type = native1.ImageAssetTypeMain
 	var numMainImage = 0
@@ -1178,6 +1178,9 @@ func (a *adapter) extractAdmNative(adType int32, content *content, bidType openr
 			}
 			responseAsset.Video = &videoObject
 		} else if asset.Img != nil {
+			if len(content.MetaData.ImageInfo) == imgIndex && asset.Img.Type == native1.ImageAssetTypeMain {
+				continue
+			}
 			var imgObject nativeResponse.Image
 			imgObject.URL = ""
 			imgObject.Type = asset.Img.Type
diff --git a/currency/currency.go b/currency/currency.go
new file mode 100644
index 00000000..060649f9
--- /dev/null
+++ b/currency/currency.go
@@ -0,0 +1,42 @@
+package currency
+
+import (
+	"github.com/prebid/prebid-server/v2/openrtb_ext"
+)
+
+func GetAuctionCurrencyRates(currencyConverter *RateConverter, requestRates *openrtb_ext.ExtRequestCurrency) Conversions {
+	if currencyConverter == nil && requestRates == nil {
+		return nil
+	}
+
+	if requestRates == nil {
+		// No bidRequest.ext.currency field was found, use PBS rates as usual
+		return currencyConverter.Rates()
+	}
+
+	// currencyConverter will never be nil, refer main.serve(), adding this check for future usecases
+	if currencyConverter == nil {
+		return NewRates(requestRates.ConversionRates)
+	}
+
+	// If bidRequest.ext.currency.usepbsrates is nil, we understand its value as true. It will be false
+	// only if it's explicitly set to false
+	usePbsRates := requestRates.UsePBSRates == nil || *requestRates.UsePBSRates
+
+	if !usePbsRates {
+		// At this point, we can safely assume the ConversionRates map is not empty because
+		// validateCustomRates(bidReqCurrencyRates *openrtb_ext.ExtRequestCurrency) would have
+		// thrown an error under such conditions.
+		return NewRates(requestRates.ConversionRates)
+	}
+
+	// Both PBS and custom rates can be used, check if ConversionRates is not empty
+	if len(requestRates.ConversionRates) == 0 {
+		// Custom rates map is empty, use PBS rates only
+		return currencyConverter.Rates()
+	}
+
+	// Return an AggregateConversions object that includes both custom and PBS currency rates but will
+	// prioritize custom rates over PBS rates whenever a currency rate is found in both
+	return NewAggregateConversions(NewRates(requestRates.ConversionRates), currencyConverter.Rates())
+}
diff --git a/currency/currency_mock.go b/currency/currency_mock.go
new file mode 100644
index 00000000..f2597438
--- /dev/null
+++ b/currency/currency_mock.go
@@ -0,0 +1,20 @@
+package currency
+
+import (
+	"io"
+	"net/http"
+	"strings"
+)
+
+// MockCurrencyRatesHttpClient is a simple http client mock returning a constant response body
+type MockCurrencyRatesHttpClient struct {
+	ResponseBody string
+}
+
+func (m *MockCurrencyRatesHttpClient) Do(req *http.Request) (*http.Response, error) {
+	return &http.Response{
+		Status:     "200 OK",
+		StatusCode: http.StatusOK,
+		Body:       io.NopCloser(strings.NewReader(m.ResponseBody)),
+	}, nil
+}
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/exemplary/all-ext-case-insensitive.json b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/all-ext-case-insensitive.json
index 20997076..67633ab8 100644
--- a/endpoints/openrtb2/sample-requests/valid-whole/exemplary/all-ext-case-insensitive.json
+++ b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/all-ext-case-insensitive.json
@@ -51,7 +51,7 @@
           ]
         },
         "ext": {
-          "appnexus": {
+          "APPnexus": {
             "placementId": 12883451
           },
           "districtm": {
@@ -109,7 +109,7 @@
             "price": 1.01
           }
         ],
-        "seat": "appnexus"
+        "seat": "APPnexus"
       },
       {
         "bid": [
@@ -137,4 +137,4 @@
     "nbr": 0
   },
   "expectedReturnCode": 200
-}
\ No newline at end of file
+}
diff --git a/exchange/exchange.go b/exchange/exchange.go
index b7e26ee2..211d82fc 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -267,7 +267,7 @@ func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog
 	}
 
 	// Get currency rates conversions for the auction
-	conversions := e.getAuctionCurrencyRates(requestExtPrebid.CurrencyConversions)
+	conversions := currency.GetAuctionCurrencyRates(e.currencyConverter, requestExtPrebid.CurrencyConversions)
 
 	var floorErrs []error
 	if e.priceFloorEnabled {
@@ -577,15 +577,18 @@ func applyDealSupport(bidRequest *openrtb2.BidRequest, auc *auction, bidCategory
 	for impID, topBidsPerImp := range auc.winningBidsByBidder {
 		impDeal := impDealMap[impID]
 		for bidder, topBidsPerBidder := range topBidsPerImp {
-			maxBid := bidsToUpdate(multiBid, bidder.String())
-
+			bidderNormalized, bidderFound := openrtb_ext.NormalizeBidderName(bidder.String())
+			if !bidderFound {
+				continue
+			}
+			maxBid := bidsToUpdate(multiBid, bidderNormalized.String())
 			for i, topBid := range topBidsPerBidder {
 				if i == maxBid {
 					break
 				}
 				if topBid.DealPriority > 0 {
-					if validateDealTier(impDeal[bidder]) {
-						updateHbPbCatDur(topBid, impDeal[bidder], bidCategory)
+					if validateDealTier(impDeal[bidderNormalized]) {
+						updateHbPbCatDur(topBid, impDeal[bidderNormalized], bidCategory)
 					} else {
 						errs = append(errs, fmt.Errorf("dealTier configuration invalid for bidder '%s', imp ID '%s'", string(bidder), impID))
 					}
@@ -1362,34 +1365,6 @@ func (e *exchange) getBidCacheInfo(bid *entities.PbsOrtbBid, auction *auction) (
 	return
 }
 
-func (e *exchange) getAuctionCurrencyRates(requestRates *openrtb_ext.ExtRequestCurrency) currency.Conversions {
-	if requestRates == nil {
-		// No bidRequest.ext.currency field was found, use PBS rates as usual
-		return e.currencyConverter.Rates()
-	}
-
-	// If bidRequest.ext.currency.usepbsrates is nil, we understand its value as true. It will be false
-	// only if it's explicitly set to false
-	usePbsRates := requestRates.UsePBSRates == nil || *requestRates.UsePBSRates
-
-	if !usePbsRates {
-		// At this point, we can safely assume the ConversionRates map is not empty because
-		// validateCustomRates(bidReqCurrencyRates *openrtb_ext.ExtRequestCurrency) would have
-		// thrown an error under such conditions.
-		return currency.NewRates(requestRates.ConversionRates)
-	}
-
-	// Both PBS and custom rates can be used, check if ConversionRates is not empty
-	if len(requestRates.ConversionRates) == 0 {
-		// Custom rates map is empty, use PBS rates only
-		return e.currencyConverter.Rates()
-	}
-
-	// Return an AggregateConversions object that includes both custom and PBS currency rates but will
-	// prioritize custom rates over PBS rates whenever a currency rate is found in both
-	return currency.NewAggregateConversions(currency.NewRates(requestRates.ConversionRates), e.currencyConverter.Rates())
-}
-
 func findCacheID(bid *entities.PbsOrtbBid, auction *auction) (string, bool) {
 	if bid != nil && bid.Bid != nil && auction != nil {
 		if id, found := auction.cacheIds[bid.Bid]; found {
diff --git a/modules/moduledeps/deps.go b/modules/moduledeps/deps.go
index 08b18349..a1fa8917 100644
--- a/modules/moduledeps/deps.go
+++ b/modules/moduledeps/deps.go
@@ -1,9 +1,14 @@
 package moduledeps
 
-import "net/http"
+import (
+	"net/http"
+
+	"github.com/prebid/prebid-server/v2/currency"
+)
 
 // ModuleDeps provides dependencies that custom modules may need for hooks execution.
 // Additional dependencies can be added here if modules need something more.
 type ModuleDeps struct {
-	HTTPClient *http.Client
+	HTTPClient    *http.Client
+	RateConvertor *currency.RateConverter
 }
diff --git a/openrtb_ext/deal_tier.go b/openrtb_ext/deal_tier.go
index b1d8ee11..bd8d681d 100644
--- a/openrtb_ext/deal_tier.go
+++ b/openrtb_ext/deal_tier.go
@@ -38,7 +38,9 @@ func ReadDealTiersFromImp(imp openrtb2.Imp) (DealTierBidderMap, error) {
 	}
 	for bidder, param := range impPrebidExt.Prebid.Bidders {
 		if param.DealTier != nil {
-			dealTiers[BidderName(bidder)] = *param.DealTier
+			if bidderNormalized, bidderFound := NormalizeBidderName(bidder); bidderFound {
+				dealTiers[bidderNormalized] = *param.DealTier
+			}
 		}
 	}
 
diff --git a/router/router.go b/router/router.go
index 99fe2e5d..d89d1f59 100644
--- a/router/router.go
+++ b/router/router.go
@@ -191,7 +191,7 @@ func New(cfg *config.Configuration, rateConvertor *currency.RateConverter) (r *R
 		syncerKeys = append(syncerKeys, k)
 	}
 
-	moduleDeps := moduledeps.ModuleDeps{HTTPClient: generalHttpClient}
+	moduleDeps := moduledeps.ModuleDeps{HTTPClient: generalHttpClient, RateConvertor: rateConvertor}
 	repo, moduleStageNames, err := modules.NewBuilder().Build(cfg.Hooks.Modules, moduleDeps)
 	if err != nil {
 		glog.Fatalf("Failed to init hook modules: %v", err)
diff --git a/static/bidder-info/bliink.yaml b/static/bidder-info/bliink.yaml
index bcdacc42..4650e9f8 100644
--- a/static/bidder-info/bliink.yaml
+++ b/static/bidder-info/bliink.yaml
@@ -15,6 +15,9 @@ capabilities:
       - video
       - native
 userSync:
+  iframe:
+    url: "https://tag.bliink.io/usersync.html?gdpr={{.GDPR}}&gdprConsent={{.GDPRConsent}}&uspConsent={{.USPrivacy}}&redirect={{.RedirectURL}}"
+    userMacro: "$UID"
   redirect:
     url: "https://cookiesync.api.bliink.io/getuid?gdpr={{.GDPR}}&consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&url={{.RedirectURL}}"
     userMacro: "$UID"
diff --git a/static/bidder-info/indicue.yaml b/static/bidder-info/indicue.yaml
new file mode 100644
index 00000000..1353424d
--- /dev/null
+++ b/static/bidder-info/indicue.yaml
@@ -0,0 +1,9 @@
+aliasOf: adtelligent
+endpoint: "http://ghb.console.indicue.com/pbs/ortb"
+maintainer:
+  email: "ops@indicue.com"
+userSync:
+  # Indicue ssp supports user syncing, but requires configuration by the host. contact this
+  # bidder directly at the email address in this file to ask about enabling user sync.
+  supports:
+    - iframe


Test patch:
diff --git a/adapters/huaweiads/huaweiadstest/exemplary/nativeMultiSizesByRange.json b/adapters/huaweiads/huaweiadstest/exemplary/nativeMultiSizesByRange.json
index e9128ce1..92d62bac 100644
--- a/adapters/huaweiads/huaweiadstest/exemplary/nativeMultiSizesByRange.json
+++ b/adapters/huaweiads/huaweiadstest/exemplary/nativeMultiSizesByRange.json
@@ -94,7 +94,7 @@
                   "901",
                   "905"
                 ],
-                "format":[{"w":225,"h":150},{"w":300,"h":250},{"w":320,"h":250},{"w":640,"h":360},{"w":720,"h":1280},{"w":1080,"h":170},{"w":1080,"h":430},{"w":1080,"h":432},{"w":1080,"h":504},{"w":1080,"h":607},{"w":1080,"h":1620},{"w":1200,"h":627},{"w":1280,"h":720},{"w":1312,"h":768},{"w":1920,"h":1080}],
+                "format":[{"w":225,"h":150},{"w":300,"h":250},{"w":320,"h":250},{"w":640,"h":360},{"w":1080,"h":170},{"w":1080,"h":430},{"w":1080,"h":432},{"w":1080,"h":504},{"w":1080,"h":607},{"w":1080,"h":1620},{"w":1200,"h":627},{"w":1280,"h":720},{"w":1312,"h":768},{"w":1920,"h":1080}],
                 "test": 1
               }
             ],
diff --git a/adapters/huaweiads/huaweiadstest/exemplary/nativeSingleImage.json b/adapters/huaweiads/huaweiadstest/exemplary/nativeSingleImage.json
index c422f92b..4faec81d 100644
--- a/adapters/huaweiads/huaweiadstest/exemplary/nativeSingleImage.json
+++ b/adapters/huaweiads/huaweiadstest/exemplary/nativeSingleImage.json
@@ -111,10 +111,6 @@
                     "w": 640,
                     "h": 360
                 },
-                {
-                    "w": 720,
-                    "h": 1280
-                },
                 {
                     "w": 1080,
                     "h": 170
@@ -244,11 +240,11 @@
                     "imageInfo": [
                       {
                         "checkSha256Flag": 0,
-                        "height": 1280,
+                        "height": 150,
                         "imageType": "img",
                         "sha256": "8baa56fdb2702b9fb044d95b328936160cd245764375cdb25a4ab504f4ae2e19",
                         "url": "http://image.jpg",
-                        "width": 720
+                        "width": 225
                       }
                     ],
                     "label": "%E6%89%8B%E6%9C%BA%E6%B7%98%E5%AE%9D",
@@ -362,12 +358,12 @@
               "huaweiads"
             ],
             "crid": "58022259",
-            "adm": "{\"ver\":\"1.2\",\"assets\":[{\"id\":101,\"title\":{\"text\":\"/test/\",\"len\":6}},{\"id\":102,\"img\":{\"type\":3,\"url\":\"http://image.jpg\",\"w\":720,\"h\":1280}},{\"id\":103,\"data\":{\"label\":\"desc\",\"value\":\"\"}},{\"id\":105,\"data\":{\"type\":12,\"value\":\"install\"}}],\"link\":{\"url\":\"https://ads.huawei.com/usermgtportal/home/index.html#/\",\"clicktrackers\":[\"http://test/click\"]},\"eventtrackers\":[{\"event\":1,\"method\":1,\"url\":\"http://test/imp\"}]}",
+            "adm": "{\"ver\":\"1.2\",\"assets\":[{\"id\":101,\"title\":{\"text\":\"/test/\",\"len\":6}},{\"id\":102,\"img\":{\"type\":3,\"url\":\"http://image.jpg\",\"w\":225,\"h\":150}},{\"id\":103,\"data\":{\"label\":\"desc\",\"value\":\"\"}},{\"id\":105,\"data\":{\"type\":12,\"value\":\"install\"}}],\"link\":{\"url\":\"https://ads.huawei.com/usermgtportal/home/index.html#/\",\"clicktrackers\":[\"http://test/click\"]},\"eventtrackers\":[{\"event\":1,\"method\":1,\"url\":\"http://test/imp\"}]}",
             "id": "test-imp-id",
             "impid": "test-imp-id",
             "price": 2.8,
-            "h": 1280,
-            "w": 720
+            "h": 150,
+            "w": 225
           },
           "type": "native"
         }
diff --git a/adapters/huaweiads/huaweiadstest/exemplary/nativeThreeImageIncludeIcon.json b/adapters/huaweiads/huaweiadstest/exemplary/nativeThreeImageIncludeIcon.json
index a11f2ca8..6c33294b 100644
--- a/adapters/huaweiads/huaweiadstest/exemplary/nativeThreeImageIncludeIcon.json
+++ b/adapters/huaweiads/huaweiadstest/exemplary/nativeThreeImageIncludeIcon.json
@@ -111,10 +111,6 @@
                     "w": 640,
                     "h": 360
                 },
-                {
-                    "w": 720,
-                    "h": 1280
-                },
                 {
                     "w": 1080,
                     "h": 170
diff --git a/adapters/huaweiads/huaweiadstest/supplemental/bad_response_not_native.json b/adapters/huaweiads/huaweiadstest/supplemental/bad_response_not_native.json
index 20cfb2b8..75bf3f25 100644
--- a/adapters/huaweiads/huaweiadstest/supplemental/bad_response_not_native.json
+++ b/adapters/huaweiads/huaweiadstest/supplemental/bad_response_not_native.json
@@ -112,10 +112,6 @@
                     "w": 640,
                     "h": 360
                 },
-                {
-                    "w": 720,
-                    "h": 1280
-                },
                 {
                     "w": 1080,
                     "h": 170
diff --git a/currency/currency_test.go b/currency/currency_test.go
new file mode 100644
index 00000000..d462cc8b
--- /dev/null
+++ b/currency/currency_test.go
@@ -0,0 +1,199 @@
+package currency
+
+import (
+	"testing"
+	"time"
+
+	"github.com/prebid/prebid-server/v2/openrtb_ext"
+	"github.com/prebid/prebid-server/v2/util/jsonutil"
+	"github.com/prebid/prebid-server/v2/util/ptrutil"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestGetAuctionCurrencyRates(t *testing.T) {
+	pbsRates := map[string]map[string]float64{
+		"MXN": {
+			"USD": 20.13,
+			"EUR": 27.82,
+			"JPY": 5.09, // "MXN" to "JPY" rate not found in customRates
+		},
+	}
+
+	customRates := map[string]map[string]float64{
+		"MXN": {
+			"USD": 25.00, // different rate than in pbsRates
+			"EUR": 27.82, // same as in pbsRates
+			"GBP": 31.12, // not found in pbsRates at all
+		},
+	}
+
+	expectedRateEngineRates := map[string]map[string]float64{
+		"MXN": {
+			"USD": 25.00, // rates engine will prioritize the value found in custom rates
+			"EUR": 27.82, // same value in both the engine reads the custom entry first
+			"JPY": 5.09,  // the engine will find it in the pbsRates conversions
+			"GBP": 31.12, // the engine will find it in the custom conversions
+		},
+	}
+
+	setupMockRateConverter := func(pbsRates map[string]map[string]float64) *RateConverter {
+		if pbsRates == nil {
+			return nil
+		}
+
+		jsonPbsRates, err := jsonutil.Marshal(pbsRates)
+		if err != nil {
+			t.Fatalf("Failed to marshal PBS rates: %v", err)
+		}
+
+		// Init mock currency conversion service
+		mockCurrencyClient := &MockCurrencyRatesHttpClient{
+			ResponseBody: `{"dataAsOf":"2018-09-12","conversions":` + string(jsonPbsRates) + `}`,
+		}
+
+		return NewRateConverter(
+			mockCurrencyClient,
+			"currency.fake.com",
+			24*time.Hour,
+		)
+	}
+
+	type args struct {
+		currencyConverter *RateConverter
+		requestRates      *openrtb_ext.ExtRequestCurrency
+	}
+	tests := []struct {
+		name        string
+		args        args
+		assertRates map[string]map[string]float64
+	}{
+		{
+			name: "valid ConversionRates, valid pbsRates, false UsePBSRates. Resulting rates identical to customRates",
+			args: args{
+				currencyConverter: setupMockRateConverter(pbsRates),
+				requestRates: &openrtb_ext.ExtRequestCurrency{
+					ConversionRates: customRates,
+					UsePBSRates:     ptrutil.ToPtr(false),
+				},
+			},
+			assertRates: customRates,
+		},
+		{
+			name: "valid ConversionRates, valid pbsRates, true UsePBSRates. Resulting rates are a mix but customRates gets priority",
+			args: args{
+				currencyConverter: setupMockRateConverter(pbsRates),
+				requestRates: &openrtb_ext.ExtRequestCurrency{
+					ConversionRates: customRates,
+					UsePBSRates:     ptrutil.ToPtr(true),
+				},
+			},
+			assertRates: expectedRateEngineRates,
+		},
+		{
+			name: "valid ConversionRates, nil pbsRates, false UsePBSRates. Resulting rates identical to customRates",
+			args: args{
+				currencyConverter: nil,
+				requestRates: &openrtb_ext.ExtRequestCurrency{
+					ConversionRates: customRates,
+					UsePBSRates:     ptrutil.ToPtr(false),
+				},
+			},
+			assertRates: customRates,
+		},
+		{
+			name: "valid ConversionRates, nil pbsRates, true UsePBSRates. Resulting rates identical to customRates",
+			args: args{
+				currencyConverter: nil,
+				requestRates: &openrtb_ext.ExtRequestCurrency{
+					ConversionRates: customRates,
+					UsePBSRates:     ptrutil.ToPtr(true),
+				},
+			},
+			assertRates: customRates,
+		},
+		{
+			name: "empty ConversionRates, valid pbsRates, false UsePBSRates. Because pbsRates cannot be used, disable currency conversion",
+			args: args{
+				currencyConverter: setupMockRateConverter(pbsRates),
+				requestRates: &openrtb_ext.ExtRequestCurrency{
+					// ConversionRates inCustomRates not initialized makes for a zero-length map
+					UsePBSRates: ptrutil.ToPtr(false),
+				},
+			},
+			assertRates: nil,
+		},
+		{
+			name: "nil ConversionRates, valid pbsRates, true UsePBSRates. Resulting rates will be identical to pbsRates",
+			args: args{
+				currencyConverter: setupMockRateConverter(pbsRates),
+				requestRates:      nil,
+			},
+			assertRates: pbsRates,
+		},
+		{
+			name: "empty ConversionRates, nil pbsRates, false UsePBSRates. No conversion rates available, disable currency conversion",
+			args: args{
+				currencyConverter: setupMockRateConverter(pbsRates),
+				requestRates: &openrtb_ext.ExtRequestCurrency{
+					// ConversionRates inCustomRates not initialized makes for a zero-length map
+					UsePBSRates: ptrutil.ToPtr(false),
+				},
+			},
+			assertRates: nil,
+		},
+
+		{
+			name: "empty ConversionRates, nil pbsRates, true UsePBSRates. No conversion rates available, disable currency conversion",
+			args: args{
+				currencyConverter: nil,
+				requestRates: &openrtb_ext.ExtRequestCurrency{
+					// ConversionRates inCustomRates not initialized makes for a zero-length map
+					UsePBSRates: ptrutil.ToPtr(true),
+				},
+			},
+			assertRates: nil,
+		},
+		{
+			name: "nil customRates, nil pbsRates. No conversion rates available, disable currency conversion",
+			args: args{
+				currencyConverter: nil,
+				requestRates:      nil,
+			},
+			assertRates: nil,
+		},
+		{
+			name: "empty ConversionRates, valid pbsRates, true UsePBSRates. Resulting rates will be identical to pbsRates",
+			args: args{
+				currencyConverter: setupMockRateConverter(pbsRates),
+				requestRates: &openrtb_ext.ExtRequestCurrency{
+					// ConversionRates inCustomRates not initialized makes for a zero-length map
+					UsePBSRates: ptrutil.ToPtr(true),
+				},
+			},
+			assertRates: pbsRates,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if tt.args.currencyConverter != nil {
+				tt.args.currencyConverter.Run()
+			}
+			auctionRates := GetAuctionCurrencyRates(tt.args.currencyConverter, tt.args.requestRates)
+			if tt.args.currencyConverter == nil && tt.args.requestRates == nil && tt.assertRates == nil {
+				assert.Nil(t, auctionRates)
+			} else if tt.assertRates == nil {
+				rate, err := auctionRates.GetRate("USD", "MXN")
+				assert.Error(t, err, tt.name)
+				assert.Equal(t, float64(0), rate, tt.name)
+			} else {
+				for fromCurrency, rates := range tt.assertRates {
+					for toCurrency, expectedRate := range rates {
+						actualRate, err := auctionRates.GetRate(fromCurrency, toCurrency)
+						assert.NoError(t, err, tt.name)
+						assert.Equal(t, expectedRate, actualRate, tt.name)
+					}
+				}
+			}
+		})
+	}
+}
diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go
index 8f7ec30d..defe9402 100644
--- a/exchange/exchange_test.go
+++ b/exchange/exchange_test.go
@@ -6,7 +6,6 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
-	"io"
 	"math"
 	"net/http"
 	"net/http/httptest"
@@ -573,9 +572,8 @@ func TestTwoBiddersDebugDisabledAndEnabled(t *testing.T) {
 }
 
 func TestOverrideWithCustomCurrency(t *testing.T) {
-
-	mockCurrencyClient := &fakeCurrencyRatesHttpClient{
-		responseBody: `{"dataAsOf":"2018-09-12","conversions":{"USD":{"MXN":10.00}}}`,
+	mockCurrencyClient := &currency.MockCurrencyRatesHttpClient{
+		ResponseBody: `{"dataAsOf":"2018-09-12","conversions":{"USD":{"MXN":10.00}}}`,
 	}
 	mockCurrencyConverter := currency.NewRateConverter(
 		mockCurrencyClient,
@@ -741,11 +739,11 @@ func TestOverrideWithCustomCurrency(t *testing.T) {
 }
 
 func TestAdapterCurrency(t *testing.T) {
-	fakeCurrencyClient := &fakeCurrencyRatesHttpClient{
-		responseBody: `{"dataAsOf":"2018-09-12","conversions":{"USD":{"MXN":10.00}}}`,
+	mockCurrencyClient := &currency.MockCurrencyRatesHttpClient{
+		ResponseBody: `{"dataAsOf":"2018-09-12","conversions":{"USD":{"MXN":10.00}}}`,
 	}
 	currencyConverter := currency.NewRateConverter(
-		fakeCurrencyClient,
+		mockCurrencyClient,
 		"currency.fake.com",
 		24*time.Hour,
 	)
@@ -825,12 +823,11 @@ func (mpf *mockPriceFloorFetcher) Fetch(configs config.AccountPriceFloors) (*ope
 func (mpf *mockPriceFloorFetcher) Stop() {}
 
 func TestFloorsSignalling(t *testing.T) {
-
-	fakeCurrencyClient := &fakeCurrencyRatesHttpClient{
-		responseBody: `{"dataAsOf":"2023-04-10","conversions":{"USD":{"MXN":10.00}}}`,
+	mockCurrencyClient := &currency.MockCurrencyRatesHttpClient{
+		ResponseBody: `{"dataAsOf":"2023-04-10","conversions":{"USD":{"MXN":10.00}}}`,
 	}
 	currencyConverter := currency.NewRateConverter(
-		fakeCurrencyClient,
+		mockCurrencyClient,
 		"currency.com",
 		24*time.Hour,
 	)
@@ -994,208 +991,6 @@ func TestFloorsSignalling(t *testing.T) {
 	}
 
 }
-func TestGetAuctionCurrencyRates(t *testing.T) {
-
-	pbsRates := map[string]map[string]float64{
-		"MXN": {
-			"USD": 20.13,
-			"EUR": 27.82,
-			"JPY": 5.09, // "MXN" to "JPY" rate not found in customRates
-		},
-	}
-
-	customRates := map[string]map[string]float64{
-		"MXN": {
-			"USD": 25.00, // different rate than in pbsRates
-			"EUR": 27.82, // same as in pbsRates
-			"GBP": 31.12, // not found in pbsRates at all
-		},
-	}
-
-	expectedRateEngineRates := map[string]map[string]float64{
-		"MXN": {
-			"USD": 25.00, // rates engine will prioritize the value found in custom rates
-			"EUR": 27.82, // same value in both the engine reads the custom entry first
-			"JPY": 5.09,  // the engine will find it in the pbsRates conversions
-			"GBP": 31.12, // the engine will find it in the custom conversions
-		},
-	}
-
-	boolTrue := true
-	boolFalse := false
-
-	type testInput struct {
-		pbsRates       map[string]map[string]float64
-		bidExtCurrency *openrtb_ext.ExtRequestCurrency
-	}
-	type testOutput struct {
-		constantRates  bool
-		resultingRates map[string]map[string]float64
-	}
-	testCases := []struct {
-		desc     string
-		given    testInput
-		expected testOutput
-	}{
-		{
-			"valid pbsRates, valid ConversionRates, false UsePBSRates. Resulting rates identical to customRates",
-			testInput{
-				pbsRates: pbsRates,
-				bidExtCurrency: &openrtb_ext.ExtRequestCurrency{
-					ConversionRates: customRates,
-					UsePBSRates:     &boolFalse,
-				},
-			},
-			testOutput{
-				resultingRates: customRates,
-			},
-		},
-		{
-			"valid pbsRates, valid ConversionRates, true UsePBSRates. Resulting rates are a mix but customRates gets priority",
-			testInput{
-				pbsRates: pbsRates,
-				bidExtCurrency: &openrtb_ext.ExtRequestCurrency{
-					ConversionRates: customRates,
-					UsePBSRates:     &boolTrue,
-				},
-			},
-			testOutput{
-				resultingRates: expectedRateEngineRates,
-			},
-		},
-		{
-			"nil pbsRates, valid ConversionRates, false UsePBSRates. Resulting rates identical to customRates",
-			testInput{
-				pbsRates: nil,
-				bidExtCurrency: &openrtb_ext.ExtRequestCurrency{
-					ConversionRates: customRates,
-					UsePBSRates:     &boolFalse,
-				},
-			},
-			testOutput{
-				resultingRates: customRates,
-			},
-		},
-		{
-			"nil pbsRates, valid ConversionRates, true UsePBSRates. Resulting rates identical to customRates",
-			testInput{
-				pbsRates: nil,
-				bidExtCurrency: &openrtb_ext.ExtRequestCurrency{
-					ConversionRates: customRates,
-					UsePBSRates:     &boolTrue,
-				},
-			},
-			testOutput{
-				resultingRates: customRates,
-			},
-		},
-		{
-			"valid pbsRates, empty ConversionRates, false UsePBSRates. Because pbsRates cannot be used, default to constant rates",
-			testInput{
-				pbsRates: pbsRates,
-				bidExtCurrency: &openrtb_ext.ExtRequestCurrency{
-					// ConversionRates inCustomRates not initialized makes for a zero-length map
-					UsePBSRates: &boolFalse,
-				},
-			},
-			testOutput{
-				constantRates: true,
-			},
-		},
-		{
-			"valid pbsRates, nil ConversionRates, UsePBSRates defaults to true. Resulting rates will be identical to pbsRates",
-			testInput{
-				pbsRates:       pbsRates,
-				bidExtCurrency: nil,
-			},
-			testOutput{
-				resultingRates: pbsRates,
-			},
-		},
-		{
-			"nil pbsRates, empty ConversionRates, false UsePBSRates. Default to constant rates",
-			testInput{
-				pbsRates: nil,
-				bidExtCurrency: &openrtb_ext.ExtRequestCurrency{
-					// ConversionRates inCustomRates not initialized makes for a zero-length map
-					UsePBSRates: &boolFalse,
-				},
-			},
-			testOutput{
-				constantRates: true,
-			},
-		},
-		{
-			"customRates empty, UsePBSRates set to true, pbsRates are nil. Return default constant rates converter",
-			testInput{
-				pbsRates: nil,
-				bidExtCurrency: &openrtb_ext.ExtRequestCurrency{
-					// ConversionRates inCustomRates not initialized makes for a zero-length map
-					UsePBSRates: &boolTrue,
-				},
-			},
-			testOutput{
-				constantRates: true,
-			},
-		},
-		{
-			"nil customRates, nil pbsRates, UsePBSRates defaults to true. Return default constant rates converter",
-			testInput{
-				pbsRates:       nil,
-				bidExtCurrency: nil,
-			},
-			testOutput{
-				constantRates: true,
-			},
-		},
-	}
-
-	for _, tc := range testCases {
-
-		// Test setup:
-		jsonPbsRates, err := jsonutil.Marshal(tc.given.pbsRates)
-		if err != nil {
-			t.Fatalf("Failed to marshal PBS rates: %v", err)
-		}
-
-		// Init mock currency conversion service
-		mockCurrencyClient := &fakeCurrencyRatesHttpClient{
-			responseBody: `{"dataAsOf":"2018-09-12","conversions":` + string(jsonPbsRates) + `}`,
-		}
-		mockCurrencyConverter := currency.NewRateConverter(
-			mockCurrencyClient,
-			"currency.fake.com",
-			24*time.Hour,
-		)
-		mockCurrencyConverter.Run()
-
-		e := new(exchange)
-		e.currencyConverter = mockCurrencyConverter
-
-		// Run test
-		auctionRates := e.getAuctionCurrencyRates(tc.given.bidExtCurrency)
-
-		// When fromCurrency and toCurrency are the same, a rate of 1.00 is always expected
-		rate, err := auctionRates.GetRate("USD", "USD")
-		assert.NoError(t, err, tc.desc)
-		assert.Equal(t, float64(1), rate, tc.desc)
-
-		// If we expect an error, assert we have one along with a conversion rate of zero
-		if tc.expected.constantRates {
-			rate, err := auctionRates.GetRate("USD", "MXN")
-			assert.Error(t, err, tc.desc)
-			assert.Equal(t, float64(0), rate, tc.desc)
-		} else {
-			for fromCurrency, rates := range tc.expected.resultingRates {
-				for toCurrency, expectedRate := range rates {
-					actualRate, err := auctionRates.GetRate(fromCurrency, toCurrency)
-					assert.NoError(t, err, tc.desc)
-					assert.Equal(t, expectedRate, actualRate, tc.desc)
-				}
-			}
-		}
-	}
-}
 
 func TestReturnCreativeEndToEnd(t *testing.T) {
 	sampleAd := "<?xml version=\"1.0\" encoding=\"UTF-8\"?><VAST ...></VAST>"
@@ -3595,92 +3390,168 @@ func TestUpdateRejections(t *testing.T) {
 }
 
 func TestApplyDealSupport(t *testing.T) {
+	type testInput struct {
+		dealPriority int
+		impExt       json.RawMessage
+		targ         map[string]string
+		bidderName   openrtb_ext.BidderName
+	}
+
+	type testOutput struct {
+		hbPbCatDur        string
+		dealErr           string
+		dealTierSatisfied bool
+	}
+
 	testCases := []struct {
-		description               string
-		dealPriority              int
-		impExt                    json.RawMessage
-		targ                      map[string]string
-		expectedHbPbCatDur        string
-		expectedDealErr           string
-		expectedDealTierSatisfied bool
+		description string
+		in          testInput
+		expected    testOutput
 	}{
 		{
-			description:  "hb_pb_cat_dur should be modified",
-			dealPriority: 5,
-			impExt:       json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}}}`),
-			targ: map[string]string{
-				"hb_pb_cat_dur": "12.00_movies_30s",
+			description: "hb_pb_cat_dur should be modified",
+			in: testInput{
+				dealPriority: 5,
+				impExt:       json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}}}`),
+				targ: map[string]string{
+					"hb_pb_cat_dur": "12.00_movies_30s",
+				},
+				bidderName: openrtb_ext.BidderName("appnexus"),
+			},
+			expected: testOutput{
+				hbPbCatDur:        "tier5_movies_30s",
+				dealErr:           "",
+				dealTierSatisfied: true,
 			},
-			expectedHbPbCatDur:        "tier5_movies_30s",
-			expectedDealErr:           "",
-			expectedDealTierSatisfied: true,
 		},
 		{
-			description:  "hb_pb_cat_dur should not be modified due to priority not exceeding min",
-			dealPriority: 9,
-			impExt:       json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 10, "prefix": "tier"}, "placementId": 10433394}}}}`),
-			targ: map[string]string{
-				"hb_pb_cat_dur": "12.00_medicine_30s",
+			description: "hb_pb_cat_dur should be modified even with a mixed case bidder in the impExt",
+			in: testInput{
+				dealPriority: 5,
+				impExt:       json.RawMessage(`{"prebid": {"bidder": {"APPnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}}}`),
+				targ: map[string]string{
+					"hb_pb_cat_dur": "12.00_movies_30s",
+				},
+				bidderName: openrtb_ext.BidderName("appnexus"),
+			},
+			expected: testOutput{
+				hbPbCatDur:        "tier5_movies_30s",
+				dealErr:           "",
+				dealTierSatisfied: true,
 			},
-			expectedHbPbCatDur:        "12.00_medicine_30s",
-			expectedDealErr:           "",
-			expectedDealTierSatisfied: false,
 		},
 		{
-			description:  "hb_pb_cat_dur should not be modified due to invalid config",
-			dealPriority: 5,
-			impExt:       json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 5, "prefix": ""}, "placementId": 10433394}}}}`),
-			targ: map[string]string{
-				"hb_pb_cat_dur": "12.00_games_30s",
+			description: "hb_pb_cat_dur should be modified even with a mixed case bidder in the winningBidsByBidder map",
+			in: testInput{
+				dealPriority: 5,
+				impExt:       json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}}}`),
+				targ: map[string]string{
+					"hb_pb_cat_dur": "12.00_movies_30s",
+				},
+				bidderName: openrtb_ext.BidderName("APPnexus"),
+			},
+			expected: testOutput{
+				hbPbCatDur:        "tier5_movies_30s",
+				dealErr:           "",
+				dealTierSatisfied: true,
 			},
-			expectedHbPbCatDur:        "12.00_games_30s",
-			expectedDealErr:           "dealTier configuration invalid for bidder 'appnexus', imp ID 'imp_id1'",
-			expectedDealTierSatisfied: false,
 		},
 		{
-			description:  "hb_pb_cat_dur should not be modified due to deal priority of 0",
-			dealPriority: 0,
-			impExt:       json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}}}`),
-			targ: map[string]string{
-				"hb_pb_cat_dur": "12.00_auto_30s",
+			description: "hb_pb_cat_dur should not be modified due to unknown bidder in the winningBidsByBidder map",
+			in: testInput{
+				dealPriority: 9,
+				impExt:       json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 10, "prefix": "tier"}, "placementId": 10433394}}}}`),
+				targ: map[string]string{
+					"hb_pb_cat_dur": "12.00_medicine_30s",
+				},
+				bidderName: openrtb_ext.BidderName("unknown"),
+			},
+			expected: testOutput{
+				hbPbCatDur:        "12.00_medicine_30s",
+				dealErr:           "",
+				dealTierSatisfied: false,
+			},
+		},
+		{
+			description: "hb_pb_cat_dur should not be modified due to priority not exceeding min",
+			in: testInput{
+				dealPriority: 9,
+				impExt:       json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 10, "prefix": "tier"}, "placementId": 10433394}}}}`),
+				targ: map[string]string{
+					"hb_pb_cat_dur": "12.00_medicine_30s",
+				},
+				bidderName: openrtb_ext.BidderName("appnexus"),
+			},
+			expected: testOutput{
+				hbPbCatDur:        "12.00_medicine_30s",
+				dealErr:           "",
+				dealTierSatisfied: false,
+			},
+		},
+		{
+			description: "hb_pb_cat_dur should not be modified due to invalid config",
+			in: testInput{
+				dealPriority: 5,
+				impExt:       json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 5, "prefix": ""}, "placementId": 10433394}}}}`),
+				targ: map[string]string{
+					"hb_pb_cat_dur": "12.00_games_30s",
+				},
+				bidderName: openrtb_ext.BidderName("appnexus"),
+			},
+			expected: testOutput{
+				hbPbCatDur:        "12.00_games_30s",
+				dealErr:           "dealTier configuration invalid for bidder 'appnexus', imp ID 'imp_id1'",
+				dealTierSatisfied: false,
+			},
+		},
+		{
+			description: "hb_pb_cat_dur should not be modified due to deal priority of 0",
+			in: testInput{
+				dealPriority: 0,
+				impExt:       json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}}}`),
+				targ: map[string]string{
+					"hb_pb_cat_dur": "12.00_auto_30s",
+				},
+				bidderName: openrtb_ext.BidderName("appnexus"),
+			},
+			expected: testOutput{
+				hbPbCatDur:        "12.00_auto_30s",
+				dealErr:           "",
+				dealTierSatisfied: false,
 			},
-			expectedHbPbCatDur:        "12.00_auto_30s",
-			expectedDealErr:           "",
-			expectedDealTierSatisfied: false,
 		},
 	}
 
-	bidderName := openrtb_ext.BidderName("appnexus")
 	for _, test := range testCases {
 		bidRequest := &openrtb2.BidRequest{
 			ID: "some-request-id",
 			Imp: []openrtb2.Imp{
 				{
 					ID:  "imp_id1",
-					Ext: test.impExt,
+					Ext: test.in.impExt,
 				},
 			},
 		}
 
-		bid := entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, nil, test.dealPriority, false, "", 0, "USD", ""}
+		bid := entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, nil, test.in.dealPriority, false, "", 0, "USD", ""}
 		bidCategory := map[string]string{
-			bid.Bid.ID: test.targ["hb_pb_cat_dur"],
+			bid.Bid.ID: test.in.targ["hb_pb_cat_dur"],
 		}
 
 		auc := &auction{
 			winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 				"imp_id1": {
-					bidderName: {&bid},
+					test.in.bidderName: {&bid},
 				},
 			},
 		}
 
 		dealErrs := applyDealSupport(bidRequest, auc, bidCategory, nil)
 
-		assert.Equal(t, test.expectedHbPbCatDur, bidCategory[auc.winningBidsByBidder["imp_id1"][bidderName][0].Bid.ID], test.description)
-		assert.Equal(t, test.expectedDealTierSatisfied, auc.winningBidsByBidder["imp_id1"][bidderName][0].DealTierSatisfied, "expectedDealTierSatisfied=%v when %v", test.expectedDealTierSatisfied, test.description)
-		if len(test.expectedDealErr) > 0 {
-			assert.Containsf(t, dealErrs, errors.New(test.expectedDealErr), "Expected error message not found in deal errors")
+		assert.Equal(t, test.expected.hbPbCatDur, bidCategory[auc.winningBidsByBidder["imp_id1"][test.in.bidderName][0].Bid.ID], test.description)
+		assert.Equal(t, test.expected.dealTierSatisfied, auc.winningBidsByBidder["imp_id1"][test.in.bidderName][0].DealTierSatisfied, "expected.dealTierSatisfied=%v when %v", test.expected.dealTierSatisfied, test.description)
+		if len(test.expected.dealErr) > 0 {
+			assert.Containsf(t, dealErrs, errors.New(test.expected.dealErr), "Expected error message not found in deal errors")
 		}
 	}
 }
@@ -5469,19 +5340,6 @@ func (nilCategoryFetcher) FetchCategories(ctx context.Context, primaryAdServer,
 	return "", nil
 }
 
-// fakeCurrencyRatesHttpClient is a simple http client mock returning a constant response body
-type fakeCurrencyRatesHttpClient struct {
-	responseBody string
-}
-
-func (m *fakeCurrencyRatesHttpClient) Do(req *http.Request) (*http.Response, error) {
-	return &http.Response{
-		Status:     "200 OK",
-		StatusCode: http.StatusOK,
-		Body:       io.NopCloser(strings.NewReader(m.responseBody)),
-	}, nil
-}
-
 type mockBidder struct {
 	mock.Mock
 	lastExtraRequestInfo *adapters.ExtraRequestInfo
@@ -5951,3 +5809,65 @@ func TestBuildMultiBidMap(t *testing.T) {
 		}
 	}
 }
+
+func TestBidsToUpdate(t *testing.T) {
+	type testInput struct {
+		multiBid map[string]openrtb_ext.ExtMultiBid
+		bidder   string
+	}
+	testCases := []struct {
+		desc     string
+		in       testInput
+		expected int
+	}{
+		{
+			desc:     "Empty multibid map. Expect openrtb_ext.DefaultBidLimit",
+			in:       testInput{},
+			expected: openrtb_ext.DefaultBidLimit,
+		},
+		{
+			desc: "Empty bidder. Expect openrtb_ext.DefaultBidLimit",
+			in: testInput{
+				multiBid: map[string]openrtb_ext.ExtMultiBid{
+					"appnexus": {
+						Bidder:  "appnexus",
+						MaxBids: ptrutil.ToPtr(2),
+					},
+				},
+			},
+			expected: openrtb_ext.DefaultBidLimit,
+		},
+		{
+			desc: "bidder finds a match in multibid map but TargetBidderCodePrefix is empty. Expect openrtb_ext.DefaultBidLimit",
+			in: testInput{
+				multiBid: map[string]openrtb_ext.ExtMultiBid{
+					"appnexus": {
+						Bidder:  "appnexus",
+						MaxBids: ptrutil.ToPtr(2),
+					},
+				},
+				bidder: "appnexus",
+			},
+			expected: openrtb_ext.DefaultBidLimit,
+		},
+		{
+			desc: "multibid element with non-empty TargetBidderCodePrefix matches bidder. Expect MaxBids value",
+			in: testInput{
+				multiBid: map[string]openrtb_ext.ExtMultiBid{
+					"appnexus": {
+						Bidder:                 "appnexus",
+						MaxBids:                ptrutil.ToPtr(2),
+						TargetBidderCodePrefix: "aPrefix",
+					},
+				},
+				bidder: "appnexus",
+			},
+			expected: 2,
+		},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.desc, func(t *testing.T) {
+			assert.Equal(t, tc.expected, bidsToUpdate(tc.in.multiBid, tc.in.bidder), tc.desc)
+		})
+	}
+}
diff --git a/openrtb_ext/deal_tier_test.go b/openrtb_ext/deal_tier_test.go
index b8607748..22bf5d18 100644
--- a/openrtb_ext/deal_tier_test.go
+++ b/openrtb_ext/deal_tier_test.go
@@ -56,6 +56,16 @@ func TestReadDealTiersFromImp(t *testing.T) {
 			impExt:         json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "anyPrefix"}, "placementId": 12345}}}}`),
 			expectedResult: DealTierBidderMap{BidderAppnexus: {Prefix: "anyPrefix", MinDealTier: 5}},
 		},
+		{
+			description:    "imp.ext.prebid.bidder - one but it's not found in the Adapter Bidder list",
+			impExt:         json.RawMessage(`{"prebid": {"bidder": {"unknown": {"dealTier": {"minDealTier": 5, "prefix": "anyPrefix"}, "placementId": 12345}}}}`),
+			expectedResult: DealTierBidderMap{},
+		},
+		{
+			description:    "imp.ext.prebid.bidder - one but case is different from the Adapter Bidder list",
+			impExt:         json.RawMessage(`{"prebid": {"bidder": {"APpNExUS": {"dealTier": {"minDealTier": 5, "prefix": "anyPrefix"}, "placementId": 12345}}}}`),
+			expectedResult: DealTierBidderMap{BidderAppnexus: {Prefix: "anyPrefix", MinDealTier: 5}},
+		},
 		{
 			description:    "imp.ext.prebid.bidder - one with other params",
 			impExt:         json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "anyPrefix"}, "placementId": 12345}}, "supportdeals": true}, "tid": "1234"}`),
@@ -66,6 +76,11 @@ func TestReadDealTiersFromImp(t *testing.T) {
 			impExt:         json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "appnexusPrefix"}, "placementId": 12345}, "rubicon": {"dealTier": {"minDealTier": 8, "prefix": "rubiconPrefix"}, "placementId": 12345}}}}`),
 			expectedResult: DealTierBidderMap{BidderAppnexus: {Prefix: "appnexusPrefix", MinDealTier: 5}, BidderRubicon: {Prefix: "rubiconPrefix", MinDealTier: 8}},
 		},
+		{
+			description:    "imp.ext.prebid.bidder - same bidder listed twice but with different case the last one prevails",
+			impExt:         json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 100, "prefix": "appnexusPrefix"}, "placementId": 12345},"APpNExUS": {"dealTier": {"minDealTier": 5, "prefix": "APpNExUSPrefix"}, "placementId": 12345}}}}`),
+			expectedResult: DealTierBidderMap{BidderAppnexus: {Prefix: "APpNExUSPrefix", MinDealTier: 5}},
+		},
 		{
 			description:    "imp.ext.prebid.bidder - one without deal tier",
 			impExt:         json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"placementId": 12345}}}}`),