Repository: prebid/prebid-server
Pull request #3313 (2023-11-23 10:54:24)

Fix: single bid processing from alternatebiddercode
In alternatebiddercodes feature (https://github.com/prebid/prebid-server/issues/2174) it it possible that prebid-server gets a bid only from alternatebidder and not the primary bidder. Ex. For PubMatic-GroupM setup, PubMatic adapter would return a GroupM bid only.

For the above scenario, 

1. [L769](https://github.com/prebid/prebid-server/blob/v2.0.2/exchange/exchange.go#L769) , extraRespInfo.bidsFound is  set to 'false' because 'brw.bidder' is 'pubmatic' but seatBid.Seat is 'groupm'
2. [L377](https://github.com/prebid/prebid-server/blob/v2.0.2/exchange/exchange.go#L377), anyBidsReturned flag is set to false .
3. if anyBidsReturned=false then Floor-enforcement + category-mapping + deal-support etc are not being applied to the bid-response.Even targetting-keys + event-trackers are missing in the bid.ext

This PR contains the fix for the issue, also to capture such issue I've introduced the unit-test case to cover 'getallbids' function. Currently only addressed the required scenarions in the test function but it can be extended in future as per requirement. 

Refer - [sample_request.txt](https://github.com/prebid/prebid-server/files/13449191/sample_request.txt)



Patch:
diff --git a/adapters/adhese/adhese.go b/adapters/adhese/adhese.go
index 56d60be2..29c21ef3 100644
--- a/adapters/adhese/adhese.go
+++ b/adapters/adhese/adhese.go
@@ -141,6 +141,9 @@ func (a *AdheseAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externalR
 		return nil, []error{err, WrapServerError(fmt.Sprintf("Response %v could not be parsed as generic Adhese bid.", string(response.Body)))}
 	}
 
+	if len(adheseBidResponseArray) == 0 {
+		return nil, nil
+	}
 	var adheseBid = adheseBidResponseArray[0]
 
 	if adheseBid.Origin == "JERLICIA" {
diff --git a/adapters/adhese/adhesetest/supplemental/res-no_bids.json b/adapters/adhese/adhesetest/supplemental/res-no_bids_204.json
similarity index 100%
rename from adapters/adhese/adhesetest/supplemental/res-no_bids.json
rename to adapters/adhese/adhesetest/supplemental/res-no_bids_204.json
diff --git a/adapters/yieldmo/yieldmo.go b/adapters/yieldmo/yieldmo.go
index 795d32b8..69841e88 100644
--- a/adapters/yieldmo/yieldmo.go
+++ b/adapters/yieldmo/yieldmo.go
@@ -30,6 +30,10 @@ type Ext struct {
 	Gpid        string `json:"gpid,omitempty"`
 }
 
+type ExtBid struct {
+	MediaType string `json:"mediatype,omitempty"`
+}
+
 func (a *YieldmoAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
 	var errs []error
 	var adapterRequests []*adapters.RequestData
@@ -138,14 +142,18 @@ func (a *YieldmoAdapter) MakeBids(internalRequest *openrtb2.BidRequest, external
 
 	for _, sb := range bidResp.SeatBid {
 		for i := range sb.Bid {
+			bidType, err := getMediaTypeForImp(sb.Bid[i])
+			if err != nil {
+				continue
+			}
+
 			bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
 				Bid:     &sb.Bid[i],
-				BidType: getMediaTypeForImp(sb.Bid[i].ImpID, internalRequest.Imp),
+				BidType: bidType,
 			})
 		}
 	}
 	return bidResponse, nil
-
 }
 
 // Builder builds a new instance of the Yieldmo adapter for the given bidder with the given config.
@@ -156,12 +164,21 @@ func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server co
 	return bidder, nil
 }
 
-func getMediaTypeForImp(impId string, imps []openrtb2.Imp) openrtb_ext.BidType {
-	//default to video unless banner exists in impression
-	for _, imp := range imps {
-		if imp.ID == impId && imp.Banner != nil {
-			return openrtb_ext.BidTypeBanner
-		}
+// Retrieve the media type corresponding to the bid from the bid.ext object
+func getMediaTypeForImp(bid openrtb2.Bid) (openrtb_ext.BidType, error) {
+	var bidExt ExtBid
+	if err := json.Unmarshal(bid.Ext, &bidExt); err != nil {
+		return "", &errortypes.BadInput{Message: err.Error()}
+	}
+
+	switch bidExt.MediaType {
+	case "banner":
+		return openrtb_ext.BidTypeBanner, nil
+	case "video":
+		return openrtb_ext.BidTypeVideo, nil
+	case "native":
+		return openrtb_ext.BidTypeNative, nil
+	default:
+		return "", fmt.Errorf("invalid BidType: %s", bidExt.MediaType)
 	}
-	return openrtb_ext.BidTypeVideo
 }
diff --git a/config/bidderinfo.go b/config/bidderinfo.go
index d6d0ba79..409393c2 100644
--- a/config/bidderinfo.go
+++ b/config/bidderinfo.go
@@ -124,6 +124,14 @@ type Syncer struct {
 
 	// SupportCORS identifies if CORS is supported for the user syncing endpoints.
 	SupportCORS *bool `yaml:"supportCors" mapstructure:"support_cors"`
+
+	// SkipWhen allows bidders to specify when they don't want to sync
+	SkipWhen *SkipWhen `yaml:"skipwhen" mapstructure:"skipwhen"`
+}
+
+type SkipWhen struct {
+	GDPR   bool     `yaml:"gdpr" mapstructure:"gdpr"`
+	GPPSID []string `yaml:"gpp_sid" mapstructure:"gpp_sid"`
 }
 
 // SyncerEndpoint specifies the configuration of the URL returned by the /cookie_sync endpoint
diff --git a/endpoints/cookie_sync.go b/endpoints/cookie_sync.go
index f2ffb2fb..6d69aa82 100644
--- a/endpoints/cookie_sync.go
+++ b/endpoints/cookie_sync.go
@@ -61,7 +61,7 @@ func NewCookieSyncEndpoint(
 	}
 
 	return &cookieSyncEndpoint{
-		chooser: usersync.NewChooser(syncersByBidder, bidderHashSet),
+		chooser: usersync.NewChooser(syncersByBidder, bidderHashSet, config.BidderInfos),
 		config:  config,
 		privacyConfig: usersyncPrivacyConfig{
 			gdprConfig:             config.GDPR,
@@ -180,8 +180,10 @@ func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, ma
 			ccpaParsedPolicy: ccpaParsedPolicy,
 			activityControl:  activityControl,
 			activityRequest:  privacy.NewRequestFromPolicies(privacyPolicies),
+			gdprSignal:       gdprSignal,
 		},
 		SyncTypeFilter: syncTypeFilter,
+		GPPSID:         request.GPPSID,
 	}
 	return rx, privacyMacros, nil
 }
@@ -554,6 +556,7 @@ type usersyncPrivacy struct {
 	ccpaParsedPolicy ccpa.ParsedPolicy
 	activityControl  privacy.ActivityControl
 	activityRequest  privacy.ActivityRequest
+	gdprSignal       gdpr.Signal
 }
 
 func (p usersyncPrivacy) GDPRAllowsHostCookie() bool {
@@ -577,3 +580,7 @@ func (p usersyncPrivacy) ActivityAllowsUserSync(bidder string) bool {
 		privacy.Component{Type: privacy.ComponentTypeBidder, Name: bidder},
 		p.activityRequest)
 }
+
+func (p usersyncPrivacy) GDPRInScope() bool {
+	return p.gdprSignal == gdpr.SignalYes
+}
diff --git a/endpoints/setuid.go b/endpoints/setuid.go
index f9c55fc8..aae31f10 100644
--- a/endpoints/setuid.go
+++ b/endpoints/setuid.go
@@ -126,7 +126,6 @@ func NewSetUIDEndpoint(cfg *config.Configuration, syncersByBidder map[string]use
 				handleBadStatus(w, http.StatusBadRequest, metrics.SetUidBadRequest, err, metricsEngine, &so)
 				return
 			}
-			w.Write([]byte("Warning: " + err.Error()))
 		}
 
 		tcf2Cfg := tcf2CfgBuilder(cfg.GDPR.TCF2, account.GDPR)
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 211d82fc..446dfaf9 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -762,6 +762,7 @@ func (e *exchange) getAllBids(
 					} else {
 						adapterBids[bidderName] = seatBid
 					}
+					extraRespInfo.bidsFound = true
 				}
 				// collect fledgeAuctionConfigs separately from bids, as empty seatBids may be discarded
 				extraRespInfo.fledge = collectFledgeFromSeatBid(extraRespInfo.fledge, bidderName, brw.adapter, seatBid)
@@ -769,10 +770,6 @@ func (e *exchange) getAllBids(
 		}
 		//but we need to add all bidders data to adapterExtra to have metrics and other metadata
 		adapterExtra[brw.bidder] = brw.adapterExtra
-
-		if !extraRespInfo.bidsFound && adapterBids[brw.bidder] != nil && len(adapterBids[brw.bidder].Bids) > 0 {
-			extraRespInfo.bidsFound = true
-		}
 	}
 
 	return adapterBids, adapterExtra, extraRespInfo
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/static/bidder-info/boldwin.yaml b/static/bidder-info/boldwin.yaml
index 39c1caf3..f35d08ca 100644
--- a/static/bidder-info/boldwin.yaml
+++ b/static/bidder-info/boldwin.yaml
@@ -13,3 +13,7 @@ capabilities:
     - banner
     - video
     - native
+userSync:
+  redirect:
+    url: "https://sync.videowalldirect.com/pbserver?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&ccpa={{.USPrivacy}}&redir={{.RedirectURL}}"
+    userMacro: "[UID]"
\ No newline at end of file
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/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"]
-}
diff --git a/usersync/chooser.go b/usersync/chooser.go
index 0615be56..68acf195 100644
--- a/usersync/chooser.go
+++ b/usersync/chooser.go
@@ -3,6 +3,7 @@ package usersync
 import (
 	"strings"
 
+	"github.com/prebid/prebid-server/v2/config"
 	"github.com/prebid/prebid-server/v2/openrtb_ext"
 )
 
