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
12 changes: 12 additions & 0 deletions config/bidderinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ type Syncer struct {
// SupportCORS identifies if CORS is supported for the user syncing endpoints.
SupportCORS *bool `yaml:"supportCors" mapstructure:"support_cors"`

// FormatOverride allows a bidder to override their callback type "b" for iframe, "i" for redirect
FormatOverride string `yaml:"formatOverride" mapstructure:"format_override"`
SyntaxNode marked this conversation as resolved.
Show resolved Hide resolved

// Enabled signifies whether a bidder is enabled/disabled for user sync
Enabled *bool `yaml:"enabled" mapstructure:"enabled"`

Expand Down Expand Up @@ -206,6 +209,11 @@ type InfoReaderFromDisk struct {
Path string
}

const (
SyncResponseFormatIFrame = "b" // b = blank HTML response
SyncResponseFormatRedirect = "i" // i = image response
)
SyntaxNode marked this conversation as resolved.
Show resolved Hide resolved

func (r InfoReaderFromDisk) Read() (map[string][]byte, error) {
bidderConfigs, err := os.ReadDir(r.Path)
if err != nil {
Expand Down Expand Up @@ -562,6 +570,10 @@ func validateSyncer(bidderInfo BidderInfo) error {
return nil
}

if bidderInfo.Syncer.FormatOverride != SyncResponseFormatIFrame && bidderInfo.Syncer.FormatOverride != SyncResponseFormatRedirect && bidderInfo.Syncer.FormatOverride != "" {
return fmt.Errorf("syncer could not be created, invalid format override value: %s", bidderInfo.Syncer.FormatOverride)
}

for _, supports := range bidderInfo.Syncer.Supports {
if !strings.EqualFold(supports, "iframe") && !strings.EqualFold(supports, "redirect") {
return fmt.Errorf("syncer could not be created, invalid supported endpoint: %s", supports)
Expand Down
35 changes: 35 additions & 0 deletions config/bidderinfo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,7 @@ func TestBidderInfoValidationPositive(t *testing.T) {
URL: "http://bidderB.com/usersync",
UserMacro: "UID",
},
FormatOverride: SyncResponseFormatRedirect,
},
},
"bidderC": BidderInfo{
Expand Down Expand Up @@ -627,6 +628,9 @@ func TestBidderInfoValidationPositive(t *testing.T) {
},
},
},
Syncer: &Syncer{
FormatOverride: SyncResponseFormatIFrame,
},
},
}
errs := bidderInfos.validate(make([]error, 0))
Expand Down Expand Up @@ -1318,6 +1322,37 @@ func TestBidderInfoValidationNegative(t *testing.T) {
errors.New("parent bidder: bidderC not found for an alias: bidderB"),
},
},
{
"Invalid format override value",
BidderInfos{
"bidderB": BidderInfo{
Endpoint: "http://bidderA.com/openrtb2",
Maintainer: &MaintainerInfo{
Email: "maintainer@bidderA.com",
},
Capabilities: &CapabilitiesInfo{
App: &PlatformInfo{
MediaTypes: []openrtb_ext.BidType{
openrtb_ext.BidTypeBanner,
openrtb_ext.BidTypeNative,
},
},
Site: &PlatformInfo{
MediaTypes: []openrtb_ext.BidType{
openrtb_ext.BidTypeBanner,
openrtb_ext.BidTypeNative,
},
},
},
Syncer: &Syncer{
FormatOverride: "x",
},
},
},
[]error{
errors.New("syncer could not be created, invalid format override value: x"),
},
},
}

