Repository: prebid/prebid-server
Pull request #3310 (2023-11-20 17:15:27)

Fix: refactor flakey category dedupe test


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/adapters/ix/ix.go b/adapters/ix/ix.go
index f000cbc1..35b3f7f2 100644
--- a/adapters/ix/ix.go
+++ b/adapters/ix/ix.go
@@ -34,6 +34,15 @@ type IxDiag struct {
 	MultipleSiteIds string `json:"multipleSiteIds,omitempty"`
 }
 
+type auctionConfig struct {
+	BidId  string          `json:"bidId,omitempty"`
+	Config json.RawMessage `json:"config,omitempty"`
+}
+
+type ixRespExt struct {
+	AuctionConfig []auctionConfig `json:"protectedAudienceAuctionConfigs,omitempty"`
+}
+
 func (a *IxAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
 	requests := make([]*adapters.RequestData, 0, len(request.Imp))
 	errs := make([]error, 0)
@@ -273,6 +282,26 @@ func (a *IxAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externalReque
 		}
 	}
 
+	if bidResponse.Ext != nil {
+		var bidRespExt ixRespExt
+		if err := json.Unmarshal(bidResponse.Ext, &bidRespExt); err != nil {
+			return nil, append(errs, err)
+		}
+
+		if bidRespExt.AuctionConfig != nil {
+			bidderResponse.FledgeAuctionConfigs = make([]*openrtb_ext.FledgeAuctionConfig, 0, len(bidRespExt.AuctionConfig))
+			for _, config := range bidRespExt.AuctionConfig {
+				if config.Config != nil {
+					fledgeAuctionConfig := &openrtb_ext.FledgeAuctionConfig{
+						ImpId:  config.BidId,
+						Config: config.Config,
+					}
+					bidderResponse.FledgeAuctionConfigs = append(bidderResponse.FledgeAuctionConfigs, fledgeAuctionConfig)
+				}
+			}
+		}
+	}
+
 	return bidderResponse, errs
 }
 
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/adapter_builders.go b/exchange/adapter_builders.go
index c2b79170..825dd411 100755
--- a/exchange/adapter_builders.go
+++ b/exchange/adapter_builders.go
@@ -280,7 +280,6 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder {
 		openrtb_ext.BidderEVolution:         evolution.Builder,
 		openrtb_ext.BidderFlipp:             flipp.Builder,
 		openrtb_ext.BidderFreewheelSSP:      freewheelssp.Builder,
-		openrtb_ext.BidderFreewheelSSPOld:   freewheelssp.Builder,
 		openrtb_ext.BidderFRVRAdNetwork:     frvradn.Builder,
 		openrtb_ext.BidderGamma:             gamma.Builder,
 		openrtb_ext.BidderGamoshi:           gamoshi.Builder,
@@ -370,7 +369,6 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder {
 		openrtb_ext.BidderTrafficGate:       trafficgate.Builder,
 		openrtb_ext.BidderTriplelift:        triplelift.Builder,
 		openrtb_ext.BidderTripleliftNative:  triplelift_native.Builder,
-		openrtb_ext.BidderTrustX:            grid.Builder,
 		openrtb_ext.BidderUcfunnel:          ucfunnel.Builder,
 		openrtb_ext.BidderUndertone:         undertone.Builder,
 		openrtb_ext.BidderUnicorn:           unicorn.Builder,
@@ -384,7 +382,6 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder {
 		openrtb_ext.BidderVrtcal:            vrtcal.Builder,
 		openrtb_ext.BidderXeworks:           xeworks.Builder,
 		openrtb_ext.BidderYahooAds:          yahooAds.Builder,
-		openrtb_ext.BidderYahooSSP:          yahooAds.Builder,
 		openrtb_ext.BidderYeahmobi:          yeahmobi.Builder,
 		openrtb_ext.BidderYieldlab:          yieldlab.Builder,
 		openrtb_ext.BidderYieldmo:           yieldmo.Builder,
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/bidders.go b/openrtb_ext/bidders.go
index 5e6eb553..1f6eebbf 100644
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -100,7 +100,6 @@ var coreBidderNames []BidderName = []BidderName{
 	BidderEVolution,
 	BidderFlipp,
 	BidderFreewheelSSP,
-	BidderFreewheelSSPOld,
 	BidderFRVRAdNetwork,
 	BidderGamma,
 	BidderGamoshi,
@@ -190,7 +189,6 @@ var coreBidderNames []BidderName = []BidderName{
 	BidderTrafficGate,
 	BidderTriplelift,
 	BidderTripleliftNative,
-	BidderTrustX,
 	BidderUcfunnel,
 	BidderUndertone,
 	BidderUnicorn,
@@ -204,7 +202,6 @@ var coreBidderNames []BidderName = []BidderName{
 	BidderVrtcal,
 	BidderXeworks,
 	BidderYahooAds,
-	BidderYahooSSP,
 	BidderYeahmobi,
 	BidderYieldlab,
 	BidderYieldmo,
@@ -381,7 +378,6 @@ const (
 	BidderEVolution         BidderName = "e_volution"
 	BidderFlipp             BidderName = "flipp"
 	BidderFreewheelSSP      BidderName = "freewheelssp"
-	BidderFreewheelSSPOld   BidderName = "freewheel-ssp"
 	BidderFRVRAdNetwork     BidderName = "frvradn"
 	BidderGamma             BidderName = "gamma"
 	BidderGamoshi           BidderName = "gamoshi"
@@ -471,7 +467,6 @@ const (
 	BidderTrafficGate       BidderName = "trafficgate"
 	BidderTriplelift        BidderName = "triplelift"
 	BidderTripleliftNative  BidderName = "triplelift_native"
-	BidderTrustX            BidderName = "trustx"
 	BidderUcfunnel          BidderName = "ucfunnel"
 	BidderUndertone         BidderName = "undertone"
 	BidderUnicorn           BidderName = "unicorn"
@@ -485,7 +480,6 @@ const (
 	BidderVrtcal            BidderName = "vrtcal"
 	BidderXeworks           BidderName = "xeworks"
 	BidderYahooAds          BidderName = "yahooAds"
-	BidderYahooSSP          BidderName = "yahoossp"
 	BidderYeahmobi          BidderName = "yeahmobi"
 	BidderYieldlab          BidderName = "yieldlab"
 	BidderYieldmo           BidderName = "yieldmo"
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/freewheel-ssp.yaml b/static/bidder-info/freewheel-ssp.yaml
index 0c0a11ed..43c1ca16 100644
--- a/static/bidder-info/freewheel-ssp.yaml
+++ b/static/bidder-info/freewheel-ssp.yaml
@@ -1,16 +1,5 @@
-endpoint: "https://ads.stickyadstv.com/openrtb/dsp"
-maintainer:
-  email: prebid-maintainer@freewheel.com
-gvlVendorID: 285
-modifyingVastXmlAllowed: true
-capabilities:
-  app:
-    mediaTypes:
-      - video
-  site:
-    mediaTypes:
-      - video
+aliasOf: freewheelssp
 userSync:
   iframe:
     url: "https://ads.stickyadstv.com/pbs-user-sync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&r={{.RedirectURL}}"
-    userMacro: "{viewerid}"
+    userMacro: "{viewerid}"
\ No newline at end of file
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
diff --git a/static/bidder-info/trustx.yaml b/static/bidder-info/trustx.yaml
index 5e407782..1c60ac34 100644
--- a/static/bidder-info/trustx.yaml
+++ b/static/bidder-info/trustx.yaml
@@ -1,17 +1,9 @@
-endpoint: "https://grid.bidswitch.net/sp_bid?sp=trustx"
+aliasOf: grid
 maintainer:
   email: "grid-tech@themediagrid.com"
 gvlVendorID: 686
-capabilities:
-  app:
-    mediaTypes:
-      - banner
-      - video
-  site:
-    mediaTypes:
-      - banner
-      - video
 userSync:
   redirect:
     url: "https://x.bidswitch.net/check_uuid/{{.RedirectURL}}?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}"
-    userMacro: "${BSW_UUID}"
\ No newline at end of file
+    userMacro: "${BSW_UUID}"
+    
\ No newline at end of file
diff --git a/static/bidder-info/yahoossp.yaml b/static/bidder-info/yahoossp.yaml
index 8c23c01e..81695624 100644
--- a/static/bidder-info/yahoossp.yaml
+++ b/static/bidder-info/yahoossp.yaml
@@ -1,16 +1,6 @@
-endpoint: "https://s2shb.ssp.yahoo.com/admax/bid/partners/PBS"
+aliasOf: yahooAds
 maintainer:
   email: "hb-fe-tech@oath.com"
-gvlVendorID: 25
-capabilities:
-  app:
-    mediaTypes:
-      - banner
-      - video
-  site:
-    mediaTypes:
-      - banner
-      - video
 userSync:
   # yahoossp 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.
diff --git a/static/bidder-params/freewheel-ssp.json b/static/bidder-params/freewheel-ssp.json
deleted file mode 100644
index 23375f76..00000000
--- a/static/bidder-params/freewheel-ssp.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
-  "$schema": "http://json-schema.org/draft-04/schema#",
-  "title": "FreewheelSSP old Adapter Params",
-  "description": "A schema which validates params accepted by the FreewheelSSP adapter",
-  "type": "object",
-
-  "properties": {
-    "zoneId": {
-      "type": ["integer", "string"],
-      "description": "Zone ID"
-    }
-  },
-
-  "required": ["zoneId"]
-}
diff --git a/static/bidder-params/trustx.json b/static/bidder-params/trustx.json
deleted file mode 100644
index efedf9de..00000000
--- a/static/bidder-params/trustx.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
-  "$schema": "http://json-schema.org/draft-04/schema#",
-  "title": "TrustX Adapter Params",
-  "description": "A schema which validates params accepted by TrustX adapter",
-  "type": "object",
-  "properties": {
-    "uid": {
-      "type": "integer",
-      "description": "An ID which identifies this placement of the impression"
-    }
-  },
-  "required": []
-}
diff --git a/static/bidder-params/yahoossp.json b/static/bidder-params/yahoossp.json
deleted file mode 100644
index 9d971149..00000000
--- a/static/bidder-params/yahoossp.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
-	"$schema": "http://json-schema.org/draft-04/schema#",
-	"title": "YahooSSP Adapter Params",
-	"description": "A schema which validates params accepted by the YahooSSP adapter",
-	"type": "object",
-	"properties": {
-		"dcn": {
-			"type": "string",
-			"minLength": 1,
-			"description": "Site ID provided by One Mobile"
-		},
-		"pos": {
-			"type": "string",
-			"minLength": 1,
-			"description": "Placement ID"
-		}
-	},
-	"required": ["dcn", "pos"]
-}


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/adapters/ix/ix_test.go b/adapters/ix/ix_test.go
index e2651ca5..5e2450e7 100644
--- a/adapters/ix/ix_test.go
+++ b/adapters/ix/ix_test.go
@@ -274,3 +274,109 @@ func TestMakeRequestsErrIxDiag(t *testing.T) {
 	_, errs := bidder.MakeRequests(req, nil)
 	assert.Len(t, errs, 1)
 }
+
+func TestPABidResponse(t *testing.T) {
+	bidder := &IxAdapter{}
+
+	mockedReq := &openrtb2.BidRequest{
+		Imp: []openrtb2.Imp{{
+			ID: "1_1",
+			Banner: &openrtb2.Banner{
+				Format: []openrtb2.Format{{W: 300, H: 250}},
+			},
+			Ext: json.RawMessage(
+				`{
+					"ae": 1,
+					"bidder": {
+						"siteID": "123456"
+					}
+				}`,
+			)},
+		},
+	}
+	mockedExtReq := &adapters.RequestData{}
+	mockedBidResponse := &openrtb2.BidResponse{
+		ID: "test-1",
+		SeatBid: []openrtb2.SeatBid{{
+			Seat: "Buyer",
+			Bid: []openrtb2.Bid{{
+				ID:    "1",
+				ImpID: "1_1",
+				Price: 1.23,
+				AdID:  "123",
+				Ext: json.RawMessage(
+					`{
+						"prebid": {
+							"video": {
+								"duration": 60,
+								"primary_category": "IAB18-1"
+							}
+						}
+					}`,
+				),
+			}},
+		}},
+	}
+
+	testCases := []struct {
+		name        string
+		ext         json.RawMessage
+		expectedLen int
+	}{
+		{
+			name: "properly formatted",
+			ext: json.RawMessage(
+				`{
+					"protectedAudienceAuctionConfigs": [{
+						"bidId": "test-imp-id",
+						"config": {
+							"seller": "https://seller.com",
+							"decisionLogicUrl": "https://ssp.com/decision-logic.js",
+							"interestGroupBuyers": [
+								"https://buyer.com"
+							],
+							"sellerSignals": {
+								"callbackUrl": "https://callbackurl.com"
+							},
+							"perBuyerSignals": {
+								"https://buyer.com": []
+							}
+						}
+					}]
+				}`,
+			),
+			expectedLen: 1,
+		},
+		{
+			name:        "no protected audience auction configs returned",
+			ext:         json.RawMessage(`{}`),
+			expectedLen: 0,
+		},
+		{
+			name: "no config",
+			ext: json.RawMessage(
+				`{
+					"protectedAudienceAuctionConfigs": [{
+						"bidId": "test-imp-id"
+					}]
+				}`,
+			),
+			expectedLen: 0,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			mockedBidResponse.Ext = tc.ext
+			body, _ := json.Marshal(mockedBidResponse)
+			mockedRes := &adapters.ResponseData{
+				StatusCode: 200,
+				Body:       body,
+			}
+			bidResponse, errors := bidder.MakeBids(mockedReq, mockedExtReq, mockedRes)
+
+			assert.Nil(t, errors)
+			assert.Len(t, bidResponse.FledgeAuctionConfigs, tc.expectedLen)
+		})
+	}
+}
diff --git a/adapters/ix/ixtest/exemplary/fledge.json b/adapters/ix/ixtest/exemplary/fledge.json
new file mode 100644
index 00000000..919c7076
--- /dev/null
+++ b/adapters/ix/ixtest/exemplary/fledge.json
@@ -0,0 +1,171 @@
+{
+    "mockBidRequest": {
+        "id": "test-request-id",
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 250
+                        },
+                        {
+                            "w": 300,
+                            "h": 600
+                        },
+                        {
+                            "w": 600,
+                            "h": 800
+                        }
+                    ]
+                },
+                "ext": {
+                    "bidder": {
+                        "siteId": "569749"
+                    },
+                    "ae": 1
+                }
+            }
+        ],
+        "site": {
+            "page": "https://www.example.com/"
+        }
+    },
+    "httpCalls": [
+        {
+            "expectedRequest": {
+                "uri": "http://host/endpoint",
+                "body": {
+                    "id": "test-request-id",
+                    "imp": [
+                        {
+                            "id": "test-imp-id",
+                            "banner": {
+                                "format": [
+                                    {
+                                        "w": 300,
+                                        "h": 250
+                                    },
+                                    {
+                                        "w": 300,
+                                        "h": 600
+                                    },
+                                    {
+                                        "w": 600,
+                                        "h": 800
+                                    }
+                                ]
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "siteId": "569749"
+                                },
+                                "ae": 1
+                            }
+                        }
+                    ],
+                    "site": {
+                        "page": "https://www.example.com/",
+                        "publisher": {
+                            "id": "569749"
+                        }
+                    }
+                }
+            },
+            "mockResponse": {
+                "status": 200,
+                "body": {
+                    "id": "test-request-id",
+                    "seatbid": [
+                        {
+                            "seat": "958",
+                            "bid": [
+                                {
+                                    "id": "7706636740145184841",
+                                    "impid": "test-imp-id",
+                                    "price": 0.5,
+                                    "adid": "29681110",
+                                    "adm": "some-test-ad",
+                                    "adomain": [
+                                        "https://advertiser.example.com"
+                                    ],
+                                    "cid": "958",
+                                    "crid": "29681110",
+                                    "h": 250,
+                                    "w": 300,
+                                    "ext": {
+                                        "ix": {}
+                                    }
+                                }
+                            ]
+                        }
+                    ],
+                    "bidid": "5778926625248726496",
+                    "cur": "USD",
+                    "ext": {
+                        "protectedAudienceAuctionConfigs": [{
+                            "bidId": "test-imp-id",
+                            "config": {
+                                "seller": "https://seller.com",
+                                "decisionLogicUrl": "https://ssp.com/decision-logic.js",
+                                "interestGroupBuyers": [
+                                    "https://buyer.com"
+                                ],
+                                "sellerSignals": {
+                                    "callbackURL": "https://callbackurl.com"
+                                },
+                                "perBuyerSignals": {
+                                    "https://buyer.com": []
+                                }
+                            }
+                        }]
+                    }
+                }
+            }
+        }
+    ],
+    "expectedBidResponses": [
+        {
+            "currency": "USD",
+            "bids": [
+                {
+                    "bid": {
+                        "id": "7706636740145184841",
+                        "impid": "test-imp-id",
+                        "price": 0.5,
+                        "adm": "some-test-ad",
+                        "adid": "29681110",
+                        "adomain": [
+                            "https://advertiser.example.com"
+                        ],
+                        "cid": "958",
+                        "crid": "29681110",
+                        "w": 300,
+                        "h": 250,
+                        "ext": {
+                            "ix": {}
+                        }
+                    },
+                    "type": "banner"
+                }
+            ],
+            "fledgeauctionconfigs": [{
+                "impid": "test-imp-id",
+                "config": {
+                    "seller": "https://seller.com",
+                    "decisionLogicUrl": "https://ssp.com/decision-logic.js",
+                    "interestGroupBuyers": [
+                        "https://buyer.com"
+                    ],
+                    "sellerSignals": {
+                        "callbackURL": "https://callbackurl.com"
+                    },
+                    "perBuyerSignals": {
+                        "https://buyer.com": []
+                    }
+                }
+            }]
+        }
+    ]
+}
diff --git a/adapters/ix/ixtest/supplemental/bad-fledge.json b/adapters/ix/ixtest/supplemental/bad-fledge.json
new file mode 100644
index 00000000..97cc91e4
--- /dev/null
+++ b/adapters/ix/ixtest/supplemental/bad-fledge.json
@@ -0,0 +1,134 @@
+{
+    "mockBidRequest": {
+        "id": "test-request-id",
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 250
+                        },
+                        {
+                            "w": 300,
+                            "h": 600
+                        },
+                        {
+                            "w": 600,
+                            "h": 800
+                        }
+                    ]
+                },
+                "ext": {
+                    "bidder": {
+                        "siteId": "569749"
+                    },
+                    "ae": 1
+                }
+            }
+        ],
+        "site": {
+            "page": "https://www.example.com/"
+        }
+    },
+    "httpCalls": [
+        {
+            "expectedRequest": {
+                "uri": "http://host/endpoint",
+                "body": {
+                    "id": "test-request-id",
+                    "imp": [
+                        {
+                            "id": "test-imp-id",
+                            "banner": {
+                                "format": [
+                                    {
+                                        "w": 300,
+                                        "h": 250
+                                    },
+                                    {
+                                        "w": 300,
+                                        "h": 600
+                                    },
+                                    {
+                                        "w": 600,
+                                        "h": 800
+                                    }
+                                ]
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "siteId": "569749"
+                                },
+                                "ae": 1
+                            }
+                        }
+                    ],
+                    "site": {
+                        "page": "https://www.example.com/",
+                        "publisher": {
+                            "id": "569749"
+                        }
+                    }
+                }
+            },
+            "mockResponse": {
+                "status": 200,
+                "body": {
+                    "id": "test-request-id",
+                    "seatbid": [
+                        {
+                            "seat": "958",
+                            "bid": [
+                                {
+                                    "id": "7706636740145184841",
+                                    "impid": "test-imp-id",
+                                    "price": 0.5,
+                                    "adid": "29681110",
+                                    "adm": "some-test-ad",
+                                    "adomain": [
+                                        "https://advertiser.example.com"
+                                    ],
+                                    "cid": "958",
+                                    "crid": "29681110",
+                                    "h": 250,
+                                    "w": 300,
+                                    "ext": {
+                                        "ix": {}
+                                    }
+                                }
+                            ]
+                        }
+                    ],
+                    "bidid": "5778926625248726496",
+                    "cur": "USD",
+                    "ext": {
+                        "protectedAudienceAuctionConfigs": [{
+                            "bidId": 123,
+                            "config": {
+                                "seller": "https://seller.com",
+                                "decisionLogicUrl": "https://ssp.com/decision-logic.js",
+                                "interestGroupBuyers": [
+                                    "https://buyer.com"
+                                ],
+                                "sellerSignals": {
+                                    "callbackURL": "https://callbackurl.com"
+                                },
+                                "perBuyerSignals": {
+                                    "https://buyer.com": []
+                                }
+                            }
+                        }]
+                    }
+                }
+            }
+        }
+    ],
+    "expectedMakeBidsErrors": [
+        {
+            "value": "json: cannot unmarshal number into Go struct field auctionConfig.protectedAudienceAuctionConfigs.bidId of type string",
+            "comparison": "literal"
+        }
+    ]
+}
diff --git a/adapters/ix/ixtest/supplemental/fledge-no-bid.json b/adapters/ix/ixtest/supplemental/fledge-no-bid.json
new file mode 100644
index 00000000..139dd579
--- /dev/null
+++ b/adapters/ix/ixtest/supplemental/fledge-no-bid.json
@@ -0,0 +1,137 @@
+{
+    "mockBidRequest": {
+        "id": "test-request-id",
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 250
+                        },
+                        {
+                            "w": 300,
+                            "h": 600
+                        },
+                        {
+                            "w": 600,
+                            "h": 800
+                        }
+                    ]
+                },
+                "ext": {
+                    "bidder": {
+                        "siteId": "569749"
+                    },
+                    "ae": 1
+                }
+            }
+        ],
+        "site": {
+            "page": "https://www.example.com/"
+        }
+    },
+    "httpCalls": [
+        {
+            "expectedRequest": {
+                "uri": "http://host/endpoint",
+                "body": {
+                    "id": "test-request-id",
+                    "imp": [
+                        {
+                            "id": "test-imp-id",
+                            "banner": {
+                                "format": [
+                                    {
+                                        "w": 300,
+                                        "h": 250
+                                    },
+                                    {
+                                        "w": 300,
+                                        "h": 600
+                                    },
+                                    {
+                                        "w": 600,
+                                        "h": 800
+                                    }
+                                ]
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "siteId": "569749"
+                                },
+                                "ae": 1
+                            }
+                        }
+                    ],
+                    "site": {
+                        "page": "https://www.example.com/",
+                        "publisher": {
+                            "id": "569749"
+                        }
+                    }
+                }
+            },
+            "mockResponse": {
+                "status": 200,
+                "body": {
+                    "id": "test-request-id",
+                    "seatbid": [
+                        {
+                            "seat": "958",
+                            "bid": [
+                                {
+                                    "id": "7706636740145184841",
+                                    "impid": "test-imp-id",
+                                    "price": 0.5,
+                                    "adid": "29681110",
+                                    "adm": "some-test-ad",
+                                    "adomain": [
+                                        "https://advertiser.example.com"
+                                    ],
+                                    "cid": "958",
+                                    "crid": "29681110",
+                                    "h": 250,
+                                    "w": 300,
+                                    "ext": {
+                                        "ix": {}
+                                    }
+                                }
+                            ]
+                        }
+                    ],
+                    "bidid": "5778926625248726496",
+                    "cur": "USD"
+                }
+            }
+        }
+    ],
+    "expectedBidResponses": [
+        {
+            "currency": "USD",
+            "bids": [
+                {
+                    "bid": {
+                        "id": "7706636740145184841",
+                        "impid": "test-imp-id",
+                        "price": 0.5,
+                        "adm": "some-test-ad",
+                        "adid": "29681110",
+                        "adomain": [
+                            "https://advertiser.example.com"
+                        ],
+                        "cid": "958",
+                        "crid": "29681110",
+                        "w": 300,
+                        "h": 250,
+                        "ext": {
+                            "ix": {}
+                        }
+                    },
+                    "type": "banner"
+                }
+            ]
+        }
+    ]
+}
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..cc5c7ac0 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>"
@@ -2975,83 +2770,80 @@ func TestCategoryMappingTranslateCategoriesFalse(t *testing.T) {
 }
 
 func TestCategoryDedupe(t *testing.T) {
-
 	categoriesFetcher, error := newCategoryFetcher("./test/category-mapping")
 	if error != nil {
 		t.Errorf("Failed to create a category Fetcher: %v", error)
 	}
-
 	requestExt := newExtRequest()
-
 	targData := &targetData{
 		priceGranularity: *requestExt.Prebid.Targeting.PriceGranularity,
 		includeWinners:   true,
 	}
 
-	adapterBids := make(map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid)
+	// bid3 and bid5 will be same price, category, and duration so one of them should be removed based on the dedupe generator
+	bid1 := openrtb2.Bid{ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: []string{"IAB1-3"}, W: 1, H: 1}
+	bid2 := openrtb2.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 15.0000, Cat: []string{"IAB1-4"}, W: 1, H: 1}
+	bid3 := openrtb2.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 20.0000, Cat: []string{"IAB1-3"}, W: 1, H: 1}
+	bid4 := openrtb2.Bid{ID: "bid_id4", ImpID: "imp_id4", Price: 20.0000, Cat: []string{"IAB1-INVALID"}, W: 1, H: 1}
+	bid5 := openrtb2.Bid{ID: "bid_id5", ImpID: "imp_id5", Price: 20.0000, Cat: []string{"IAB1-3"}, W: 1, H: 1}
 
