Repository: prebid/prebid-server
Pull request #2269 (2022-06-08 00:24:34)

Fix for missed user.buyerUID


Patch:
diff --git a/adapters/unicorn/unicorn.go b/adapters/unicorn/unicorn.go
index 8d1413f4..75de6e53 100644
--- a/adapters/unicorn/unicorn.go
+++ b/adapters/unicorn/unicorn.go
@@ -64,6 +64,14 @@ func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapte
 		}
 	}
 
+	if len(request.Cur) == 0 {
+		request.Cur = []string{"JPY"}
+	} else if isOtherThanJPY(request.Cur) {
+		return nil, []error{&errortypes.BadInput{
+			Message: "Only JPY is supported",
+		}}
+	}
+
 	err := modifyImps(request)
 	if err != nil {
 		return nil, []error{err}
@@ -98,6 +106,15 @@ func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapte
 	return []*adapters.RequestData{requestData}, nil
 }
 
+func isOtherThanJPY(cur []string) bool {
+	for _, c := range cur {
+		if c != "JPY" {
+			return true
+		}
+	}
+	return false
+}
+
 func getHeaders(request *openrtb2.BidRequest) http.Header {
 	headers := http.Header{}
 	headers.Add("Content-Type", "application/json;charset=utf-8")
diff --git a/endpoints/setuid.go b/endpoints/setuid.go
index 014ff312..2dc76e57 100644
--- a/endpoints/setuid.go
+++ b/endpoints/setuid.go
@@ -10,10 +10,12 @@ import (
 	"time"
 
 	"github.com/julienschmidt/httprouter"
+	accountService "github.com/prebid/prebid-server/account"
 	"github.com/prebid/prebid-server/analytics"
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/gdpr"
 	"github.com/prebid/prebid-server/metrics"
+	"github.com/prebid/prebid-server/stored_requests"
 	"github.com/prebid/prebid-server/usersync"
 	"github.com/prebid/prebid-server/util/httputil"
 )
@@ -26,8 +28,8 @@ const (
 	chromeiOSStrLen = len(chromeiOSStr)
 )
 
-func NewSetUIDEndpoint(cfg config.HostCookie, syncersByBidder map[string]usersync.Syncer, perms gdpr.Permissions, pbsanalytics analytics.PBSAnalyticsModule, metricsEngine metrics.MetricsEngine) httprouter.Handle {
-	cookieTTL := time.Duration(cfg.TTL) * 24 * time.Hour
+func NewSetUIDEndpoint(cfg *config.Configuration, syncersByBidder map[string]usersync.Syncer, perms gdpr.Permissions, pbsanalytics analytics.PBSAnalyticsModule, accountsFetcher stored_requests.AccountFetcher, metricsEngine metrics.MetricsEngine) httprouter.Handle {
+	cookieTTL := time.Duration(cfg.HostCookie.TTL) * 24 * time.Hour
 
 	// convert map of syncers by bidder to map of syncers by key
 	// - its safe to assume that if multiple bidders map to the same key, the syncers are interchangeable.
@@ -44,7 +46,7 @@ func NewSetUIDEndpoint(cfg config.HostCookie, syncersByBidder map[string]usersyn
 
 		defer pbsanalytics.LogSetUIDObject(&so)
 
-		pc := usersync.ParseCookieFromRequest(r, &cfg)
+		pc := usersync.ParseCookieFromRequest(r, &cfg.HostCookie)
 		if !pc.AllowSyncs() {
 			w.WriteHeader(http.StatusUnauthorized)
 			metricsEngine.RecordSetUid(metrics.SetUidOptOut)
@@ -73,6 +75,20 @@ func NewSetUIDEndpoint(cfg config.HostCookie, syncersByBidder map[string]usersyn
 			return
 		}
 
+		accountID := query.Get("account")
+		if accountID == "" {
+			accountID = metrics.PublisherUnknown
+		}
+		// the fetched account is not currently used. It'll be used to get account-specific GDPR config in a future PR.
+		_, fetchErrs := accountService.GetAccount(context.Background(), cfg, accountsFetcher, accountID)
+		if len(fetchErrs) > 0 {
+			w.WriteHeader(http.StatusBadRequest)
+			w.Write([]byte(combineErrors(fetchErrs).Error()))
+			metricsEngine.RecordSetUid(metrics.SetUidBadRequest)
+			so.Status = http.StatusBadRequest
+			return
+		}
+
 		if shouldReturn, status, body := preventSyncsGDPR(query.Get("gdpr"), query.Get("gdpr_consent"), perms); shouldReturn {
 			w.WriteHeader(status)
 			w.Write([]byte(body))
@@ -101,7 +117,7 @@ func NewSetUIDEndpoint(cfg config.HostCookie, syncersByBidder map[string]usersyn
 		}
 
 		setSiteCookie := siteCookieCheck(r.UserAgent())
-		pc.SetCookieOnResponse(w, setSiteCookie, &cfg, cookieTTL)
+		pc.SetCookieOnResponse(w, setSiteCookie, &cfg.HostCookie, cookieTTL)
 
 		switch responseFormat {
 		case "i":
diff --git a/exchange/utils.go b/exchange/utils.go
index 103a7f7e..083a9aa5 100644
--- a/exchange/utils.go
+++ b/exchange/utils.go
@@ -155,7 +155,6 @@ func cleanOpenRTBRequests(ctx context.Context,
 
 		if bidRequestAllowed {
 			privacyEnforcement.Apply(bidderRequest.BidRequest)
-
 			allowedBidderRequests = append(allowedBidderRequests, bidderRequest)
 		}
 	}
@@ -737,6 +736,11 @@ func applyFPD(fpd *firstpartydata.ResolvedFirstPartyData, bidReq *openrtb2.BidRe
 		bidReq.App = fpd.App
 	}
 	if fpd.User != nil {
+		//BuyerUID is a value obtained between fpd extraction and fpd application.
+		//BuyerUID needs to be set back to fpd before applying this fpd to final bidder request
+		if bidReq.User != nil && len(bidReq.User.BuyerUID) > 0 {
+			fpd.User.BuyerUID = bidReq.User.BuyerUID
+		}
 		bidReq.User = fpd.User
 	}
 }
diff --git a/router/router.go b/router/router.go
index 509f81ba..65fa8ef9 100644
--- a/router/router.go
+++ b/router/router.go
@@ -262,7 +262,7 @@ func New(cfg *config.Configuration, rateConvertor *currency.RateConverter) (r *R
 		RecaptchaSecret:  cfg.RecaptchaSecret,
 	}
 
-	r.GET("/setuid", endpoints.NewSetUIDEndpoint(cfg.HostCookie, syncersByBidder, gdprPerms, pbsAnalytics, r.MetricsEngine))
+	r.GET("/setuid", endpoints.NewSetUIDEndpoint(cfg, syncersByBidder, gdprPerms, pbsAnalytics, accounts, r.MetricsEngine))
 	r.GET("/getuids", endpoints.NewGetUIDsEndpoint(cfg.HostCookie))
 	r.POST("/optout", userSyncDeps.OptOut)
 	r.GET("/optout", userSyncDeps.OptOut)


Test patch:
diff --git a/adapters/unicorn/unicorntest/exemplary/banner-app-without-ext.json b/adapters/unicorn/unicorntest/exemplary/banner-app-without-ext.json
index 8b5423a5..e56e601e 100644
--- a/adapters/unicorn/unicorntest/exemplary/banner-app-without-ext.json
+++ b/adapters/unicorn/unicorntest/exemplary/banner-app-without-ext.json
@@ -85,6 +85,7 @@
             "ver": "1.9.0"
           },
           "at": 1,
+          "cur": ["JPY"],
           "device": {
             "connectiontype": 1,
             "ext": {
diff --git a/adapters/unicorn/unicorntest/exemplary/banner-app-without-placementid.json b/adapters/unicorn/unicorntest/exemplary/banner-app-without-placementid.json
index c05d3b6f..9940ae71 100644
--- a/adapters/unicorn/unicorntest/exemplary/banner-app-without-placementid.json
+++ b/adapters/unicorn/unicorntest/exemplary/banner-app-without-placementid.json
@@ -28,7 +28,7 @@
         }
       }
     ],
-    "cur": ["USD"],
+    "cur": ["JPY"],
     "app": {
       "publisher": {
         "id": "test"
@@ -96,7 +96,7 @@
             "ver": "1.9.0"
           },
           "at": 1,
-          "cur": ["USD"],
+          "cur": ["JPY"],
           "device": {
             "connectiontype": 1,
             "ext": {
diff --git a/adapters/unicorn/unicorntest/exemplary/not-have-cur.json b/adapters/unicorn/unicorntest/exemplary/not-have-cur.json
new file mode 100644
index 00000000..1f9298c4
--- /dev/null
+++ b/adapters/unicorn/unicorntest/exemplary/not-have-cur.json
@@ -0,0 +1,243 @@
+{
+  "mockBidRequest": {
+    "at": 1,
+    "imp": [
+      {
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ],
+          "api": [5]
+        },
+        "id": "29D2F33E-F865-40DA-9320-16EF77935254",
+        "secure": 1,
+        "ext": {
+          "prebid": {
+            "storedrequest": {
+              "id": "test_unicorn"
+            }
+          },
+          "bidder": {
+            "accountId": 199578,
+            "publisherId": 123456,
+            "mediaId": "test_media",
+            "placementId": "test_placement"
+          },
+          "context": {
+            "data": {
+              "firstPartyData1": ["firstPartyData1"],
+              "uuid": ["xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"]
+            }
+          }
+        }
+      }
+    ],
+    "app": {
+      "publisher": {
+        "id": "test"
+      },
+      "ext": {
+        "prebid": {
+          "source": "prebid-mobile",
+          "version": "1.9.0"
+        }
+      },
+      "bundle": "net.ada.test",
+      "ver": "1.9.0"
+    },
+    "id": "CFD24FB1-916F-467D-8825-34892B315DB7",
+    "source": {
+      "tid": "A4B3EA9F-FF57-4716-AC85-6CBF6A46CFBD"
+    },
+    "device": {
+      "make": "Apple",
+      "osv": "14.4",
+      "connectiontype": 1,
+      "os": "iOS",
+      "w": 320,
+      "model": "Simulator",
+      "ifa": "00000000-0000-0000-0000-000000000000",
+      "devtime": 1612413327,
+      "h": 568,
+      "pxratio": 2,
+      "ext": {
+        "atts": 0
+      }
+    },
+    "user": {
+      "gender": "O"
+    },
+    "ext": {
+      "prebid": {
+        "targeting": {},
+        "cache": {
+          "bids": {}
+        },
+        "storedrequest": {
+          "id": "test"
+        }
+      }
+    }
+  },
+
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://ds.uncn.jp",
+        "body": {
+          "app": {
+            "bundle": "net.ada.test",
+            "ext": {
+              "prebid": {
+                "source": "prebid-mobile",
+                "version": "1.9.0"
+              }
+            },
+            "publisher": {
+              "id": "test"
+            },
+            "ver": "1.9.0"
+          },
+          "at": 1,
+          "cur": ["JPY"],
+          "device": {
+            "connectiontype": 1,
+            "ext": {
+              "atts": 0
+            },
+            "h": 568,
+            "ifa": "00000000-0000-0000-0000-000000000000",
+            "make": "Apple",
+            "model": "Simulator",
+            "os": "iOS",
+            "osv": "14.4",
+            "pxratio": 2,
+            "w": 320
+          },
+          "ext": {
+            "prebid": {
+              "bidder": null,
+              "is_rewarded_inventory": 0,
+              "storedrequest": {
+                "id": "test"
+              }
+            },
+            "accountId": 199578
+          },
+          "id": "CFD24FB1-916F-467D-8825-34892B315DB7",
+          "imp": [
+            {
+              "banner": {
+                "api": [5],
+                "format": [
+                  {
+                    "h": 250,
+                    "w": 300
+                  }
+                ]
+              },
+              "ext": {
+                "bidder": {
+                  "accountId": 199578,
+                  "publisherId": 123456,
+                  "mediaId": "test_media",
+                  "placementId": "test_placement"
+                },
+                "context": {
+                  "data": {
+                    "firstPartyData1": ["firstPartyData1"],
+                    "uuid": ["xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"]
+                  }
+                }
+              },
+              "tagid": "test_placement",
+              "id": "29D2F33E-F865-40DA-9320-16EF77935254",
+              "secure": 1
+            }
+          ],
+          "source": {
+            "ext": {
+              "bidder": "unicorn",
+              "stype": "prebid_server_uncn"
+            },
+            "tid": "A4B3EA9F-FF57-4716-AC85-6CBF6A46CFBD"
+          },
+          "user": {
+            "gender": "O"
+          }
+        }
+      },
+      "mockResponse": {
+        "url": "https://ds.uncn.jp",
+        "status": 200,
+        "headers": {
+          "Content-Type": ["application/json"],
+          "Date": ["Thu, 04 Feb 2021 06:36:31 GMT"],
+          "Vary": ["Accept-Encoding"]
+        },
+        "body": {
+          "bidid": "f1248280-24ec-4fbc-bdf5-6a866e7dcea4",
+          "cur": "JPY",
+          "ext": {},
+          "id": "CFD24FB1-916F-467D-8825-34892B315DB7",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "adid": "uoNYbq1L_-123456",
+                  "adm": "awesome domain",
+                  "adomain": ["example.com"],
+                  "attr": [],
+                  "bundle": "example.com",
+                  "cid": "7314",
+                  "crid": "-123456",
+                  "ext": {},
+                  "h": 250,
+                  "id": "1",
+                  "impid": "29D2F33E-F865-40DA-9320-16EF77935254",
+                  "iurl": "https://example.com/banner.png",
+                  "price": 73.283,
+                  "w": 300
+                }
+              ],
+              "group": 0,
+              "seat": "274"
+            }
+          ],
+          "units": 0
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "Currency": "JPY",
+      "bids": [
+        {
+          "Bid": {
+            "id": "1",
+            "impid": "29D2F33E-F865-40DA-9320-16EF77935254",
+            "price": 73.283,
+            "adm": "awesome domain",
+            "adid": "uoNYbq1L_-123456",
+            "adomain": ["example.com"],
+            "bundle": "example.com",
+            "iurl": "https://example.com/banner.png",
+            "cid": "7314",
+            "crid": "-123456",
+            "w": 300,
+            "h": 250,
+            "ext": {}
+          },
+          "type": "banner",
+          "BidType": "banner",
+          "BidVideo": null,
+          "DealPriority": 0
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/unicorn/unicorntest/supplemental/is-cur-usd-specified.json b/adapters/unicorn/unicorntest/supplemental/is-cur-usd-specified.json
new file mode 100644
index 00000000..3ee20f73
--- /dev/null
+++ b/adapters/unicorn/unicorntest/supplemental/is-cur-usd-specified.json
@@ -0,0 +1,90 @@
+{
+  "mockBidRequest": {
+    "at": 1,
+    "imp": [
+      {
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ],
+          "api": [5]
+        },
+        "id": "29D2F33E-F865-40DA-9320-16EF77935254",
+        "secure": 1,
+        "ext": {
+          "prebid": {
+            "storedrequest": {
+              "id": "test_unicorn"
+            }
+          },
+          "bidder": {
+            "accountId": 199578,
+            "publisherId": 123456,
+            "mediaId": "test_media",
+            "placementId": "test_placement"
+          },
+          "context": {
+            "data": {
+              "firstPartyData1": ["firstPartyData1"],
+              "uuid": ["xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"]
+            }
+          }
+        }
+      }
+    ],
+    "cur": ["USD"],
+    "app": {
+      "publisher": {
+        "id": "test"
+      },
+      "ext": {
+        "prebid": {
+          "source": "prebid-mobile",
+          "version": "1.9.0"
+        }
+      },
+      "bundle": "net.ada.test",
+      "ver": "1.9.0"
+    },
+    "id": "CFD24FB1-916F-467D-8825-34892B315DB7",
+    "source": {
+      "tid": "A4B3EA9F-FF57-4716-AC85-6CBF6A46CFBD"
+    },
+    "device": {
+      "make": "Apple",
+      "osv": "14.4",
+      "connectiontype": 1,
+      "os": "iOS",
+      "w": 320,
+      "model": "Simulator",
+      "ifa": "00000000-0000-0000-0000-000000000000",
+      "devtime": 1612413327,
+      "h": 568,
+      "pxratio": 2,
+      "ext": {
+        "atts": 0
+      }
+    },
+    "user": {
+      "gender": "O"
+    },
+    "ext": {
+      "prebid": {
+        "targeting": {},
+        "cache": {
+          "bids": {}
+        },
+        "storedrequest": {
+          "id": "test"
+        }
+      }
+    }
+  },
+  "expectedMakeRequestsErrors" : [{
+    "value": "Only JPY is supported",
+    "comparison": "literal"
+  }]
+}
diff --git a/endpoints/setuid_test.go b/endpoints/setuid_test.go
index b27fd20c..dd73d8a8 100644
--- a/endpoints/setuid_test.go
+++ b/endpoints/setuid_test.go
@@ -2,6 +2,7 @@ package endpoints
 
 import (
 	"context"
+	"encoding/json"
 	"errors"
 	"net/http"
 	"net/http/httptest"
@@ -232,6 +233,26 @@ func TestSetUIDEndpoint(t *testing.T) {
 			expectedBody:           `"f" query param is invalid. must be "b" or "i"`,
 			description:            "Set uid for valid bidder with invalid format",
 		},
+		{
+			uri:                    "/setuid?bidder=pubmatic&uid=123&account=valid_acct",
+			syncersBidderNameToKey: map[string]string{"pubmatic": "pubmatic"},
+			existingSyncs:          nil,
+			gdprAllowsHostCookies:  true,
+			expectedSyncs:          map[string]string{"pubmatic": "123"},
+			expectedStatusCode:     http.StatusOK,
+			expectedHeaders:        map[string]string{"Content-Type": "text/html", "Content-Length": "0"},
+			description:            "Set uid for valid bidder with valid account provided",
+		},
+		{
+			uri:                    "/setuid?bidder=pubmatic&uid=123&account=disabled_acct",
+			syncersBidderNameToKey: map[string]string{"pubmatic": "pubmatic"},
+			existingSyncs:          nil,
+			gdprAllowsHostCookies:  true,
+			expectedSyncs:          nil,
+			expectedStatusCode:     http.StatusBadRequest,
+			expectedBody:           "account is disabled, please reach out to the prebid server host",
+			description:            "Set uid for valid bidder with valid disabled account provided",
+		},
 	}
 
 	metrics := &metricsConf.NilMetricsEngine{}
@@ -518,6 +539,8 @@ func makeRequest(uri string, existingSyncs map[string]string) *http.Request {
 
 func doRequest(req *http.Request, metrics metrics.MetricsEngine, syncersBidderNameToKey map[string]string, gdprAllowsHostCookies, gdprReturnsError, gdprReturnsMalformedError bool) *httptest.ResponseRecorder {
 	cfg := config.Configuration{}
+	cfg.MarshalAccountDefaults()
+
 	perms := &mockPermsSetUID{
 		allowHost:           gdprAllowsHostCookies,
 		errorHost:           gdprReturnsError,
@@ -530,7 +553,12 @@ func doRequest(req *http.Request, metrics metrics.MetricsEngine, syncersBidderNa
 		syncersByBidder[bidderName] = fakeSyncer{key: syncerKey, defaultSyncType: usersync.SyncTypeIFrame}
 	}
 
-	endpoint := NewSetUIDEndpoint(cfg.HostCookie, syncersByBidder, perms, analytics, metrics)
+	fakeAccountsFetcher := FakeAccountsFetcher{AccountData: map[string]json.RawMessage{
+		"valid_acct":    json.RawMessage(`{"disabled":false}`),
+		"disabled_acct": json.RawMessage(`{"disabled":true}`),
+	}}
+
+	endpoint := NewSetUIDEndpoint(&cfg, syncersByBidder, perms, analytics, fakeAccountsFetcher, metrics)
 	response := httptest.NewRecorder()
 	endpoint(response, req, nil)
 	return response
diff --git a/exchange/utils_test.go b/exchange/utils_test.go
index 1d12f766..d97e6dc7 100644
--- a/exchange/utils_test.go
+++ b/exchange/utils_test.go
@@ -599,6 +599,7 @@ func TestCleanOpenRTBRequestsWithFPD(t *testing.T) {
 				assert.Equal(t, fpd[bidderName].Site.Name, bidderRequest.BidRequest.Site.Name, "Incorrect FPD site name")
 				assert.Equal(t, fpd[bidderName].App.Name, bidderRequest.BidRequest.App.Name, "Incorrect FPD app name")
 				assert.Equal(t, fpd[bidderName].User.Keywords, bidderRequest.BidRequest.User.Keywords, "Incorrect FPD user keywords")
+				assert.Equal(t, test.req.BidRequestWrapper.User.BuyerUID, bidderRequest.BidRequest.User.BuyerUID, "Incorrect FPD user buyerUID")
 			} else {
 				assert.Equal(t, "", bidderRequest.BidRequest.Site.Name, "Incorrect FPD site name")
 				assert.Equal(t, "", bidderRequest.BidRequest.User.Keywords, "Incorrect FPD user keywords")
@@ -2956,58 +2957,60 @@ func TestCleanOpenRTBRequestsSChainMultipleBidders(t *testing.T) {
 }
 
 func TestApplyFPD(t *testing.T) {
-	fpdBidderTest := map[openrtb_ext.BidderName]*firstpartydata.ResolvedFirstPartyData{}
-	bidderTest := openrtb_ext.BidderName("test")
-	fpdBidderTest[bidderTest] = &firstpartydata.ResolvedFirstPartyData{Site: nil, App: nil, User: nil}
-
-	fpdBidderNotNilFPD := map[openrtb_ext.BidderName]*firstpartydata.ResolvedFirstPartyData{}
-	bidderNotNilFPD := openrtb_ext.BidderName("notNilFPD")
-	fpdBidderNotNilFPD[bidderNotNilFPD] = &firstpartydata.ResolvedFirstPartyData{Site: &openrtb2.Site{ID: "SiteId"}, App: &openrtb2.App{ID: "AppId"}, User: &openrtb2.User{ID: "UserId"}}
-
-	fpdBidderApp := map[openrtb_ext.BidderName]*firstpartydata.ResolvedFirstPartyData{}
-	bidderApp := openrtb_ext.BidderName("AppFPD")
-	fpdBidderApp[bidderApp] = &firstpartydata.ResolvedFirstPartyData{App: &openrtb2.App{ID: "AppId"}}
 
 	testCases := []struct {
 		description     string
-		inputFpd        map[openrtb_ext.BidderName]*firstpartydata.ResolvedFirstPartyData
-		bidderName      openrtb_ext.BidderName
+		inputFpd        firstpartydata.ResolvedFirstPartyData
 		inputRequest    openrtb2.BidRequest
 		expectedRequest openrtb2.BidRequest
 	}{
 		{
 			description:     "req.Site defined; bidderFPD.Site not defined; expect request.Site remains the same",
-			inputFpd:        fpdBidderTest,
-			bidderName:      bidderTest,
+			inputFpd:        firstpartydata.ResolvedFirstPartyData{Site: nil, App: nil, User: nil},
 			inputRequest:    openrtb2.BidRequest{Site: &openrtb2.Site{ID: "SiteId"}},
 			expectedRequest: openrtb2.BidRequest{Site: &openrtb2.Site{ID: "SiteId"}},
 		},
 		{
 			description: "req.Site, req.App, req.User are not defined; bidderFPD.App, bidderFPD.Site and bidderFPD.User defined; " +
 				"expect req.Site, req.App, req.User to be overriden by bidderFPD.App, bidderFPD.Site and bidderFPD.User",
-			inputFpd:        fpdBidderNotNilFPD,
-			bidderName:      bidderNotNilFPD,
+			inputFpd:        firstpartydata.ResolvedFirstPartyData{Site: &openrtb2.Site{ID: "SiteId"}, App: &openrtb2.App{ID: "AppId"}, User: &openrtb2.User{ID: "UserId"}},
 			inputRequest:    openrtb2.BidRequest{},
 			expectedRequest: openrtb2.BidRequest{Site: &openrtb2.Site{ID: "SiteId"}, App: &openrtb2.App{ID: "AppId"}, User: &openrtb2.User{ID: "UserId"}},
 		},
 		{
 			description:     "req.Site, defined; bidderFPD.App defined; expect request.App to be overriden by bidderFPD.App; expect req.Site remains the same",
-			inputFpd:        fpdBidderApp,
-			bidderName:      bidderApp,
+			inputFpd:        firstpartydata.ResolvedFirstPartyData{App: &openrtb2.App{ID: "AppId"}},
 			inputRequest:    openrtb2.BidRequest{Site: &openrtb2.Site{ID: "SiteId"}},
 			expectedRequest: openrtb2.BidRequest{Site: &openrtb2.Site{ID: "SiteId"}, App: &openrtb2.App{ID: "AppId"}},
 		},
 		{
 			description:     "req.Site, req.App defined; bidderFPD.App defined; expect request.App to be overriden by bidderFPD.App",
-			inputFpd:        fpdBidderApp,
-			bidderName:      bidderApp,
+			inputFpd:        firstpartydata.ResolvedFirstPartyData{App: &openrtb2.App{ID: "AppId"}},
 			inputRequest:    openrtb2.BidRequest{Site: &openrtb2.Site{ID: "SiteId"}, App: &openrtb2.App{ID: "TestAppId"}},
 			expectedRequest: openrtb2.BidRequest{Site: &openrtb2.Site{ID: "SiteId"}, App: &openrtb2.App{ID: "AppId"}},
 		},
+		{
+			description:     "req.User is defined; bidderFPD.User defined; req.User has BuyerUID. Expect to see user.BuyerUID in result request",
+			inputFpd:        firstpartydata.ResolvedFirstPartyData{Site: &openrtb2.Site{ID: "SiteId"}, App: &openrtb2.App{ID: "AppId"}, User: &openrtb2.User{ID: "UserId"}},
+			inputRequest:    openrtb2.BidRequest{User: &openrtb2.User{ID: "UserIdIn", BuyerUID: "12345"}},
+			expectedRequest: openrtb2.BidRequest{User: &openrtb2.User{ID: "UserId", BuyerUID: "12345"}, Site: &openrtb2.Site{ID: "SiteId"}, App: &openrtb2.App{ID: "AppId"}},
+		},
+		{
+			description:     "req.User is defined; bidderFPD.User defined; req.User has BuyerUID with zero length. Expect to see empty user.BuyerUID in result request",
+			inputFpd:        firstpartydata.ResolvedFirstPartyData{Site: &openrtb2.Site{ID: "SiteId"}, App: &openrtb2.App{ID: "AppId"}, User: &openrtb2.User{ID: "UserId"}},
+			inputRequest:    openrtb2.BidRequest{User: &openrtb2.User{ID: "UserIdIn", BuyerUID: ""}},
+			expectedRequest: openrtb2.BidRequest{User: &openrtb2.User{ID: "UserId"}, Site: &openrtb2.Site{ID: "SiteId"}, App: &openrtb2.App{ID: "AppId"}},
+		},
+		{
+			description:     "req.User is not defined; bidderFPD.User defined and has BuyerUID. Expect to see user.BuyerUID in result request",
+			inputFpd:        firstpartydata.ResolvedFirstPartyData{Site: &openrtb2.Site{ID: "SiteId"}, App: &openrtb2.App{ID: "AppId"}, User: &openrtb2.User{ID: "UserId", BuyerUID: "FPDBuyerUID"}},
+			inputRequest:    openrtb2.BidRequest{},
+			expectedRequest: openrtb2.BidRequest{Site: &openrtb2.Site{ID: "SiteId"}, App: &openrtb2.App{ID: "AppId"}, User: &openrtb2.User{ID: "UserId", BuyerUID: "FPDBuyerUID"}},
+		},
 	}
 
 	for _, testCase := range testCases {
-		applyFPD(testCase.inputFpd[testCase.bidderName], &testCase.inputRequest)
+		applyFPD(&testCase.inputFpd, &testCase.inputRequest)
 		assert.Equal(t, testCase.expectedRequest, testCase.inputRequest, fmt.Sprintf("incorrect request after applying fpd, testcase %s", testCase.description))
 	}
 }