for _, test := range testCases {
Expand Down
2 changes: 1 addition & 1 deletion endpoints/cookie_sync_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2094,7 +2094,7 @@ func (m *MockSyncer) Key() string {
return args.String(0)
}

func (m *MockSyncer) DefaultSyncType() usersync.SyncType {
func (m *MockSyncer) DefaultResponseFormat() usersync.SyncType {
args := m.Called()
return args.Get(0).(usersync.SyncType)
}
Expand Down
2 changes: 1 addition & 1 deletion endpoints/setuid.go
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ func getResponseFormat(query url.Values, syncer usersync.Syncer) (string, error)
formatEmpty := len(format) == 0 || format[0] == ""

if !formatProvided || formatEmpty {
switch syncer.DefaultSyncType() {
switch syncer.DefaultResponseFormat() {
case usersync.SyncTypeIFrame:
return "b", nil
case usersync.SyncTypeRedirect:
Expand Down
60 changes: 52 additions & 8 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
formatOverride string
expectedSyncs map[string]string
expectedBody string
expectedStatusCode int
Expand Down Expand Up @@ -346,14 +347,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",
syncersBidderNameToKey: map[string]string{"pubmatic": "pubmatic"},
existingSyncs: nil,
gdprAllowsHostCookies: true,
formatOverride: "i",
expectedSyncs: map[string]string{"pubmatic": "123"},
expectedStatusCode: http.StatusOK,
expectedHeaders: map[string]string{"Content-Length": "86", "Content-Type": "image/png"},
description: "Format not provided in URL, but formatOverride is defined",
},
}

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.formatOverride)
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 @@ -477,7 +489,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 @@ -1343,7 +1355,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 @@ -1359,7 +1371,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 @@ -1455,6 +1467,30 @@ 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{}},
syncer: fakeSyncer{key: "a", formatOverride: "i"},
expectedFormat: "i",
description: "format not provided, but formatOverride is defined, expect i",
},
{
urlValues: url.Values{"f": []string{}},
syncer: fakeSyncer{key: "a", formatOverride: "b"},
expectedFormat: "b",
description: "format not provided, but formatOverride is defined, expect b",
},
{
urlValues: url.Values{"f": []string{}},
syncer: fakeSyncer{key: "a", formatOverride: "b", defaultSyncType: usersync.SyncTypeRedirect},
expectedFormat: "b",
description: "format not provided, default is defined but formatOverride is defined as well, expect b",
},
}

