diff --git a/endpoints/cookie_sync_test.go b/endpoints/cookie_sync_test.go index 63820543180..78f7fae433c 100644 --- a/endpoints/cookie_sync_test.go +++ b/endpoints/cookie_sync_test.go @@ -20,9 +20,7 @@ import ( func TestCookieSyncNoCookies(t *testing.T) { rr := doPost(`{"bidders":["appnexus", "audienceNetwork", "random"]}`, nil) - if rr.Code != http.StatusOK { - t.Fatalf("Wrong status: %d (%s)", rr.Code, rr.Body) - } + assertIntsMatch(t, http.StatusOK, rr.Code) assertSyncsExist(t, rr.Body.Bytes(), "appnexus", "audienceNetwork") assertStatus(t, rr.Body.Bytes(), "no_cookie") } @@ -32,9 +30,7 @@ func TestCookieSyncHasCookies(t *testing.T) { "adnxs": "1234", "audienceNetwork": "2345", }) - if rr.Code != http.StatusOK { - t.Fatalf("Wrong status: %d", rr.Code) - } + assertIntsMatch(t, http.StatusOK, rr.Code) assertSyncsExist(t, rr.Body.Bytes()) assertStatus(t, rr.Body.Bytes(), "ok") } @@ -42,9 +38,7 @@ func TestCookieSyncHasCookies(t *testing.T) { // Make sure that an empty bidders array returns no syncs func TestCookieSyncEmptyBidders(t *testing.T) { rr := doPost(`{"bidders": []}`, nil) - if rr.Code != http.StatusOK { - t.Fatalf("Wrong status: %d (%s)", rr.Code, rr.Body) - } + assertIntsMatch(t, http.StatusOK, rr.Code) assertSyncsExist(t, rr.Body.Bytes()) assertStatus(t, rr.Body.Bytes(), "no_cookie") } @@ -52,9 +46,7 @@ func TestCookieSyncEmptyBidders(t *testing.T) { // Make sure that all syncs are returned if "bidders" isn't a key func TestCookieSyncNoBidders(t *testing.T) { rr := doPost("{}", nil) - if rr.Code != http.StatusOK { - t.Fatalf("Wrong status: %d (%s)", rr.Code, rr.Body) - } + assertIntsMatch(t, http.StatusOK, rr.Code) assertSyncsExist(t, rr.Body.Bytes(), "appnexus", "audienceNetwork", "lifestreet", "pubmatic") assertStatus(t, rr.Body.Bytes(), "no_cookie") } diff --git a/endpoints/setuid.go b/endpoints/setuid.go new file mode 100644 index 00000000000..dffc67d87ca --- /dev/null +++ b/endpoints/setuid.go @@ -0,0 +1,79 @@ +package endpoints + +import ( + "net/http" + "strings" + "time" + + "github.com/julienschmidt/httprouter" + "github.com/prebid/prebid-server/analytics" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/prebid/prebid-server/pbsmetrics" + "github.com/prebid/prebid-server/usersync" +) + +func NewSetUIDEndpoint(cfg config.HostCookie, pbsanalytics analytics.PBSAnalyticsModule, metrics pbsmetrics.MetricsEngine) httprouter.Handle { + cookieTTL := time.Duration(cfg.TTL) * 24 * time.Hour + return httprouter.Handle(func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + so := analytics.SetUIDObject{ + Status: http.StatusOK, + Errors: make([]error, 0), + } + + defer pbsanalytics.LogSetUIDObject(&so) + + pc := usersync.ParsePBSCookieFromRequest(r, &cfg.OptOutCookie) + if !pc.AllowSyncs() { + w.WriteHeader(http.StatusUnauthorized) + metrics.RecordUserIDSet(pbsmetrics.UserLabels{Action: pbsmetrics.RequestActionOptOut}) + so.Status = http.StatusUnauthorized + return + } + + query := getRawQueryMap(r.URL.RawQuery) + bidder := query["bidder"] + if bidder == "" { + w.WriteHeader(http.StatusBadRequest) + metrics.RecordUserIDSet(pbsmetrics.UserLabels{Action: pbsmetrics.RequestActionErr}) + so.Status = http.StatusBadRequest + return + } + so.Bidder = bidder + + uid := query["uid"] + so.UID = uid + + var err error = nil + if uid == "" { + pc.Unsync(bidder) + } else { + err = pc.TrySync(bidder, uid) + } + + if err == nil { + labels := pbsmetrics.UserLabels{ + Action: pbsmetrics.RequestActionSet, + Bidder: openrtb_ext.BidderName(bidder), + } + metrics.RecordUserIDSet(labels) + so.Success = true + } + + pc.SetCookieOnResponse(w, cfg.Domain, cookieTTL) + }) +} + +func getRawQueryMap(query string) map[string]string { + m := make(map[string]string) + for _, kv := range strings.SplitN(query, "&", -1) { + if len(kv) == 0 { + continue + } + pair := strings.SplitN(kv, "=", 2) + if len(pair) == 2 { + m[pair[0]] = pair[1] + } + } + return m +} diff --git a/endpoints/setuid_test.go b/endpoints/setuid_test.go new file mode 100644 index 00000000000..42ff08e4f77 --- /dev/null +++ b/endpoints/setuid_test.go @@ -0,0 +1,125 @@ +package endpoints + +import ( + "net/http" + "net/http/httptest" + "regexp" + "testing" + "time" + + "github.com/prebid/prebid-server/usersync" + + "github.com/prebid/prebid-server/openrtb_ext" + + "github.com/prebid/prebid-server/pbsmetrics" + + analyticsConf "github.com/prebid/prebid-server/analytics/config" + "github.com/prebid/prebid-server/config" +) + +func TestNormalSet(t *testing.T) { + response := doRequest(makeRequest("/setuid?bidder=pubmatic&uid=123", nil)) + assertIntsMatch(t, http.StatusOK, response.Code) + + cookie := parseCookieString(t, response) + assertIntsMatch(t, 1, cookie.LiveSyncCount()) + assertBoolsMatch(t, true, cookie.HasLiveSync("pubmatic")) + assertSyncValue(t, cookie, "pubmatic", "123") +} + +func TestUnset(t *testing.T) { + response := doRequest(makeRequest("/setuid?bidder=pubmatic", map[string]string{"pubmatic": "1234"})) + assertIntsMatch(t, http.StatusOK, response.Code) + + cookie := parseCookieString(t, response) + assertIntsMatch(t, 0, cookie.LiveSyncCount()) +} + +func TestMergeSet(t *testing.T) { + response := doRequest(makeRequest("/setuid?bidder=pubmatic&uid=123", map[string]string{"rubicon": "def"})) + assertIntsMatch(t, http.StatusOK, response.Code) + + cookie := parseCookieString(t, response) + assertIntsMatch(t, 2, cookie.LiveSyncCount()) + assertBoolsMatch(t, true, cookie.HasLiveSync("pubmatic")) + assertBoolsMatch(t, true, cookie.HasLiveSync("rubicon")) + assertSyncValue(t, cookie, "pubmatic", "123") + assertSyncValue(t, cookie, "rubicon", "def") +} + +func TestNoBidder(t *testing.T) { + response := doRequest(makeRequest("/setuid?uid=123", nil)) + assertIntsMatch(t, http.StatusBadRequest, response.Code) +} + +func TestOptedOut(t *testing.T) { + request := httptest.NewRequest("GET", "/setuid?bidder=pubmatic&uid=123", nil) + cookie := usersync.NewPBSCookie() + cookie.SetPreference(false) + addCookie(request, cookie) + response := doRequest(request) + + assertIntsMatch(t, http.StatusUnauthorized, response.Code) +} + +func makeRequest(uri string, existingSyncs map[string]string) *http.Request { + request := httptest.NewRequest("GET", uri, nil) + if len(existingSyncs) > 0 { + pbsCookie := usersync.NewPBSCookie() + for family, value := range existingSyncs { + pbsCookie.TrySync(family, value) + } + addCookie(request, pbsCookie) + } + return request +} + +func doRequest(req *http.Request) *httptest.ResponseRecorder { + cfg := config.Configuration{} + endpoint := NewSetUIDEndpoint(cfg.HostCookie, analyticsConf.NewPBSAnalytics(&cfg.Analytics), pbsmetrics.NewMetricsEngine(&cfg, openrtb_ext.BidderList())) + response := httptest.NewRecorder() + endpoint(response, req, nil) + return response +} + +func addCookie(req *http.Request, cookie *usersync.PBSCookie) { + req.AddCookie(cookie.ToHTTPCookie(time.Duration(1) * time.Hour)) +} + +func parseCookieString(t *testing.T, response *httptest.ResponseRecorder) *usersync.PBSCookie { + cookieString := response.Header().Get("Set-Cookie") + parser := regexp.MustCompile("uids=(.*?);") + res := parser.FindStringSubmatch(cookieString) + assertIntsMatch(t, 2, len(res)) + httpCookie := http.Cookie{ + Name: "uids", + Value: res[1], + } + return usersync.ParsePBSCookie(&httpCookie) +} + +func assertIntsMatch(t *testing.T, expected int, actual int) { + t.Helper() + if expected != actual { + t.Errorf("Expected %d, got %d", expected, actual) + } +} + +func assertBoolsMatch(t *testing.T, expected bool, actual bool) { + t.Helper() + if expected != actual { + t.Errorf("Expected %t, got %t", expected, actual) + } +} + +func assertStringsMatch(t *testing.T, expected string, actual string) { + t.Helper() + if expected != actual { + t.Errorf("Expected %s, got %s", expected, actual) + } +} + +func assertSyncValue(t *testing.T, cookie *usersync.PBSCookie, family string, expectedValue string) { + got, _, _ := cookie.GetUID(family) + assertStringsMatch(t, expectedValue, got) +} diff --git a/pbs/usersync.go b/pbs/usersync.go index 249651def72..25808639c05 100644 --- a/pbs/usersync.go +++ b/pbs/usersync.go @@ -13,7 +13,6 @@ import ( "github.com/julienschmidt/httprouter" "github.com/prebid/prebid-server/analytics" "github.com/prebid/prebid-server/config" - "github.com/prebid/prebid-server/openrtb_ext" "github.com/prebid/prebid-server/pbsmetrics" "github.com/prebid/prebid-server/ssl" "github.com/prebid/prebid-server/usersync" @@ -73,69 +72,6 @@ func (deps *UserSyncDeps) GetUIDs(w http.ResponseWriter, r *http.Request, _ http return } -func getRawQueryMap(query string) map[string]string { - m := make(map[string]string) - for _, kv := range strings.SplitN(query, "&", -1) { - if len(kv) == 0 { - continue - } - pair := strings.SplitN(kv, "=", 2) - if len(pair) == 2 { - m[pair[0]] = pair[1] - } - } - return m -} - -func (deps *UserSyncDeps) SetUID(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - - so := analytics.SetUIDObject{ - Status: http.StatusOK, - Errors: make([]error, 0), - } - - defer deps.PBSAnalytics.LogSetUIDObject(&so) - - pc := usersync.ParsePBSCookieFromRequest(r, &deps.HostCookieSettings.OptOutCookie) - if !pc.AllowSyncs() { - w.WriteHeader(http.StatusUnauthorized) - deps.MetricsEngine.RecordUserIDSet(pbsmetrics.UserLabels{Action: pbsmetrics.RequestActionOptOut}) - so.Status = http.StatusUnauthorized - return - } - - query := getRawQueryMap(r.URL.RawQuery) - bidder := query["bidder"] - if bidder == "" { - w.WriteHeader(http.StatusBadRequest) - deps.MetricsEngine.RecordUserIDSet(pbsmetrics.UserLabels{Action: pbsmetrics.RequestActionErr}) - so.Status = http.StatusBadRequest - return - } - so.Bidder = bidder - - uid := query["uid"] - so.UID = uid - - var err error = nil - if uid == "" { - pc.Unsync(bidder) - } else { - err = pc.TrySync(bidder, uid) - } - - if err == nil { - labels := pbsmetrics.UserLabels{ - Action: pbsmetrics.RequestActionSet, - Bidder: openrtb_ext.BidderName(bidder), - } - deps.MetricsEngine.RecordUserIDSet(labels) - so.Success = true - } - - pc.SetCookieOnResponse(w, deps.HostCookieSettings.Domain, deps.HostCookieSettings.TTL) -} - // Struct for parsing json in google's response type googleResponse struct { Success bool diff --git a/pbs_light.go b/pbs_light.go index 8b93cd782cc..a8751a7cc09 100644 --- a/pbs_light.go +++ b/pbs_light.go @@ -772,7 +772,7 @@ func serve(cfg *config.Configuration) error { } router.GET("/getuids", userSyncDeps.GetUIDs) - router.GET("/setuid", userSyncDeps.SetUID) + router.GET("/setuid", endpoints.NewSetUIDEndpoint(cfg.HostCookie, pbsAnalytics, metricsEngine)) router.POST("/optout", userSyncDeps.OptOut) router.GET("/optout", userSyncDeps.OptOut)