-	cats1 := []string{"IAB1-3"}
-	cats2 := []string{"IAB1-4"}
-	// bid3 will be same price, category, and duration as bid1 so one of them should get removed
-	cats4 := []string{"IAB1-2000"}
-	bid1 := openrtb2.Bid{ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: cats1, W: 1, H: 1}
-	bid2 := openrtb2.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 15.0000, Cat: cats2, W: 1, H: 1}
-	bid3 := openrtb2.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 20.0000, Cat: cats1, W: 1, H: 1}
-	bid4 := openrtb2.Bid{ID: "bid_id4", ImpID: "imp_id4", Price: 20.0000, Cat: cats4, W: 1, H: 1}
-	bid5 := openrtb2.Bid{ID: "bid_id5", ImpID: "imp_id5", Price: 20.0000, Cat: cats1, W: 1, H: 1}
+	bid1_1 := entities.PbsOrtbBid{Bid: &bid1, BidType: "video", BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, OriginalBidCPM: 10.0000, OriginalBidCur: "USD"}
+	bid1_2 := entities.PbsOrtbBid{Bid: &bid2, BidType: "video", BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 50}, OriginalBidCPM: 15.0000, OriginalBidCur: "USD"}
+	bid1_3 := entities.PbsOrtbBid{Bid: &bid3, BidType: "video", BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, OriginalBidCPM: 20.0000, OriginalBidCur: "USD"}
+	bid1_4 := entities.PbsOrtbBid{Bid: &bid4, BidType: "video", BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, OriginalBidCPM: 20.0000, OriginalBidCur: "USD"}
+	bid1_5 := entities.PbsOrtbBid{Bid: &bid5, BidType: "video", BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, OriginalBidCPM: 20.0000, OriginalBidCur: "USD"}
 
