Repository: prebid/prebid-server
Pull request #1642 (2021-01-06 19:58:18)

Fix appnexus adapter not setting currency in the bid response
It has been reported that appnexus may not be behaving properly in foreign currency contexts. It appears that the adapter does not set the curr (currency), field in the bid response. This pull request makes this correction and adds a JSON test file but proper assertion of the Currency field of the BidderResponse inside adapters/adapterstest/test_json.go will be implemented in a separate PR being developed by @mansinahar

NOTE: Recreated from https://github.com/prebid/prebid-server/pull/1598 to solve strange merge state issue.


Patch:
diff --git a/adapters/appnexus/appnexus.go b/adapters/appnexus/appnexus.go
index e6398b4a..0a8146fd 100644
--- a/adapters/appnexus/appnexus.go
+++ b/adapters/appnexus/appnexus.go
@@ -604,6 +604,9 @@ func (a *AppNexusAdapter) MakeBids(internalRequest *openrtb.BidRequest, external
 			}
 		}
 	}
+	if bidResp.Cur != "" {
+		bidResponse.Currency = bidResp.Cur
+	}
 	return bidResponse, errs
 }
 
diff --git a/exchange/gdpr.go b/exchange/gdpr.go
index 2e2343d6..f464cac2 100644
--- a/exchange/gdpr.go
+++ b/exchange/gdpr.go
@@ -4,25 +4,21 @@ import (
 	"encoding/json"
 
 	"github.com/mxmCherry/openrtb"
+	"github.com/prebid/prebid-server/gdpr"
 )
 
 // ExtractGDPR will pull the gdpr flag from an openrtb request