for _, test := range testCases {
Expand Down Expand Up @@ -1554,7 +1590,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, formatOverride string) *httptest.ResponseRecorder {
cfg := config.Configuration{
AccountRequired: cfgAccountRequired,
AccountDefaults: config.Account{},
Expand Down Expand Up @@ -1585,7 +1621,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, formatOverride: formatOverride}
if priorityGroups == nil {
cfg.UserSync.PriorityGroups = [][]string{{}}
cfg.UserSync.PriorityGroups[0] = append(cfg.UserSync.PriorityGroups[0], bidderName)
Expand Down Expand Up @@ -1676,14 +1712,22 @@ func (g *fakePermsSetUID) AuctionActivitiesAllowed(ctx context.Context, bidderCo
type fakeSyncer struct {
key string
defaultSyncType usersync.SyncType
formatOverride string
}

func (s fakeSyncer) Key() string {
return s.key
}

func (s fakeSyncer) DefaultSyncType() usersync.SyncType {
return s.defaultSyncType
func (s fakeSyncer) DefaultResponseFormat() usersync.SyncType {
switch s.formatOverride {
case "b":
return usersync.SyncTypeIFrame
case "i":
return usersync.SyncTypeRedirect
default:
return s.defaultSyncType
}
}

func (s fakeSyncer) SupportsType(syncTypes []usersync.SyncType) bool {
Expand Down
3 changes: 2 additions & 1 deletion usersync/chooser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -717,13 +717,14 @@ type fakeSyncer struct {
key string
supportsIFrame bool
supportsRedirect bool
formatOverride string
}

func (s fakeSyncer) Key() string {
return s.key
}

func (s fakeSyncer) DefaultSyncType() SyncType {
func (s fakeSyncer) DefaultResponseFormat() SyncType {
return SyncTypeIFrame
}

Expand Down
32 changes: 20 additions & 12 deletions usersync/syncer.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ type Syncer interface {
// necessarily, a one-to-one mapping with a bidder.
Key() string

// DefaultSyncType is the default SyncType for this syncer.
DefaultSyncType() SyncType
// DefaultResponseFormat is the default SyncType for this syncer.
DefaultResponseFormat() SyncType

// SupportsType returns true if the syncer supports at least one of the specified sync types.
SupportsType(syncTypes []SyncType) bool
Expand All @@ -50,13 +50,9 @@ type standardSyncer struct {
iframe *template.Template
redirect *template.Template
supportCORS bool
formatOverride string
}

const (
setuidSyncTypeIFrame = "b" // b = blank HTML response
setuidSyncTypeRedirect = "i" // i = image response
)

// NewSyncer creates a new Syncer from the provided configuration, or return an error if macro substition
// fails or an endpoint url is invalid.
func NewSyncer(hostConfig config.UserSync, syncerConfig config.Syncer, bidder string) (Syncer, error) {
Expand All @@ -72,11 +68,12 @@ func NewSyncer(hostConfig config.UserSync, syncerConfig config.Syncer, bidder st
key: syncerConfig.Key,
defaultSyncType: resolveDefaultSyncType(syncerConfig),
supportCORS: syncerConfig.SupportCORS != nil && *syncerConfig.SupportCORS,
formatOverride: syncerConfig.FormatOverride,
}

if syncerConfig.IFrame != nil {
var err error
syncer.iframe, err = buildTemplate(bidder, setuidSyncTypeIFrame, hostConfig, syncerConfig.ExternalURL, *syncerConfig.IFrame)
syncer.iframe, err = buildTemplate(bidder, config.SyncResponseFormatIFrame, hostConfig, syncerConfig.ExternalURL, *syncerConfig.IFrame, syncerConfig.FormatOverride)
if err != nil {
return nil, fmt.Errorf("iframe %v", err)
}
Expand All @@ -87,7 +84,7 @@ func NewSyncer(hostConfig config.UserSync, syncerConfig config.Syncer, bidder st

if syncerConfig.Redirect != nil {
var err error
syncer.redirect, err = buildTemplate(bidder, setuidSyncTypeRedirect, hostConfig, syncerConfig.ExternalURL, *syncerConfig.Redirect)
syncer.redirect, err = buildTemplate(bidder, config.SyncResponseFormatRedirect, hostConfig, syncerConfig.ExternalURL, *syncerConfig.Redirect, syncerConfig.FormatOverride)
if err != nil {
return nil, fmt.Errorf("redirect %v", err)
}
Expand Down Expand Up @@ -117,12 +114,16 @@ var (
macroRegex = regexp.MustCompile(`{{\s*\..*?\s*}}`)
)

func buildTemplate(bidderName, syncTypeValue string, hostConfig config.UserSync, syncerExternalURL string, syncerEndpoint config.SyncerEndpoint) (*template.Template, error) {
func buildTemplate(bidderName, syncTypeValue string, hostConfig config.UserSync, syncerExternalURL string, syncerEndpoint config.SyncerEndpoint, formatOverride string) (*template.Template, error) {
redirectTemplate := syncerEndpoint.RedirectURL
if redirectTemplate == "" {
redirectTemplate = hostConfig.RedirectURL
}

if formatOverride != "" {
syncTypeValue = formatOverride
}

externalURL := chooseExternalURL(syncerEndpoint.ExternalURL, syncerExternalURL, hostConfig.ExternalURL)

redirectURL := macroRegexSyncerKey.ReplaceAllLiteralString(redirectTemplate, bidderName)
Expand Down Expand Up @@ -189,8 +190,15 @@ func (s standardSyncer) Key() string {
return s.key
}

func (s standardSyncer) DefaultSyncType() SyncType {
return s.defaultSyncType
func (s standardSyncer) DefaultResponseFormat() SyncType {
switch s.formatOverride {
case config.SyncResponseFormatIFrame:
return SyncTypeIFrame
case config.SyncResponseFormatRedirect:
return SyncTypeRedirect
default:
return s.defaultSyncType
}
}

func (s standardSyncer) SupportsType(syncTypes []SyncType) bool {
Expand Down
34 changes: 32 additions & 2 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 @@ -322,7 +323,7 @@ func TestBuildTemplate(t *testing.T) {
}

for _, test := range testCases {
result, err := buildTemplate(key, syncTypeValue, hostConfig, test.givenSyncerExternalURL, test.givenSyncerEndpoint)
result, err := buildTemplate(key, syncTypeValue, hostConfig, test.givenSyncerExternalURL, test.givenSyncerEndpoint, "")

if test.expectedError == "" {
assert.NoError(t, err, test.description+":err")
Expand Down Expand Up @@ -480,7 +481,36 @@ func TestSyncerKey(t *testing.T) {

func TestSyncerDefaultSyncType(t *testing.T) {
syncer := standardSyncer{defaultSyncType: SyncTypeRedirect}
assert.Equal(t, SyncTypeRedirect, syncer.DefaultSyncType())
assert.Equal(t, SyncTypeRedirect, syncer.DefaultResponseFormat())
}

func TestSyncerDefaultResponseFormat(t *testing.T) {
testCases := []struct {
description string
givenSyncer standardSyncer
expectedSyncType SyncType
}{
{
description: "IFrame",
givenSyncer: standardSyncer{formatOverride: config.SyncResponseFormatIFrame},
expectedSyncType: SyncTypeIFrame,
},
{
description: "Default with Redirect Override",
givenSyncer: standardSyncer{defaultSyncType: SyncTypeIFrame, formatOverride: config.SyncResponseFormatRedirect},
expectedSyncType: SyncTypeRedirect,
},
{
description: "Default with no override",
givenSyncer: standardSyncer{defaultSyncType: SyncTypeRedirect},
expectedSyncType: SyncTypeRedirect,
},
}

for _, test := range testCases {
syncType := test.givenSyncer.DefaultResponseFormat()
assert.Equal(t, test.expectedSyncType, syncType, test.description)
}
}

func TestSyncerSupportsType(t *testing.T) {
Expand Down
Loading