Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow Bidder to Override Callback Type in /setuid #3301

Merged
merged 10 commits into from
Jan 12, 2024
3 changes: 3 additions & 0 deletions config/bidderinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ type Syncer struct {

// SupportCORS identifies if CORS is supported for the user syncing endpoints.
SupportCORS *bool `yaml:"supportCors" mapstructure:"support_cors"`

// ForceSyncType allows a bidder to override their callback type "b" for iframe, "i" for redirect
ForceSyncType string `yaml:"forceSyncType" mapstructure:"force_sync_type"`
AlexBVolcy marked this conversation as resolved.
Show resolved Hide resolved
}

// SyncerEndpoint specifies the configuration of the URL returned by the /cookie_sync endpoint
Expand Down
4 changes: 4 additions & 0 deletions endpoints/cookie_sync_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1996,6 +1996,10 @@ func (m *MockSyncer) GetSync(syncTypes []usersync.SyncType, privacyMacros macros
return args.Get(0).(usersync.Sync), args.Error(1)
}

func (m *MockSyncer) ForceResponseFormat() string {
return ""
}
AlexBVolcy marked this conversation as resolved.
Show resolved Hide resolved

type MockAnalyticsRunner struct {
mock.Mock
}
Expand Down
4 changes: 4 additions & 0 deletions endpoints/setuid.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,10 @@ func isSyncerPriority(bidderNameFromSyncerQuery string, priorityGroups [][]strin
// Returns either "b" (iframe), "i" (redirect), or an empty string "" (legacy behavior of an
// empty response body with no content type).
func getResponseFormat(query url.Values, syncer usersync.Syncer) (string, error) {
if syncer.ForceResponseFormat() != "" {
return syncer.ForceResponseFormat(), nil
}

format, formatProvided := query["f"]
formatEmpty := len(format) == 0 || format[0] == ""

Expand Down
47 changes: 41 additions & 6 deletions endpoints/setuid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ func TestSetUIDEndpoint(t *testing.T) {
gdprAllowsHostCookies bool
gdprReturnsError bool
gdprMalformed bool
forceSyncType string
expectedSyncs map[string]string
expectedBody string
expectedStatusCode int
Expand Down Expand Up @@ -336,14 +337,25 @@ func TestSetUIDEndpoint(t *testing.T) {
expectedStatusCode: http.StatusBadRequest,
expectedBody: "invalid gpp_sid encoding, must be a csv list of integers",
},
{
uri: "/setuid?bidder=pubmatic&uid=123&f=b",
syncersBidderNameToKey: map[string]string{"pubmatic": "pubmatic"},
existingSyncs: nil,
gdprAllowsHostCookies: true,
forceSyncType: "i",
expectedSyncs: map[string]string{"pubmatic": "123"},
expectedStatusCode: http.StatusOK,
expectedHeaders: map[string]string{"Content-Length": "86", "Content-Type": "image/png"},
description: "Set uid for valid bidder with iframe format, but it's overriden by forceType",
},
}

analytics := analyticsBuild.New(&config.Analytics{})
metrics := &metricsConf.NilMetricsEngine{}

for _, test := range testCases {
response := doRequest(makeRequest(test.uri, test.existingSyncs), analytics, metrics,
test.syncersBidderNameToKey, test.gdprAllowsHostCookies, test.gdprReturnsError, test.gdprMalformed, false, 0, nil)
test.syncersBidderNameToKey, test.gdprAllowsHostCookies, test.gdprReturnsError, test.gdprMalformed, false, 0, nil, test.forceSyncType)
assert.Equal(t, test.expectedStatusCode, response.Code, "Test Case: %s. /setuid returned unexpected error code", test.description)

if test.expectedSyncs != nil {
Expand Down Expand Up @@ -467,7 +479,7 @@ func TestSetUIDPriorityEjection(t *testing.T) {
request.AddCookie(httpCookie)

// Make Request to /setuid
response := doRequest(request, analytics, &metricsConf.NilMetricsEngine{}, syncersByBidder, true, false, false, false, test.givenMaxCookieSize, test.givenPriorityGroups)
response := doRequest(request, analytics, &metricsConf.NilMetricsEngine{}, syncersByBidder, true, false, false, false, test.givenMaxCookieSize, test.givenPriorityGroups, "")

if test.expectedWarning != "" {
assert.Equal(t, test.expectedWarning, response.Body.String(), test.description)
Expand Down Expand Up @@ -1333,7 +1345,7 @@ func TestSetUIDEndpointMetrics(t *testing.T) {
for _, v := range test.cookies {
addCookie(req, v)
}
response := doRequest(req, analyticsEngine, metricsEngine, test.syncersBidderNameToKey, test.gdprAllowsHostCookies, false, false, test.cfgAccountRequired, 0, nil)
response := doRequest(req, analyticsEngine, metricsEngine, test.syncersBidderNameToKey, test.gdprAllowsHostCookies, false, false, test.cfgAccountRequired, 0, nil, "")

assert.Equal(t, test.expectedResponseCode, response.Code, test.description)
analyticsEngine.AssertExpectations(t)
Expand All @@ -1349,7 +1361,7 @@ func TestOptedOut(t *testing.T) {
syncersBidderNameToKey := map[string]string{"pubmatic": "pubmatic"}
analytics := analyticsBuild.New(&config.Analytics{})
metrics := &metricsConf.NilMetricsEngine{}
response := doRequest(request, analytics, metrics, syncersBidderNameToKey, true, false, false, false, 0, nil)
response := doRequest(request, analytics, metrics, syncersBidderNameToKey, true, false, false, false, 0, nil, "")

assert.Equal(t, http.StatusUnauthorized, response.Code)
}
Expand Down Expand Up @@ -1445,6 +1457,24 @@ func TestGetResponseFormat(t *testing.T) {
expectedFormat: "i",
description: "parameter given is empty (by empty item), use default sync type redirect",
},
{
urlValues: url.Values{"f": []string{""}},
syncer: fakeSyncer{key: "a", defaultSyncType: usersync.SyncTypeRedirect},
expectedFormat: "i",
description: "parameter given is empty (by empty item), use default sync type redirect",
},
{
urlValues: url.Values{"f": []string{"b"}},
syncer: fakeSyncer{key: "a", forceSyncType: "i"},
expectedFormat: "i",
description: "parameter given as `b`, but force sync type is opposite",
},
{
urlValues: url.Values{"f": []string{"i"}},
syncer: fakeSyncer{key: "a", forceSyncType: "b"},
expectedFormat: "b",
description: "parameter given as `i`, but force sync type is opposite",
},
}

for _, test := range testCases {
Expand Down Expand Up @@ -1544,7 +1574,7 @@ func makeRequest(uri string, existingSyncs map[string]string) *http.Request {
return request
}

func doRequest(req *http.Request, analytics analytics.Runner, metrics metrics.MetricsEngine, syncersBidderNameToKey map[string]string, gdprAllowsHostCookies, gdprReturnsError, gdprReturnsMalformedError, cfgAccountRequired bool, maxCookieSize int, priorityGroups [][]string) *httptest.ResponseRecorder {
func doRequest(req *http.Request, analytics analytics.Runner, metrics metrics.MetricsEngine, syncersBidderNameToKey map[string]string, gdprAllowsHostCookies, gdprReturnsError, gdprReturnsMalformedError, cfgAccountRequired bool, maxCookieSize int, priorityGroups [][]string, forceSyncType string) *httptest.ResponseRecorder {
cfg := config.Configuration{
AccountRequired: cfgAccountRequired,
AccountDefaults: config.Account{},
Expand Down Expand Up @@ -1575,7 +1605,7 @@ func doRequest(req *http.Request, analytics analytics.Runner, metrics metrics.Me

syncersByBidder := make(map[string]usersync.Syncer)
for bidderName, syncerKey := range syncersBidderNameToKey {
syncersByBidder[bidderName] = fakeSyncer{key: syncerKey, defaultSyncType: usersync.SyncTypeIFrame}
syncersByBidder[bidderName] = fakeSyncer{key: syncerKey, defaultSyncType: usersync.SyncTypeIFrame, forceSyncType: forceSyncType}
if priorityGroups == nil {
cfg.UserSync.PriorityGroups = [][]string{{}}
cfg.UserSync.PriorityGroups[0] = append(cfg.UserSync.PriorityGroups[0], bidderName)
Expand Down Expand Up @@ -1666,6 +1696,7 @@ func (g *fakePermsSetUID) AuctionActivitiesAllowed(ctx context.Context, bidderCo
type fakeSyncer struct {
key string
defaultSyncType usersync.SyncType
forceSyncType string
}

func (s fakeSyncer) Key() string {
Expand All @@ -1684,6 +1715,10 @@ func (s fakeSyncer) GetSync(syncTypes []usersync.SyncType, privacyMacros macros.
return usersync.Sync{}, nil
}

func (s fakeSyncer) ForceResponseFormat() string {
return s.forceSyncType
}

func ToHTTPCookie(cookie *usersync.Cookie) (*http.Cookie, error) {
encoder := usersync.Base64Encoder{}
encodedCookie, err := encoder.Encode(cookie)
Expand Down
5 changes: 5 additions & 0 deletions usersync/chooser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,7 @@ type fakeSyncer struct {
key string
supportsIFrame bool
supportsRedirect bool
forceSyncType string
}

func (s fakeSyncer) Key() string {
Expand All @@ -531,6 +532,10 @@ func (fakeSyncer) GetSync([]SyncType, macros.UserSyncPrivacy) (Sync, error) {
return Sync{}, nil
}

func (s fakeSyncer) ForceResponseFormat() string {
return s.forceSyncType
}

type fakePrivacy struct {
gdprAllowsHostCookie bool
gdprAllowsBidderSync bool
Expand Down
16 changes: 16 additions & 0 deletions usersync/syncer.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ type Syncer interface {
// GetSync returns a user sync for the user's device to perform, or an error if the none of the
// sync types are supported or if macro substitution fails.
GetSync(syncTypes []SyncType, userSyncMacros macros.UserSyncPrivacy) (Sync, error)

// ForceResponseFromat returns the callback format as specified in a bidders config
ForceResponseFormat() string
}

// Sync represents a user sync to be performed by the user's device.
Expand All @@ -50,6 +53,7 @@ type standardSyncer struct {
iframe *template.Template
redirect *template.Template
supportCORS bool
forceSyncType string
}

const (
Expand All @@ -72,6 +76,7 @@ func NewSyncer(hostConfig config.UserSync, syncerConfig config.Syncer, bidder st
key: syncerConfig.Key,
defaultSyncType: resolveDefaultSyncType(syncerConfig),
supportCORS: syncerConfig.SupportCORS != nil && *syncerConfig.SupportCORS,
forceSyncType: syncerConfig.ForceSyncType,
}

if syncerConfig.IFrame != nil {
Expand Down Expand Up @@ -266,3 +271,14 @@ func (s standardSyncer) chooseTemplate(syncType SyncType) *template.Template {
return nil
}
}

func (s standardSyncer) ForceResponseFormat() string {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is no longer used after the refactor and can be deleted.

switch s.forceSyncType {
case setuidSyncTypeIFrame:
return s.forceSyncType
case setuidSyncTypeRedirect:
return s.forceSyncType
default:
return ""
}
}
43 changes: 38 additions & 5 deletions usersync/syncer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ func TestNewSyncer(t *testing.T) {
givenIFrameConfig *config.SyncerEndpoint
givenRedirectConfig *config.SyncerEndpoint
givenExternalURL string
givenForceType string
expectedError string
expectedDefault SyncType
expectedIFrame string
Expand Down Expand Up @@ -97,6 +98,7 @@ func TestNewSyncer(t *testing.T) {
givenExternalURL: "http://syncer.com",
givenIFrameConfig: iframeConfig,
givenRedirectConfig: redirectConfig,
givenForceType: "i",
AlexBVolcy marked this conversation as resolved.
Show resolved Hide resolved
expectedDefault: SyncTypeIFrame,
expectedIFrame: "https://bidder.com/iframe?redirect=http%3A%2F%2Fsyncer.com%2Fhost",
expectedRedirect: "https://bidder.com/redirect?redirect=http%3A%2F%2Fsyncer.com%2Fhost",
Expand All @@ -105,11 +107,12 @@ func TestNewSyncer(t *testing.T) {

for _, test := range testCases {
syncerConfig := config.Syncer{
Key: test.givenKey,
SupportCORS: &supportCORS,
IFrame: test.givenIFrameConfig,
Redirect: test.givenRedirectConfig,
ExternalURL: test.givenExternalURL,
Key: test.givenKey,
SupportCORS: &supportCORS,
IFrame: test.givenIFrameConfig,
Redirect: test.givenRedirectConfig,
ExternalURL: test.givenExternalURL,
ForceSyncType: test.givenForceType,
}

result, err := NewSyncer(hostConfig, syncerConfig, test.givenBidderName)
Expand All @@ -120,6 +123,7 @@ func TestNewSyncer(t *testing.T) {
result := result.(standardSyncer)
assert.Equal(t, test.givenKey, result.key, test.description+":key")
assert.Equal(t, supportCORS, result.supportCORS, test.description+":cors")
assert.Equal(t, test.givenForceType, result.forceSyncType, test.description+":cors")
assert.Equal(t, test.expectedDefault, result.defaultSyncType, test.description+":default_sync")

if test.expectedIFrame == "" {
Expand Down Expand Up @@ -779,3 +783,32 @@ func TestSyncerChooseTemplate(t *testing.T) {
assert.Equal(t, test.expectedTemplate, result, test.description)
}
}

func TestSyncerForceResponseFormat(t *testing.T) {
testCases := []struct {
description string
givenForceType string
expectedForceType string
}{
{
description: "IFrame",
givenForceType: "b",
expectedForceType: "b",
},
{
description: "Redirect",
givenForceType: "i",
expectedForceType: "i",
},
{
description: "Empty",
expectedForceType: "",
},
SyntaxNode marked this conversation as resolved.
Show resolved Hide resolved
}

for _, test := range testCases {
syncer := standardSyncer{forceSyncType: test.givenForceType}
forceType := syncer.ForceResponseFormat()
assert.Equal(t, test.expectedForceType, forceType, test.description)
}
}
Loading