diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go index 862311daa6e..6b4fa6890b8 100644 --- a/endpoints/openrtb2/amp_auction.go +++ b/endpoints/openrtb2/amp_auction.go @@ -310,7 +310,7 @@ func (deps *endpointDeps) parseAmpRequest(httpRequest *http.Request) (req *openr // At this point, we should have a valid request that definitely has Targeting and Cache turned on - e = deps.validateRequest(req) + e = deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: req}) errs = append(errs, e...) return } diff --git a/endpoints/openrtb2/amp_auction_test.go b/endpoints/openrtb2/amp_auction_test.go index 61164bd7272..2ce1180d50b 100644 --- a/endpoints/openrtb2/amp_auction_test.go +++ b/endpoints/openrtb2/amp_auction_test.go @@ -66,6 +66,7 @@ func TestGoodAmpRequests(t *testing.T) { var response AmpResponse if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil { + t.Errorf("AMP response was: %s", recorder.Body.Bytes()) t.Fatalf("Error unmarshalling response: %s", err.Error()) } diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go index c9f2bbdb68f..a6b3479a36c 100644 --- a/endpoints/openrtb2/auction.go +++ b/endpoints/openrtb2/auction.go @@ -174,10 +174,17 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http return } + // rebuild/resync the request in the request wrapper. + if err := req.RebuildRequest(); err != nil { + errL = append(errL, err) + writeError(errL, w, &labels) + return + } + secGPC := r.Header.Get("Sec-GPC") auctionRequest := exchange.AuctionRequest{ - BidRequest: req, + BidRequest: req.BidRequest, Account: *account, UserSyncs: usersyncs, RequestType: labels.RType, @@ -188,7 +195,7 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http } response, err := deps.ex.HoldAuction(ctx, auctionRequest, nil) - ao.Request = req + ao.Request = req.BidRequest ao.Response = response ao.Account = account if err != nil { @@ -227,8 +234,9 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http // possible, it will return errors with messages that suggest improvements. // // If the errors list has at least one element, then no guarantees are made about the returned request. -func (deps *endpointDeps) parseRequest(httpRequest *http.Request) (req *openrtb2.BidRequest, errs []error) { - req = &openrtb2.BidRequest{} +func (deps *endpointDeps) parseRequest(httpRequest *http.Request) (req *openrtb_ext.RequestWrapper, errs []error) { + req = &openrtb_ext.RequestWrapper{} + req.BidRequest = &openrtb2.BidRequest{} errs = nil // Pull the request body into a buffer, so we have it for later usage. @@ -258,20 +266,20 @@ func (deps *endpointDeps) parseRequest(httpRequest *http.Request) (req *openrtb2 return } - if err := json.Unmarshal(requestJson, req); err != nil { + if err := json.Unmarshal(requestJson, req.BidRequest); err != nil { errs = []error{err} return } // Populate any "missing" OpenRTB fields with info from other sources, (e.g. HTTP request headers). - deps.setFieldsImplicitly(httpRequest, req) + deps.setFieldsImplicitly(httpRequest, req.BidRequest) if err := processInterstitials(req); err != nil { errs = []error{err} return } - lmt.ModifyForIOS(req) + lmt.ModifyForIOS(req.BidRequest) errL := deps.validateRequest(req) if len(errL) > 0 { @@ -296,7 +304,7 @@ func parseTimeout(requestJson []byte, defaultTimeout time.Duration) time.Duratio return defaultTimeout } -func (deps *endpointDeps) validateRequest(req *openrtb2.BidRequest) []error { +func (deps *endpointDeps) validateRequest(req *openrtb_ext.RequestWrapper) []error { errL := []error{} if req.ID == "" { return []error{errors.New("request missing required field: \"id\"")} @@ -318,34 +326,39 @@ func (deps *endpointDeps) validateRequest(req *openrtb2.BidRequest) []error { // If automatically filling source TID is enabled then validate that // source.TID exists and If it doesn't, fill it with a randomly generated UUID if deps.cfg.AutoGenSourceTID { - if err := validateAndFillSourceTID(req); err != nil { + if err := validateAndFillSourceTID(req.BidRequest); err != nil { return []error{err} } } var aliases map[string]string - if bidExt, err := deps.parseBidExt(req.Ext); err != nil { + reqExt, err := req.GetRequestExt() + if err != nil { + return []error{fmt.Errorf("request.ext is invalid: %v", err)} + } + reqPrebid := reqExt.GetPrebid() + if err := deps.parseBidExt(req); err != nil { return []error{err} - } else if bidExt != nil { - aliases = bidExt.Prebid.Aliases + } else if reqPrebid != nil { + aliases = reqPrebid.Aliases if err := deps.validateAliases(aliases); err != nil { return []error{err} } - if err := deps.validateBidAdjustmentFactors(bidExt.Prebid.BidAdjustmentFactors, aliases); err != nil { + if err := deps.validateBidAdjustmentFactors(reqPrebid.BidAdjustmentFactors, aliases); err != nil { return []error{err} } - if err := validateSChains(bidExt); err != nil { + if err := validateSChains(reqPrebid.SChains); err != nil { return []error{err} } - if err := deps.validateEidPermissions(bidExt, aliases); err != nil { + if err := deps.validateEidPermissions(reqPrebid.Data, aliases); err != nil { return []error{err} } - if err := validateCustomRates(bidExt.Prebid.CurrencyConversions); err != nil { + if err := validateCustomRates(reqPrebid.CurrencyConversions); err != nil { return []error{err} } } @@ -354,19 +367,19 @@ func (deps *endpointDeps) validateRequest(req *openrtb2.BidRequest) []error { return append(errL, errors.New("request.site or request.app must be defined, but not both.")) } - if err := deps.validateSite(req.Site); err != nil { + if err := deps.validateSite(req); err != nil { return append(errL, err) } - if err := deps.validateApp(req.App); err != nil { + if err := deps.validateApp(req); err != nil { return append(errL, err) } - if err := deps.validateUser(req.User, aliases); err != nil { + if err := deps.validateUser(req, aliases); err != nil { return append(errL, err) } - if err := validateRegs(req.Regs); err != nil { + if err := validateRegs(req); err != nil { return append(errL, err) } @@ -374,17 +387,18 @@ func (deps *endpointDeps) validateRequest(req *openrtb2.BidRequest) []error { return append(errL, err) } - if ccpaPolicy, err := ccpa.ReadFromRequest(req); err != nil { + if ccpaPolicy, err := ccpa.ReadFromRequestWrapper(req); err != nil { return append(errL, err) } else if _, err := ccpaPolicy.Parse(exchange.GetValidBidders(aliases)); err != nil { if _, invalidConsent := err.(*errortypes.Warning); invalidConsent { errL = append(errL, &errortypes.Warning{ Message: fmt.Sprintf("CCPA consent is invalid and will be ignored. (%v)", err), WarningCode: errortypes.InvalidPrivacyConsentWarningCode}) - consentWriter := ccpa.ConsentWriter{Consent: ""} - if err := consentWriter.Write(req); err != nil { - return append(errL, fmt.Errorf("Unable to remove invalid CCPA consent from the request. (%v)", err)) + regsExt, err := req.GetRegExt() + if err != nil { + return append(errL, err) } + regsExt.SetUSPrivacy("") } else { return append(errL, err) } @@ -437,8 +451,8 @@ func (deps *endpointDeps) validateBidAdjustmentFactors(adjustmentFactors map[str return nil } -func validateSChains(req *openrtb_ext.ExtRequest) error { - _, err := exchange.BidderToPrebidSChains(req) +func validateSChains(sChains []*openrtb_ext.ExtRequestPrebidSChain) error { + _, err := exchange.BidderToPrebidSChains(sChains) return err } @@ -466,13 +480,13 @@ func validateCustomRates(bidReqCurrencyRates *openrtb_ext.ExtRequestCurrency) er return nil } -func (deps *endpointDeps) validateEidPermissions(req *openrtb_ext.ExtRequest, aliases map[string]string) error { - if req == nil || req.Prebid.Data == nil { +func (deps *endpointDeps) validateEidPermissions(prebid *openrtb_ext.ExtRequestPrebidData, aliases map[string]string) error { + if prebid == nil { return nil } - uniqueSources := make(map[string]struct{}, len(req.Prebid.Data.EidPermissions)) - for i, eid := range req.Prebid.Data.EidPermissions { + uniqueSources := make(map[string]struct{}, len(prebid.EidPermissions)) + for i, eid := range prebid.EidPermissions { if len(eid.Source) == 0 { return fmt.Errorf(`request.ext.prebid.data.eidpermissions[%d] missing required field: "source"`, i) } @@ -1045,15 +1059,11 @@ func isBidderToValidate(bidder string) bool { } } -func (deps *endpointDeps) parseBidExt(ext json.RawMessage) (*openrtb_ext.ExtRequest, error) { - if len(ext) < 1 { - return nil, nil +func (deps *endpointDeps) parseBidExt(req *openrtb_ext.RequestWrapper) error { + if _, err := req.GetRequestExt(); err != nil { + return fmt.Errorf("request.ext is invalid: %v", err) } - var tmpExt openrtb_ext.ExtRequest - if err := json.Unmarshal(ext, &tmpExt); err != nil { - return nil, fmt.Errorf("request.ext is invalid: %v", err) - } - return &tmpExt, nil + return nil } func (deps *endpointDeps) validateAliases(aliases map[string]string) error { @@ -1073,120 +1083,112 @@ func (deps *endpointDeps) validateAliases(aliases map[string]string) error { return nil } -func (deps *endpointDeps) validateSite(site *openrtb2.Site) error { - if site == nil { +func (deps *endpointDeps) validateSite(req *openrtb_ext.RequestWrapper) error { + if req.Site == nil { return nil } - if site.ID == "" && site.Page == "" { + if req.Site.ID == "" && req.Site.Page == "" { return errors.New("request.site should include at least one of request.site.id or request.site.page.") } - if len(site.Ext) > 0 { - var s openrtb_ext.ExtSite - if err := json.Unmarshal(site.Ext, &s); err != nil { - return err - } + siteExt, err := req.GetSiteExt() + if err != nil { + return err + } + siteAmp := siteExt.GetAmp() + if siteAmp < 0 || siteAmp > 1 { + return errors.New(`request.site.ext.amp must be either 1, 0, or undefined`) } return nil } -func (deps *endpointDeps) validateApp(app *openrtb2.App) error { - if app == nil { +func (deps *endpointDeps) validateApp(req *openrtb_ext.RequestWrapper) error { + if req.App == nil { return nil } - if app.ID != "" { - if _, found := deps.cfg.BlacklistedAppMap[app.ID]; found { - return &errortypes.BlacklistedApp{Message: fmt.Sprintf("Prebid-server does not process requests from App ID: %s", app.ID)} - } - } - - if len(app.Ext) > 0 { - var a openrtb_ext.ExtApp - if err := json.Unmarshal(app.Ext, &a); err != nil { - return err + if req.App.ID != "" { + if _, found := deps.cfg.BlacklistedAppMap[req.App.ID]; found { + return &errortypes.BlacklistedApp{Message: fmt.Sprintf("Prebid-server does not process requests from App ID: %s", req.App.ID)} } } - return nil + _, err := req.GetAppExt() + return err } -func (deps *endpointDeps) validateUser(user *openrtb2.User, aliases map[string]string) error { - if user == nil { - return nil - } - +func (deps *endpointDeps) validateUser(req *openrtb_ext.RequestWrapper, aliases map[string]string) error { // The following fields were previously uints in the OpenRTB library we use, but have // since been changed to ints. We decided to maintain the non-negative check. - if user.Geo != nil && user.Geo.Accuracy < 0 { - return errors.New("request.user.geo.accuracy must be a positive number") - } - - if user.Ext != nil { - // Creating ExtUser object - var userExt openrtb_ext.ExtUser - if err := json.Unmarshal(user.Ext, &userExt); err == nil { - // Check if the buyeruids are valid - if userExt.Prebid != nil { - if len(userExt.Prebid.BuyerUIDs) < 1 { - return errors.New(`request.user.ext.prebid requires a "buyeruids" property with at least one ID defined. If none exist, then request.user.ext.prebid should not be defined.`) - } - for bidderName := range userExt.Prebid.BuyerUIDs { - if _, ok := deps.bidderMap[bidderName]; !ok { - if _, ok := aliases[bidderName]; !ok { - return fmt.Errorf("request.user.ext.%s is neither a known bidder name nor an alias in request.ext.prebid.aliases.", bidderName) - } - } + if req != nil && req.BidRequest != nil && req.User != nil { + if req.User.Geo != nil && req.User.Geo.Accuracy < 0 { + return errors.New("request.user.geo.accuracy must be a positive number") + } + } + + userExt, err := req.GetUserExt() + if err != nil { + return fmt.Errorf("request.user.ext object is not valid: %v", err) + } + // Check if the buyeruids are valid + prebid := userExt.GetPrebid() + if prebid != nil { + if len(prebid.BuyerUIDs) < 1 { + return errors.New(`request.user.ext.prebid requires a "buyeruids" property with at least one ID defined. If none exist, then request.user.ext.prebid should not be defined.`) + } + for bidderName := range prebid.BuyerUIDs { + if _, ok := deps.bidderMap[bidderName]; !ok { + if _, ok := aliases[bidderName]; !ok { + return fmt.Errorf("request.user.ext.%s is neither a known bidder name nor an alias in request.ext.prebid.aliases.", bidderName) } } - // Check Universal User ID - if userExt.Eids != nil { - if len(userExt.Eids) == 0 { - return fmt.Errorf("request.user.ext.eids must contain at least one element or be undefined") - } - uniqueSources := make(map[string]struct{}, len(userExt.Eids)) - for eidIndex, eid := range userExt.Eids { - if eid.Source == "" { - return fmt.Errorf("request.user.ext.eids[%d] missing required field: \"source\"", eidIndex) - } - if _, ok := uniqueSources[eid.Source]; ok { - return fmt.Errorf("request.user.ext.eids must contain unique sources") - } - uniqueSources[eid.Source] = struct{}{} + } + } + // Check Universal User ID + eids := userExt.GetEid() + if eids != nil { + if len(*eids) == 0 { + return errors.New("request.user.ext.eids must contain at least one element or be undefined") + } + uniqueSources := make(map[string]struct{}, len(*eids)) + for eidIndex, eid := range *eids { + if eid.Source == "" { + return fmt.Errorf("request.user.ext.eids[%d] missing required field: \"source\"", eidIndex) + } + if _, ok := uniqueSources[eid.Source]; ok { + return errors.New("request.user.ext.eids must contain unique sources") + } + uniqueSources[eid.Source] = struct{}{} - if eid.ID == "" && eid.Uids == nil { - return fmt.Errorf("request.user.ext.eids[%d] must contain either \"id\" or \"uids\" field", eidIndex) - } - if eid.ID == "" { - if len(eid.Uids) == 0 { - return fmt.Errorf("request.user.ext.eids[%d].uids must contain at least one element or be undefined", eidIndex) - } - for uidIndex, uid := range eid.Uids { - if uid.ID == "" { - return fmt.Errorf("request.user.ext.eids[%d].uids[%d] missing required field: \"id\"", eidIndex, uidIndex) - } - } + if eid.ID == "" && eid.Uids == nil { + return fmt.Errorf("request.user.ext.eids[%d] must contain either \"id\" or \"uids\" field", eidIndex) + } + if eid.ID == "" { + if len(eid.Uids) == 0 { + return fmt.Errorf("request.user.ext.eids[%d].uids must contain at least one element or be undefined", eidIndex) + } + for uidIndex, uid := range eid.Uids { + if uid.ID == "" { + return fmt.Errorf("request.user.ext.eids[%d].uids[%d] missing required field: \"id\"", eidIndex, uidIndex) } } } - } else { - return fmt.Errorf("request.user.ext object is not valid: %v", err) } } return nil } -func validateRegs(regs *openrtb2.Regs) error { - if regs != nil && len(regs.Ext) > 0 { - var regsExt openrtb_ext.ExtRegs - if err := json.Unmarshal(regs.Ext, ®sExt); err != nil { - return fmt.Errorf("request.regs.ext is invalid: %v", err) - } - if regsExt.GDPR != nil && (*regsExt.GDPR < 0 || *regsExt.GDPR > 1) { - return errors.New("request.regs.ext.gdpr must be either 0 or 1.") - } +func validateRegs(req *openrtb_ext.RequestWrapper) error { + regsExt, err := req.GetRegExt() + if err != nil { + return fmt.Errorf("request.regs.ext is invalid: %v", err) + } + regExt := regsExt.GetExt() + gdprJSON, hasGDPR := regExt["gdpr"] + if hasGDPR && (string(gdprJSON) != "0" && string(gdprJSON) != "1") { + return errors.New("request.regs.ext.gdpr must be either 0 or 1.") } return nil } diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go index bcdac13dc06..4835dd92943 100644 --- a/endpoints/openrtb2/auction_test.go +++ b/endpoints/openrtb2/auction_test.go @@ -1587,7 +1587,7 @@ func TestCurrencyTrunc(t *testing.T) { Cur: []string{"USD", "EUR"}, } - errL := deps.validateRequest(&req) + errL := deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req}) 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}) @@ -1633,14 +1633,12 @@ func TestCCPAInvalid(t *testing.T) { }, } - errL := deps.validateRequest(&req) + errL := deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req}) expectedWarning := errortypes.Warning{ Message: "CCPA consent is invalid and will be ignored. (request.regs.ext.us_privacy must contain 4 characters)", WarningCode: errortypes.InvalidPrivacyConsentWarningCode} assert.ElementsMatch(t, errL, []error{&expectedWarning}) - - assert.Empty(t, req.Regs.Ext, "Invalid Consent Removed From Request") } func TestNoSaleInvalid(t *testing.T) { @@ -1684,7 +1682,7 @@ func TestNoSaleInvalid(t *testing.T) { Ext: json.RawMessage(`{"prebid": {"nosale": ["*", "appnexus"]} }`), } - errL := deps.validateRequest(&req) + errL := deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req}) 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}) @@ -1731,7 +1729,7 @@ func TestValidateSourceTID(t *testing.T) { }, } - deps.validateRequest(&req) + deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req}) assert.NotEmpty(t, req.Source.TID, "Expected req.Source.TID to be filled with a randomly generated UID") } @@ -1773,7 +1771,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(&req) + errL := deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req}) 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}) @@ -1992,7 +1990,7 @@ func TestEidPermissionsInvalid(t *testing.T) { Ext: json.RawMessage(`{"prebid": {"data": {"eidpermissions": [{"source":"a", "bidders":[]}]} } }`), } - errL := deps.validateRequest(&req) + errL := deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req}) expectedError := errors.New(`request.ext.prebid.data.eidpermissions[0] missing or empty required field: "bidders"`) assert.ElementsMatch(t, errL, []error{expectedError}) @@ -2007,11 +2005,6 @@ func TestValidateEidPermissions(t *testing.T) { request *openrtb_ext.ExtRequest expectedError error }{ - { - description: "Valid - Nil ext", - request: nil, - expectedError: nil, - }, { description: "Valid - Empty ext", request: &openrtb_ext.ExtRequest{}, @@ -2096,7 +2089,7 @@ func TestValidateEidPermissions(t *testing.T) { endpoint := &endpointDeps{bidderMap: knownBidders} for _, test := range testCases { - result := endpoint.validateEidPermissions(test.request, knownAliases) + result := endpoint.validateEidPermissions(test.request.Prebid.Data, knownAliases) assert.Equal(t, test.expectedError, result, test.description) } } diff --git a/endpoints/openrtb2/interstitial.go b/endpoints/openrtb2/interstitial.go index 1aa2a7fc890..359bae11d4c 100644 --- a/endpoints/openrtb2/interstitial.go +++ b/endpoints/openrtb2/interstitial.go @@ -1,7 +1,6 @@ package openrtb2 import ( - "encoding/json" "fmt" "github.com/mxmCherry/openrtb/v15/openrtb2" @@ -10,26 +9,27 @@ import ( "github.com/prebid/prebid-server/openrtb_ext" ) -func processInterstitials(req *openrtb2.BidRequest) error { - var devExt openrtb_ext.ExtDevice +func processInterstitials(req *openrtb_ext.RequestWrapper) error { unmarshalled := true for i := range req.Imp { if req.Imp[i].Instl == 1 { + var prebid *openrtb_ext.ExtDevicePrebid if unmarshalled { if req.Device.Ext == nil { // No special interstitial support requested, so bail as there is nothing to do return nil } - err := json.Unmarshal(req.Device.Ext, &devExt) + deviceExt, err := req.GetDeviceExt() if err != nil { return err } - if devExt.Prebid.Interstitial == nil { + prebid = deviceExt.GetPrebid() + if prebid.Interstitial == nil { // No special interstitial support requested, so bail as there is nothing to do return nil } } - err := processInterstitialsForImp(&req.Imp[i], &devExt, req.Device) + err := processInterstitialsForImp(&req.Imp[i], prebid, req.Device) if err != nil { return err } @@ -38,7 +38,7 @@ func processInterstitials(req *openrtb2.BidRequest) error { return nil } -func processInterstitialsForImp(imp *openrtb2.Imp, devExt *openrtb_ext.ExtDevice, device *openrtb2.Device) error { +func processInterstitialsForImp(imp *openrtb2.Imp, devExtPrebid *openrtb_ext.ExtDevicePrebid, device *openrtb2.Device) error { var maxWidth, maxHeight, minWidth, minHeight int64 if imp.Banner == nil { // custom interstitial support is only available for banner requests. @@ -56,8 +56,8 @@ func processInterstitialsForImp(imp *openrtb2.Imp, devExt *openrtb_ext.ExtDevice maxWidth = device.W maxHeight = device.H } - minWidth = (maxWidth * int64(devExt.Prebid.Interstitial.MinWidthPerc)) / 100 - minHeight = (maxHeight * int64(devExt.Prebid.Interstitial.MinHeightPerc)) / 100 + minWidth = (maxWidth * devExtPrebid.Interstitial.MinWidthPerc) / 100 + minHeight = (maxHeight * devExtPrebid.Interstitial.MinHeightPerc) / 100 imp.Banner.Format = genInterstitialFormat(minWidth, maxWidth, minHeight, maxHeight) if len(imp.Banner.Format) == 0 { return &errortypes.BadInput{Message: fmt.Sprintf("Unable to set interstitial size list for Imp id=%s (No valid sizes between %dx%d and %dx%d)", imp.ID, minWidth, minHeight, maxWidth, maxHeight)} diff --git a/endpoints/openrtb2/interstitial_test.go b/endpoints/openrtb2/interstitial_test.go index 1d7ad9e3d6b..fe0ed966c3c 100644 --- a/endpoints/openrtb2/interstitial_test.go +++ b/endpoints/openrtb2/interstitial_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/mxmCherry/openrtb/v15/openrtb2" + "github.com/prebid/prebid-server/openrtb_ext" "github.com/stretchr/testify/assert" ) @@ -34,7 +35,7 @@ var request = &openrtb2.BidRequest{ func TestInterstitial(t *testing.T) { myRequest := request - if err := processInterstitials(myRequest); err != nil { + if err := processInterstitials(&openrtb_ext.RequestWrapper{BidRequest: myRequest}); err != nil { t.Fatalf("Error processing interstitials: %v", err) } targetFormat := []openrtb2.Format{ diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/invalid-source.json b/endpoints/openrtb2/sample-requests/invalid-whole/invalid-source.json index 8385f924a56..5aa7fd4dea1 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/invalid-source.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/invalid-source.json @@ -39,5 +39,5 @@ ] }, "expectedReturnCode": 400, - "expectedErrorMessage": "Invalid request: json: cannot unmarshal object into Go struct field ExtAppPrebid.prebid.source of type string" + "expectedErrorMessage": "Invalid request: json: cannot unmarshal object into Go struct field ExtAppPrebid.source of type string" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-gdpr-string.json b/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-gdpr-string.json index afdabdab7cf..4a315911906 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-gdpr-string.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-gdpr-string.json @@ -44,5 +44,5 @@ } }, "expectedReturnCode": 400, - "expectedErrorMessage": "Invalid request: request.regs.ext is invalid: json: cannot unmarshal string into Go struct field ExtRegs.gdpr of type int8\n" + "expectedErrorMessage": "Invalid request: request.regs.ext.gdpr must be either 0 or 1.\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-malformed.json b/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-malformed.json index a8e94008cf1..ab44e3e2428 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-malformed.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-malformed.json @@ -42,5 +42,5 @@ } }, "expectedReturnCode": 400, - "expectedErrorMessage": "Invalid request: request.regs.ext is invalid: json: cannot unmarshal string into Go value of type openrtb_ext.ExtRegs\n" + "expectedErrorMessage": "Invalid request: request.regs.ext is invalid: json: cannot unmarshal string into Go value of type map[string]json.RawMessage\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-consent-int.json b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-consent-int.json index b61be105df0..a26db8a5695 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-consent-int.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-consent-int.json @@ -46,5 +46,5 @@ } }, "expectedReturnCode": 400, - "expectedErrorMessage": "Invalid request: request.user.ext object is not valid: json: cannot unmarshal number into Go struct field ExtUser.consent of type string\n" + "expectedErrorMessage": "Invalid request: request.user.ext object is not valid: json: cannot unmarshal number into Go value of type string\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/user-gdpr-consent-invalid.json b/endpoints/openrtb2/sample-requests/invalid-whole/user-gdpr-consent-invalid.json index 08eed44b2b0..c4646550dd2 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/user-gdpr-consent-invalid.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/user-gdpr-consent-invalid.json @@ -41,5 +41,5 @@ } }, "expectedReturnCode": 400, - "expectedErrorMessage": "Invalid request: request.user.ext object is not valid: json: cannot unmarshal number into Go struct field ExtUser.consent of type string" + "expectedErrorMessage": "Invalid request: request.user.ext object is not valid: json: cannot unmarshal number into Go value of type string" } diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go index 0af3ba512bb..227f6c4a943 100644 --- a/endpoints/openrtb2/video_auction.go +++ b/endpoints/openrtb2/video_auction.go @@ -241,7 +241,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re // Populate any "missing" OpenRTB fields with info from other sources, (e.g. HTTP request headers). deps.setFieldsImplicitly(r, bidReq) // move after merge - errL = deps.validateRequest(bidReq) + errL = deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: bidReq}) if errortypes.ContainsFatalError(errL) { handleError(&labels, w, errL, &vo, &debugLog) return diff --git a/endpoints/openrtb2/video_auction_test.go b/endpoints/openrtb2/video_auction_test.go index 9f0859a32cd..5452d6c2c39 100644 --- a/endpoints/openrtb2/video_auction_test.go +++ b/endpoints/openrtb2/video_auction_test.go @@ -1087,11 +1087,13 @@ func TestCCPA(t *testing.T) { description string testFilePath string expectConsentString bool + expectEmptyConsent bool }{ { description: "Missing Consent", testFilePath: "sample-requests/video/video_valid_sample.json", expectConsentString: false, + expectEmptyConsent: true, }, { description: "Valid Consent", @@ -1132,7 +1134,7 @@ func TestCCPA(t *testing.T) { } if test.expectConsentString { assert.Len(t, extRegs.USPrivacy, 4, test.description+":consent") - } else { + } else if test.expectEmptyConsent { assert.Empty(t, extRegs.USPrivacy, test.description+":consent") } diff --git a/exchange/utils.go b/exchange/utils.go index 0befeacdedd..120152466a8 100644 --- a/exchange/utils.go +++ b/exchange/utils.go @@ -28,18 +28,16 @@ var integrationTypeMap = map[metrics.RequestType]config.IntegrationType{ const unknownBidder string = "" -func BidderToPrebidSChains(req *openrtb_ext.ExtRequest) (map[string]*openrtb_ext.ExtRequestPrebidSChainSChain, error) { +func BidderToPrebidSChains(sChains []*openrtb_ext.ExtRequestPrebidSChain) (map[string]*openrtb_ext.ExtRequestPrebidSChainSChain, error) { bidderToSChains := make(map[string]*openrtb_ext.ExtRequestPrebidSChainSChain) - if req != nil { - for _, schainWrapper := range req.Prebid.SChains { - for _, bidder := range schainWrapper.Bidders { - if _, present := bidderToSChains[bidder]; present { - return nil, fmt.Errorf("request.ext.prebid.schains contains multiple schains for bidder %s; "+ - "it must contain no more than one per bidder.", bidder) - } else { - bidderToSChains[bidder] = &schainWrapper.SChain - } + for _, schainWrapper := range sChains { + for _, bidder := range schainWrapper.Bidders { + if _, present := bidderToSChains[bidder]; present { + return nil, fmt.Errorf("request.ext.prebid.schains contains multiple schains for bidder %s; "+ + "it must contain no more than one per bidder.", bidder) + } else { + bidderToSChains[bidder] = &schainWrapper.SChain } } } @@ -178,7 +176,8 @@ func ccpaEnabled(account *config.Account, privacyConfig config.Privacy, requestT } func extractCCPA(orig *openrtb2.BidRequest, privacyConfig config.Privacy, account *config.Account, aliases map[string]string, requestType config.IntegrationType) (privacy.PolicyEnforcer, error) { - ccpaPolicy, err := ccpa.ReadFromRequest(orig) + // Quick extra wrapper until RequestWrapper makes its way into CleanRequests + ccpaPolicy, err := ccpa.ReadFromRequestWrapper(&openrtb_ext.RequestWrapper{BidRequest: orig}) if err != nil { return privacy.NilPolicyEnforcer{}, err } @@ -217,9 +216,12 @@ func getAuctionBidderRequests(req AuctionRequest, var sChainsByBidder map[string]*openrtb_ext.ExtRequestPrebidSChainSChain - sChainsByBidder, err = BidderToPrebidSChains(requestExt) - if err != nil { - return nil, []error{err} + // Quick extra wrapper until RequestWrapper makes its way into CleanRequests + if requestExt != nil { + sChainsByBidder, err = BidderToPrebidSChains(requestExt.Prebid.SChains) + if err != nil { + return nil, []error{err} + } } reqExt, err := getExtJson(req.BidRequest, requestExt) diff --git a/exchange/utils_test.go b/exchange/utils_test.go index 5a9fa187f62..1d13928b59c 100644 --- a/exchange/utils_test.go +++ b/exchange/utils_test.go @@ -1908,7 +1908,7 @@ func TestBidderToPrebidChains(t *testing.T) { }, } - output, err := BidderToPrebidSChains(&input) + output, err := BidderToPrebidSChains(input.Prebid.SChains) assert.Nil(t, err) assert.Equal(t, len(output), 4) @@ -1934,7 +1934,7 @@ func TestBidderToPrebidChainsDiscardMultipleChainsForBidder(t *testing.T) { }, } - output, err := BidderToPrebidSChains(&input) + output, err := BidderToPrebidSChains(input.Prebid.SChains) assert.NotNil(t, err) assert.Nil(t, output) @@ -1947,7 +1947,7 @@ func TestBidderToPrebidChainsNilSChains(t *testing.T) { }, } - output, err := BidderToPrebidSChains(&input) + output, err := BidderToPrebidSChains(input.Prebid.SChains) assert.Nil(t, err) assert.Equal(t, len(output), 0) @@ -1960,7 +1960,7 @@ func TestBidderToPrebidChainsZeroLengthSChains(t *testing.T) { }, } - output, err := BidderToPrebidSChains(&input) + output, err := BidderToPrebidSChains(input.Prebid.SChains) assert.Nil(t, err) assert.Equal(t, len(output), 0) diff --git a/go.sum b/go.sum index 4cb5863dd41..df2bfb9b459 100644 --- a/go.sum +++ b/go.sum @@ -80,9 +80,11 @@ github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.1 h1:foqVmeWDD6yYpK+Yz3fHyNIxFYNxswxqNFjSKe+vI54= github.com/onsi/ginkgo v1.16.1/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.11.0 h1:+CqWgvj0OZycCaqclBD1pxKHAU+tOkHmQIWvDHq2aug= diff --git a/openrtb_ext/device.go b/openrtb_ext/device.go index cc06f3806cf..1e8605562d2 100644 --- a/openrtb_ext/device.go +++ b/openrtb_ext/device.go @@ -70,8 +70,8 @@ type ExtDevicePrebid struct { // ExtDeviceInt defines the contract for bidrequest.device.ext.prebid.interstitial type ExtDeviceInt struct { - MinWidthPerc uint64 `json:"minwidtheperc"` - MinHeightPerc uint64 `json:"minheightperc"` + MinWidthPerc int64 `json:"minwidtheperc"` + MinHeightPerc int64 `json:"minheightperc"` } func (edi *ExtDeviceInt) UnmarshalJSON(b []byte) error { @@ -85,7 +85,7 @@ func (edi *ExtDeviceInt) UnmarshalJSON(b []byte) error { if err != nil || perc < 0 || perc > 100 { return &errortypes.BadInput{Message: "request.device.ext.prebid.interstitial.minwidthperc must be a number between 0 and 100"} } - edi.MinWidthPerc = uint64(perc) + edi.MinWidthPerc = int64(perc) } if value, dataType, _, _ := jsonparser.Get(b, "minheightperc"); dataType != jsonparser.Number { return &errortypes.BadInput{Message: "request.device.ext.prebid.interstitial.minheightperc must be a number between 0 and 100"} @@ -94,7 +94,7 @@ func (edi *ExtDeviceInt) UnmarshalJSON(b []byte) error { if err != nil || perc < 0 || perc > 100 { return &errortypes.BadInput{Message: "request.device.ext.prebid.interstitial.minheightperc must be a number between 0 and 100"} } - edi.MinHeightPerc = uint64(perc) + edi.MinHeightPerc = int64(perc) } return nil } diff --git a/openrtb_ext/request_wrapper.go b/openrtb_ext/request_wrapper.go new file mode 100644 index 00000000000..276f8f5eebe --- /dev/null +++ b/openrtb_ext/request_wrapper.go @@ -0,0 +1,787 @@ +package openrtb_ext + +import ( + "encoding/json" + "errors" + + "github.com/mxmCherry/openrtb/v15/openrtb2" +) + +// RequestWrapper wraps the OpenRTB request to provide a storage location for unmarshalled ext fields, so they +// will not need to be unmarshalled multiple times. +// +// To start with, the wrapper can be created for a request 'req' via: +// reqWrapper := openrtb_ext.RequestWrapper{BidRequest: req} +// +// In order to access an object's ext field, fetch it via: +// userExt, err := reqWrapper.GetUserExt() +// or other Get method as appropriate. +// +// To read or write values, use the Ext objects Get and Set methods. If you need to write to a field that has its own Set +// method, use that to set the value rather than using SetExt() with that change done in the map; when rewritting the +// ext JSON the code will overwrite the the values in the map with the values stored in the seperate fields. +// +// userPrebid := userExt.GetPrebid() +// userExt.SetConsent(consentString) +// +// The GetExt() and SetExt() should only be used to access fields that have not already been resolved in the object. +// Using SetExt() at all is a strong hint that the ext object should be extended to support the new fields being set +// in the map. +// +// NOTE: The RequestWrapper methods (particularly the ones calling (un)Marshal are not thread safe) + +type RequestWrapper struct { + *openrtb2.BidRequest + userExt *UserExt + deviceExt *DeviceExt + requestExt *RequestExt + appExt *AppExt + regExt *RegExt + siteExt *SiteExt +} + +func (rw *RequestWrapper) GetUserExt() (*UserExt, error) { + if rw.userExt != nil { + return rw.userExt, nil + } + rw.userExt = &UserExt{} + if rw.BidRequest == nil || rw.User == nil || rw.User.Ext == nil { + return rw.userExt, rw.userExt.unmarshal(json.RawMessage{}) + } + + return rw.userExt, rw.userExt.unmarshal(rw.User.Ext) +} + +func (rw *RequestWrapper) GetDeviceExt() (*DeviceExt, error) { + if rw.deviceExt != nil { + return rw.deviceExt, nil + } + rw.deviceExt = &DeviceExt{} + if rw.BidRequest == nil || rw.Device == nil || rw.Device.Ext == nil { + return rw.deviceExt, rw.deviceExt.unmarshal(json.RawMessage{}) + } + return rw.deviceExt, rw.deviceExt.unmarshal(rw.Device.Ext) +} + +func (rw *RequestWrapper) GetRequestExt() (*RequestExt, error) { + if rw.requestExt != nil { + return rw.requestExt, nil + } + rw.requestExt = &RequestExt{} + if rw.BidRequest == nil || rw.Ext == nil { + return rw.requestExt, rw.requestExt.unmarshal(json.RawMessage{}) + } + return rw.requestExt, rw.requestExt.unmarshal(rw.Ext) +} + +func (rw *RequestWrapper) GetAppExt() (*AppExt, error) { + if rw.appExt != nil { + return rw.appExt, nil + } + rw.appExt = &AppExt{} + if rw.BidRequest == nil || rw.App == nil || rw.App.Ext == nil { + return rw.appExt, rw.appExt.unmarshal(json.RawMessage{}) + } + return rw.appExt, rw.appExt.unmarshal(rw.App.Ext) +} + +func (rw *RequestWrapper) GetRegExt() (*RegExt, error) { + if rw.regExt != nil { + return rw.regExt, nil + } + rw.regExt = &RegExt{} + if rw.BidRequest == nil || rw.Regs == nil || rw.Regs.Ext == nil { + return rw.regExt, rw.regExt.unmarshal(json.RawMessage{}) + } + return rw.regExt, rw.regExt.unmarshal(rw.Regs.Ext) +} + +func (rw *RequestWrapper) GetSiteExt() (*SiteExt, error) { + if rw.siteExt != nil { + return rw.siteExt, nil + } + rw.siteExt = &SiteExt{} + if rw.BidRequest == nil || rw.Site == nil || rw.Site.Ext == nil { + return rw.siteExt, rw.siteExt.unmarshal(json.RawMessage{}) + } + return rw.siteExt, rw.siteExt.unmarshal(rw.Site.Ext) +} + +func (rw *RequestWrapper) RebuildRequest() error { + if rw.BidRequest == nil { + return errors.New("Requestwrapper Sync called on a nil BidRequest") + } + + if err := rw.rebuildUserExt(); err != nil { + return err + } + if err := rw.rebuildDeviceExt(); err != nil { + return err + } + if err := rw.rebuildRequestExt(); err != nil { + return err + } + if err := rw.rebuildAppExt(); err != nil { + return err + } + if err := rw.rebuildRegExt(); err != nil { + return err + } + if err := rw.rebuildSiteExt(); err != nil { + return err + } + + return nil +} + +func (rw *RequestWrapper) rebuildUserExt() error { + if rw.BidRequest.User == nil && rw.userExt != nil && rw.userExt.Dirty() { + rw.User = &openrtb2.User{} + } + if rw.userExt != nil && rw.userExt.Dirty() { + userJson, err := rw.userExt.marshal() + if err != nil { + return err + } + rw.User.Ext = userJson + } + return nil +} + +func (rw *RequestWrapper) rebuildDeviceExt() error { + if rw.Device == nil && rw.deviceExt != nil && rw.deviceExt.Dirty() { + rw.Device = &openrtb2.Device{} + } + if rw.deviceExt != nil && rw.deviceExt.Dirty() { + deviceJson, err := rw.deviceExt.marshal() + if err != nil { + return err + } + rw.Device.Ext = deviceJson + } + return nil +} + +func (rw *RequestWrapper) rebuildRequestExt() error { + if rw.requestExt != nil && rw.requestExt.Dirty() { + requestJson, err := rw.requestExt.marshal() + if err != nil { + return err + } + rw.Ext = requestJson + } + return nil +} + +func (rw *RequestWrapper) rebuildAppExt() error { + if rw.App == nil && rw.appExt != nil && rw.appExt.Dirty() { + rw.App = &openrtb2.App{} + } + if rw.appExt != nil && rw.appExt.Dirty() { + appJson, err := rw.appExt.marshal() + if err != nil { + return err + } + rw.App.Ext = appJson + } + return nil +} + +func (rw *RequestWrapper) rebuildRegExt() error { + if rw.Regs == nil && rw.regExt != nil && rw.regExt.Dirty() { + rw.Regs = &openrtb2.Regs{} + } + if rw.regExt != nil && rw.regExt.Dirty() { + regsJson, err := rw.regExt.marshal() + if err != nil { + return err + } + rw.Regs.Ext = regsJson + } + return nil +} + +func (rw *RequestWrapper) rebuildSiteExt() error { + if rw.Site == nil && rw.siteExt != nil && rw.siteExt.Dirty() { + rw.Site = &openrtb2.Site{} + } + if rw.siteExt != nil && rw.siteExt.Dirty() { + siteJson, err := rw.siteExt.marshal() + if err != nil { + return err + } + rw.Regs.Ext = siteJson + } + return nil +} + +// --------------------------------------------------------------- +// UserExt provides an interface for request.user.ext +// --------------------------------------------------------------- + +type UserExt struct { + ext map[string]json.RawMessage + extDirty bool + consent *string + consentDirty bool + prebid *ExtUserPrebid + prebidDirty bool + eids *[]ExtUserEid + eidsDirty bool +} + +func (ue *UserExt) unmarshal(extJson json.RawMessage) error { + if len(ue.ext) != 0 || ue.Dirty() { + return nil + } + ue.ext = make(map[string]json.RawMessage) + if len(extJson) == 0 { + return nil + } + + if err := json.Unmarshal(extJson, &ue.ext); err != nil { + return err + } + + consentJson, hasConsent := ue.ext["consent"] + if hasConsent { + if err := json.Unmarshal(consentJson, &ue.consent); err != nil { + return err + } + } + + prebidJson, hasPrebid := ue.ext["prebid"] + if hasPrebid { + ue.prebid = &ExtUserPrebid{} + if err := json.Unmarshal(prebidJson, ue.prebid); err != nil { + return err + } + } + + eidsJson, hasEids := ue.ext["eids"] + if hasEids { + ue.eids = &[]ExtUserEid{} + if err := json.Unmarshal(eidsJson, ue.eids); err != nil { + return err + } + } + + return nil +} + +func (ue *UserExt) marshal() (json.RawMessage, error) { + if ue.consentDirty { + consentJson, err := json.Marshal(ue.consent) + if err != nil { + return nil, err + } + if len(consentJson) > 2 { + ue.ext["consent"] = json.RawMessage(consentJson) + } else { + delete(ue.ext, "consent") + } + ue.consentDirty = false + } + + if ue.prebidDirty { + prebidJson, err := json.Marshal(ue.prebid) + if err != nil { + return nil, err + } + if len(prebidJson) > 2 { + ue.ext["prebid"] = json.RawMessage(prebidJson) + } else { + delete(ue.ext, "prebid") + } + ue.prebidDirty = false + } + + if ue.eidsDirty { + if len(*ue.eids) > 0 { + eidsJson, err := json.Marshal(ue.eids) + if err != nil { + return nil, err + } + ue.ext["eids"] = json.RawMessage(eidsJson) + } else { + delete(ue.ext, "eids") + } + ue.eidsDirty = false + } + + ue.extDirty = false + if len(ue.ext) == 0 { + return nil, nil + } + return json.Marshal(ue.ext) + +} + +func (ue *UserExt) Dirty() bool { + return ue.extDirty || ue.eidsDirty || ue.prebidDirty || ue.consentDirty +} + +func (ue *UserExt) GetExt() map[string]json.RawMessage { + ext := make(map[string]json.RawMessage) + for k, v := range ue.ext { + ext[k] = v + } + return ext +} + +func (ue *UserExt) SetExt(ext map[string]json.RawMessage) { + ue.ext = ext + ue.extDirty = true +} + +func (ue *UserExt) GetConsent() *string { + if ue.consent == nil { + return nil + } + consent := *ue.consent + return &consent +} + +func (ue *UserExt) SetConsent(consent *string) { + ue.consent = consent + ue.consentDirty = true +} + +func (ue *UserExt) GetPrebid() *ExtUserPrebid { + if ue.prebid == nil { + return nil + } + prebid := *ue.prebid + return &prebid +} + +func (ue *UserExt) SetPrebid(prebid *ExtUserPrebid) { + ue.prebid = prebid + ue.prebidDirty = true +} + +func (ue *UserExt) GetEid() *[]ExtUserEid { + if ue.eids == nil { + return nil + } + eids := *ue.eids + return &eids +} + +func (ue *UserExt) SetEid(eid *[]ExtUserEid) { + ue.eids = eid + ue.eidsDirty = true +} + +// --------------------------------------------------------------- +// RequestExt provides an interface for request.ext +// --------------------------------------------------------------- + +type RequestExt struct { + ext map[string]json.RawMessage + extDirty bool + prebid *ExtRequestPrebid + prebidDirty bool +} + +func (re *RequestExt) unmarshal(extJson json.RawMessage) error { + if len(re.ext) != 0 || re.Dirty() { + return nil + } + re.ext = make(map[string]json.RawMessage) + if len(extJson) == 0 { + return nil + } + err := json.Unmarshal(extJson, &re.ext) + if err != nil { + return err + } + prebidJson, hasPrebid := re.ext["prebid"] + if hasPrebid { + re.prebid = &ExtRequestPrebid{} + err = json.Unmarshal(prebidJson, re.prebid) + } + + return err +} + +func (re *RequestExt) marshal() (json.RawMessage, error) { + if re.prebidDirty { + prebidJson, err := json.Marshal(re.prebid) + if err != nil { + return nil, err + } + if len(prebidJson) > 2 { + re.ext["prebid"] = json.RawMessage(prebidJson) + } else { + delete(re.ext, "prebid") + } + re.prebidDirty = false + } + + re.extDirty = false + if len(re.ext) == 0 { + return nil, nil + } + return json.Marshal(re.ext) +} + +func (re *RequestExt) Dirty() bool { + return re.extDirty || re.prebidDirty +} + +func (re *RequestExt) GetExt() map[string]json.RawMessage { + ext := make(map[string]json.RawMessage) + for k, v := range re.ext { + ext[k] = v + } + return ext +} + +func (re *RequestExt) SetExt(ext map[string]json.RawMessage) { + re.ext = ext + re.extDirty = true +} + +func (re *RequestExt) GetPrebid() *ExtRequestPrebid { + if re.prebid == nil { + return nil + } + prebid := *re.prebid + return &prebid +} + +func (re *RequestExt) SetPrebid(prebid *ExtRequestPrebid) { + re.prebid = prebid + re.prebidDirty = true +} + +// --------------------------------------------------------------- +// DeviceExt provides an interface for request.device.ext +// --------------------------------------------------------------- +// NOTE: openrtb_ext/device.go:ParseDeviceExtATTS() uses ext.atts, as read only, via jsonparser, only for IOS. +// Doesn't seem like we will see any performance savings by parsing atts at this point, and as it is read only, +// we don't need to worry about write conflicts. Note here in case additional uses of atts evolve as things progress. +// --------------------------------------------------------------- + +type DeviceExt struct { + ext map[string]json.RawMessage + extDirty bool + prebid *ExtDevicePrebid + prebidDirty bool +} + +func (de *DeviceExt) unmarshal(extJson json.RawMessage) error { + if len(de.ext) != 0 || de.Dirty() { + return nil + } + de.ext = make(map[string]json.RawMessage) + if len(extJson) == 0 { + return nil + } + err := json.Unmarshal(extJson, &de.ext) + if err != nil { + return err + } + prebidJson, hasPrebid := de.ext["prebid"] + if hasPrebid { + de.prebid = &ExtDevicePrebid{} + err = json.Unmarshal(prebidJson, de.prebid) + } + + return err +} + +func (de *DeviceExt) marshal() (json.RawMessage, error) { + if de.prebidDirty { + prebidJson, err := json.Marshal(de.prebid) + if err != nil { + return nil, err + } + if len(prebidJson) > 2 { + de.ext["prebid"] = json.RawMessage(prebidJson) + } else { + delete(de.ext, "prebid") + } + de.prebidDirty = false + } + + de.extDirty = false + if len(de.ext) == 0 { + return nil, nil + } + return json.Marshal(de.ext) +} + +func (de *DeviceExt) Dirty() bool { + return de.extDirty || de.prebidDirty +} + +func (de *DeviceExt) GetExt() map[string]json.RawMessage { + ext := make(map[string]json.RawMessage) + for k, v := range de.ext { + ext[k] = v + } + return ext +} + +func (de *DeviceExt) SetExt(ext map[string]json.RawMessage) { + de.ext = ext + de.extDirty = true +} + +func (de *DeviceExt) GetPrebid() *ExtDevicePrebid { + if de.prebid == nil { + return nil + } + prebid := *de.prebid + return &prebid +} + +func (de *DeviceExt) SetPrebid(prebid *ExtDevicePrebid) { + de.prebid = prebid + de.prebidDirty = true +} + +// --------------------------------------------------------------- +// AppExt provides an interface for request.app.ext +// --------------------------------------------------------------- + +type AppExt struct { + ext map[string]json.RawMessage + extDirty bool + prebid *ExtAppPrebid + prebidDirty bool +} + +func (ae *AppExt) unmarshal(extJson json.RawMessage) error { + if len(ae.ext) != 0 || ae.Dirty() { + return nil + } + ae.ext = make(map[string]json.RawMessage) + if len(extJson) == 0 { + return nil + } + err := json.Unmarshal(extJson, &ae.ext) + if err != nil { + return err + } + prebidJson, hasPrebid := ae.ext["prebid"] + if hasPrebid { + ae.prebid = &ExtAppPrebid{} + err = json.Unmarshal(prebidJson, ae.prebid) + } + + return err +} + +func (ae *AppExt) marshal() (json.RawMessage, error) { + if ae.prebidDirty { + prebidJson, err := json.Marshal(ae.prebid) + if err != nil { + return nil, err + } + if len(prebidJson) > 2 { + ae.ext["prebid"] = json.RawMessage(prebidJson) + } else { + delete(ae.ext, "prebid") + } + ae.prebidDirty = false + } + + ae.extDirty = false + if len(ae.ext) == 0 { + return nil, nil + } + return json.Marshal(ae.ext) +} + +func (ae *AppExt) Dirty() bool { + return ae.extDirty || ae.prebidDirty +} + +func (ae *AppExt) GetExt() map[string]json.RawMessage { + ext := make(map[string]json.RawMessage) + for k, v := range ae.ext { + ext[k] = v + } + return ext +} + +func (ae *AppExt) SetExt(ext map[string]json.RawMessage) { + ae.ext = ext + ae.extDirty = true +} + +func (ae *AppExt) GetPrebid() *ExtAppPrebid { + if ae.prebid == nil { + return nil + } + prebid := *ae.prebid + return &prebid +} + +func (ae *AppExt) SetPrebid(prebid *ExtAppPrebid) { + ae.prebid = prebid + ae.prebidDirty = true +} + +// --------------------------------------------------------------- +// RegExt provides an interface for request.regs.ext +// --------------------------------------------------------------- + +type RegExt struct { + ext map[string]json.RawMessage + extDirty bool + usPrivacy string + usPrivacyDirty bool +} + +func (re *RegExt) unmarshal(extJson json.RawMessage) error { + if len(re.ext) != 0 || re.Dirty() { + return nil + } + re.ext = make(map[string]json.RawMessage) + if len(extJson) == 0 { + return nil + } + err := json.Unmarshal(extJson, &re.ext) + if err != nil { + return err + } + uspJson, hasUsp := re.ext["us_privacy"] + if hasUsp { + err = json.Unmarshal(uspJson, &re.usPrivacy) + } + + return err +} + +func (re *RegExt) marshal() (json.RawMessage, error) { + if re.usPrivacyDirty { + if len(re.usPrivacy) > 0 { + rawjson, err := json.Marshal(re.usPrivacy) + if err != nil { + return nil, err + } + re.ext["us_privacy"] = rawjson + } else { + delete(re.ext, "us_privacy") + } + re.usPrivacyDirty = false + } + + re.extDirty = false + if len(re.ext) == 0 { + return nil, nil + } + return json.Marshal(re.ext) +} + +func (re *RegExt) Dirty() bool { + return re.extDirty || re.usPrivacyDirty +} + +func (re *RegExt) GetExt() map[string]json.RawMessage { + ext := make(map[string]json.RawMessage) + for k, v := range re.ext { + ext[k] = v + } + return ext +} + +func (re *RegExt) SetExt(ext map[string]json.RawMessage) { + re.ext = ext + re.extDirty = true +} + +func (re *RegExt) GetUSPrivacy() string { + uSPrivacy := re.usPrivacy + return uSPrivacy +} + +func (re *RegExt) SetUSPrivacy(uSPrivacy string) { + re.usPrivacy = uSPrivacy + re.usPrivacyDirty = true +} + +// --------------------------------------------------------------- +// SiteExt provides an interface for request.site.ext +// --------------------------------------------------------------- + +type SiteExt struct { + ext map[string]json.RawMessage + extDirty bool + amp int8 + ampDirty bool +} + +func (se *SiteExt) unmarshal(extJson json.RawMessage) error { + if len(se.ext) != 0 || se.Dirty() { + return nil + } + se.ext = make(map[string]json.RawMessage) + if len(extJson) == 0 { + return nil + } + err := json.Unmarshal(extJson, &se.ext) + if err != nil { + return err + } + AmpJson, hasAmp := se.ext["amp"] + if hasAmp { + err = json.Unmarshal(AmpJson, &se.amp) + if err != nil { + err = errors.New(`request.site.ext.amp must be either 1, 0, or undefined`) + } + } + + return err +} + +func (se *SiteExt) marshal() (json.RawMessage, error) { + if se.ampDirty { + ampJson, err := json.Marshal(se.amp) + if err != nil { + return nil, err + } + if len(ampJson) > 2 { + se.ext["amp"] = json.RawMessage(ampJson) + } else { + delete(se.ext, "amp") + } + se.ampDirty = false + } + + se.extDirty = false + if len(se.ext) == 0 { + return nil, nil + } + return json.Marshal(se.ext) +} + +func (se *SiteExt) Dirty() bool { + return se.extDirty || se.ampDirty +} + +func (se *SiteExt) GetExt() map[string]json.RawMessage { + ext := make(map[string]json.RawMessage) + for k, v := range se.ext { + ext[k] = v + } + return ext +} + +func (se *SiteExt) SetExt(ext map[string]json.RawMessage) { + se.ext = ext + se.extDirty = true +} + +func (se *SiteExt) GetAmp() int8 { + return se.amp +} + +func (se *SiteExt) SetUSPrivacy(amp int8) { + se.amp = amp + se.ampDirty = true +} diff --git a/openrtb_ext/request_wrapper_test.go b/openrtb_ext/request_wrapper_test.go new file mode 100644 index 00000000000..06cad49aedf --- /dev/null +++ b/openrtb_ext/request_wrapper_test.go @@ -0,0 +1,22 @@ +package openrtb_ext + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// Some minimal tests to get code coverage above 30%. The real tests are when other modules use these structures. + +func TestUserExt(t *testing.T) { + userExt := &UserExt{} + + userExt.unmarshal(nil) + assert.Equal(t, false, userExt.Dirty(), "New UserExt should not be dirty.") + assert.Nil(t, userExt.GetConsent(), "Empty UserExt should have nil consent") + + newConsent := "NewConsent" + userExt.SetConsent(&newConsent) + assert.Equal(t, "NewConsent", *userExt.GetConsent()) + +} diff --git a/privacy/ccpa/consentwriter.go b/privacy/ccpa/consentwriter.go index 41f1c39447b..451f2b40238 100644 --- a/privacy/ccpa/consentwriter.go +++ b/privacy/ccpa/consentwriter.go @@ -1,8 +1,12 @@ package ccpa -import "github.com/mxmCherry/openrtb/v15/openrtb2" +import ( + "github.com/mxmCherry/openrtb/v15/openrtb2" + "github.com/prebid/prebid-server/openrtb_ext" +) -// ConsentWriter implements the PolicyWriter interface for CCPA. +// ConsentWriter implements the old PolicyWriter interface for CCPA. +// This is used where we have not converted to RequestWrapper yet type ConsentWriter struct { Consent string } @@ -12,12 +16,11 @@ func (c ConsentWriter) Write(req *openrtb2.BidRequest) error { if req == nil { return nil } - - regs, err := buildRegs(c.Consent, req.Regs) - if err != nil { + reqWrap := &openrtb_ext.RequestWrapper{BidRequest: req} + if regsExt, err := reqWrap.GetRegExt(); err == nil { + regsExt.SetUSPrivacy(c.Consent) + } else { return err } - req.Regs = regs - - return nil + return reqWrap.RebuildRequest() } diff --git a/privacy/ccpa/consentwriter_test.go b/privacy/ccpa/consentwriter_test.go index d59428626b8..28dfd41785e 100644 --- a/privacy/ccpa/consentwriter_test.go +++ b/privacy/ccpa/consentwriter_test.go @@ -5,10 +5,60 @@ import ( "testing" "github.com/mxmCherry/openrtb/v15/openrtb2" + "github.com/prebid/prebid-server/openrtb_ext" "github.com/stretchr/testify/assert" ) +// RegExt.SetUSPrivacy() is the new ConsentWriter func TestConsentWriter(t *testing.T) { + consent := "anyConsent" + testCases := []struct { + description string + request *openrtb2.BidRequest + expected *openrtb2.BidRequest + expectedError bool + }{ + { + description: "Nil Request", + request: nil, + expected: nil, + }, + { + description: "Success", + request: &openrtb2.BidRequest{}, + expected: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":"anyConsent"}`)}, + }, + }, + { + description: "Error With Regs.Ext - Does Not Mutate", + request: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{Ext: json.RawMessage(`malformed}`)}, + }, + expectedError: false, + expected: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{Ext: json.RawMessage(`malformed}`)}, + }, + }, + } + + for _, test := range testCases { + + reqWrapper := &openrtb_ext.RequestWrapper{BidRequest: test.request} + var err error + regsExt, err1 := reqWrapper.GetRegExt() + if err1 == nil { + regsExt.SetUSPrivacy(consent) + if reqWrapper.BidRequest != nil { + err = reqWrapper.RebuildRequest() + } + } + assertError(t, test.expectedError, err, test.description) + assert.Equal(t, test.expected, reqWrapper.BidRequest, test.description) + } +} + +func TestConsentWriterLegacy(t *testing.T) { consent := "anyConsent" testCases := []struct { description string diff --git a/privacy/ccpa/policy.go b/privacy/ccpa/policy.go index d57ba8deaa4..39322317df5 100644 --- a/privacy/ccpa/policy.go +++ b/privacy/ccpa/policy.go @@ -1,8 +1,6 @@ package ccpa import ( - "encoding/json" - "errors" "fmt" "github.com/mxmCherry/openrtb/v15/openrtb2" @@ -15,8 +13,8 @@ type Policy struct { NoSaleBidders []string } -// ReadFromRequest extracts the CCPA regulatory information from an OpenRTB bid request. -func ReadFromRequest(req *openrtb2.BidRequest) (Policy, error) { +// ReadFromRequestWrapper extracts the CCPA regulatory information from an OpenRTB bid request. +func ReadFromRequestWrapper(req *openrtb_ext.RequestWrapper) (Policy, error) { var consent string var noSaleBidders []string @@ -25,174 +23,80 @@ func ReadFromRequest(req *openrtb2.BidRequest) (Policy, error) { } // Read consent from request.regs.ext - if req.Regs != nil && len(req.Regs.Ext) > 0 { - var ext openrtb_ext.ExtRegs - if err := json.Unmarshal(req.Regs.Ext, &ext); err != nil { - return Policy{}, fmt.Errorf("error reading request.regs.ext: %s", err) - } - consent = ext.USPrivacy + regsExt, err := req.GetRegExt() + if err != nil { + return Policy{}, fmt.Errorf("error reading request.regs.ext: %s", err) + } + if regsExt != nil { + consent = regsExt.GetUSPrivacy() } - // Read no sale bidders from request.ext.prebid - if len(req.Ext) > 0 { - var ext openrtb_ext.ExtRequest - if err := json.Unmarshal(req.Ext, &ext); err != nil { - return Policy{}, fmt.Errorf("error reading request.ext.prebid: %s", err) - } - noSaleBidders = ext.Prebid.NoSale + reqExt, err := req.GetRequestExt() + if err != nil { + return Policy{}, fmt.Errorf("error reading request.ext: %s", err) + } + reqPrebid := reqExt.GetPrebid() + if reqPrebid != nil { + noSaleBidders = reqPrebid.NoSale } return Policy{consent, noSaleBidders}, nil } +func ReadFromRequest(req *openrtb2.BidRequest) (Policy, error) { + return ReadFromRequestWrapper(&openrtb_ext.RequestWrapper{BidRequest: req}) +} + // Write mutates an OpenRTB bid request with the CCPA regulatory information. -func (p Policy) Write(req *openrtb2.BidRequest) error { +func (p Policy) Write(req *openrtb_ext.RequestWrapper) error { if req == nil { return nil } - regs, err := buildRegs(p.Consent, req.Regs) + regsExt, err := req.GetRegExt() if err != nil { return err } - ext, err := buildExt(p.NoSaleBidders, req.Ext) + + reqExt, err := req.GetRequestExt() if err != nil { return err } - req.Regs = regs - req.Ext = ext + regsExt.SetUSPrivacy(p.Consent) + setPrebidNoSale(p.NoSaleBidders, reqExt) return nil } -func buildRegs(consent string, regs *openrtb2.Regs) (*openrtb2.Regs, error) { - if consent == "" { - return buildRegsClear(regs) - } - return buildRegsWrite(consent, regs) -} - -func buildRegsClear(regs *openrtb2.Regs) (*openrtb2.Regs, error) { - if regs == nil || len(regs.Ext) == 0 { - return regs, nil - } - - var extMap map[string]interface{} - if err := json.Unmarshal(regs.Ext, &extMap); err != nil { - return nil, err - } - - delete(extMap, "us_privacy") - - // Remove entire ext if it's now empty - if len(extMap) == 0 { - regsResult := *regs - regsResult.Ext = nil - return ®sResult, nil - } - - // Marshal ext if there are still other fields - var regsResult openrtb2.Regs - ext, err := json.Marshal(extMap) - if err == nil { - regsResult = *regs - regsResult.Ext = ext - } - return ®sResult, err -} - -func buildRegsWrite(consent string, regs *openrtb2.Regs) (*openrtb2.Regs, error) { - if regs == nil { - return marshalRegsExt(openrtb2.Regs{}, openrtb_ext.ExtRegs{USPrivacy: consent}) - } - - if regs.Ext == nil { - return marshalRegsExt(*regs, openrtb_ext.ExtRegs{USPrivacy: consent}) - } - - var extMap map[string]interface{} - if err := json.Unmarshal(regs.Ext, &extMap); err != nil { - return nil, err - } - - extMap["us_privacy"] = consent - return marshalRegsExt(*regs, extMap) -} - -func marshalRegsExt(regs openrtb2.Regs, ext interface{}) (*openrtb2.Regs, error) { - extJSON, err := json.Marshal(ext) - if err == nil { - regs.Ext = extJSON - } - return ®s, err -} - -func buildExt(noSaleBidders []string, ext json.RawMessage) (json.RawMessage, error) { +func setPrebidNoSale(noSaleBidders []string, ext *openrtb_ext.RequestExt) { if len(noSaleBidders) == 0 { - return buildExtClear(ext) + setPrebidNoSaleClear(ext) + } else { + setPrebidNoSaleWrite(noSaleBidders, ext) } - return buildExtWrite(noSaleBidders, ext) } -func buildExtClear(ext json.RawMessage) (json.RawMessage, error) { - if len(ext) == 0 { - return ext, nil - } - - var extMap map[string]interface{} - if err := json.Unmarshal(ext, &extMap); err != nil { - return nil, err - } - - prebidExt, exists := extMap["prebid"] - if !exists { - return ext, nil - } - - // Verify prebid is an object - prebidExtMap, ok := prebidExt.(map[string]interface{}) - if !ok { - return nil, errors.New("request.ext.prebid is not a json object") +func setPrebidNoSaleClear(ext *openrtb_ext.RequestExt) { + prebid := ext.GetPrebid() + if prebid == nil { + return } // Remove no sale member - delete(prebidExtMap, "nosale") - if len(prebidExtMap) == 0 { - delete(extMap, "prebid") - } - - // Remove entire ext if it's empty - if len(extMap) == 0 { - return nil, nil - } - - return json.Marshal(extMap) + prebid.NoSale = []string{} + ext.SetPrebid(prebid) } -func buildExtWrite(noSaleBidders []string, ext json.RawMessage) (json.RawMessage, error) { - if len(ext) == 0 { - return json.Marshal(openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{NoSale: noSaleBidders}}) - } - - var extMap map[string]interface{} - if err := json.Unmarshal(ext, &extMap); err != nil { - return nil, err +func setPrebidNoSaleWrite(noSaleBidders []string, ext *openrtb_ext.RequestExt) { + if ext == nil { + // This should hopefully not be possible. The only caller insures that this has been initialized + return } - var prebidExt map[string]interface{} - if prebidExtInterface, exists := extMap["prebid"]; exists { - // Reference Existing Prebid Ext Map - if prebidExtMap, ok := prebidExtInterface.(map[string]interface{}); ok { - prebidExt = prebidExtMap - } else { - return nil, errors.New("request.ext.prebid is not a json object") - } - } else { - // Create New Empty Prebid Ext Map - prebidExt = make(map[string]interface{}) - extMap["prebid"] = prebidExt + prebid := ext.GetPrebid() + if prebid == nil { + prebid = &openrtb_ext.ExtRequestPrebid{} } - - prebidExt["nosale"] = noSaleBidders - return json.Marshal(extMap) + prebid.NoSale = noSaleBidders + ext.SetPrebid(prebid) } diff --git a/privacy/ccpa/policy_test.go b/privacy/ccpa/policy_test.go index 416ebffa31a..ca6d0f8acf2 100644 --- a/privacy/ccpa/policy_test.go +++ b/privacy/ccpa/policy_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/mxmCherry/openrtb/v15/openrtb2" + "github.com/prebid/prebid-server/openrtb_ext" "github.com/stretchr/testify/assert" ) @@ -155,7 +156,8 @@ func TestReadFromRequest(t *testing.T) { } for _, test := range testCases { - result, err := ReadFromRequest(test.request) + reqWrapper := &openrtb_ext.RequestWrapper{BidRequest: test.request} + result, err := ReadFromRequestWrapper(reqWrapper) assertError(t, test.expectedError, err, test.description) assert.Equal(t, test.expectedPolicy, result, test.description) } @@ -209,9 +211,20 @@ func TestWrite(t *testing.T) { } for _, test := range testCases { - err := test.policy.Write(test.request) + reqWrapper := &openrtb_ext.RequestWrapper{BidRequest: test.request} + var err error + _, err = reqWrapper.GetRegExt() + if err == nil { + _, err = reqWrapper.GetRequestExt() + if err == nil { + err = test.policy.Write(reqWrapper) + if err == nil && reqWrapper.BidRequest != nil { + err = reqWrapper.RebuildRequest() + } + } + } assertError(t, test.expectedError, err, test.description) - assert.Equal(t, test.expected, test.request, test.description) + assert.Equal(t, test.expected, reqWrapper.BidRequest, test.description) } } @@ -237,6 +250,9 @@ func TestBuildRegs(t *testing.T) { regs: &openrtb2.Regs{ Ext: json.RawMessage(`malformed`), }, + expected: &openrtb2.Regs{ + Ext: json.RawMessage(`malformed`), + }, expectedError: true, }, { @@ -253,14 +269,22 @@ func TestBuildRegs(t *testing.T) { regs: &openrtb2.Regs{ Ext: json.RawMessage(`malformed`), }, + expected: &openrtb2.Regs{ + Ext: json.RawMessage(`malformed`), + }, expectedError: true, }, } for _, test := range testCases { - result, err := buildRegs(test.consent, test.regs) + request := &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{Regs: test.regs}} + regsExt, err := request.GetRegExt() + if err == nil { + regsExt.SetUSPrivacy(test.consent) + request.RebuildRequest() + } assertError(t, test.expectedError, err, test.description) - assert.Equal(t, test.expected, result, test.description) + assert.Equal(t, test.expected, request.Regs, test.description) } } @@ -274,7 +298,7 @@ func TestBuildRegsClear(t *testing.T) { { description: "Nil Regs", regs: nil, - expected: nil, + expected: &openrtb2.Regs{Ext: nil}, }, { description: "Nil Regs.Ext", @@ -297,21 +321,28 @@ func TestBuildRegsClear(t *testing.T) { expected: &openrtb2.Regs{Ext: json.RawMessage(`{"other":"any"}`)}, }, { - description: "Invalid Regs.Ext Type - Still Cleared", - regs: &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":123}`)}, - expected: &openrtb2.Regs{}, + description: "Invalid Regs.Ext Type - Returns Error, doesn't clear", + regs: &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":123}`)}, + expected: &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":123}`)}, + expectedError: true, }, { description: "Malformed Regs.Ext", regs: &openrtb2.Regs{Ext: json.RawMessage(`malformed`)}, + expected: &openrtb2.Regs{Ext: json.RawMessage(`malformed`)}, expectedError: true, }, } for _, test := range testCases { - result, err := buildRegsClear(test.regs) + request := &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{Regs: test.regs}} + regsExt, err := request.GetRegExt() + if err == nil { + regsExt.SetUSPrivacy("") + request.RebuildRequest() + } assertError(t, test.expectedError, err, test.description) - assert.Equal(t, test.expected, result, test.description) + assert.Equal(t, test.expected, request.Regs, test.description) } } @@ -354,23 +385,30 @@ func TestBuildRegsWrite(t *testing.T) { expected: &openrtb2.Regs{Ext: json.RawMessage(`{"other":"any","us_privacy":"anyConsent"}`)}, }, { - description: "Invalid Regs.Ext Type - Still Overwrites", - consent: "anyConsent", - regs: &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":123}`)}, - expected: &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":"anyConsent"}`)}, + description: "Invalid Regs.Ext Type - Doesn't Overwrite", + consent: "anyConsent", + regs: &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":123}`)}, + expected: &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":123}`)}, + expectedError: true, }, { description: "Malformed Regs.Ext", consent: "anyConsent", regs: &openrtb2.Regs{Ext: json.RawMessage(`malformed`)}, + expected: &openrtb2.Regs{Ext: json.RawMessage(`malformed`)}, expectedError: true, }, } for _, test := range testCases { - result, err := buildRegsWrite(test.consent, test.regs) + request := &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{Regs: test.regs}} + regsExt, err := request.GetRegExt() + if err == nil { + regsExt.SetUSPrivacy(test.consent) + request.RebuildRequest() + } assertError(t, test.expectedError, err, test.description) - assert.Equal(t, test.expected, result, test.description) + assert.Equal(t, test.expected, request.Regs, test.description) } } @@ -415,7 +453,14 @@ func TestBuildExt(t *testing.T) { } for _, test := range testCases { - result, err := buildExt(test.noSaleBidders, test.ext) + request := &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{Ext: test.ext}} + reqExt, err := request.GetRequestExt() + var result json.RawMessage + if err == nil { + setPrebidNoSale(test.noSaleBidders, reqExt) + err = request.RebuildRequest() + result = request.Ext + } assertError(t, test.expectedError, err, test.description) assert.Equal(t, test.expected, result, test.description) } @@ -460,13 +505,13 @@ func TestBuildExtClear(t *testing.T) { }, { description: "Leaves Other Ext.Prebid Values", - ext: json.RawMessage(`{"prebid":{"nosale":["a","b"],"other":"any"}}`), - expected: json.RawMessage(`{"prebid":{"other":"any"}}`), + ext: json.RawMessage(`{"prebid":{"nosale":["a","b"],"aliases":{"a":"b"}}}`), + expected: json.RawMessage(`{"prebid":{"aliases":{"a":"b"}}}`), }, { description: "Leaves All Other Values", - ext: json.RawMessage(`{"other":"ABC","prebid":{"nosale":["a","b"],"other":"123"}}`), - expected: json.RawMessage(`{"other":"ABC","prebid":{"other":"123"}}`), + ext: json.RawMessage(`{"other":"ABC","prebid":{"nosale":["a","b"],"supportdeals":true}}`), + expected: json.RawMessage(`{"other":"ABC","prebid":{"supportdeals":true}}`), }, { description: "Malformed Ext", @@ -486,7 +531,14 @@ func TestBuildExtClear(t *testing.T) { } for _, test := range testCases { - result, err := buildExtClear(test.ext) + request := &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{Ext: test.ext}} + reqExt, err := request.GetRequestExt() + var result json.RawMessage + if err == nil { + setPrebidNoSaleClear(reqExt) + err = request.RebuildRequest() + result = request.Ext + } assertError(t, test.expectedError, err, test.description) assert.Equal(t, test.expected, result, test.description) } @@ -539,43 +591,56 @@ func TestBuildExtWrite(t *testing.T) { { description: "Leaves Other Ext.Prebid Values", noSaleBidders: []string{"a", "b"}, - ext: json.RawMessage(`{"prebid":{"other":"any"}}`), - expected: json.RawMessage(`{"prebid":{"nosale":["a","b"],"other":"any"}}`), + ext: json.RawMessage(`{"prebid":{"supportdeals":true}}`), + expected: json.RawMessage(`{"prebid":{"supportdeals":true,"nosale":["a","b"]}}`), }, { description: "Leaves All Other Values", noSaleBidders: []string{"a", "b"}, - ext: json.RawMessage(`{"other":"ABC","prebid":{"other":"123"}}`), - expected: json.RawMessage(`{"other":"ABC","prebid":{"nosale":["a","b"],"other":"123"}}`), + ext: json.RawMessage(`{"other":"ABC","prebid":{"aliases":{"a":"b"}}}`), + expected: json.RawMessage(`{"other":"ABC","prebid":{"aliases":{"a":"b"},"nosale":["a","b"]}}`), }, { description: "Invalid Ext.Prebid No Sale Type - Still Overrides", noSaleBidders: []string{"a", "b"}, ext: json.RawMessage(`{"prebid":{"nosale":123}}`), - expected: json.RawMessage(`{"prebid":{"nosale":["a","b"]}}`), + expected: json.RawMessage(`{"prebid":{"nosale":123}}`), + expectedError: true, }, { description: "Invalid Ext.Prebid Type ", noSaleBidders: []string{"a", "b"}, ext: json.RawMessage(`{"prebid":"wrongtype"}`), + expected: json.RawMessage(`{"prebid":"wrongtype"}`), expectedError: true, }, { description: "Malformed Ext", noSaleBidders: []string{"a", "b"}, ext: json.RawMessage(`{malformed`), + expected: json.RawMessage(`{malformed`), expectedError: true, }, { description: "Malformed Ext.Prebid", noSaleBidders: []string{"a", "b"}, ext: json.RawMessage(`{"prebid":malformed}`), + expected: json.RawMessage(`{"prebid":malformed}`), expectedError: true, }, } for _, test := range testCases { - result, err := buildExtWrite(test.noSaleBidders, test.ext) + request := &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{Ext: test.ext}} + reqExt, err := request.GetRequestExt() + var result json.RawMessage + if err == nil { + setPrebidNoSaleWrite(test.noSaleBidders, reqExt) + err = request.RebuildRequest() + result = request.Ext + } else { + result = test.ext + } assertError(t, test.expectedError, err, test.description) assert.Equal(t, test.expected, result, test.description) }