Skip to content

Commit

Permalink
Allow Bidder to Override Callback Type in /setuid (prebid#3301)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexBVolcy authored Jan 12, 2024
1 parent 03314e1 commit 3789cf9
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 25 deletions.
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"`

// 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
)

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: "[email protected]",
},
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 @@ -720,13 +720,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

0 comments on commit 3789cf9

Please sign in to comment.