-func extractGDPR(bidRequest *openrtb.BidRequest, usersyncIfAmbiguous bool) (gdpr int) {
+func extractGDPR(bidRequest *openrtb.BidRequest) gdpr.Signal {
 	var re regsExt
 	var err error
 	if bidRequest.Regs != nil {
 		err = json.Unmarshal(bidRequest.Regs.Ext, &re)
 	}
 	if re.GDPR == nil || err != nil {
-		if usersyncIfAmbiguous {
-			gdpr = 0
-		} else {
-			gdpr = 1
-		}
+		return gdpr.SignalAmbiguous
 	} else {
-		gdpr = *re.GDPR
+		return gdpr.Signal(*re.GDPR)
 	}
-	return
 }
 
 // ExtractConsent will pull the consent string from an openrtb request
diff --git a/exchange/utils.go b/exchange/utils.go
index a803e4e2..21c31a29 100644
--- a/exchange/utils.go
+++ b/exchange/utils.go
@@ -75,8 +75,9 @@ func cleanOpenRTBRequests(ctx context.Context,
 		return
 	}
 
-	gdpr := extractGDPR(req.BidRequest, usersyncIfAmbiguous)
+	gdprSignal := extractGDPR(req.BidRequest)
 	consent := extractConsent(req.BidRequest)
+	gdprEnforced := gdprSignal == gdpr.SignalYes || (gdprSignal == gdpr.SignalAmbiguous && !usersyncIfAmbiguous)
 
 	ccpaEnforcer, err := extractCCPA(req.BidRequest, privacyConfig, &req.Account, aliases, integrationTypeMap[req.LegacyLabels.RType])
 	if err != nil {
@@ -97,9 +98,9 @@ func cleanOpenRTBRequests(ctx context.Context,
 	privacyLabels.COPPAEnforced = privacyEnforcement.COPPA
 	privacyLabels.LMTEnforced = lmtEnforcer.ShouldEnforce(unknownBidder)
 
-	gdprEnabled := gdprEnabled(&req.Account, privacyConfig, integrationTypeMap[req.LegacyLabels.RType])
+	gdprEnforced = gdprEnforced && gdprEnabled(&req.Account, privacyConfig, integrationTypeMap[req.LegacyLabels.RType])
 
-	if gdpr == 1 && gdprEnabled {
+	if gdprEnforced {
 		privacyLabels.GDPREnforced = true
 		parsedConsent, err := vendorconsent.ParseString(consent)
 		if err == nil {
@@ -114,9 +115,9 @@ func cleanOpenRTBRequests(ctx context.Context,
 		privacyEnforcement.CCPA = ccpaEnforcer.ShouldEnforce(bidderRequest.BidderName.String())
 
 		// GDPR
-		if gdpr == 1 && gdprEnabled {
+		if gdprEnforced {
 			var publisherID = req.LegacyLabels.PubID
-			_, geo, id, err := gDPR.PersonalInfoAllowed(ctx, bidderRequest.BidderCoreName, publisherID, consent)
+			_, geo, id, err := gDPR.PersonalInfoAllowed(ctx, bidderRequest.BidderCoreName, publisherID, gdprSignal, consent)
 			privacyEnforcement.GDPRGeo = !geo && err == nil
 			privacyEnforcement.GDPRID = !id && err == nil
 		} else {
diff --git a/gdpr/gdpr.go b/gdpr/gdpr.go
index 60a7cc1e..eea7fa5a 100644
--- a/gdpr/gdpr.go
+++ b/gdpr/gdpr.go
@@ -23,7 +23,7 @@ type Permissions interface {
 	// Determines whether or not to send PI information to a bidder, or mask it out.
 	//
 	// If the consent string was nonsensical, the returned error will be an ErrorMalformedConsent.
-	PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, bool, error)
+	PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, gdprSignal Signal, consent string) (bool, bool, bool, error)
 }
 
 // Versions of the GDPR TCF technical specification.
diff --git a/gdpr/impl.go b/gdpr/impl.go
index 5b3b86fe..e1822ece 100644
--- a/gdpr/impl.go
+++ b/gdpr/impl.go
@@ -19,6 +19,14 @@ import (
 //
 // Nothing in this file is exported. Public APIs can be found in gdpr.go
 
+type Signal int
+
+const (
+	SignalAmbiguous Signal = -1
+	SignalNo        Signal = 0
+	SignalYes       Signal = 1
+)
+
 type permissionsImpl struct {
 	cfg             config.GDPR
 	vendorIDs       map[openrtb_ext.BidderName]uint16
@@ -42,21 +50,35 @@ func (p *permissionsImpl) BidderSyncAllowed(ctx context.Context, bidder openrtb_
 	return false, nil
 }
 
-func (p *permissionsImpl) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, bool, error) {
-	_, ok := p.cfg.NonStandardPublisherMap[PublisherID]
-	if ok {
+func (p *permissionsImpl) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, gdprSignal Signal, consent string) (allowPI bool, allowGeo bool, allowID bool, err error) {
+	if _, ok := p.cfg.NonStandardPublisherMap[PublisherID]; ok {
 		return true, true, true, nil
 	}
 
-	id, ok := p.vendorIDs[bidder]
-	if ok {
-		return p.allowPI(ctx, id, consent)
+	if gdprSignal == SignalAmbiguous {
+		if p.cfg.UsersyncIfAmbiguous {
+			gdprSignal = SignalNo
+		} else {
+			gdprSignal = SignalYes
+		}
 	}
 
-	if consent == "" {
-		return p.cfg.UsersyncIfAmbiguous, p.cfg.UsersyncIfAmbiguous, p.cfg.UsersyncIfAmbiguous, nil
+	if gdprSignal == SignalNo {
+		return true, true, true, nil
+	}
+
+	if consent == "" && gdprSignal == SignalYes {
+		return false, false, false, nil
+	}
+
+	if id, ok := p.vendorIDs[bidder]; ok {
+		return p.allowPI(ctx, id, consent)
 	}
 
+	return p.defaultVendorPermissions()
+}
+
+func (p *permissionsImpl) defaultVendorPermissions() (allowPI bool, allowGeo bool, allowID bool, err error) {
 	return false, false, false, nil
 }
 
@@ -95,11 +117,6 @@ func (p *permissionsImpl) allowSync(ctx context.Context, vendorID uint16, consen
 }
 
 func (p *permissionsImpl) allowPI(ctx context.Context, vendorID uint16, consent string) (bool, bool, bool, error) {
-	// If we're not given a consent string, respect the preferences in the app config.
-	if consent == "" {
-		return p.cfg.UsersyncIfAmbiguous, p.cfg.UsersyncIfAmbiguous, p.cfg.UsersyncIfAmbiguous, nil
-	}
-
 	parsedConsent, vendor, err := p.parseVendor(ctx, vendorID, consent)
 	if err != nil {
 		return false, false, false, err
@@ -217,7 +234,7 @@ func (a AlwaysAllow) BidderSyncAllowed(ctx context.Context, bidder openrtb_ext.B
 	return true, nil
 }
 
-func (a AlwaysAllow) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, bool, error) {
+func (a AlwaysAllow) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, gdprSignal Signal, consent string) (bool, bool, bool, error) {
 	return true, true, true, nil
 }
 
@@ -232,6 +249,6 @@ func (a AlwaysFail) BidderSyncAllowed(ctx context.Context, bidder openrtb_ext.Bi
 	return false, nil
 }
 
-func (a AlwaysFail) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, bool, error) {
+func (a AlwaysFail) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, gdprSignal Signal, consent string) (bool, bool, bool, error) {
 	return false, false, false, nil
 }


Test patch:
diff --git a/adapters/appnexus/appnexustest/exemplary/simple-banner-foreign-currency.json b/adapters/appnexus/appnexustest/exemplary/simple-banner-foreign-currency.json
new file mode 100644
index 00000000..b46c6f5f
--- /dev/null
+++ b/adapters/appnexus/appnexustest/exemplary/simple-banner-foreign-currency.json
@@ -0,0 +1,141 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            },
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "placement_id": 1
+          }
+        }
+      }
+    ]
+  },
+
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://ib.adnxs.com/openrtb2",
+        "body": {
+          "id": "test-request-id",
+          "ext": {
+            "appnexus": {
+              "hb_source": 5
+            },
+            "prebid": {}
+          },
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  },
+                  {
+                    "w": 300,
+                    "h": 600
+                  }
+                ],
+                "w": 300,
+                "h": 250
+              },
+              "ext": {
+                "appnexus": {
+                  "placement_id": 1
+                }
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "seat": "958",
+              "bid": [{
+                "id": "7706636740145184841",
+                "impid": "test-imp-id",
+                "price": 0.500000,
+                "adid": "29681110",
+                "adm": "some-test-ad",
+                "adomain": ["appnexus.com"],
+                "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110",
+                "cid": "958",
+                "crid": "29681110",
+                "h": 250,
+                "w": 300,
+                "ext": {
+                  "appnexus": {
+                    "brand_id": 1,
+                    "brand_category_id": 1,
+                    "auction_id": 8189378542222915032,
+                    "bid_ad_type": 0,
+                    "bidder_id": 2,
+                    "ranking_price": 0.000000,
+                    "deal_priority": 5
+                  }
+                }
+              }]
+            }
+          ],
+          "bidid": "5778926625248726496",
+          "cur": "MXN"
+        }
+      }
+    }
+  ],
+
+  "expectedBidResponses": [
+    {
+      "currency": "MXN",
+      "bids": [
+        {
+          "bid": {
+            "id": "7706636740145184841",
+            "impid": "test-imp-id",
+            "price": 0.5,
+            "adm": "some-test-ad",
+            "adid": "29681110",
+            "adomain": ["appnexus.com"],
+            "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110",
+            "cid": "958",
+            "crid": "29681110",
+            "w": 300,
+            "h": 250,
+            "cat": ["IAB20-3"],
+            "ext": {
+              "appnexus": {
+                "brand_id": 1,
+                "brand_category_id": 1,
+                "auction_id": 8189378542222915032,
+                "bid_ad_type": 0,
+                "bidder_id": 2,
+                "ranking_price": 0.000000,
+                "deal_priority": 5
+              }
+            }
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
diff --git a/endpoints/auction_test.go b/endpoints/auction_test.go
index b5f0989e..6a237e39 100644
--- a/endpoints/auction_test.go
+++ b/endpoints/auction_test.go
@@ -420,7 +420,7 @@ func (m *auctionMockPermissions) BidderSyncAllowed(ctx context.Context, bidder o
 	return m.allowBidderSync, nil
 }
 
-func (m *auctionMockPermissions) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, bool, error) {
+func (m *auctionMockPermissions) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, gdprSignal gdpr.Signal, consent string) (bool, bool, bool, error) {
 	return m.allowPI, m.allowGeo, m.allowID, nil
 }
 
diff --git a/endpoints/cookie_sync_test.go b/endpoints/cookie_sync_test.go
index ee29125b..c790fcd9 100644
--- a/endpoints/cookie_sync_test.go
+++ b/endpoints/cookie_sync_test.go
@@ -254,6 +254,6 @@ func (g *gdprPerms) BidderSyncAllowed(ctx context.Context, bidder openrtb_ext.Bi
 	return ok, nil
 }
 
-func (g *gdprPerms) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, bool, error) {
+func (g *gdprPerms) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, gdprSignal gdpr.Signal, consent string) (bool, bool, bool, error) {
 	return true, true, true, nil
 }
diff --git a/endpoints/setuid_test.go b/endpoints/setuid_test.go
index 1f2c4040..60285958 100644
--- a/endpoints/setuid_test.go
+++ b/endpoints/setuid_test.go
@@ -11,6 +11,7 @@ import (
 	"time"
 
 	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/gdpr"
 	"github.com/prebid/prebid-server/metrics"
 	"github.com/prebid/prebid-server/privacy"
 	"github.com/prebid/prebid-server/usersync"
@@ -437,7 +438,7 @@ func (g *mockPermsSetUID) BidderSyncAllowed(ctx context.Context, bidder openrtb_
 	return false, nil
 }
 
-func (g *mockPermsSetUID) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, bool, error) {
+func (g *mockPermsSetUID) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, gdprSignal gdpr.Signal, consent string) (bool, bool, bool, error) {
 	return g.allowPI, g.allowPI, g.allowPI, nil
 }
 
diff --git a/exchange/gdpr_test.go b/exchange/gdpr_test.go
index 2d8fff86..f7906104 100644
--- a/exchange/gdpr_test.go
+++ b/exchange/gdpr_test.go
@@ -5,40 +5,97 @@ import (
 	"testing"
 
 	"github.com/mxmCherry/openrtb"
+	"github.com/prebid/prebid-server/gdpr"
 	"github.com/stretchr/testify/assert"
 )
 
-func TestExtractGDPRFound(t *testing.T) {
-	gdprTest := openrtb.BidRequest{
-		User: &openrtb.User{
-			Ext: json.RawMessage(`{"consent": "BOS2bx5OS2bx5ABABBAAABoAAAAAFA"}`),
+func TestExtractGDPR(t *testing.T) {
+	tests := []struct {
+		description string
+		giveRegs    *openrtb.Regs
+		wantGDPR    gdpr.Signal
+	}{
+		{
+			description: "Regs Ext GDPR = 0",
+			giveRegs:    &openrtb.Regs{Ext: json.RawMessage(`{"gdpr": 0}`)},
+			wantGDPR:    gdpr.SignalNo,
 		},
-		Regs: &openrtb.Regs{
-			Ext: json.RawMessage(`{"gdpr": 1}`),
+		{
+			description: "Regs Ext GDPR = 1",
+			giveRegs:    &openrtb.Regs{Ext: json.RawMessage(`{"gdpr": 1}`)},
+			wantGDPR:    gdpr.SignalYes,
+		},
+		{
+			description: "Regs Ext GDPR = null",
+			giveRegs:    &openrtb.Regs{Ext: json.RawMessage(`{"gdpr": null}`)},
+			wantGDPR:    gdpr.SignalAmbiguous,
+		},
+		{
+			description: "Regs is nil",
+			giveRegs:    nil,
+			wantGDPR:    gdpr.SignalAmbiguous,
+		},
+		{
+			description: "Regs Ext is nil",
+			giveRegs:    &openrtb.Regs{Ext: nil},
+			wantGDPR:    gdpr.SignalAmbiguous,
+		},
+		{
+			description: "JSON unmarshal error",
+			giveRegs:    &openrtb.Regs{Ext: json.RawMessage(`{"`)},
+			wantGDPR:    gdpr.SignalAmbiguous,
 		},
 	}
-	gdpr := extractGDPR(&gdprTest, false)
-	consent := extractConsent(&gdprTest)
-	assert.Equal(t, 1, gdpr)
-	assert.Equal(t, "BOS2bx5OS2bx5ABABBAAABoAAAAAFA", consent)
-
-	gdprTest.Regs.Ext = json.RawMessage(`{"gdpr": 0}`)
-	gdpr = extractGDPR(&gdprTest, true)
-	consent = extractConsent(&gdprTest)
-	assert.Equal(t, 0, gdpr)
-	assert.Equal(t, "BOS2bx5OS2bx5ABABBAAABoAAAAAFA", consent)
-}
 
-func TestGDPRUnknown(t *testing.T) {
-	gdprTest := openrtb.BidRequest{}
+	for _, tt := range tests {
+		bidReq := openrtb.BidRequest{
+			Regs: tt.giveRegs,
+		}
 
-	gdpr := extractGDPR(&gdprTest, false)
-	consent := extractConsent(&gdprTest)
-	assert.Equal(t, 1, gdpr)
-	assert.Equal(t, "", consent)
+		result := extractGDPR(&bidReq)
+		assert.Equal(t, tt.wantGDPR, result, tt.description)
+	}
+}
 
-	gdpr = extractGDPR(&gdprTest, true)
-	consent = extractConsent(&gdprTest)
-	assert.Equal(t, 0, gdpr)
+func TestExtractConsent(t *testing.T) {
+	tests := []struct {
+		description string
+		giveUser    *openrtb.User
+		wantConsent string
+	}{
+		{
+			description: "User Ext Consent is not empty",
+			giveUser:    &openrtb.User{Ext: json.RawMessage(`{"consent": "BOS2bx5OS2bx5ABABBAAABoAAAAAFA"}`)},
+			wantConsent: "BOS2bx5OS2bx5ABABBAAABoAAAAAFA",
+		},
+		{
+			description: "User Ext Consent is empty",
+			giveUser:    &openrtb.User{Ext: json.RawMessage(`{"consent": ""}`)},
+			wantConsent: "",
+		},
+		{
+			description: "User Ext is nil",
+			giveUser:    &openrtb.User{Ext: nil},
+			wantConsent: "",
+		},
+		{
+			description: "User is nil",
+			giveUser:    nil,
+			wantConsent: "",
+		},
+		{
+			description: "JSON unmarshal error",
+			giveUser:    &openrtb.User{Ext: json.RawMessage(`{`)},
+			wantConsent: "",
+		},
+	}
+
+	for _, tt := range tests {
+		bidReq := openrtb.BidRequest{
+			User: tt.giveUser,
+		}
 
+		result := extractConsent(&bidReq)
+		assert.Equal(t, tt.wantConsent, result, tt.description)
+	}
 }
diff --git a/exchange/utils_test.go b/exchange/utils_test.go
index 757eb658..f9a04f25 100644
--- a/exchange/utils_test.go
+++ b/exchange/utils_test.go
@@ -10,6 +10,7 @@ import (
 	"github.com/mxmCherry/openrtb"
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/errortypes"
+	"github.com/prebid/prebid-server/gdpr"
 	"github.com/prebid/prebid-server/metrics"
 	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/stretchr/testify/assert"
@@ -30,7 +31,7 @@ func (p *permissionsMock) BidderSyncAllowed(ctx context.Context, bidder openrtb_
 	return true, nil
 }
 
-func (p *permissionsMock) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, bool, error) {
+func (p *permissionsMock) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, gdpr gdpr.Signal, consent string) (bool, bool, bool, error) {
 	return p.personalInfoAllowed, p.personalInfoAllowed, p.personalInfoAllowed, nil
 }
 
@@ -1053,6 +1054,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
 		gdpr                string
 		gdprConsent         string
 		gdprScrub           bool
+		userSyncIfAmbiguous bool
 		expectPrivacyLabels metrics.PrivacyLabels
 	}{
 		{
@@ -1151,6 +1153,32 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
 				GDPRTCFVersion: "",
 			},
 		},
+		{
+			description:         "Enforce - Ambiguous signal, don't sync user if ambiguous",
+			gdprAccountEnabled:  nil,
+			gdprHostEnabled:     true,
+			gdpr:                "null",
+			gdprConsent:         "BONV8oqONXwgmADACHENAO7pqzAAppY",
+			gdprScrub:           true,
+			userSyncIfAmbiguous: false,
+			expectPrivacyLabels: metrics.PrivacyLabels{
+				GDPREnforced:   true,
+				GDPRTCFVersion: metrics.TCFVersionV1,
+			},
+		},
+		{
+			description:         "Not Enforce - Ambiguous signal, sync user if ambiguous",
+			gdprAccountEnabled:  nil,
+			gdprHostEnabled:     true,
+			gdpr:                "null",
+			gdprConsent:         "BONV8oqONXwgmADACHENAO7pqzAAppY",
+			gdprScrub:           false,
+			userSyncIfAmbiguous: true,
+			expectPrivacyLabels: metrics.PrivacyLabels{
+				GDPREnforced:   false,
+				GDPRTCFVersion: "",
+			},
+		},
 	}
 
 	for _, test := range testCases {
@@ -1162,7 +1190,8 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
 
 		privacyConfig := config.Privacy{
 			GDPR: config.GDPR{
-				Enabled: test.gdprHostEnabled,
+				Enabled:             test.gdprHostEnabled,
+				UsersyncIfAmbiguous: test.userSyncIfAmbiguous,
 				TCF2: config.TCF2{
 					Enabled: true,
 				},
@@ -1186,7 +1215,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
 			auctionReq,
 			nil,
 			&permissionsMock{personalInfoAllowed: !test.gdprScrub},
-			true,
+			test.userSyncIfAmbiguous,
 			privacyConfig)
 		result := results[0]
 
diff --git a/gdpr/impl_test.go b/gdpr/impl_test.go
index 793eb967..a7d4b26a 100644
--- a/gdpr/impl_test.go
+++ b/gdpr/impl_test.go
@@ -177,20 +177,107 @@ func TestMalformedConsent(t *testing.T) {
 }
 
 func TestAllowPersonalInfo(t *testing.T) {
+	bidderAllowedByConsent := openrtb_ext.BidderAppnexus
+	bidderBlockedByConsent := openrtb_ext.BidderRubicon
+	consent := "BOS2bx5OS2bx5ABABBAAABoAAAABBwAA"
+
+	tests := []struct {
+		description         string
+		bidderName          openrtb_ext.BidderName
+		publisherID         string
+		userSyncIfAmbiguous bool
+		gdpr                Signal
+		consent             string
+		allowPI             bool
+	}{
+		{
+			description:         "Allow PI - Non standard publisher",
+			bidderName:          bidderBlockedByConsent,
+			publisherID:         "appNexusAppID",
+			userSyncIfAmbiguous: false,
+			gdpr:                SignalYes,
+			consent:             consent,
+			allowPI:             true,
+		},
+		{
+			description:         "Allow PI - known vendor with No GDPR",
+			bidderName:          bidderBlockedByConsent,
+			userSyncIfAmbiguous: false,
+			gdpr:                SignalNo,
+			consent:             consent,
+			allowPI:             true,
+		},
+		{
+			description:         "Allow PI - known vendor with Yes GDPR",
+			bidderName:          bidderAllowedByConsent,
+			userSyncIfAmbiguous: false,
+			gdpr:                SignalYes,
+			consent:             consent,
+			allowPI:             true,
+		},
+		{
+			description:         "PI allowed according to host setting UserSyncIfAmbiguous true - known vendor with ambiguous GDPR and empty consent",
+			bidderName:          bidderAllowedByConsent,
+			userSyncIfAmbiguous: true,
+			gdpr:                SignalAmbiguous,
+			consent:             "",
+			allowPI:             true,
+		},
+		{
+			description:         "PI allowed according to host setting UserSyncIfAmbiguous true - known vendor with ambiguous GDPR and non-empty consent",
+			bidderName:          bidderAllowedByConsent,
+			userSyncIfAmbiguous: true,
+			gdpr:                SignalAmbiguous,
+			consent:             consent,
+			allowPI:             true,
+		},
+		{
+			description:         "PI allowed according to host setting UserSyncIfAmbiguous false - known vendor with ambiguous GDPR and empty consent",
+			bidderName:          bidderAllowedByConsent,
+			userSyncIfAmbiguous: false,
+			gdpr:                SignalAmbiguous,
+			consent:             "",
+			allowPI:             false,
+		},
+		{
+			description:         "PI allowed according to host setting UserSyncIfAmbiguous false - known vendor with ambiguous GDPR and non-empty consent",
+			bidderName:          bidderAllowedByConsent,
+			userSyncIfAmbiguous: false,
+			gdpr:                SignalAmbiguous,
+			consent:             consent,
+			allowPI:             true,
+		},
+		{
+			description:         "Don't allow PI - known vendor with Yes GDPR and empty consent",
+			bidderName:          bidderAllowedByConsent,
+			userSyncIfAmbiguous: false,
+			gdpr:                SignalYes,
+			consent:             "",
+			allowPI:             false,
+		},
+		{
+			description:         "Don't allow PI - default vendor with Yes GDPR and non-empty consent",
+			bidderName:          bidderBlockedByConsent,
+			userSyncIfAmbiguous: false,
+			gdpr:                SignalYes,
+			consent:             consent,
+			allowPI:             false,
+		},
+	}
+
 	vendorListData := tcf1MarshalVendorList(tcf1VendorList{
 		VendorListVersion: 1,
 		Vendors: []tcf1Vendor{
-			{ID: 2, Purposes: []int{1}},    // cookie reads/writes
-			{ID: 3, Purposes: []int{1, 3}}, // ad personalization
+			{ID: 2, Purposes: []int{1, 3}},
 		},
 	})
 	perms := permissionsImpl{
 		cfg: config.GDPR{
-			HostVendorID: 2,
+			HostVendorID:            2,
+			NonStandardPublisherMap: map[string]struct{}{"appNexusAppID": {}},
 		},
 		vendorIDs: map[openrtb_ext.BidderName]uint16{
 			openrtb_ext.BidderAppnexus: 2,
-			openrtb_ext.BidderPubmatic: 3,
 		},
 		fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){
 			tcf1SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{
@@ -202,20 +289,14 @@ func TestAllowPersonalInfo(t *testing.T) {
 		},
 	}
 
-	// PI needs both purposes to succeed
-	allowPI, _, _, err := perms.PersonalInfoAllowed(context.Background(), openrtb_ext.BidderAppnexus, "", "BOS2bx5OS2bx5ABABBAAABoAAAABBwAA")
-	assertNilErr(t, err)
-	assertBoolsEqual(t, false, allowPI)
+	for _, tt := range tests {
+		perms.cfg.UsersyncIfAmbiguous = tt.userSyncIfAmbiguous
 
-	allowPI, _, _, err = perms.PersonalInfoAllowed(context.Background(), openrtb_ext.BidderPubmatic, "", "BOS2bx5OS2bx5ABABBAAABoAAAABBwAA")
-	assertNilErr(t, err)
-	assertBoolsEqual(t, true, allowPI)
+		allowPI, _, _, err := perms.PersonalInfoAllowed(context.Background(), tt.bidderName, tt.publisherID, tt.gdpr, tt.consent)
 
-	// Assert that an item that otherwise would not be allowed PI access, gets approved because it is found in the GDPR.NonStandardPublishers array
-	perms.cfg.NonStandardPublisherMap = map[string]struct{}{"appNexusAppID": {}}
-	allowPI, _, _, err = perms.PersonalInfoAllowed(context.Background(), openrtb_ext.BidderAppnexus, "appNexusAppID", "BOS2bx5OS2bx5ABABBAAABoAAAABBwAA")
-	assertNilErr(t, err)
-	assertBoolsEqual(t, true, allowPI)
+		assert.Nil(t, err, tt.description)
+		assert.Equal(t, tt.allowPI, allowPI, tt.description)
+	}
 }
 
 func buildTCF2VendorList34() tcf2VendorList {
@@ -318,7 +399,7 @@ func TestAllowPersonalInfoTCF2(t *testing.T) {
 	}
 
 	for _, td := range testDefs {
-		allowPI, allowGeo, allowID, err := perms.PersonalInfoAllowed(context.Background(), td.bidder, "", td.consent)
+		allowPI, allowGeo, allowID, err := perms.PersonalInfoAllowed(context.Background(), td.bidder, "", SignalYes, td.consent)
 		assert.NoErrorf(t, err, "Error processing PersonalInfoAllowed for %s", td.description)
 		assert.EqualValuesf(t, td.allowPI, allowPI, "AllowPI failure on %s", td.description)
 		assert.EqualValuesf(t, td.allowGeo, allowGeo, "AllowGeo failure on %s", td.description)
@@ -344,7 +425,7 @@ func TestAllowPersonalInfoWhitelistTCF2(t *testing.T) {
 	}
 	// Assert that an item that otherwise would not be allowed PI access, gets approved because it is found in the GDPR.NonStandardPublishers array
 	perms.cfg.NonStandardPublisherMap = map[string]struct{}{"appNexusAppID": {}}
-	allowPI, allowGeo, allowID, err := perms.PersonalInfoAllowed(context.Background(), openrtb_ext.BidderAppnexus, "appNexusAppID", "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA")
+	allowPI, allowGeo, allowID, err := perms.PersonalInfoAllowed(context.Background(), openrtb_ext.BidderAppnexus, "appNexusAppID", SignalYes, "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA")
 	assert.NoErrorf(t, err, "Error processing PersonalInfoAllowed")
 	assert.EqualValuesf(t, true, allowPI, "AllowPI failure")
 	assert.EqualValuesf(t, true, allowGeo, "AllowGeo failure")
@@ -398,7 +479,7 @@ func TestAllowPersonalInfoTCF2PubRestrict(t *testing.T) {
 	}
 
 	for _, td := range testDefs {
-		allowPI, allowGeo, allowID, err := perms.PersonalInfoAllowed(context.Background(), td.bidder, "", td.consent)
+		allowPI, allowGeo, allowID, err := perms.PersonalInfoAllowed(context.Background(), td.bidder, "", SignalYes, td.consent)
 		assert.NoErrorf(t, err, "Error processing PersonalInfoAllowed for %s", td.description)
 		assert.EqualValuesf(t, td.allowPI, allowPI, "AllowPI failure on %s", td.description)
 		assert.EqualValuesf(t, td.allowGeo, allowGeo, "AllowGeo failure on %s", td.description)
@@ -454,7 +535,7 @@ func TestAllowPersonalInfoTCF2PurposeOneTrue(t *testing.T) {
 	}
 
 	for _, td := range testDefs {
-		allowPI, allowGeo, allowID, err := perms.PersonalInfoAllowed(context.Background(), td.bidder, "", td.consent)
+		allowPI, allowGeo, allowID, err := perms.PersonalInfoAllowed(context.Background(), td.bidder, "", SignalYes, td.consent)
 		assert.NoErrorf(t, err, "Error processing PersonalInfoAllowed for %s", td.description)
 		assert.EqualValuesf(t, td.allowPI, allowPI, "AllowPI failure on %s", td.description)
 		assert.EqualValuesf(t, td.allowGeo, allowGeo, "AllowGeo failure on %s", td.description)
@@ -511,7 +592,7 @@ func TestAllowPersonalInfoTCF2PurposeOneFalse(t *testing.T) {
 	}
 
 	for _, td := range testDefs {
-		allowPI, allowGeo, allowID, err := perms.PersonalInfoAllowed(context.Background(), td.bidder, "", td.consent)
+		allowPI, allowGeo, allowID, err := perms.PersonalInfoAllowed(context.Background(), td.bidder, "", SignalYes, td.consent)
 		assert.NoErrorf(t, err, "Error processing PersonalInfoAllowed for %s", td.description)
 		assert.EqualValuesf(t, td.allowPI, allowPI, "AllowPI failure on %s", td.description)
 		assert.EqualValuesf(t, td.allowGeo, allowGeo, "AllowGeo failure on %s", td.description)