@@ -14,7 +15,7 @@ type Chooser interface {
 }
 
 // NewChooser returns a new instance of the standard chooser implementation.
-func NewChooser(bidderSyncerLookup map[string]Syncer, biddersKnown map[string]struct{}) Chooser {
+func NewChooser(bidderSyncerLookup map[string]Syncer, biddersKnown map[string]struct{}, bidderInfo map[string]config.BidderInfo) Chooser {
 	bidders := make([]string, 0, len(bidderSyncerLookup))
 
 	for k := range bidderSyncerLookup {
@@ -27,6 +28,7 @@ func NewChooser(bidderSyncerLookup map[string]Syncer, biddersKnown map[string]st
 		bidderChooser:            standardBidderChooser{shuffler: randomShuffler{}},
 		normalizeValidBidderName: openrtb_ext.NormalizeBidderName,
 		biddersKnown:             biddersKnown,
+		bidderInfo:               bidderInfo,
 	}
 }
 
@@ -37,6 +39,7 @@ type Request struct {
 	Limit          int
 	Privacy        Privacy
 	SyncTypeFilter SyncTypeFilter
+	GPPSID         string
 	Debug          bool
 }
 
@@ -92,6 +95,9 @@ const (
 	// StatusBlockedByPrivacy specifies a bidder sync url is not allowed by privacy activities
 	StatusBlockedByPrivacy
 
+	// StatusBlockedByRegulationScope specifies the bidder chose to not sync given GDPR being in scope or because of a GPPSID
+	StatusBlockedByRegulationScope
+
 	// StatusUnconfiguredBidder refers to a bidder who hasn't been configured to have a syncer key, but is known by Prebid Server
 	StatusUnconfiguredBidder
 )
@@ -99,6 +105,7 @@ const (
 // Privacy determines which privacy policies will be enforced for a user sync request.
 type Privacy interface {
 	GDPRAllowsHostCookie() bool
+	GDPRInScope() bool
 	GDPRAllowsBidderSync(bidder string) bool
 	CCPAAllowsBidderSync(bidder string) bool
 	ActivityAllowsUserSync(bidder string) bool
@@ -111,6 +118,7 @@ type standardChooser struct {
 	bidderChooser            bidderChooser
 	normalizeValidBidderName func(name string) (openrtb_ext.BidderName, bool)
 	biddersKnown             map[string]struct{}
+	bidderInfo               map[string]config.BidderInfo
 }
 
 // Choose randomly selects user syncers which are permitted by the user's privacy settings and
@@ -136,7 +144,7 @@ func (c standardChooser) Choose(request Request, cookie *Cookie) Result {
 		if _, ok := biddersSeen[bidders[i]]; ok {
 			continue
 		}
-		syncer, evaluation := c.evaluate(bidders[i], syncersSeen, request.SyncTypeFilter, request.Privacy, cookie)
+		syncer, evaluation := c.evaluate(bidders[i], syncersSeen, request.SyncTypeFilter, request.Privacy, cookie, request.GPPSID)
 
 		biddersEvaluated = append(biddersEvaluated, evaluation)
 		if evaluation.Status == StatusOK {
@@ -148,7 +156,7 @@ func (c standardChooser) Choose(request Request, cookie *Cookie) Result {
 	return Result{Status: StatusOK, BiddersEvaluated: biddersEvaluated, SyncersChosen: syncersChosen}
 }
 
-func (c standardChooser) evaluate(bidder string, syncersSeen map[string]struct{}, syncTypeFilter SyncTypeFilter, privacy Privacy, cookie *Cookie) (Syncer, BidderEvaluation) {
+func (c standardChooser) evaluate(bidder string, syncersSeen map[string]struct{}, syncTypeFilter SyncTypeFilter, privacy Privacy, cookie *Cookie, GPPSID string) (Syncer, BidderEvaluation) {
 	bidderNormalized, exists := c.normalizeValidBidderName(bidder)
 	if !exists {
 		return nil, BidderEvaluation{Status: StatusUnknownBidder, Bidder: bidder}
@@ -186,5 +194,17 @@ func (c standardChooser) evaluate(bidder string, syncersSeen map[string]struct{}
 		return nil, BidderEvaluation{Status: StatusBlockedByPrivacy, Bidder: bidder, SyncerKey: syncer.Key()}
 	}
 
+	if privacy.GDPRInScope() && c.bidderInfo[bidder].Syncer != nil && c.bidderInfo[bidder].Syncer.SkipWhen != nil && c.bidderInfo[bidder].Syncer.SkipWhen.GDPR {
+		return nil, BidderEvaluation{Status: StatusBlockedByRegulationScope, Bidder: bidder, SyncerKey: syncer.Key()}
+	}
+
+	if c.bidderInfo[bidder].Syncer != nil && c.bidderInfo[bidder].Syncer.SkipWhen != nil {
+		for _, gppSID := range c.bidderInfo[bidder].Syncer.SkipWhen.GPPSID {
+			if gppSID == GPPSID {
+				return nil, BidderEvaluation{Status: StatusBlockedByRegulationScope, Bidder: bidder, SyncerKey: syncer.Key()}
+			}
+		}
+	}
+
 	return syncer, BidderEvaluation{Status: StatusOK, Bidder: bidder, SyncerKey: syncer.Key()}
 }


Test patch:
diff --git a/adapters/adhese/adhesetest/supplemental/res-no_bids_200.json b/adapters/adhese/adhesetest/supplemental/res-no_bids_200.json
new file mode 100644
index 00000000..2a778bdf
--- /dev/null
+++ b/adapters/adhese/adhesetest/supplemental/res-no_bids_200.json
@@ -0,0 +1,54 @@
+{
+  "mockBidRequest": {
+    "id": "test-req",
+    "user": {
+      "ext": {
+        "consent" : "dummy"
+      }
+    },
+    "imp": [
+      {
+        "id": "test-req",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "account": "demo",
+            "location": "_adhese_prebid_demo_",
+            "format": "leaderboard",
+            "targets": 
+            {
+              "ci": ["gent", "brussels"],
+              "ag": ["55"],
+              "tl": ["all"]
+            }
+          }
+        }
+      }
+    ],
+    "site": {
+      "id": "test",
+      "publisher": {
+        "id": "123"
+      }
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://ads-demo.adhese.com/json/sl_adhese_prebid_demo_-leaderboard/ag55/cigent;brussels/tlall/xtdummy"
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": []
+      }
+    }
+  ],
+  "expectedBidResponses": []
+}
\ No newline at end of file
diff --git a/adapters/yieldmo/yieldmotest/exemplary/app-banner.json b/adapters/yieldmo/yieldmotest/exemplary/app-banner.json
index e674cf11..92609cae 100644
--- a/adapters/yieldmo/yieldmotest/exemplary/app-banner.json
+++ b/adapters/yieldmo/yieldmotest/exemplary/app-banner.json
@@ -65,7 +65,11 @@
                   "adm": "some-test-ad",
                   "crid": "crid_10",
                   "h": 250,
-                  "w": 300
+                  "w": 300,
+                  "ext":
+                  {
+                    "mediatype": "banner"
+                  }
                 }
               ]
             }
@@ -87,7 +91,11 @@
             "adm": "some-test-ad",
             "crid": "crid_10",
             "w": 300,
-            "h": 250
+            "h": 250,
+            "ext":
+            {
+              "mediatype": "banner"
+            }
           },
           "type": "banner"
         }