-	bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""}
-	bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 50}, nil, nil, 0, false, "", 15.0000, "USD", ""}
-	bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 20.0000, "USD", ""}
-	bid1_4 := entities.PbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 20.0000, "USD", ""}
-	bid1_5 := entities.PbsOrtbBid{&bid5, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 20.0000, "USD", ""}
+	bidderName1 := openrtb_ext.BidderName("appnexus")
 
-	selectedBids := make(map[string]int)
-	expectedCategories := map[string]string{
-		"bid_id1": "10.00_Electronics_30s",
-		"bid_id2": "14.00_Sports_50s",
-		"bid_id3": "20.00_Electronics_30s",
-		"bid_id5": "20.00_Electronics_30s",
+	tests := []struct {
+		name                 string
+		dedupeGeneratorValue bool
+		expectedBids         []*entities.PbsOrtbBid
+		expectedCategories   map[string]string
+	}{
+		{
+			name:                 "bid_id5_selected_over_bid_id3",
+			dedupeGeneratorValue: true,
+			expectedBids:         []*entities.PbsOrtbBid{&bid1_2, &bid1_5},
+			expectedCategories: map[string]string{
+				"bid_id2": "14.00_Sports_50s",
+				"bid_id5": "20.00_Electronics_30s",
+			},
+		},
+		{
+			name:                 "bid_id3_selected_over_bid_id5",
+			dedupeGeneratorValue: false,
+			expectedBids:         []*entities.PbsOrtbBid{&bid1_2, &bid1_3},
+			expectedCategories: map[string]string{
+				"bid_id2": "14.00_Sports_50s",
+				"bid_id3": "20.00_Electronics_30s",
+			},
+		},
 	}
 
-	numIterations := 10
-
-	// Run the function many times, this should be enough for the 50% chance of which bid to remove to remove bid1 sometimes
-	// and bid3 others. It's conceivably possible (but highly unlikely) that the same bid get chosen every single time, but
-	// if you notice false fails from this test increase numIterations to make it even less likely to happen.
-	for i := 0; i < numIterations; i++ {
-		innerBids := []*entities.PbsOrtbBid{
-			&bid1_1,
-			&bid1_2,
-			&bid1_3,
-			&bid1_4,
-			&bid1_5,
-		}
-
-		seatBid := entities.PbsOrtbSeatBid{Bids: innerBids, Currency: "USD"}
-		bidderName1 := openrtb_ext.BidderName("appnexus")
-
-		adapterBids[bidderName1] = &seatBid
-
-		bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &nonBids{})
-
-		assert.Equal(t, nil, err, "Category mapping error should be empty")
-		assert.Equal(t, 3, len(rejections), "There should be 2 bid rejection messages")
-		assert.Regexpf(t, regexp.MustCompile(`bid rejected \[bid ID: bid_id(1|3)\] reason: Bid was deduplicated`), rejections[0], "Rejection message did not match expected")
-		assert.Equal(t, "bid rejected [bid ID: bid_id4] reason: Category mapping file for primary ad server: 'freewheel', publisher: '' not found", rejections[1], "Rejection message did not match expected")
-		assert.Equal(t, 2, len(adapterBids[bidderName1].Bids), "Bidders number doesn't match")
-		assert.Equal(t, 2, len(bidCategory), "Bidders category mapping doesn't match")
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			adapterBids := map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
+				bidderName1: {
+					Bids: []*entities.PbsOrtbBid{
+						&bid1_1,
+						&bid1_2,
+						&bid1_3,
+						&bid1_4,
+						&bid1_5,
+					},
+					Currency: "USD",
+				},
+			}
+			deduplicateGenerator := fakeRandomDeduplicateBidBooleanGenerator{returnValue: tt.dedupeGeneratorValue}
+			bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &deduplicateGenerator, &nonBids{})
 
