diff --git a/config/account.go b/config/account.go index ee131873e35..a31e3bea616 100644 --- a/config/account.go +++ b/config/account.go @@ -337,6 +337,16 @@ type AccountPrivacy struct { AllowActivities *AllowActivities `mapstructure:"allowactivities" json:"allowactivities"` IPv6Config IPv6 `mapstructure:"ipv6" json:"ipv6"` IPv4Config IPv4 `mapstructure:"ipv4" json:"ipv4"` + PrivacySandbox PrivacySandbox `mapstructure:"privacysandbox" json:"privacysandbox"` +} + +type PrivacySandbox struct { + CookieDeprecation CookieDeprecation `mapstructure:"cookiedeprecation"` +} + +type CookieDeprecation struct { + Enabled bool `mapstructure:"enabled"` + TTLSec int `mapstructure:"ttl_sec"` } type IPv6 struct { diff --git a/config/config.go b/config/config.go index f0e9f5c7fa7..ac6fc74e1a5 100644 --- a/config/config.go +++ b/config/config.go @@ -1107,6 +1107,8 @@ func SetupViper(v *viper.Viper, filename string, bidderInfos BidderInfos) { v.SetDefault("account_defaults.price_floors.fetch.max_age_sec", 86400) v.SetDefault("account_defaults.price_floors.fetch.period_sec", 3600) v.SetDefault("account_defaults.price_floors.fetch.max_schema_dims", 0) + v.SetDefault("account_defaults.privacy.privacysandbox.cookiedeprecation.enabled", false) + v.SetDefault("account_defaults.privacy.privacysandbox.cookiedeprecation.ttl_sec", 604800) v.SetDefault("account_defaults.events_enabled", false) v.SetDefault("account_defaults.privacy.ipv6.anon_keep_bits", 56) diff --git a/config/config_test.go b/config/config_test.go index a551c1be66e..14ef1aeef81 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -203,6 +203,8 @@ func TestDefaults(t *testing.T) { cmpInts(t, "account_defaults.price_floors.fetch.period_sec", 3600, cfg.AccountDefaults.PriceFloors.Fetcher.Period) cmpInts(t, "account_defaults.price_floors.fetch.max_age_sec", 86400, cfg.AccountDefaults.PriceFloors.Fetcher.MaxAge) cmpInts(t, "account_defaults.price_floors.fetch.max_schema_dims", 0, cfg.AccountDefaults.PriceFloors.Fetcher.MaxSchemaDims) + cmpBools(t, "account_defaults.privacy.privacysandbox.cookiedeprecation.enabled", false, cfg.AccountDefaults.Privacy.PrivacySandbox.CookieDeprecation.Enabled) + cmpInts(t, "account_defaults.privacy.privacysandbox.cookiedeprecation.ttl_sec", 604800, cfg.AccountDefaults.Privacy.PrivacySandbox.CookieDeprecation.TTLSec) cmpBools(t, "account_defaults.events.enabled", false, cfg.AccountDefaults.Events.Enabled) @@ -503,6 +505,10 @@ account_defaults: anon_keep_bits: 50 ipv4: anon_keep_bits: 20 + privacysandbox: + cookiedeprecation: + enabled: true + ttl_sec: 86400 tmax_adjustments: enabled: true bidder_response_duration_min_ms: 700 @@ -622,6 +628,9 @@ func TestFullConfig(t *testing.T) { cmpInts(t, "account_defaults.privacy.ipv6.anon_keep_bits", 50, cfg.AccountDefaults.Privacy.IPv6Config.AnonKeepBits) cmpInts(t, "account_defaults.privacy.ipv4.anon_keep_bits", 20, cfg.AccountDefaults.Privacy.IPv4Config.AnonKeepBits) + cmpBools(t, "account_defaults.privacy.cookiedeprecation.enabled", true, cfg.AccountDefaults.Privacy.PrivacySandbox.CookieDeprecation.Enabled) + cmpInts(t, "account_defaults.privacy.cookiedeprecation.ttl_sec", 86400, cfg.AccountDefaults.Privacy.PrivacySandbox.CookieDeprecation.TTLSec) + // Assert compression related defaults cmpBools(t, "compression.request.enable_gzip", true, cfg.Compression.Request.GZIP) cmpBools(t, "compression.response.enable_gzip", false, cfg.Compression.Response.GZIP) diff --git a/endpoints/cookie_sync.go b/endpoints/cookie_sync.go index 84d6d7847ef..d6e0b31a096 100644 --- a/endpoints/cookie_sync.go +++ b/endpoints/cookie_sync.go @@ -9,6 +9,7 @@ import ( "net/http" "strconv" "strings" + "time" "github.com/golang/glog" "github.com/julienschmidt/httprouter" @@ -29,8 +30,11 @@ import ( "github.com/prebid/prebid-server/v2/usersync" "github.com/prebid/prebid-server/v2/util/jsonutil" stringutil "github.com/prebid/prebid-server/v2/util/stringutil" + "github.com/prebid/prebid-server/v2/util/timeutil" ) +const receiveCookieDeprecation = "receive-cookie-deprecation" + var ( errCookieSyncOptOut = errors.New("User has opted out") errCookieSyncBody = errors.New("Failed to read request body") @@ -73,6 +77,7 @@ func NewCookieSyncEndpoint( metrics: metrics, pbsAnalytics: analyticsRunner, accountsFetcher: accountsFetcher, + time: &timeutil.RealTime{}, } } @@ -83,10 +88,12 @@ type cookieSyncEndpoint struct { metrics metrics.MetricsEngine pbsAnalytics analytics.Runner accountsFetcher stored_requests.AccountFetcher + time timeutil.Time } func (c *cookieSyncEndpoint) Handle(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - request, privacyMacros, err := c.parseRequest(r) + request, privacyMacros, account, err := c.parseRequest(r) + c.setCookieDeprecationHeader(w, r, account) if err != nil { c.writeParseRequestErrorMetrics(err) c.handleError(w, err, http.StatusBadRequest) @@ -113,16 +120,16 @@ func (c *cookieSyncEndpoint) Handle(w http.ResponseWriter, r *http.Request, _ ht } } -func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, macros.UserSyncPrivacy, error) { +func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, macros.UserSyncPrivacy, *config.Account, error) { defer r.Body.Close() body, err := io.ReadAll(r.Body) if err != nil { - return usersync.Request{}, macros.UserSyncPrivacy{}, errCookieSyncBody + return usersync.Request{}, macros.UserSyncPrivacy{}, nil, errCookieSyncBody } request := cookieSyncRequest{} if err := jsonutil.UnmarshalValid(body, &request); err != nil { - return usersync.Request{}, macros.UserSyncPrivacy{}, fmt.Errorf("JSON parsing failed: %s", err.Error()) + return usersync.Request{}, macros.UserSyncPrivacy{}, nil, fmt.Errorf("JSON parsing failed: %s", err.Error()) } if request.Account == "" { @@ -130,7 +137,7 @@ func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, ma } account, fetchErrs := accountService.GetAccount(context.Background(), c.config, c.accountsFetcher, request.Account, c.metrics) if len(fetchErrs) > 0 { - return usersync.Request{}, macros.UserSyncPrivacy{}, combineErrors(fetchErrs) + return usersync.Request{}, macros.UserSyncPrivacy{}, nil, combineErrors(fetchErrs) } request = c.setLimit(request, account.CookieSync) @@ -138,7 +145,7 @@ func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, ma privacyMacros, gdprSignal, privacyPolicies, err := extractPrivacyPolicies(request, c.privacyConfig.gdprConfig.DefaultValue) if err != nil { - return usersync.Request{}, macros.UserSyncPrivacy{}, err + return usersync.Request{}, macros.UserSyncPrivacy{}, account, err } ccpaParsedPolicy := ccpa.ParsedPolicy{} @@ -156,7 +163,7 @@ func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, ma syncTypeFilter, err := parseTypeFilter(request.FilterSettings) if err != nil { - return usersync.Request{}, macros.UserSyncPrivacy{}, err + return usersync.Request{}, macros.UserSyncPrivacy{}, account, err } gdprRequestInfo := gdpr.RequestInfo{ @@ -185,7 +192,7 @@ func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, ma SyncTypeFilter: syncTypeFilter, GPPSID: request.GPPSID, } - return rx, privacyMacros, nil + return rx, privacyMacros, account, nil } func extractPrivacyPolicies(request cookieSyncRequest, usersyncDefaultGDPRValue string) (macros.UserSyncPrivacy, gdpr.Signal, privacy.Policies, error) { @@ -455,11 +462,38 @@ func (c *cookieSyncEndpoint) handleResponse(w http.ResponseWriter, tf usersync.S }) w.Header().Set("Content-Type", "application/json; charset=utf-8") + enc := json.NewEncoder(w) enc.SetEscapeHTML(false) enc.Encode(response) } +func (c *cookieSyncEndpoint) setCookieDeprecationHeader(w http.ResponseWriter, r *http.Request, account *config.Account) { + if rcd, err := r.Cookie(receiveCookieDeprecation); err == nil && rcd != nil { + return + } + if account == nil || !account.Privacy.PrivacySandbox.CookieDeprecation.Enabled { + return + } + cookie := &http.Cookie{ + Name: receiveCookieDeprecation, + Value: "1", + Secure: true, + HttpOnly: true, + Path: "/", + SameSite: http.SameSiteNoneMode, + Expires: c.time.Now().Add(time.Second * time.Duration(account.Privacy.PrivacySandbox.CookieDeprecation.TTLSec)), + } + setCookiePartitioned(w, cookie) +} + +// setCookiePartitioned temporary substitute for http.SetCookie(w, cookie) until it supports Partitioned cookie type. Refer https://github.com/golang/go/issues/62490 +func setCookiePartitioned(w http.ResponseWriter, cookie *http.Cookie) { + if v := cookie.String(); v != "" { + w.Header().Add("Set-Cookie", v+"; Partitioned;") + } +} + func mapBidderStatusToAnalytics(from []cookieSyncResponseBidder) []*analytics.CookieSyncBidder { to := make([]*analytics.CookieSyncBidder, len(from)) for i, b := range from { diff --git a/endpoints/cookie_sync_test.go b/endpoints/cookie_sync_test.go index adfdcb22fab..35d7dd4baf5 100644 --- a/endpoints/cookie_sync_test.go +++ b/endpoints/cookie_sync_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "fmt" "io" "net/http" "net/http/httptest" @@ -11,6 +12,7 @@ import ( "strings" "testing" "testing/iotest" + "time" "github.com/prebid/prebid-server/v2/analytics" "github.com/prebid/prebid-server/v2/config" @@ -23,11 +25,19 @@ import ( "github.com/prebid/prebid-server/v2/privacy/ccpa" "github.com/prebid/prebid-server/v2/usersync" "github.com/prebid/prebid-server/v2/util/ptrutil" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) +// fakeTime implements the Time interface +type fakeTime struct { + time time.Time +} + +func (ft *fakeTime) Now() time.Time { + return ft.time +} + func TestNewCookieSyncEndpoint(t *testing.T) { var ( syncersByBidder = map[string]usersync.Syncer{"a": &MockSyncer{}} @@ -111,14 +121,16 @@ func TestCookieSyncHandle(t *testing.T) { cookieWithSyncs.Sync("foo", "anyID") testCases := []struct { - description string - givenCookie *usersync.Cookie - givenBody io.Reader - givenChooserResult usersync.Result - expectedStatusCode int - expectedBody string - setMetricsExpectations func(*metrics.MetricsEngineMock) - setAnalyticsExpectations func(*MockAnalyticsRunner) + description string + givenCookie *usersync.Cookie + givenBody io.Reader + givenChooserResult usersync.Result + givenAccountData map[string]json.RawMessage + expectedStatusCode int + expectedBody string + setMetricsExpectations func(*metrics.MetricsEngineMock) + setAnalyticsExpectations func(*MockAnalyticsRunner) + expectedCookieDeprecationHeader bool }{ { description: "Request With Cookie", @@ -285,6 +297,42 @@ func TestCookieSyncHandle(t *testing.T) { a.On("LogCookieSyncObject", &expected).Once() }, }, + { + description: "CookieDeprecation-Set", + givenCookie: cookieWithSyncs, + givenBody: strings.NewReader(`{"account": "testAccount"}`), + givenChooserResult: usersync.Result{ + Status: usersync.StatusOK, + BiddersEvaluated: []usersync.BidderEvaluation{{Bidder: "a", SyncerKey: "aSyncer", Status: usersync.StatusAlreadySynced}}, + SyncersChosen: []usersync.SyncerChoice{{Bidder: "a", Syncer: &syncer}}, + }, + givenAccountData: map[string]json.RawMessage{ + "testAccount": json.RawMessage(`{"id":"1","privacy":{"privacysandbox":{"cookiedeprecation":{"enabled":true,"ttlsec":86400}}}}`), + }, + expectedStatusCode: 200, + expectedCookieDeprecationHeader: true, + expectedBody: `{"status":"ok","bidder_status":[` + + `{"bidder":"a","no_cookie":true,"usersync":{"url":"aURL","type":"redirect","supportCORS":true}}` + + `]}` + "\n", + setMetricsExpectations: func(m *metrics.MetricsEngineMock) { + m.On("RecordCookieSync", metrics.CookieSyncOK).Once() + m.On("RecordSyncerRequest", "aSyncer", metrics.SyncerCookieSyncAlreadySynced).Once() + }, + setAnalyticsExpectations: func(a *MockAnalyticsRunner) { + expected := analytics.CookieSyncObject{ + Status: 200, + Errors: nil, + BidderStatus: []*analytics.CookieSyncBidder{ + { + BidderCode: "a", + NoCookie: true, + UsersyncInfo: &analytics.UsersyncInfo{URL: "aURL", Type: "redirect", SupportCORS: true}, + }, + }, + } + a.On("LogCookieSyncObject", &expected).Once() + }, + }, } for _, test := range testCases { @@ -294,7 +342,9 @@ func TestCookieSyncHandle(t *testing.T) { mockAnalytics := MockAnalyticsRunner{} test.setAnalyticsExpectations(&mockAnalytics) - fakeAccountFetcher := FakeAccountsFetcher{} + fakeAccountFetcher := FakeAccountsFetcher{ + AccountData: test.givenAccountData, + } gdprPermsBuilder := fakePermissionsBuilder{ permissions: &fakePermissions{}, @@ -329,6 +379,7 @@ func TestCookieSyncHandle(t *testing.T) { metrics: &mockMetrics, pbsAnalytics: &mockAnalytics, accountsFetcher: &fakeAccountFetcher, + time: &fakeTime{time: time.Date(2024, 2, 22, 9, 42, 4, 13, time.UTC)}, } assert.NoError(t, endpoint.config.MarshalAccountDefaults()) @@ -336,6 +387,16 @@ func TestCookieSyncHandle(t *testing.T) { assert.Equal(t, test.expectedStatusCode, writer.Code, test.description+":status_code") assert.Equal(t, test.expectedBody, writer.Body.String(), test.description+":body") + + gotCookie := writer.Header().Get("Set-Cookie") + if test.expectedCookieDeprecationHeader { + wantCookieTTL := endpoint.time.Now().Add(time.Second * time.Duration(86400)).UTC().Format(http.TimeFormat) + wantCookie := fmt.Sprintf("receive-cookie-deprecation=1; Path=/; Expires=%v; HttpOnly; Secure; SameSite=None; Partitioned;", wantCookieTTL) + assert.Equal(t, wantCookie, gotCookie, test.description) + } else { + assert.Empty(t, gotCookie, test.description) + } + mockMetrics.AssertExpectations(t) mockAnalytics.AssertExpectations(t) } @@ -1060,7 +1121,7 @@ func TestCookieSyncParseRequest(t *testing.T) { }}, } assert.NoError(t, endpoint.config.MarshalAccountDefaults()) - request, privacyPolicies, err := endpoint.parseRequest(httpRequest) + request, privacyPolicies, _, err := endpoint.parseRequest(httpRequest) if test.expectedError == "" { assert.NoError(t, err, test.description+":err") @@ -2207,3 +2268,131 @@ func getDefaultActivityConfig(componentName string, allow bool) *config.AccountP }, } } + +func TestSetCookieDeprecationHeader(t *testing.T) { + getTestRequest := func(addCookie bool) *http.Request { + r := httptest.NewRequest("POST", "/cookie_sync", nil) + if addCookie { + r.AddCookie(&http.Cookie{Name: receiveCookieDeprecation, Value: "1"}) + } + return r + } + + tests := []struct { + name string + responseWriter http.ResponseWriter + request *http.Request + account *config.Account + expectedCookieDeprecationHeader bool + }{ + { + name: "not-present-account-nil", + request: getTestRequest(false), + responseWriter: httptest.NewRecorder(), + account: nil, + expectedCookieDeprecationHeader: false, + }, + { + name: "not-present-cookiedeprecation-disabled", + request: getTestRequest(false), + responseWriter: httptest.NewRecorder(), + account: &config.Account{ + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + CookieDeprecation: config.CookieDeprecation{ + Enabled: false, + }, + }, + }, + }, + expectedCookieDeprecationHeader: false, + }, + { + name: "present-cookiedeprecation-disabled", + request: getTestRequest(true), + responseWriter: httptest.NewRecorder(), + account: &config.Account{ + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + CookieDeprecation: config.CookieDeprecation{ + Enabled: false, + }, + }, + }, + }, + expectedCookieDeprecationHeader: false, + }, + { + name: "present-cookiedeprecation-enabled", + request: getTestRequest(true), + responseWriter: httptest.NewRecorder(), + account: &config.Account{ + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + CookieDeprecation: config.CookieDeprecation{ + Enabled: true, + TTLSec: 86400, + }, + }, + }, + }, + + expectedCookieDeprecationHeader: false, + }, + { + name: "present-account-nil", + request: getTestRequest(true), + responseWriter: httptest.NewRecorder(), + account: nil, + expectedCookieDeprecationHeader: false, + }, + { + name: "not-present-cookiedeprecation-enabled", + request: getTestRequest(false), + responseWriter: httptest.NewRecorder(), + account: &config.Account{ + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + CookieDeprecation: config.CookieDeprecation{ + Enabled: true, + TTLSec: 86400, + }, + }, + }, + }, + expectedCookieDeprecationHeader: true, + }, + { + name: "failed-to-read-cookiedeprecation-enabled", + request: &http.Request{}, // nil cookie. error: http: named cookie not present + responseWriter: httptest.NewRecorder(), + account: &config.Account{ + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + CookieDeprecation: config.CookieDeprecation{ + Enabled: true, + TTLSec: 86400, + }, + }, + }, + }, + expectedCookieDeprecationHeader: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &cookieSyncEndpoint{ + time: &fakeTime{time: time.Date(2024, 2, 22, 9, 42, 4, 13, time.UTC)}, + } + c.setCookieDeprecationHeader(tt.responseWriter, tt.request, tt.account) + gotCookie := tt.responseWriter.Header().Get("Set-Cookie") + if tt.expectedCookieDeprecationHeader { + wantCookieTTL := c.time.Now().Add(time.Second * time.Duration(86400)).UTC().Format(http.TimeFormat) + wantCookie := fmt.Sprintf("receive-cookie-deprecation=1; Path=/; Expires=%v; HttpOnly; Secure; SameSite=None; Partitioned;", wantCookieTTL) + assert.Equal(t, wantCookie, gotCookie, ":set_cookie_deprecation_header") + } else { + assert.Empty(t, gotCookie, ":set_cookie_deprecation_header") + } + }) + } +} diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go index a6ad8d3fc65..6d14ed7d69d 100644 --- a/endpoints/openrtb2/amp_auction.go +++ b/endpoints/openrtb2/amp_auction.go @@ -230,6 +230,19 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h return } + hasStoredResponses := len(storedAuctionResponses) > 0 + errs := deps.validateRequest(account, r, reqWrapper, true, hasStoredResponses, storedBidResponses, false) + errL = append(errL, errs...) + ao.Errors = append(ao.Errors, errs...) + if errortypes.ContainsFatalError(errs) { + w.WriteHeader(http.StatusBadRequest) + for _, err := range errortypes.FatalOnly(errs) { + w.Write([]byte(fmt.Sprintf("Invalid request: %s\n", err.Error()))) + } + labels.RequestStatus = metrics.RequestStatusBadInput + return + } + tcf2Config := gdpr.NewTCF2Config(deps.cfg.GDPR.TCF2, account.GDPR) activityControl = privacy.NewActivityControl(&account.Privacy) @@ -497,10 +510,6 @@ func (deps *endpointDeps) parseAmpRequest(httpRequest *http.Request) (req *openr return } - hasStoredResponses := len(storedAuctionResponses) > 0 - e = deps.validateRequest(req, true, hasStoredResponses, storedBidResponses, false) - errs = append(errs, e...) - return } diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go index c7beceb1b52..2aafe6808ef 100644 --- a/endpoints/openrtb2/auction.go +++ b/endpoints/openrtb2/auction.go @@ -60,6 +60,7 @@ import ( const storedRequestTimeoutMillis = 50 const ampChannel = "amp" const appChannel = "app" +const secCookieDeprecation = "Sec-Cookie-Deprecation" var ( dntKey string = http.CanonicalHeaderKey("DNT") @@ -552,7 +553,7 @@ func (deps *endpointDeps) parseRequest(httpRequest *http.Request, labels *metric } hasStoredResponses := len(storedAuctionResponses) > 0 - errL := deps.validateRequest(req, false, hasStoredResponses, storedBidResponses, hasStoredBidRequest) + errL := deps.validateRequest(account, httpRequest, req, false, hasStoredResponses, storedBidResponses, hasStoredBidRequest) if len(errL) > 0 { errs = append(errs, errL...) } @@ -746,7 +747,7 @@ func mergeBidderParamsImpExtPrebid(impExt *openrtb_ext.ImpExt, reqExtParams map[ return nil } -func (deps *endpointDeps) validateRequest(req *openrtb_ext.RequestWrapper, isAmp bool, hasStoredResponses bool, storedBidResp stored_responses.ImpBidderStoredResp, hasStoredBidRequest bool) []error { +func (deps *endpointDeps) validateRequest(account *config.Account, httpReq *http.Request, req *openrtb_ext.RequestWrapper, isAmp bool, hasStoredResponses bool, storedBidResp stored_responses.ImpBidderStoredResp, hasStoredBidRequest bool) []error { errL := []error{} if req.ID == "" { return []error{errors.New("request missing required field: \"id\"")} @@ -875,6 +876,10 @@ func (deps *endpointDeps) validateRequest(req *openrtb_ext.RequestWrapper, isAmp return append(errL, err) } + if err := validateOrFillCDep(httpReq, req, account); err != nil { + errL = append(errL, err) + } + if ccpaPolicy, err := ccpa.ReadFromRequestWrapper(req, gpp); err != nil { errL = append(errL, err) if errortypes.ContainsFatalError([]error{err}) { @@ -1922,6 +1927,35 @@ func validateDevice(device *openrtb2.Device) error { return nil } +func validateOrFillCDep(httpReq *http.Request, req *openrtb_ext.RequestWrapper, account *config.Account) error { + if account == nil || !account.Privacy.PrivacySandbox.CookieDeprecation.Enabled { + return nil + } + + deviceExt, err := req.GetDeviceExt() + if err != nil { + return err + } + + if deviceExt.GetCDep() != "" { + return nil + } + + secCookieDeprecation := httpReq.Header.Get(secCookieDeprecation) + if secCookieDeprecation == "" { + return nil + } + if len(secCookieDeprecation) > 100 { + return &errortypes.Warning{ + Message: "request.device.ext.cdep must not exceed 100 characters", + WarningCode: errortypes.SecCookieDeprecationLenWarningCode, + } + } + + deviceExt.SetCDep(secCookieDeprecation) + return nil +} + func validateExactlyOneInventoryType(reqWrapper *openrtb_ext.RequestWrapper) error { // Prep for mutual exclusion check diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go index a9e16c9490b..8880384ee0d 100644 --- a/endpoints/openrtb2/auction_test.go +++ b/endpoints/openrtb2/auction_test.go @@ -1644,6 +1644,8 @@ func TestValidateRequest(t *testing.T) { description string givenIsAmp bool givenRequestWrapper *openrtb_ext.RequestWrapper + givenHttpRequest *http.Request + givenAccount *config.Account expectedErrorList []error expectedChannelObject *openrtb_ext.ExtRequestPrebidChannel }{ @@ -1864,7 +1866,7 @@ func TestValidateRequest(t *testing.T) { } for _, test := range testCases { - errorList := deps.validateRequest(test.givenRequestWrapper, test.givenIsAmp, false, nil, false) + errorList := deps.validateRequest(test.givenAccount, test.givenHttpRequest, test.givenRequestWrapper, test.givenIsAmp, false, nil, false) assert.Equalf(t, test.expectedErrorList, errorList, "Error doesn't match: %s\n", test.description) if len(errorList) == 0 { @@ -3047,7 +3049,7 @@ func TestCurrencyTrunc(t *testing.T) { Cur: []string{"USD", "EUR"}, } - errL := deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req}, false, false, nil, false) + errL := deps.validateRequest(nil, nil, &openrtb_ext.RequestWrapper{BidRequest: &req}, false, false, nil, false) expectedError := errortypes.Warning{Message: "A prebid request can only process one currency. Taking the first currency in the list, USD, as the active currency"} assert.ElementsMatch(t, errL, []error{&expectedError}) @@ -3098,7 +3100,7 @@ func TestCCPAInvalid(t *testing.T) { }, } - errL := deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req}, false, false, nil, false) + errL := deps.validateRequest(nil, nil, &openrtb_ext.RequestWrapper{BidRequest: &req}, false, false, nil, false) expectedWarning := errortypes.Warning{ Message: "CCPA consent is invalid and will be ignored. (request.regs.ext.us_privacy must contain 4 characters)", @@ -3152,7 +3154,7 @@ func TestNoSaleInvalid(t *testing.T) { Ext: json.RawMessage(`{"prebid": {"nosale": ["*", "appnexus"]} }`), } - errL := deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req}, false, false, nil, false) + errL := deps.validateRequest(nil, nil, &openrtb_ext.RequestWrapper{BidRequest: &req}, false, false, nil, false) expectedError := errors.New("request.ext.prebid.nosale is invalid: can only specify all bidders if no other bidders are provided") assert.ElementsMatch(t, errL, []error{expectedError}) @@ -3204,7 +3206,7 @@ func TestValidateSourceTID(t *testing.T) { }, } - deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req}, false, false, nil, false) + deps.validateRequest(nil, nil, &openrtb_ext.RequestWrapper{BidRequest: &req}, false, false, nil, false) assert.NotEmpty(t, req.Source.TID, "Expected req.Source.TID to be filled with a randomly generated UID") } @@ -3251,7 +3253,7 @@ func TestSChainInvalid(t *testing.T) { Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller1.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}, {"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller2.com","sid":"00002","rid":"BidRequest2","hp":1}],"ver":"1.0"}}]}}`), } - errL := deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req}, false, false, nil, false) + errL := deps.validateRequest(nil, nil, &openrtb_ext.RequestWrapper{BidRequest: &req}, false, false, nil, false) expectedError := errors.New("request.ext.prebid.schains contains multiple schains for bidder appnexus; it must contain no more than one per bidder.") assert.ElementsMatch(t, errL, []error{expectedError}) @@ -3820,7 +3822,7 @@ func TestEidPermissionsInvalid(t *testing.T) { Ext: json.RawMessage(`{"prebid": {"data": {"eidpermissions": [{"source":"a", "bidders":[]}]} } }`), } - errL := deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req}, false, false, nil, false) + errL := deps.validateRequest(nil, nil, &openrtb_ext.RequestWrapper{BidRequest: &req}, false, false, nil, false) expectedError := errors.New(`request.ext.prebid.data.eidpermissions[0] missing or empty required field: "bidders"`) assert.ElementsMatch(t, errL, []error{expectedError}) @@ -5128,6 +5130,8 @@ func TestValidateStoredResp(t *testing.T) { testCases := []struct { description string givenRequestWrapper *openrtb_ext.RequestWrapper + givenHttpRequest *http.Request + givenAccount *config.Account expectedErrorList []error hasStoredAuctionResponses bool storedBidResponses stored_responses.ImpBidderStoredResp @@ -5669,7 +5673,7 @@ func TestValidateStoredResp(t *testing.T) { for _, test := range testCases { t.Run(test.description, func(t *testing.T) { - errorList := deps.validateRequest(test.givenRequestWrapper, false, test.hasStoredAuctionResponses, test.storedBidResponses, false) + errorList := deps.validateRequest(test.givenAccount, test.givenHttpRequest, test.givenRequestWrapper, false, test.hasStoredAuctionResponses, test.storedBidResponses, false) assert.Equalf(t, test.expectedErrorList, errorList, "Error doesn't match: %s\n", test.description) }) } @@ -6108,3 +6112,392 @@ func TestValidateAliases(t *testing.T) { func fakeNormalizeBidderName(name string) (openrtb_ext.BidderName, bool) { return openrtb_ext.BidderName(strings.ToLower(name)), true } + +func TestValidateOrFillCDep(t *testing.T) { + type args struct { + httpReq *http.Request + req *openrtb_ext.RequestWrapper + account config.Account + } + tests := []struct { + name string + args args + wantDeviceExt json.RawMessage + wantErr error + }{ + { + name: "account-nil", + wantDeviceExt: nil, + wantErr: nil, + }, + { + name: "cookie-deprecation-not-enabled", + args: args{ + httpReq: &http.Request{ + Header: http.Header{secCookieDeprecation: []string{"example_label_1"}}, + }, + req: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{}, + }, + }, + wantDeviceExt: nil, + wantErr: nil, + }, + { + name: "cookie-deprecation-disabled-explicitly", + args: args{ + httpReq: &http.Request{ + Header: http.Header{secCookieDeprecation: []string{"example_label_1"}}, + }, + req: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{}, + }, + account: config.Account{ + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + CookieDeprecation: config.CookieDeprecation{ + Enabled: false, + }, + }, + }, + }, + }, + wantDeviceExt: nil, + wantErr: nil, + }, + { + name: "cookie-deprecation-enabled-header-not-present-in-request", + args: args{ + httpReq: &http.Request{}, + req: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{}, + }, + account: config.Account{ + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + CookieDeprecation: config.CookieDeprecation{ + Enabled: true, + }, + }, + }, + }, + }, + wantDeviceExt: nil, + wantErr: nil, + }, + { + name: "header-present-request-device-nil", + args: args{ + httpReq: &http.Request{ + Header: http.Header{secCookieDeprecation: []string{"example_label_1"}}, + }, + req: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{}, + }, + account: config.Account{ + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + CookieDeprecation: config.CookieDeprecation{ + Enabled: true, + }, + }, + }, + }, + }, + wantDeviceExt: json.RawMessage(`{"cdep":"example_label_1"}`), + wantErr: nil, + }, + { + name: "header-present-request-device-ext-nil", + args: args{ + httpReq: &http.Request{ + Header: http.Header{secCookieDeprecation: []string{"example_label_1"}}, + }, + req: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Device: &openrtb2.Device{ + Ext: nil, + }, + }, + }, + account: config.Account{ + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + CookieDeprecation: config.CookieDeprecation{ + Enabled: true, + }, + }, + }, + }, + }, + wantDeviceExt: json.RawMessage(`{"cdep":"example_label_1"}`), + wantErr: nil, + }, + { + name: "header-present-request-device-ext-not-nil", + args: args{ + httpReq: &http.Request{ + Header: http.Header{secCookieDeprecation: []string{"example_label_1"}}, + }, + req: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Device: &openrtb2.Device{ + Ext: json.RawMessage(`{"foo":"bar"}`), + }, + }, + }, + account: config.Account{ + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + CookieDeprecation: config.CookieDeprecation{ + Enabled: true, + }, + }, + }, + }, + }, + wantDeviceExt: json.RawMessage(`{"cdep":"example_label_1","foo":"bar"}`), + wantErr: nil, + }, + { + name: "header-present-with-length-more-than-100", + args: args{ + httpReq: &http.Request{ + Header: http.Header{secCookieDeprecation: []string{"zjfXqGxXFI8yura8AhQl1DK2EMMmryrC8haEpAlwjoerrFfEo2MQTXUq6cSmLohI8gjsnkGU4oAzvXd4TTAESzEKsoYjRJ2zKxmEa"}}, + }, + req: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Device: &openrtb2.Device{ + Ext: json.RawMessage(`{"foo":"bar"}`), + }, + }, + }, + account: config.Account{ + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + CookieDeprecation: config.CookieDeprecation{ + Enabled: true, + }, + }, + }, + }, + }, + wantDeviceExt: json.RawMessage(`{"foo":"bar"}`), + wantErr: &errortypes.Warning{ + Message: "request.device.ext.cdep must not exceed 100 characters", + WarningCode: errortypes.SecCookieDeprecationLenWarningCode, + }, + }, + { + name: "header-present-request-device-ext-cdep-present", + args: args{ + httpReq: &http.Request{ + Header: http.Header{secCookieDeprecation: []string{"example_label_1"}}, + }, + req: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Device: &openrtb2.Device{ + Ext: json.RawMessage(`{"foo":"bar","cdep":"example_label_2"}`), + }, + }, + }, + account: config.Account{ + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + CookieDeprecation: config.CookieDeprecation{ + Enabled: true, + }, + }, + }, + }, + }, + wantDeviceExt: json.RawMessage(`{"foo":"bar","cdep":"example_label_2"}`), + wantErr: nil, + }, + { + name: "header-present-request-device-ext-invalid", + args: args{ + httpReq: &http.Request{ + Header: http.Header{secCookieDeprecation: []string{"example_label_1"}}, + }, + req: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Device: &openrtb2.Device{ + Ext: json.RawMessage(`{`), + }, + }, + }, + account: config.Account{ + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + CookieDeprecation: config.CookieDeprecation{ + Enabled: true, + }, + }, + }, + }, + }, + wantDeviceExt: json.RawMessage(`{`), + wantErr: &errortypes.FailedToUnmarshal{ + Message: "expects \" or n, but found \x00", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateOrFillCDep(tt.args.httpReq, tt.args.req, &tt.args.account) + assert.Equal(t, tt.wantErr, err) + if tt.args.req != nil { + err := tt.args.req.RebuildRequest() + assert.NoError(t, err) + } + if tt.wantDeviceExt == nil { + if tt.args.req != nil && tt.args.req.Device != nil { + assert.Nil(t, tt.args.req.Device.Ext) + } + } else { + assert.Equal(t, string(tt.wantDeviceExt), string(tt.args.req.Device.Ext)) + } + }) + } +} + +func TestValidateRequestCookieDeprecation(t *testing.T) { + testCases := + []struct { + name string + givenAccount *config.Account + httpReq *http.Request + reqWrapper *openrtb_ext.RequestWrapper + wantErrs []error + wantCDep string + }{ + { + name: "header-with-length-less-than-100", + httpReq: func() *http.Request { + req := httptest.NewRequest("POST", "/openrtb2/auction", nil) + req.Header.Set(secCookieDeprecation, "sample-value") + return req + }(), + givenAccount: &config.Account{ + ID: "1", + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + CookieDeprecation: config.CookieDeprecation{ + Enabled: true, + TTLSec: 86400, + }, + }, + }, + }, + reqWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "Some-ID", + App: &openrtb2.App{}, + Imp: []openrtb2.Imp{ + { + ID: "Some-Imp-ID", + Banner: &openrtb2.Banner{ + Format: []openrtb2.Format{ + { + W: 600, + H: 500, + }, + { + W: 300, + H: 600, + }, + }, + }, + Ext: []byte(`{"pubmatic":{"publisherId": 12345678}}`), + }, + }, + }, + }, + wantErrs: []error{}, + wantCDep: "sample-value", + }, + { + name: "header-with-length-more-than-100", + httpReq: func() *http.Request { + req := httptest.NewRequest("POST", "/openrtb2/auction", nil) + req.Header.Set(secCookieDeprecation, "zjfXqGxXFI8yura8AhQl1DK2EMMmryrC8haEpAlwjoerrFfEo2MQTXUq6cSmLohI8gjsnkGU4oAzvXd4TTAESzEKsoYjRJ2zKxmEa") + return req + }(), + givenAccount: &config.Account{ + ID: "1", + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + CookieDeprecation: config.CookieDeprecation{ + Enabled: true, + TTLSec: 86400, + }, + }, + }, + }, + reqWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "Some-ID", + App: &openrtb2.App{}, + Imp: []openrtb2.Imp{ + { + ID: "Some-Imp-ID", + Banner: &openrtb2.Banner{ + Format: []openrtb2.Format{ + { + W: 600, + H: 500, + }, + { + W: 300, + H: 600, + }, + }, + }, + Ext: []byte(`{"pubmatic":{"publisherId": 12345678}}`), + }, + }, + }, + }, + wantErrs: []error{ + &errortypes.Warning{ + Message: "request.device.ext.cdep must not exceed 100 characters", + WarningCode: errortypes.SecCookieDeprecationLenWarningCode, + }, + }, + wantCDep: "", + }, + } + + deps := &endpointDeps{ + fakeUUIDGenerator{}, + &warningsCheckExchange{}, + mockBidderParamValidator{}, + &mockStoredReqFetcher{}, + empty_fetcher.EmptyFetcher{}, + &mockAccountFetcher{}, + &config.Configuration{}, + &metricsConfig.NilMetricsEngine{}, + analyticsBuild.New(&config.Analytics{}), + map[string]string{}, + false, + []byte{}, + openrtb_ext.BuildBidderMap(), + nil, + nil, + hardcodedResponseIPValidator{response: true}, + empty_fetcher.EmptyFetcher{}, + hooks.EmptyPlanBuilder{}, + nil, + openrtb_ext.NormalizeBidderName, + } + + for _, test := range testCases { + errs := deps.validateRequest(test.givenAccount, test.httpReq, test.reqWrapper, false, false, stored_responses.ImpBidderStoredResp{}, false) + assert.Equal(t, test.wantErrs, errs) + test.reqWrapper.RebuildRequest() + deviceExt, err := test.reqWrapper.GetDeviceExt() + assert.NoError(t, err) + assert.Equal(t, test.wantCDep, deviceExt.GetCDep()) + } +} diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go index 22248f1f36c..3ddfc31aa39 100644 --- a/endpoints/openrtb2/video_auction.go +++ b/endpoints/openrtb2/video_auction.go @@ -267,12 +267,6 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re return } - errL = deps.validateRequest(bidReqWrapper, false, false, nil, false) - if errortypes.ContainsFatalError(errL) { - handleError(&labels, w, errL, &vo, &debugLog) - return - } - ctx := context.Background() timeout := deps.cfg.AuctionTimeouts.LimitAuctionTimeout(time.Duration(bidReqWrapper.TMax) * time.Millisecond) if timeout > 0 { @@ -306,6 +300,12 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re return } + errL = deps.validateRequest(account, r, bidReqWrapper, false, false, nil, false) + if errortypes.ContainsFatalError(errL) { + handleError(&labels, w, errL, &vo, &debugLog) + return + } + activityControl = privacy.NewActivityControl(&account.Privacy) secGPC := r.Header.Get("Sec-GPC") diff --git a/errortypes/code.go b/errortypes/code.go index 399dd663498..a30bb8e4bc0 100644 --- a/errortypes/code.go +++ b/errortypes/code.go @@ -32,6 +32,7 @@ const ( BidAdjustmentWarningCode FloorBidRejectionWarningCode InvalidBidResponseDSAWarningCode + SecCookieDeprecationLenWarningCode ) // Coder provides an error or warning code with severity. diff --git a/openrtb_ext/request_wrapper.go b/openrtb_ext/request_wrapper.go index d7ff5acc021..4c4eb9a6988 100644 --- a/openrtb_ext/request_wrapper.go +++ b/openrtb_ext/request_wrapper.go @@ -60,6 +60,7 @@ const ( dataKey = "data" schainKey = "schain" us_privacyKey = "us_privacy" + cdepKey = "cdep" ) // LenImp returns the number of impressions without causing the creation of ImpWrapper objects. @@ -883,6 +884,8 @@ type DeviceExt struct { extDirty bool prebid *ExtDevicePrebid prebidDirty bool + cdep string + cdepDirty bool } func (de *DeviceExt) unmarshal(extJson json.RawMessage) error { @@ -910,6 +913,13 @@ func (de *DeviceExt) unmarshal(extJson json.RawMessage) error { } } + cdepJson, hasCDep := de.ext[cdepKey] + if hasCDep && cdepJson != nil { + if err := jsonutil.Unmarshal(cdepJson, &de.cdep); err != nil { + return err + } + } + return nil } @@ -931,6 +941,19 @@ func (de *DeviceExt) marshal() (json.RawMessage, error) { de.prebidDirty = false } + if de.cdepDirty { + if len(de.cdep) > 0 { + rawjson, err := jsonutil.Marshal(de.cdep) + if err != nil { + return nil, err + } + de.ext[cdepKey] = rawjson + } else { + delete(de.ext, cdepKey) + } + de.cdepDirty = false + } + de.extDirty = false if len(de.ext) == 0 { return nil, nil @@ -939,7 +962,7 @@ func (de *DeviceExt) marshal() (json.RawMessage, error) { } func (de *DeviceExt) Dirty() bool { - return de.extDirty || de.prebidDirty + return de.extDirty || de.prebidDirty || de.cdepDirty } func (de *DeviceExt) GetExt() map[string]json.RawMessage { @@ -968,6 +991,15 @@ func (de *DeviceExt) SetPrebid(prebid *ExtDevicePrebid) { de.prebidDirty = true } +func (de *DeviceExt) GetCDep() string { + return de.cdep +} + +func (de *DeviceExt) SetCDep(cdep string) { + de.cdep = cdep + de.cdepDirty = true +} + func (de *DeviceExt) Clone() *DeviceExt { if de == nil { return nil diff --git a/openrtb_ext/request_wrapper_test.go b/openrtb_ext/request_wrapper_test.go index f04a51a4bdc..f954106d86f 100644 --- a/openrtb_ext/request_wrapper_test.go +++ b/openrtb_ext/request_wrapper_test.go @@ -767,13 +767,13 @@ func TestRebuildDeviceExt(t *testing.T) { { description: "Nil - Dirty", request: openrtb2.BidRequest{}, - requestDeviceExtWrapper: DeviceExt{prebid: &prebidContent1, prebidDirty: true}, - expectedRequest: openrtb2.BidRequest{Device: &openrtb2.Device{Ext: json.RawMessage(`{"prebid":{"interstitial":{"minwidthperc":1,"minheightperc":0}}}`)}}, + requestDeviceExtWrapper: DeviceExt{prebid: &prebidContent1, prebidDirty: true, cdep: "1", cdepDirty: true}, + expectedRequest: openrtb2.BidRequest{Device: &openrtb2.Device{Ext: json.RawMessage(`{"cdep":"1","prebid":{"interstitial":{"minwidthperc":1,"minheightperc":0}}}`)}}, }, { description: "Nil - Dirty - No Change", request: openrtb2.BidRequest{}, - requestDeviceExtWrapper: DeviceExt{prebid: nil, prebidDirty: true}, + requestDeviceExtWrapper: DeviceExt{prebid: nil, prebidDirty: true, cdep: "", cdepDirty: true}, expectedRequest: openrtb2.BidRequest{}, }, { @@ -785,37 +785,37 @@ func TestRebuildDeviceExt(t *testing.T) { { description: "Empty - Dirty", request: openrtb2.BidRequest{Device: &openrtb2.Device{}}, - requestDeviceExtWrapper: DeviceExt{prebid: &prebidContent1, prebidDirty: true}, - expectedRequest: openrtb2.BidRequest{Device: &openrtb2.Device{Ext: json.RawMessage(`{"prebid":{"interstitial":{"minwidthperc":1,"minheightperc":0}}}`)}}, + requestDeviceExtWrapper: DeviceExt{prebid: &prebidContent1, prebidDirty: true, cdep: "1", cdepDirty: true}, + expectedRequest: openrtb2.BidRequest{Device: &openrtb2.Device{Ext: json.RawMessage(`{"cdep":"1","prebid":{"interstitial":{"minwidthperc":1,"minheightperc":0}}}`)}}, }, { description: "Empty - Dirty - No Change", request: openrtb2.BidRequest{Device: &openrtb2.Device{}}, - requestDeviceExtWrapper: DeviceExt{prebid: nil, prebidDirty: true}, + requestDeviceExtWrapper: DeviceExt{prebid: nil, prebidDirty: true, cdep: "", cdepDirty: true}, expectedRequest: openrtb2.BidRequest{Device: &openrtb2.Device{}}, }, { description: "Populated - Not Dirty", - request: openrtb2.BidRequest{Device: &openrtb2.Device{Ext: json.RawMessage(`{"prebid":{"interstitial":{"minwidthperc":1,"minheightperc":0}}}`)}}, + request: openrtb2.BidRequest{Device: &openrtb2.Device{Ext: json.RawMessage(`{"cdep":"1","prebid":{"interstitial":{"minwidthperc":1,"minheightperc":0}}}`)}}, requestDeviceExtWrapper: DeviceExt{}, - expectedRequest: openrtb2.BidRequest{Device: &openrtb2.Device{Ext: json.RawMessage(`{"prebid":{"interstitial":{"minwidthperc":1,"minheightperc":0}}}`)}}, + expectedRequest: openrtb2.BidRequest{Device: &openrtb2.Device{Ext: json.RawMessage(`{"cdep":"1","prebid":{"interstitial":{"minwidthperc":1,"minheightperc":0}}}`)}}, }, { description: "Populated - Dirty", - request: openrtb2.BidRequest{Device: &openrtb2.Device{Ext: json.RawMessage(`{"prebid":{"interstitial":{"minwidthperc":1,"minheightperc":0}}}`)}}, - requestDeviceExtWrapper: DeviceExt{prebid: &prebidContent2, prebidDirty: true}, - expectedRequest: openrtb2.BidRequest{Device: &openrtb2.Device{Ext: json.RawMessage(`{"prebid":{"interstitial":{"minwidthperc":2,"minheightperc":0}}}`)}}, + request: openrtb2.BidRequest{Device: &openrtb2.Device{Ext: json.RawMessage(`{"cdep":"1","prebid":{"interstitial":{"minwidthperc":1,"minheightperc":0}}}`)}}, + requestDeviceExtWrapper: DeviceExt{prebid: &prebidContent2, prebidDirty: true, cdep: "2", cdepDirty: true}, + expectedRequest: openrtb2.BidRequest{Device: &openrtb2.Device{Ext: json.RawMessage(`{"cdep":"2","prebid":{"interstitial":{"minwidthperc":2,"minheightperc":0}}}`)}}, }, { description: "Populated - Dirty - No Change", - request: openrtb2.BidRequest{Device: &openrtb2.Device{Ext: json.RawMessage(`{"prebid":{"interstitial":{"minwidthperc":1,"minheightperc":0}}}`)}}, - requestDeviceExtWrapper: DeviceExt{prebid: &prebidContent1, prebidDirty: true}, - expectedRequest: openrtb2.BidRequest{Device: &openrtb2.Device{Ext: json.RawMessage(`{"prebid":{"interstitial":{"minwidthperc":1,"minheightperc":0}}}`)}}, + request: openrtb2.BidRequest{Device: &openrtb2.Device{Ext: json.RawMessage(`{"cdep":"1","prebid":{"interstitial":{"minwidthperc":1,"minheightperc":0}}}`)}}, + requestDeviceExtWrapper: DeviceExt{prebid: &prebidContent1, prebidDirty: true, cdep: "1", cdepDirty: true}, + expectedRequest: openrtb2.BidRequest{Device: &openrtb2.Device{Ext: json.RawMessage(`{"cdep":"1","prebid":{"interstitial":{"minwidthperc":1,"minheightperc":0}}}`)}}, }, { description: "Populated - Dirty - Cleared", - request: openrtb2.BidRequest{Device: &openrtb2.Device{Ext: json.RawMessage(`{"prebid":{"interstitial":{"minwidthperc":1,"minheightperc":0}}}`)}}, - requestDeviceExtWrapper: DeviceExt{prebid: nil, prebidDirty: true}, + request: openrtb2.BidRequest{Device: &openrtb2.Device{Ext: json.RawMessage(`{"cdep":"1","prebid":{"interstitial":{"minwidthperc":1,"minheightperc":0}}}`)}}, + requestDeviceExtWrapper: DeviceExt{prebid: nil, prebidDirty: true, cdep: "", cdepDirty: true}, expectedRequest: openrtb2.BidRequest{Device: &openrtb2.Device{}}, }, } @@ -1014,6 +1014,8 @@ func TestCloneDeviceExt(t *testing.T) { prebid: &ExtDevicePrebid{ Interstitial: &ExtDeviceInt{MinWidthPerc: 65.0, MinHeightPerc: 75.0}, }, + cdep: "1", + cdepDirty: true, }, devExtCopy: &DeviceExt{ ext: map[string]json.RawMessage{"A": json.RawMessage(`{}`), "B": json.RawMessage(`{"foo":"bar"}`)}, @@ -1021,6 +1023,8 @@ func TestCloneDeviceExt(t *testing.T) { prebid: &ExtDevicePrebid{ Interstitial: &ExtDeviceInt{MinWidthPerc: 65.0, MinHeightPerc: 75.0}, }, + cdep: "1", + cdepDirty: true, }, mutator: func(t *testing.T, devExt *DeviceExt) {}, }, @@ -1032,6 +1036,8 @@ func TestCloneDeviceExt(t *testing.T) { prebid: &ExtDevicePrebid{ Interstitial: &ExtDeviceInt{MinWidthPerc: 65.0, MinHeightPerc: 75.0}, }, + cdep: "1", + cdepDirty: true, }, devExtCopy: &DeviceExt{ ext: map[string]json.RawMessage{"A": json.RawMessage(`{}`), "B": json.RawMessage(`{"foo":"bar"}`)}, @@ -1039,6 +1045,8 @@ func TestCloneDeviceExt(t *testing.T) { prebid: &ExtDevicePrebid{ Interstitial: &ExtDeviceInt{MinWidthPerc: 65, MinHeightPerc: 75}, }, + cdep: "1", + cdepDirty: true, }, mutator: func(t *testing.T, devExt *DeviceExt) { devExt.ext["A"] = json.RawMessage(`"string"`) @@ -1047,6 +1055,8 @@ func TestCloneDeviceExt(t *testing.T) { devExt.prebid.Interstitial.MinHeightPerc = 55 devExt.prebid.Interstitial = &ExtDeviceInt{MinWidthPerc: 80} devExt.prebid = nil + devExt.cdep = "" + devExt.cdepDirty = true }, }, }