diff --git a/adapters/yieldmo/yieldmotest/exemplary/app_video.json b/adapters/yieldmo/yieldmotest/exemplary/app_video.json
index e33c37f6..6f94a30b 100644
--- a/adapters/yieldmo/yieldmotest/exemplary/app_video.json
+++ b/adapters/yieldmo/yieldmotest/exemplary/app_video.json
@@ -63,7 +63,11 @@
                   "adm": "some-test-ad",
                   "crid": "crid_10",
                   "h": 250,
-                  "w": 300
+                  "w": 300,
+                  "ext":
+                  {
+                    "mediatype": "video"
+                  }
                 }
               ]
             }
@@ -85,7 +89,11 @@
             "adm": "some-test-ad",
             "crid": "crid_10",
             "w": 300,
-            "h": 250
+            "h": 250,
+            "ext":
+            {
+              "mediatype": "video"
+            }
           },
           "type": "video"
         }
diff --git a/adapters/yieldmo/yieldmotest/exemplary/simple-banner.json b/adapters/yieldmo/yieldmotest/exemplary/simple-banner.json
index 11739ca1..ba243500 100644
--- a/adapters/yieldmo/yieldmotest/exemplary/simple-banner.json
+++ b/adapters/yieldmo/yieldmotest/exemplary/simple-banner.json
@@ -65,7 +65,11 @@
                   "adm": "some-test-ad",
                   "crid": "crid_10",
                   "h": 250,
-                  "w": 300
+                  "w": 300,
+                  "ext":
+                    {
+                      "mediatype": "banner"
+                    }
                 }
               ]
             }
@@ -87,7 +91,11 @@
             "adm": "some-test-ad",
             "crid": "crid_10",
             "w": 300,
-            "h": 250
+            "h": 250,
+            "ext":
+            {
+              "mediatype": "banner"
+            }
           },
           "type": "banner"
         }
diff --git a/adapters/yieldmo/yieldmotest/exemplary/simple_video.json b/adapters/yieldmo/yieldmotest/exemplary/simple_video.json
index aaf53124..ec65bc98 100644
--- a/adapters/yieldmo/yieldmotest/exemplary/simple_video.json
+++ b/adapters/yieldmo/yieldmotest/exemplary/simple_video.json
@@ -63,7 +63,11 @@
                   "adm": "some-test-ad",
                   "crid": "crid_10",
                   "h": 250,
-                  "w": 300
+                  "w": 300,
+                  "ext":
+                  {
+                    "mediatype": "video"
+                  }
                 }
               ]
             }
@@ -85,7 +89,11 @@
             "adm": "some-test-ad",
             "crid": "crid_10",
             "w": 300,
-            "h": 250
+            "h": 250,
+            "ext":
+            {
+              "mediatype": "video"
+            }
           },
           "type": "video"
         }
diff --git a/adapters/yieldmo/yieldmotest/exemplary/with_gpid.json b/adapters/yieldmo/yieldmotest/exemplary/with_gpid.json
index bd9e9110..d155f5db 100644
--- a/adapters/yieldmo/yieldmotest/exemplary/with_gpid.json
+++ b/adapters/yieldmo/yieldmotest/exemplary/with_gpid.json
@@ -73,7 +73,11 @@
                   "adm": "some-test-ad",
                   "crid": "crid_10",
                   "h": 250,