-		for bidId, bidCat := range bidCategory {
-			assert.Equal(t, expectedCategories[bidId], bidCat, "Category mapping doesn't match")
-			selectedBids[bidId]++
-		}
+			assert.Nil(t, err)
+			assert.Equal(t, 3, len(rejections))
+			assert.Equal(t, adapterBids[bidderName1].Bids, tt.expectedBids)
+			assert.Equal(t, bidCategory, tt.expectedCategories)
+		})
 	}
-
-	assert.Equal(t, numIterations, selectedBids["bid_id2"], "Bid 2 did not make it through every time")
-	assert.Equal(t, 0, selectedBids["bid_id1"], "Bid 1 should be rejected on every iteration due to lower price")
-	assert.NotEqual(t, 0, selectedBids["bid_id3"], "Bid 3 should be accepted at least once")
-	assert.NotEqual(t, 0, selectedBids["bid_id5"], "Bid 5 should be accepted at least once")
 }
 
 func TestNoCategoryDedupe(t *testing.T) {
@@ -3595,92 +3387,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 +5337,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 +5806,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/bidders_validate_test.go b/openrtb_ext/bidders_validate_test.go
index fed606ea..66de8183 100644
--- a/openrtb_ext/bidders_validate_test.go
+++ b/openrtb_ext/bidders_validate_test.go
@@ -50,7 +50,7 @@ func TestBidderUniquenessGatekeeping(t *testing.T) {
 	// - Exclude duplicates of adapters for the same bidder, as it's unlikely a publisher will use both.
 	var bidders []string
 	for _, bidder := range CoreBidderNames() {
-		if bidder != BidderSilverPush && bidder != BidderTripleliftNative && bidder != BidderAdkernelAdn && bidder != BidderFreewheelSSPOld && bidder != "yahooAdvertising" {
+		if bidder != BidderSilverPush && bidder != BidderTripleliftNative && bidder != BidderAdkernelAdn && bidder != "freewheel-ssp" && bidder != "yahooAdvertising" {
 			bidders = append(bidders, string(bidder))
 		}
 	}
diff --git a/openrtb_ext/deal_tier_test.go b/openrtb_ext/deal_tier_test.go
index b8607748..6b74f7a1 100644
--- a/openrtb_ext/deal_tier_test.go
+++ b/openrtb_ext/deal_tier_test.go
@@ -17,78 +17,100 @@ func TestReadDealTiersFromImp(t *testing.T) {
 		expectedErrorType error
 	}{
 		{
-			description:    "Nil",
+			description:    "nil",
 			impExt:         nil,
 			expectedResult: DealTierBidderMap{},
 		},
 		{
-			description:    "None",
+			description:    "none",
 			impExt:         json.RawMessage(``),
 			expectedResult: DealTierBidderMap{},
 		},
 		{
-			description:    "Empty Object",
+			description:    "empty_object",
 			impExt:         json.RawMessage(`{}`),
 			expectedResult: DealTierBidderMap{},
 		},
 		{
-			description:    "imp.ext - no prebid but with other params",
+			description:    "imp.ext_no_prebid_but_with_other_params",
 			impExt:         json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "anyPrefix"}, "placementId": 12345}, "tid": "1234"}`),
 			expectedResult: DealTierBidderMap{},
 		},
 		{
-			description:    "imp.ext.prebid - nil",
+			description:    "imp.ext.prebid_nil",
 			impExt:         json.RawMessage(`{"prebid": null}`),
 			expectedResult: DealTierBidderMap{},
 		},
 		{
-			description:    "imp.ext.prebid - empty",
+			description:    "imp.ext.prebid_empty",
 			impExt:         json.RawMessage(`{"prebid": {}}`),
 			expectedResult: DealTierBidderMap{},
 		},
 		{
-			description:    "imp.ext.prebid - no bidder but with other params",
+			description:    "imp.ext.prebid_no bidder but with other params",
 			impExt:         json.RawMessage(`{"prebid": {"supportdeals": true}}`),
 			expectedResult: DealTierBidderMap{},
 		},
 		{
-			description:    "imp.ext.prebid.bidder - one",
+			description:    "imp.ext.prebid.bidder_one",
 			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",
+			description:    "imp.ext.prebid.bidder_one_but_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"}`),
 			expectedResult: DealTierBidderMap{BidderAppnexus: {Prefix: "anyPrefix", MinDealTier: 5}},
 		},
 		{
-			description:    "imp.ext.prebid.bidder - multiple",
+			description:    "imp.ext.prebid.bidder_multiple",
 			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 - one without deal tier",
+			description:    "imp.ext.prebid.bidder_one_without_deal_tier",
 			impExt:         json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"placementId": 12345}}}}`),
 			expectedResult: DealTierBidderMap{},
 		},
 		{
-			description:       "imp.ext.prebid.bidder - error",
+			description:       "imp.ext.prebid.bidder_error",
 			impExt:            json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": "wrong type", "placementId": 12345}}}}`),
 			expectedErrorType: &errortypes.FailedToUnmarshal{},
 		},
 	}
 
 	for _, test := range testCases {
-		imp := openrtb2.Imp{Ext: test.impExt}
+		t.Run(test.description, func(t *testing.T) {
 
-		result, err := ReadDealTiersFromImp(imp)
+			imp := openrtb2.Imp{Ext: test.impExt}
+			result, err := ReadDealTiersFromImp(imp)
 
-		assert.Equal(t, test.expectedResult, result, test.description+":result")
+			assert.Equal(t, test.expectedResult, result)
 
-		if test.expectedErrorType != nil {
-			assert.IsType(t, test.expectedErrorType, err)
-		} else {
-			assert.NoError(t, err, test.description+":error")
-		}
+			if test.expectedErrorType != nil {
+				assert.IsType(t, test.expectedErrorType, err)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
 	}
+
+	t.Run("imp.ext.prebid.bidder_dedupe", func(t *testing.T) {
+		impExt := json.RawMessage(`{"prebid": {"bidder": {"APPNEXUS": {"dealTier": {"minDealTier": 100}},"APpNExUS": {"dealTier": {"minDealTier": 5}}}}}`)
+		imp := openrtb2.Imp{Ext: impExt}
+		result, err := ReadDealTiersFromImp(imp)
+
+		assert.Len(t, result, 1)
+		assert.NotNil(t, result["appnexus"])
+		assert.NoError(t, err)
+	})
 }