-                  "w": 300
+                  "w": 300,
+                  "ext":
+                  {
+                    "mediatype": "banner"
+                  }
                 }
               ]
             }
@@ -95,7 +99,11 @@
             "adm": "some-test-ad",
             "crid": "crid_10",
             "w": 300,
-            "h": 250
+            "h": 250,
+            "ext":
+            {
+              "mediatype": "banner"
+            }
           },
           "type": "banner"
         }
diff --git a/endpoints/cookie_sync_test.go b/endpoints/cookie_sync_test.go
index 182a6666..050e137f 100644
--- a/endpoints/cookie_sync_test.go
+++ b/endpoints/cookie_sync_test.go
@@ -45,16 +45,18 @@ func TestNewCookieSyncEndpoint(t *testing.T) {
 		analytics         = MockAnalyticsRunner{}
 		fetcher           = FakeAccountsFetcher{}
 		bidders           = map[string]openrtb_ext.BidderName{"bidderA": openrtb_ext.BidderName("bidderA"), "bidderB": openrtb_ext.BidderName("bidderB")}
+		bidderInfo        = map[string]config.BidderInfo{"bidderA": {}, "bidderB": {}}
 		biddersKnown      = map[string]struct{}{"bidderA": {}, "bidderB": {}}
 	)
 
 	endpoint := NewCookieSyncEndpoint(
 		syncersByBidder,
 		&config.Configuration{
-			UserSync:   configUserSync,
-			HostCookie: configHostCookie,
-			GDPR:       configGDPR,
-			CCPA:       config.CCPA{Enforce: configCCPAEnforce},
+			UserSync:    configUserSync,
+			HostCookie:  configHostCookie,
+			GDPR:        configGDPR,
+			CCPA:        config.CCPA{Enforce: configCCPAEnforce},
+			BidderInfos: bidderInfo,
 		},
 		gdprPermsBuilder,
 		tcf2ConfigBuilder,
@@ -66,12 +68,13 @@ func TestNewCookieSyncEndpoint(t *testing.T) {
 	result := endpoint.(*cookieSyncEndpoint)
 
 	expected := &cookieSyncEndpoint{
-		chooser: usersync.NewChooser(syncersByBidder, biddersKnown),
+		chooser: usersync.NewChooser(syncersByBidder, biddersKnown, bidderInfo),
 		config: &config.Configuration{
-			UserSync:   configUserSync,
-			HostCookie: configHostCookie,
-			GDPR:       configGDPR,
-			CCPA:       config.CCPA{Enforce: configCCPAEnforce},
+			UserSync:    configUserSync,
+			HostCookie:  configHostCookie,
+			GDPR:        configGDPR,
+			CCPA:        config.CCPA{Enforce: configCCPAEnforce},
+			BidderInfos: bidderInfo,
 		},
 		privacyConfig: usersyncPrivacyConfig{
 			gdprConfig:             configGDPR,
@@ -571,11 +574,13 @@ func TestCookieSyncParseRequest(t *testing.T) {
 					gdprPermissions:  &fakePermissions{},
 					ccpaParsedPolicy: expectedCCPAParsedPolicy,
 					activityRequest:  privacy.NewRequestFromPolicies(privacy.Policies{GPPSID: []int8{2}}),
+					gdprSignal:       1,
 				},
 				SyncTypeFilter: usersync.SyncTypeFilter{
 					IFrame:   usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
 					Redirect: usersync.NewSpecificBidderFilter([]string{"b"}, usersync.BidderFilterModeExclude),
 				},
+				GPPSID: "2",
 			},
 		},
 		{
@@ -611,6 +616,7 @@ func TestCookieSyncParseRequest(t *testing.T) {
 					gdprPermissions:  &fakePermissions{},
 					ccpaParsedPolicy: expectedCCPAParsedPolicy,
 					activityRequest:  emptyActivityPoliciesRequest,
+					gdprSignal:       1,
 				},
 				SyncTypeFilter: usersync.SyncTypeFilter{
 					IFrame:   usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
@@ -628,6 +634,7 @@ func TestCookieSyncParseRequest(t *testing.T) {
 				Privacy: usersyncPrivacy{
 					gdprPermissions: &fakePermissions{},
 					activityRequest: emptyActivityPoliciesRequest,
+					gdprSignal:      -1,
 				},
 				SyncTypeFilter: usersync.SyncTypeFilter{
 					IFrame:   usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
@@ -655,6 +662,7 @@ func TestCookieSyncParseRequest(t *testing.T) {
 				Privacy: usersyncPrivacy{
 					gdprPermissions: &fakePermissions{},
 					activityRequest: emptyActivityPoliciesRequest,
+					gdprSignal:      -1,
 				},
 				SyncTypeFilter: usersync.SyncTypeFilter{
 					IFrame:   usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
@@ -682,6 +690,7 @@ func TestCookieSyncParseRequest(t *testing.T) {
 				Privacy: usersyncPrivacy{
 					gdprPermissions: &fakePermissions{},
 					activityRequest: emptyActivityPoliciesRequest,
+					gdprSignal:      -1,
 				},
 				SyncTypeFilter: usersync.SyncTypeFilter{
 					IFrame:   usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
@@ -709,6 +718,7 @@ func TestCookieSyncParseRequest(t *testing.T) {
 				Privacy: usersyncPrivacy{
 					gdprPermissions: &fakePermissions{},
 					activityRequest: emptyActivityPoliciesRequest,
+					gdprSignal:      -1,
 				},
 				SyncTypeFilter: usersync.SyncTypeFilter{
 					IFrame:   usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
@@ -736,6 +746,7 @@ func TestCookieSyncParseRequest(t *testing.T) {
 				Privacy: usersyncPrivacy{
 					gdprPermissions: &fakePermissions{},
 					activityRequest: emptyActivityPoliciesRequest,
+					gdprSignal:      -1,
 				},
 				SyncTypeFilter: usersync.SyncTypeFilter{
 					IFrame:   usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
@@ -763,6 +774,7 @@ func TestCookieSyncParseRequest(t *testing.T) {
 				Privacy: usersyncPrivacy{
 					gdprPermissions: &fakePermissions{},
 					activityRequest: emptyActivityPoliciesRequest,
+					gdprSignal:      -1,
 				},
 				SyncTypeFilter: usersync.SyncTypeFilter{
 					IFrame:   usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
@@ -790,6 +802,7 @@ func TestCookieSyncParseRequest(t *testing.T) {
 				Privacy: usersyncPrivacy{
 					gdprPermissions: &fakePermissions{},
 					activityRequest: emptyActivityPoliciesRequest,
+					gdprSignal:      -1,
 				},
 				SyncTypeFilter: usersync.SyncTypeFilter{
 					IFrame:   usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
@@ -807,6 +820,7 @@ func TestCookieSyncParseRequest(t *testing.T) {
 				Privacy: usersyncPrivacy{
 					gdprPermissions: &fakePermissions{},
 					activityRequest: emptyActivityPoliciesRequest,
+					gdprSignal:      -1,
 				},
 				SyncTypeFilter: usersync.SyncTypeFilter{
 					IFrame:   usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
@@ -826,6 +840,7 @@ func TestCookieSyncParseRequest(t *testing.T) {
 				Privacy: usersyncPrivacy{
 					gdprPermissions: &fakePermissions{},
 					activityRequest: emptyActivityPoliciesRequest,
+					gdprSignal:      -1,
 				},
 				SyncTypeFilter: usersync.SyncTypeFilter{
 					IFrame:   usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
@@ -866,6 +881,7 @@ func TestCookieSyncParseRequest(t *testing.T) {
 				Privacy: usersyncPrivacy{
 					gdprPermissions: &fakePermissions{},
 					activityRequest: emptyActivityPoliciesRequest,
+					gdprSignal:      0,
 				},
 				SyncTypeFilter: usersync.SyncTypeFilter{
 					IFrame:   usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
@@ -892,6 +908,7 @@ func TestCookieSyncParseRequest(t *testing.T) {
 				Privacy: usersyncPrivacy{
 					gdprPermissions: &fakePermissions{},
 					activityRequest: emptyActivityPoliciesRequest,
+					gdprSignal:      -1,
 				},
 				SyncTypeFilter: usersync.SyncTypeFilter{
 					IFrame:   usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
@@ -937,6 +954,7 @@ func TestCookieSyncParseRequest(t *testing.T) {
 				Privacy: usersyncPrivacy{
 					gdprPermissions: &fakePermissions{},
 					activityRequest: emptyActivityPoliciesRequest,
+					gdprSignal:      -1,
 				},
 				SyncTypeFilter: usersync.SyncTypeFilter{
 					IFrame:   usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
@@ -969,6 +987,7 @@ func TestCookieSyncParseRequest(t *testing.T) {
 				Privacy: usersyncPrivacy{
 					gdprPermissions: &fakePermissions{},
 					activityRequest: emptyActivityPoliciesRequest,
+					gdprSignal:      -1,
 				},
 				SyncTypeFilter: usersync.SyncTypeFilter{
 					IFrame:   usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
@@ -1001,6 +1020,7 @@ func TestCookieSyncParseRequest(t *testing.T) {
 				Privacy: usersyncPrivacy{
 					gdprPermissions: &fakePermissions{},
 					activityRequest: emptyActivityPoliciesRequest,
+					gdprSignal:      0,
 				},
 				SyncTypeFilter: usersync.SyncTypeFilter{
 					IFrame:   usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
@@ -1974,6 +1994,39 @@ func TestCookieSyncActivityControlIntegration(t *testing.T) {
 	}
 }
 
+func TestUsersyncPrivacyGDPRInScope(t *testing.T) {
+	testCases := []struct {
+		description     string
+		givenGdprSignal gdpr.Signal
+		expected        bool
+	}{
+		{
+			description:     "GDPR Signal Yes",
+			givenGdprSignal: gdpr.SignalYes,
+			expected:        true,
+		},
+		{
+			description:     "GDPR Signal No",
+			givenGdprSignal: gdpr.SignalNo,
+			expected:        false,
+		},
+		{
+			description:     "GDPR Signal Ambigious",
+			givenGdprSignal: gdpr.SignalAmbiguous,
+			expected:        false,
+		},
+	}
+
+	for _, test := range testCases {
+		privacy := usersyncPrivacy{
+			gdprSignal: test.givenGdprSignal,
+		}
+
+		result := privacy.GDPRInScope()
+		assert.Equal(t, test.expected, result, test.description)
+	}
+}
+
 func TestCombineErrors(t *testing.T) {
 	testCases := []struct {
 		description    string
diff --git a/endpoints/setuid_test.go b/endpoints/setuid_test.go
index fe663797..145be3d3 100644
--- a/endpoints/setuid_test.go
+++ b/endpoints/setuid_test.go
@@ -217,15 +217,25 @@ func TestSetUIDEndpoint(t *testing.T) {
 		},
 		{
 			uri: "/setuid?bidder=pubmatic&uid=123&gpp_sid=2,4&gpp=DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA" +
-				"gdpr=1&gdpr_consent=BONciguONcjGKADACHENAOLS1rAHDAFAAEAASABQAMwAeACEAFw",
+				"&gdpr=1&gdpr_consent=BONciguONcjGKADACHENAOLS1rAHDAFAAEAASABQAMwAeACEAFw",
 			syncersBidderNameToKey: map[string]string{"pubmatic": "pubmatic"},
 			gdprAllowsHostCookies:  true,
 			existingSyncs:          nil,
 			expectedSyncs:          map[string]string{"pubmatic": "123"},
 			expectedStatusCode:     http.StatusOK,
-			expectedBody:           "Warning: 'gpp' value will be used over the one found in the deprecated 'gdpr_consent' field.",
-			expectedHeaders:        map[string]string{"Content-Type": "text/plain; charset=utf-8"},
-			description:            "Sets uid for a bidder allowed by GDPR in GPP, throws warning because GDPR legacy values weren't used",
+			expectedHeaders:        map[string]string{"Content-Type": "text/html", "Content-Length": "0"},
+			description:            "GPP value will be used over the one found in the deprecated GDPR consent field for iframe format",
+		},
+		{
+			uri: "/setuid?f=i&bidder=pubmatic&uid=123&gpp_sid=2,4&gpp=DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA" +
+				"&gdpr=1&gdpr_consent=BONciguONcjGKADACHENAOLS1rAHDAFAAEAASABQAMwAeACEAFw",
+			syncersBidderNameToKey: map[string]string{"pubmatic": "pubmatic"},
+			gdprAllowsHostCookies:  true,
+			existingSyncs:          nil,
+			expectedSyncs:          map[string]string{"pubmatic": "123"},
+			expectedStatusCode:     http.StatusOK,
+			expectedHeaders:        map[string]string{"Content-Type": "image/png", "Content-Length": "86"},
+			description:            "GPP value will be used over the one found in the deprecated GDPR consent field for redirect format",
 		},
 		{
 			uri:                    "/setuid?bidder=pubmatic&uid=123&gdpr=1&gdpr_consent=malformed",
diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go
index defe9402..aa354ea2 100644
--- a/exchange/exchange_test.go
+++ b/exchange/exchange_test.go
@@ -2770,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) {
@@ -5040,6 +5037,325 @@ func TestOverrideConfigAlternateBidderCodesWithRequestValues(t *testing.T) {
 	}
 }
 
+func TestGetAllBids(t *testing.T) {
+	noBidServer := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) }
+	server := httptest.NewServer(http.HandlerFunc(noBidServer))
+	defer server.Close()
+
+	type testIn struct {
+		bidderRequests             []BidderRequest
+		bidAdjustments             map[string]float64
+		conversions                currency.Conversions
+		accountDebugAllowed        bool
+		globalPrivacyControlHeader string
+		headerDebugAllowed         bool
+		alternateBidderCodes       openrtb_ext.ExtAlternateBidderCodes
+		experiment                 *openrtb_ext.Experiment
+		hookExecutor               hookexecution.StageExecutor
+		pbsRequestStartTime        time.Time
+		bidAdjustmentRules         map[string][]openrtb_ext.Adjustment
+		tmaxAdjustments            *TmaxAdjustmentsPreprocessed
+		adapterMap                 map[openrtb_ext.BidderName]AdaptedBidder
+	}
+	type testResults struct {
+		adapterBids   map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid
+		adapterExtra  map[openrtb_ext.BidderName]*seatResponseExtra
+		extraRespInfo extraAuctionResponseInfo
+	}
+	testCases := []struct {
+		desc     string
+		in       testIn
+		expected testResults
+	}{
+		{
+			desc: "alternateBidderCodes-config-absent: pubmatic bidder returns bids with 'pubmatic' and 'groupm' seats",
+			in: testIn{
+				bidderRequests: []BidderRequest{
+					{
+						BidderName:     "pubmatic",
+						BidderCoreName: "pubmatic",
+						BidRequest: &openrtb2.BidRequest{
+							ID: "some-request-id",
+							Imp: []openrtb2.Imp{{
+								ID: "some-impression-id",
+							}},
+						},
+					},
+				},
+				conversions:         &currency.ConstantRates{},
+				hookExecutor:        hookexecution.EmptyHookExecutor{},
+				pbsRequestStartTime: time.Now(),
+				adapterMap: map[openrtb_ext.BidderName]AdaptedBidder{
+					openrtb_ext.BidderPubmatic: AdaptBidder(&goodSingleBidder{
+						httpRequest: &adapters.RequestData{
+							Method: "POST",
+							Uri:    server.URL,
+						},
+						bidResponse: &adapters.BidderResponse{
+							Bids: []*adapters.TypedBid{
+								{Bid: &openrtb2.Bid{ID: "1"}, Seat: "pubmatic"},
+								{Bid: &openrtb2.Bid{ID: "2"}, Seat: "groupm"},
+							},
+						},
+					}, server.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderPubmatic, nil, ""),
+				},
+			},
+			expected: testResults{
+				extraRespInfo: extraAuctionResponseInfo{
+					bidsFound: true,
+				},
+				adapterExtra: map[openrtb_ext.BidderName]*seatResponseExtra{
+					"pubmatic": {
+						Warnings: []openrtb_ext.ExtBidderMessage{
+							{
+								Code:    errortypes.AlternateBidderCodeWarningCode,
+								Message: `alternateBidderCodes disabled for "pubmatic", rejecting bids for "groupm"`,
+							},
+						},
+					},
+				},
+				adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
+					"pubmatic": {
+						Bids: []*entities.PbsOrtbBid{
+							{
+								Bid: &openrtb2.Bid{
+									ID: "1",
+								},
+								OriginalBidCur: "USD",
+							},
+						},
+						Currency:  "USD",
+						Seat:      "pubmatic",
+						HttpCalls: []*openrtb_ext.ExtHttpCall{},
+					},
+				},
+			},
+		},
+		{
+			desc: "alternateBidderCodes-enabled: pubmatic bidder returns bids with 'pubmatic' and 'groupm' seats",
+			in: testIn{
+				bidderRequests: []BidderRequest{
+					{
+						BidderName:     "pubmatic",
+						BidderCoreName: "pubmatic",
+						BidRequest: &openrtb2.BidRequest{
+							ID: "some-request-id",
+							Imp: []openrtb2.Imp{{
+								ID: "some-impression-id",
+							}},
+						},
+					},
+				},
+				conversions: &currency.ConstantRates{},
+				alternateBidderCodes: openrtb_ext.ExtAlternateBidderCodes{
+					Enabled: true,
+					Bidders: map[string]openrtb_ext.ExtAdapterAlternateBidderCodes{
+						"pubmatic": {
+							Enabled:            true,
+							AllowedBidderCodes: []string{"groupm"},
+						},
+					},
+				},
+				hookExecutor:        hookexecution.EmptyHookExecutor{},
+				pbsRequestStartTime: time.Now(),
+				adapterMap: map[openrtb_ext.BidderName]AdaptedBidder{
+					openrtb_ext.BidderPubmatic: AdaptBidder(&goodSingleBidder{
+						httpRequest: &adapters.RequestData{
+							Method: "POST",
+							Uri:    server.URL,
+						},
+						bidResponse: &adapters.BidderResponse{
+							Bids: []*adapters.TypedBid{
+								{Bid: &openrtb2.Bid{ID: "1"}, Seat: "pubmatic"},
+								{Bid: &openrtb2.Bid{ID: "2"}, Seat: "groupm"},
+							},
+						},
+					}, server.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderPubmatic, nil, ""),
+				},
+			},
+			expected: testResults{
+				extraRespInfo: extraAuctionResponseInfo{
+					bidsFound: true,
+				},
+				adapterExtra: map[openrtb_ext.BidderName]*seatResponseExtra{
+					"pubmatic": {
+						Warnings: []openrtb_ext.ExtBidderMessage{},
+					},
+				},
+				adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
+					"pubmatic": {
+						Bids: []*entities.PbsOrtbBid{
+							{
+								Bid: &openrtb2.Bid{
+									ID: "1",
+								},
+								OriginalBidCur: "USD",
+							},
+						},
+						Currency:  "USD",
+						Seat:      "pubmatic",
+						HttpCalls: []*openrtb_ext.ExtHttpCall{},
+					},
+					"groupm": {
+						Bids: []*entities.PbsOrtbBid{
+							{
+								Bid: &openrtb2.Bid{
+									ID: "2",
+								},
+								OriginalBidCur: "USD",
+							},
+						},
+						Currency:  "USD",
+						Seat:      "groupm",
+						HttpCalls: []*openrtb_ext.ExtHttpCall{},
+					},
+				},
+			},
+		},
+		{
+			desc: "alternateBidderCodes-enabled: pubmatic bidder returns bids with only 'groupm' seat",
+			in: testIn{
+				bidderRequests: []BidderRequest{
+					{
+						BidderName:     "pubmatic",
+						BidderCoreName: "pubmatic",
+						BidRequest: &openrtb2.BidRequest{
+							ID: "some-request-id",
+							Imp: []openrtb2.Imp{{
+								ID: "some-impression-id",
+							}},
+						},
+					},
+				},
+				conversions: &currency.ConstantRates{},
+				alternateBidderCodes: openrtb_ext.ExtAlternateBidderCodes{
+					Enabled: true,
+					Bidders: map[string]openrtb_ext.ExtAdapterAlternateBidderCodes{
+						"pubmatic": {
+							Enabled:            true,
+							AllowedBidderCodes: []string{"groupm"},
+						},
+					},
+				},
+				hookExecutor:        hookexecution.EmptyHookExecutor{},
+				pbsRequestStartTime: time.Now(),
+				adapterMap: map[openrtb_ext.BidderName]AdaptedBidder{
+					openrtb_ext.BidderPubmatic: AdaptBidder(&goodSingleBidder{
+						httpRequest: &adapters.RequestData{
+							Method: "POST",
+							Uri:    server.URL,
+						},
+						bidResponse: &adapters.BidderResponse{
+							Bids: []*adapters.TypedBid{
+								{Bid: &openrtb2.Bid{ID: "2"}, Seat: "groupm"},
+							},
+						},
+					}, server.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderPubmatic, nil, ""),
+				},
+			},
+			expected: testResults{
+				extraRespInfo: extraAuctionResponseInfo{
+					bidsFound: true,
+				},
+				adapterExtra: map[openrtb_ext.BidderName]*seatResponseExtra{
+					"pubmatic": {
+						Warnings: []openrtb_ext.ExtBidderMessage{},
+					},
+				},
+				adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
+					"groupm": {
+						Bids: []*entities.PbsOrtbBid{
+							{
+								Bid: &openrtb2.Bid{
+									ID: "2",
+								},
+								OriginalBidCur: "USD",
+							},
+						},
+						Currency:  "USD",
+						Seat:      "groupm",
+						HttpCalls: []*openrtb_ext.ExtHttpCall{},
+					},
+				},
+			},
+		},
+		{
+			desc: "bidder responded with empty bid",
+			in: testIn{
+				bidderRequests: []BidderRequest{
+					{
+						BidderName:     "pubmatic",
+						BidderCoreName: "pubmatic",
+						BidRequest: &openrtb2.BidRequest{
+							ID: "some-request-id",
+							Imp: []openrtb2.Imp{{
+								ID: "some-impression-id",
+							}},
+						},
+					},
+				},
+				conversions: &currency.ConstantRates{},
+				alternateBidderCodes: openrtb_ext.ExtAlternateBidderCodes{
+					Enabled: true,
+					Bidders: map[string]openrtb_ext.ExtAdapterAlternateBidderCodes{
+						"pubmatic": {
+							Enabled:            true,
+							AllowedBidderCodes: []string{"groupm"},
+						},
+					},
+				},
+				hookExecutor:        hookexecution.EmptyHookExecutor{},
+				pbsRequestStartTime: time.Now(),
+				adapterMap: map[openrtb_ext.BidderName]AdaptedBidder{
+					openrtb_ext.BidderPubmatic: AdaptBidder(&goodSingleBidder{
+						httpRequest: &adapters.RequestData{
+							Method: "POST",
+							Uri:    server.URL,
+						},
+						bidResponse: &adapters.BidderResponse{},
+					}, server.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderPubmatic, nil, ""),
+				},
+			},
+			expected: testResults{
+				extraRespInfo: extraAuctionResponseInfo{
+					bidsFound: false,
+				},
+				adapterExtra: map[openrtb_ext.BidderName]*seatResponseExtra{
+					"pubmatic": {
+						Warnings: []openrtb_ext.ExtBidderMessage{},
+					},
+				},
+				adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{},
+			},
+		},
+	}
+	for _, test := range testCases {
+		t.Run(test.desc, func(t *testing.T) {
+			e := exchange{
+				cache: &wellBehavedCache{},
+				me:    &metricsConf.NilMetricsEngine{},
+				gdprPermsBuilder: fakePermissionsBuilder{
+					permissions: &permissionsMock{
+						allowAllBidders: true,
+					},
+				}.Builder,
+				adapterMap: test.in.adapterMap,
+			}
+
+			adapterBids, adapterExtra, extraRespInfo := e.getAllBids(context.Background(), test.in.bidderRequests, test.in.bidAdjustments,
+				test.in.conversions, test.in.accountDebugAllowed, test.in.globalPrivacyControlHeader, test.in.headerDebugAllowed, test.in.alternateBidderCodes, test.in.experiment,
+				test.in.hookExecutor, test.in.pbsRequestStartTime, test.in.bidAdjustmentRules, test.in.tmaxAdjustments)
+
+			assert.Equalf(t, test.expected.extraRespInfo.bidsFound, extraRespInfo.bidsFound, "extraRespInfo.bidsFound mismatch")
+			assert.Equalf(t, test.expected.adapterBids, adapterBids, "adapterBids mismatch")
+			assert.Equalf(t, len(test.expected.adapterExtra), len(adapterExtra), "adapterExtra length mismatch")
+			for adapter, extra := range test.expected.adapterExtra {
+				assert.Equalf(t, extra.Warnings, adapterExtra[adapter].Warnings, "adapterExtra.Warnings mismatch for adapter [%s]", adapter)
+			}
+		})
+	}
+}
+
 type MockSigner struct {
 	data string
 }
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 22bf5d18..6b74f7a1 100644
--- a/openrtb_ext/deal_tier_test.go
+++ b/openrtb_ext/deal_tier_test.go
@@ -17,93 +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 but it's not found in the Adapter Bidder list",
+			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",
+			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",
+			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 - 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",
+			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)
+	})
 }
diff --git a/usersync/chooser_test.go b/usersync/chooser_test.go
index 4e5d4acd..5b67361d 100644
--- a/usersync/chooser_test.go
+++ b/usersync/chooser_test.go
@@ -4,6 +4,7 @@ import (
 	"testing"
 	"time"
 
+	"github.com/prebid/prebid-server/v2/config"
 	"github.com/prebid/prebid-server/v2/openrtb_ext"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
@@ -15,6 +16,7 @@ func TestNewChooser(t *testing.T) {
 	testCases := []struct {
 		description              string
 		bidderSyncerLookup       map[string]Syncer
+		bidderInfo               map[string]config.BidderInfo
 		expectedBiddersAvailable []string
 	}{
 		{
@@ -40,7 +42,7 @@ func TestNewChooser(t *testing.T) {
 	}
 
 	for _, test := range testCases {
-		chooser, _ := NewChooser(test.bidderSyncerLookup, make(map[string]struct{})).(standardChooser)
+		chooser, _ := NewChooser(test.bidderSyncerLookup, make(map[string]struct{}), test.bidderInfo).(standardChooser)
 		assert.ElementsMatch(t, test.expectedBiddersAvailable, chooser.biddersAvailable, test.description)
 	}
 }
@@ -72,6 +74,7 @@ func TestChooserChoose(t *testing.T) {
 		givenRequest       Request
 		givenChosenBidders []string
 		givenCookie        Cookie
+		givenBidderInfo    map[string]config.BidderInfo
 		bidderNamesLookup  func(name string) (openrtb_ext.BidderName, bool)
 		expected           Result
 	}{
@@ -340,6 +343,55 @@ func TestChooserChoose(t *testing.T) {
 				SyncersChosen:    []SyncerChoice{{Bidder: "AppNexus", Syncer: fakeSyncerA}},
 			},
 		},
+		{
+			description: "Regulation Scope GDPR",
+			givenRequest: Request{
+				Privacy: &fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true, gdprInScope: true},
+				Limit:   0,
+			},
+			givenChosenBidders: []string{"a"},
+			givenCookie:        Cookie{},
+			givenBidderInfo: map[string]config.BidderInfo{
+				"a": {
+					Syncer: &config.Syncer{
+						SkipWhen: &config.SkipWhen{
+							GDPR: true,
+						},
+					},
+				},
+			},
+			bidderNamesLookup: normalizedBidderNamesLookup,
+			expected: Result{
+				Status:           StatusOK,
+				BiddersEvaluated: []BidderEvaluation{{Bidder: "a", SyncerKey: "keyA", Status: StatusBlockedByRegulationScope}},
+				SyncersChosen:    []SyncerChoice{},
+			},
+		},
+		{
+			description: "Regulation Scope GPP",
+			givenRequest: Request{
+				Privacy: &fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
+				Limit:   0,
+				GPPSID:  "2",
+			},
+			givenChosenBidders: []string{"a"},
+			givenCookie:        Cookie{},
+			givenBidderInfo: map[string]config.BidderInfo{
+				"a": {
+					Syncer: &config.Syncer{
+						SkipWhen: &config.SkipWhen{
+							GPPSID: []string{"2", "3"},
+						},
+					},
+				},
+			},
+			bidderNamesLookup: normalizedBidderNamesLookup,
+			expected: Result{
+				Status:           StatusOK,
+				BiddersEvaluated: []BidderEvaluation{{Bidder: "a", SyncerKey: "keyA", Status: StatusBlockedByRegulationScope}},
+				SyncersChosen:    []SyncerChoice{},
+			},
+		},
 	}
 
 	bidders := []string{"anyRequested"}
@@ -355,12 +407,17 @@ func TestChooserChoose(t *testing.T) {
 			On("choose", test.givenRequest.Bidders, biddersAvailable, cooperativeConfig).
 			Return(test.givenChosenBidders)
 
+		if test.givenBidderInfo == nil {
+			test.givenBidderInfo = map[string]config.BidderInfo{}
+		}
+
 		chooser := standardChooser{
 			bidderSyncerLookup:       bidderSyncerLookup,
 			biddersAvailable:         biddersAvailable,
 			bidderChooser:            mockBidderChooser,
 			normalizeValidBidderName: test.bidderNamesLookup,
 			biddersKnown:             biddersKnown,
+			bidderInfo:               test.givenBidderInfo,
 		}
 
 		result := chooser.Choose(test.givenRequest, &test.givenCookie)
@@ -392,6 +449,8 @@ func TestChooserEvaluate(t *testing.T) {
 		givenSyncersSeen            map[string]struct{}
 		givenPrivacy                fakePrivacy
 		givenCookie                 Cookie
+		givenGPPSID                 string
+		givenBidderInfo             map[string]config.BidderInfo
 		givenSyncTypeFilter         SyncTypeFilter
 		normalizedBidderNamesLookup func(name string) (openrtb_ext.BidderName, bool)
 		expectedSyncer              Syncer
@@ -541,13 +600,56 @@ func TestChooserEvaluate(t *testing.T) {
 			expectedSyncer:              nil,
 			expectedEvaluation:          BidderEvaluation{Bidder: "unconfigured", Status: StatusUnconfiguredBidder},
 		},
+		{
+			description:      "Blocked By Regulation Scope - GDPR",
+			givenBidder:      "a",
+			givenSyncersSeen: map[string]struct{}{},
+			givenPrivacy:     fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true, gdprInScope: true},
+			givenCookie:      cookieNeedsSync,
+			givenBidderInfo: map[string]config.BidderInfo{
+				"a": {
+					Syncer: &config.Syncer{
+						SkipWhen: &config.SkipWhen{
+							GDPR: true,
+						},
+					},
+				},
+			},
+			givenSyncTypeFilter:         syncTypeFilter,
+			normalizedBidderNamesLookup: normalizedBidderNamesLookup,
+			normalisedBidderName:        "a",
+			expectedSyncer:              nil,
+			expectedEvaluation:          BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusBlockedByRegulationScope},
+		},
+		{
+			description:      "Blocked By Regulation Scope - GPP",
+			givenBidder:      "a",
+			givenSyncersSeen: map[string]struct{}{},
+			givenPrivacy:     fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
+			givenCookie:      cookieNeedsSync,
+			givenBidderInfo: map[string]config.BidderInfo{
+				"a": {
+					Syncer: &config.Syncer{
+						SkipWhen: &config.SkipWhen{
+							GPPSID: []string{"2", "3"},
+						},
+					},
+				},
+			},
+			givenGPPSID:                 "2",
+			givenSyncTypeFilter:         syncTypeFilter,
+			normalizedBidderNamesLookup: normalizedBidderNamesLookup,
+			normalisedBidderName:        "a",
+			expectedSyncer:              nil,
+			expectedEvaluation:          BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusBlockedByRegulationScope},
+		},
 	}
 
 	for _, test := range testCases {
 		t.Run(test.description, func(t *testing.T) {
-			chooser, _ := NewChooser(bidderSyncerLookup, biddersKnown).(standardChooser)
+			chooser, _ := NewChooser(bidderSyncerLookup, biddersKnown, test.givenBidderInfo).(standardChooser)
 			chooser.normalizeValidBidderName = test.normalizedBidderNamesLookup
-			sync, evaluation := chooser.evaluate(test.givenBidder, test.givenSyncersSeen, test.givenSyncTypeFilter, &test.givenPrivacy, &test.givenCookie)
+			sync, evaluation := chooser.evaluate(test.givenBidder, test.givenSyncersSeen, test.givenSyncTypeFilter, &test.givenPrivacy, &test.givenCookie, test.givenGPPSID)
 
 			assert.Equal(t, test.normalisedBidderName, test.givenPrivacy.inputBidderName)
 			assert.Equal(t, test.expectedSyncer, sync, test.description+":syncer")
@@ -600,6 +702,7 @@ type fakePrivacy struct {
 	gdprAllowsBidderSync  bool
 	ccpaAllowsBidderSync  bool
 	activityAllowUserSync bool
+	gdprInScope           bool
 	inputBidderName       string
 }
 
@@ -620,3 +723,7 @@ func (p *fakePrivacy) CCPAAllowsBidderSync(bidder string) bool {
 func (p *fakePrivacy) ActivityAllowsUserSync(bidder string) bool {
 	return p.activityAllowUserSync
 }
+
+func (p *fakePrivacy) GDPRInScope() bool {
+	return p.gdprInScope
+}