diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go index 8f8d32c8fb9..db427f380ed 100644 --- a/endpoints/openrtb2/amp_auction.go +++ b/endpoints/openrtb2/amp_auction.go @@ -234,6 +234,8 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h activityControl = privacy.NewActivityControl(&account.Privacy) + hookExecutor.SetActivityControl(activityControl) + secGPC := r.Header.Get("Sec-GPC") auctionRequest := &exchange.AuctionRequest{ diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go index 72382f36b04..eb3f5c02ccb 100644 --- a/endpoints/openrtb2/auction.go +++ b/endpoints/openrtb2/auction.go @@ -205,6 +205,8 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http activityControl = privacy.NewActivityControl(&account.Privacy) + hookExecutor.SetActivityControl(activityControl) + ctx := context.Background() timeout := deps.cfg.AuctionTimeouts.LimitAuctionTimeout(time.Duration(req.TMax) * time.Millisecond) diff --git a/exchange/utils.go b/exchange/utils.go index dfe1fe448ba..53890718a1d 100644 --- a/exchange/utils.go +++ b/exchange/utils.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/prebid/prebid-server/v2/ortb" "math/rand" "strings" @@ -21,7 +22,6 @@ import ( "github.com/prebid/prebid-server/v2/gdpr" "github.com/prebid/prebid-server/v2/metrics" "github.com/prebid/prebid-server/v2/openrtb_ext" - "github.com/prebid/prebid-server/v2/ortb" "github.com/prebid/prebid-server/v2/privacy" "github.com/prebid/prebid-server/v2/privacy/ccpa" "github.com/prebid/prebid-server/v2/privacy/lmt" @@ -181,7 +181,7 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context, // FPD should be applied before policies, otherwise it overrides policies and activities restricted data applyFPD(auctionReq.FirstPartyData, bidderRequest) - reqWrapper := cloneBidderReq(bidderRequest.BidRequest) + reqWrapper := ortb.CloneBidderReq(bidderRequest.BidRequest) passIDActivityAllowed := auctionReq.Activities.Allow(privacy.ActivityTransmitUserFPD, scopedName, privacy.NewRequestFromBidRequest(*req)) if !passIDActivityAllowed { @@ -238,34 +238,6 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context, return } -// cloneBidderReq - clones bidder request and replaces req.User and req.Device with new copies -func cloneBidderReq(req *openrtb2.BidRequest) *openrtb_ext.RequestWrapper { - - // bidder request may be modified differently per bidder based on privacy configs - // new request should be created for each bidder request - // pointer fields like User and Device should be cloned and set back to the request copy - var newReq *openrtb2.BidRequest - newReq = ptrutil.Clone(req) - - if req.User != nil { - userCopy := ortb.CloneUser(req.User) - newReq.User = userCopy - } - - if req.Device != nil { - deviceCopy := ortb.CloneDevice(req.Device) - newReq.Device = deviceCopy - } - - if req.Source != nil { - sourceCopy := ortb.CloneSource(req.Source) - newReq.Source = sourceCopy - } - - reqWrapper := &openrtb_ext.RequestWrapper{BidRequest: newReq} - return reqWrapper -} - func shouldSetLegacyPrivacy(bidderInfo config.BidderInfos, bidder string) bool { binfo, defined := bidderInfo[bidder] diff --git a/hooks/hookexecution/context.go b/hooks/hookexecution/context.go index 0817078137f..f7b6a9d32e1 100644 --- a/hooks/hookexecution/context.go +++ b/hooks/hookexecution/context.go @@ -6,15 +6,17 @@ import ( "github.com/golang/glog" "github.com/prebid/prebid-server/v2/config" "github.com/prebid/prebid-server/v2/hooks/hookstage" + "github.com/prebid/prebid-server/v2/privacy" ) // executionContext holds information passed to module's hook during hook execution. type executionContext struct { - endpoint string - stage string - accountId string - account *config.Account - moduleContexts *moduleContexts + endpoint string + stage string + accountID string + account *config.Account + moduleContexts *moduleContexts + activityControl privacy.ActivityControl } func (ctx executionContext) getModuleContext(moduleName string) hookstage.ModuleInvocationContext { diff --git a/hooks/hookexecution/execution.go b/hooks/hookexecution/execution.go index 90ee9b46a9c..05cc5fb5943 100644 --- a/hooks/hookexecution/execution.go +++ b/hooks/hookexecution/execution.go @@ -3,6 +3,7 @@ package hookexecution import ( "context" "fmt" + "github.com/prebid/prebid-server/v2/ortb" "strings" "sync" "time" @@ -10,6 +11,7 @@ import ( "github.com/prebid/prebid-server/v2/hooks" "github.com/prebid/prebid-server/v2/hooks/hookstage" "github.com/prebid/prebid-server/v2/metrics" + "github.com/prebid/prebid-server/v2/privacy" ) type hookResponse[T any] struct { @@ -66,10 +68,11 @@ func executeGroup[H any, P any]( for _, hook := range group.Hooks { mCtx := executionCtx.getModuleContext(hook.Module) + newPayload := handleModuleActivities(hook.Code, executionCtx.activityControl, payload) wg.Add(1) go func(hw hooks.HookWrapper[H], moduleCtx hookstage.ModuleInvocationContext) { defer wg.Done() - executeHook(moduleCtx, hw, payload, hookHandler, group.Timeout, resp, rejected) + executeHook(moduleCtx, hw, newPayload, hookHandler, group.Timeout, resp, rejected) }(hook, mCtx) } @@ -176,7 +179,7 @@ func handleHookResponse[P any]( metricEngine metrics.MetricsEngine, ) (P, HookOutcome, *RejectError) { var rejectErr *RejectError - labels := metrics.ModuleLabels{Module: moduleReplacer.Replace(hr.HookID.ModuleCode), Stage: ctx.stage, AccountID: ctx.accountId} + labels := metrics.ModuleLabels{Module: moduleReplacer.Replace(hr.HookID.ModuleCode), Stage: ctx.stage, AccountID: ctx.accountID} metricEngine.RecordModuleCalled(labels, hr.ExecutionTime) hookOutcome := HookOutcome{ @@ -311,3 +314,29 @@ func handleHookMutations[P any]( return payload } + +func handleModuleActivities[P any](hookCode string, activityControl privacy.ActivityControl, payload P) P { + payloadData, ok := any(&payload).(hookstage.RequestUpdater) + if !ok { + return payload + } + + scopeGeneral := privacy.Component{Type: privacy.ComponentTypeGeneral, Name: hookCode} + transmitUserFPDActivityAllowed := activityControl.Allow(privacy.ActivityTransmitUserFPD, scopeGeneral, privacy.ActivityRequest{}) + + if !transmitUserFPDActivityAllowed { + // changes need to be applied to new payload and leave original payload unchanged + bidderReq := payloadData.GetBidderRequestPayload() + + bidderReqCopy := ortb.CloneBidderReq(bidderReq.BidRequest) + + privacy.ScrubUserFPD(bidderReqCopy) + + var newPayload = payload + var np = any(&newPayload).(hookstage.RequestUpdater) + np.SetBidderRequestPayload(bidderReqCopy) + return newPayload + } + + return payload +} diff --git a/hooks/hookexecution/execution_test.go b/hooks/hookexecution/execution_test.go new file mode 100644 index 00000000000..a12175e30a0 --- /dev/null +++ b/hooks/hookexecution/execution_test.go @@ -0,0 +1,151 @@ +package hookexecution + +import ( + "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/hooks/hookstage" + "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/prebid/prebid-server/v2/privacy" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestHandleModuleActivitiesBidderRequestPayload(t *testing.T) { + + testCases := []struct { + description string + hookCode string + privacyConfig *config.AccountPrivacy + inPayloadData hookstage.BidderRequestPayload + expectedPayloadData hookstage.BidderRequestPayload + }{ + { + description: "payload should change when userFPD is blocked by activity", + hookCode: "foo", + inPayloadData: hookstage.BidderRequestPayload{ + Request: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{ + User: &openrtb2.User{ID: "test_user_id"}, + }}, + }, + privacyConfig: getTransmitUFPDActivityConfig("foo", false), + expectedPayloadData: hookstage.BidderRequestPayload{ + Request: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{ + User: &openrtb2.User{ID: ""}, + }, + }}, + }, + { + description: "payload should not change when userFPD is not blocked by activity", + hookCode: "foo", + inPayloadData: hookstage.BidderRequestPayload{ + Request: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{ + User: &openrtb2.User{ID: "test_user_id"}, + }}, + }, + privacyConfig: getTransmitUFPDActivityConfig("foo", true), + expectedPayloadData: hookstage.BidderRequestPayload{ + Request: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{ + User: &openrtb2.User{ID: "test_user_id"}, + }}, + }, + }, + } + for _, test := range testCases { + t.Run(test.description, func(t *testing.T) { + //check input payload didn't change + origInPayloadData := test.inPayloadData + activityControl := privacy.NewActivityControl(test.privacyConfig) + newPayload := handleModuleActivities(test.hookCode, activityControl, test.inPayloadData) + assert.Equal(t, test.expectedPayloadData.Request.BidRequest, newPayload.Request.BidRequest) + assert.Equal(t, origInPayloadData, test.inPayloadData) + }) + } +} + +func TestHandleModuleActivitiesProcessedAuctionRequestPayload(t *testing.T) { + + testCases := []struct { + description string + hookCode string + privacyConfig *config.AccountPrivacy + inPayloadData hookstage.ProcessedAuctionRequestPayload + expectedPayloadData hookstage.ProcessedAuctionRequestPayload + }{ + { + description: "payload should change when userFPD is blocked by activity", + hookCode: "foo", + inPayloadData: hookstage.ProcessedAuctionRequestPayload{ + Request: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{ + User: &openrtb2.User{ID: "test_user_id"}, + }}, + }, + privacyConfig: getTransmitUFPDActivityConfig("foo", false), + expectedPayloadData: hookstage.ProcessedAuctionRequestPayload{ + Request: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{ + User: &openrtb2.User{ID: ""}, + }}, + }, + }, + { + description: "payload should not change when userFPD is not blocked by activity", + hookCode: "foo", + inPayloadData: hookstage.ProcessedAuctionRequestPayload{ + Request: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{ + User: &openrtb2.User{ID: "test_user_id"}, + }}, + }, + privacyConfig: getTransmitUFPDActivityConfig("foo", true), + expectedPayloadData: hookstage.ProcessedAuctionRequestPayload{ + Request: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{ + User: &openrtb2.User{ID: "test_user_id"}, + }}, + }, + }, + } + for _, test := range testCases { + t.Run(test.description, func(t *testing.T) { + //check input payload didn't change + origInPayloadData := test.inPayloadData + activityControl := privacy.NewActivityControl(test.privacyConfig) + newPayload := handleModuleActivities(test.hookCode, activityControl, test.inPayloadData) + assert.Equal(t, test.expectedPayloadData.Request.BidRequest, newPayload.Request.BidRequest) + assert.Equal(t, origInPayloadData, test.inPayloadData) + }) + } +} + +func TestHandleModuleActivitiesNoBidderRequestPayload(t *testing.T) { + + testCases := []struct { + description string + hookCode string + privacyConfig *config.AccountPrivacy + inPayloadData hookstage.RawAuctionRequestPayload + expectedPayloadData hookstage.RawAuctionRequestPayload + }{ + { + description: "payload should change when userFPD is blocked by activity", + hookCode: "foo", + inPayloadData: hookstage.RawAuctionRequestPayload{}, + privacyConfig: getTransmitUFPDActivityConfig("foo", false), + expectedPayloadData: hookstage.RawAuctionRequestPayload{}, + }, + { + description: "payload should not change when userFPD is not blocked by activity", + hookCode: "foo", + inPayloadData: hookstage.RawAuctionRequestPayload{}, + privacyConfig: getTransmitUFPDActivityConfig("foo", true), + expectedPayloadData: hookstage.RawAuctionRequestPayload{}, + }, + } + for _, test := range testCases { + t.Run(test.description, func(t *testing.T) { + //check input payload didn't change + origInPayloadData := test.inPayloadData + activityControl := privacy.NewActivityControl(test.privacyConfig) + newPayload := handleModuleActivities(test.hookCode, activityControl, test.inPayloadData) + assert.Equal(t, test.expectedPayloadData, newPayload) + assert.Equal(t, origInPayloadData, test.inPayloadData) + }) + } +} diff --git a/hooks/hookexecution/executor.go b/hooks/hookexecution/executor.go index f820b79fdb2..518a0c628a6 100644 --- a/hooks/hookexecution/executor.go +++ b/hooks/hookexecution/executor.go @@ -13,6 +13,7 @@ import ( "github.com/prebid/prebid-server/v2/hooks/hookstage" "github.com/prebid/prebid-server/v2/metrics" "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/prebid/prebid-server/v2/privacy" ) const ( @@ -43,17 +44,19 @@ type StageExecutor interface { type HookStageExecutor interface { StageExecutor SetAccount(account *config.Account) + SetActivityControl(activityControl privacy.ActivityControl) GetOutcomes() []StageOutcome } type hookExecutor struct { - account *config.Account - accountID string - endpoint string - planBuilder hooks.ExecutionPlanBuilder - stageOutcomes []StageOutcome - moduleContexts *moduleContexts - metricEngine metrics.MetricsEngine + account *config.Account + accountID string + endpoint string + planBuilder hooks.ExecutionPlanBuilder + stageOutcomes []StageOutcome + moduleContexts *moduleContexts + metricEngine metrics.MetricsEngine + activityControl privacy.ActivityControl // Mutex needed for BidderRequest and RawBidderResponse Stages as they are run in several goroutines sync.Mutex } @@ -77,6 +80,10 @@ func (e *hookExecutor) SetAccount(account *config.Account) { e.accountID = account.ID } +func (e *hookExecutor) SetActivityControl(activityControl privacy.ActivityControl) { + e.activityControl = activityControl +} + func (e *hookExecutor) GetOutcomes() []StageOutcome { return e.stageOutcomes } @@ -290,11 +297,12 @@ func (e *hookExecutor) ExecuteAuctionResponseStage(response *openrtb2.BidRespons func (e *hookExecutor) newContext(stage string) executionContext { return executionContext{ - account: e.account, - accountId: e.accountID, - endpoint: e.endpoint, - moduleContexts: e.moduleContexts, - stage: stage, + account: e.account, + accountID: e.accountID, + endpoint: e.endpoint, + moduleContexts: e.moduleContexts, + stage: stage, + activityControl: e.activityControl, } } @@ -316,6 +324,8 @@ type EmptyHookExecutor struct{} func (executor EmptyHookExecutor) SetAccount(_ *config.Account) {} +func (executor EmptyHookExecutor) SetActivityControl(_ privacy.ActivityControl) {} + func (executor EmptyHookExecutor) GetOutcomes() []StageOutcome { return []StageOutcome{} } diff --git a/hooks/hookexecution/executor_test.go b/hooks/hookexecution/executor_test.go index 90fa09e394f..1fb299204ec 100644 --- a/hooks/hookexecution/executor_test.go +++ b/hooks/hookexecution/executor_test.go @@ -18,6 +18,8 @@ import ( "github.com/prebid/prebid-server/v2/metrics" metricsConfig "github.com/prebid/prebid-server/v2/metrics/config" "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/prebid/prebid-server/v2/privacy" + "github.com/prebid/prebid-server/v2/util/ptrutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) @@ -674,8 +676,11 @@ func TestExecuteRawAuctionStage(t *testing.T) { for _, test := range testCases { t.Run(test.description, func(t *testing.T) { exec := NewHookExecutor(test.givenPlanBuilder, EndpointAuction, &metricsConfig.NilMetricsEngine{}) - exec.SetAccount(test.givenAccount) + privacyConfig := getTransmitUFPDActivityConfig("foo", false) + ac := privacy.NewActivityControl(privacyConfig) + exec.SetActivityControl(ac) + exec.SetAccount(test.givenAccount) newBody, reject := exec.ExecuteRawAuctionStage([]byte(test.givenBody)) assert.Equal(t, test.expectedReject, reject, "Unexpected stage reject.") @@ -896,6 +901,10 @@ func TestExecuteProcessedAuctionStage(t *testing.T) { for _, test := range testCases { t.Run(test.description, func(ti *testing.T) { exec := NewHookExecutor(test.givenPlanBuilder, EndpointAuction, &metricsConfig.NilMetricsEngine{}) + + privacyConfig := getTransmitUFPDActivityConfig("foo", false) + ac := privacy.NewActivityControl(privacyConfig) + exec.SetActivityControl(ac) exec.SetAccount(test.givenAccount) err := exec.ExecuteProcessedAuctionStage(&test.givenRequest) @@ -938,6 +947,7 @@ func TestExecuteBidderRequestStage(t *testing.T) { expectedReject *RejectError expectedModuleContexts *moduleContexts expectedStageOutcomes []StageOutcome + privacyConfig *config.AccountPrivacy }{ { description: "Payload not changed if hook execution plan empty", @@ -1169,6 +1179,9 @@ func TestExecuteBidderRequestStage(t *testing.T) { for _, test := range testCases { t.Run(test.description, func(t *testing.T) { exec := NewHookExecutor(test.givenPlanBuilder, EndpointAuction, &metricsConfig.NilMetricsEngine{}) + privacyConfig := getTransmitUFPDActivityConfig("foo", false) + ac := privacy.NewActivityControl(privacyConfig) + exec.SetActivityControl(ac) exec.SetAccount(test.givenAccount) reject := exec.ExecuteBidderRequestStage(&openrtb_ext.RequestWrapper{BidRequest: test.givenBidderRequest}, bidderName) @@ -1187,6 +1200,29 @@ func TestExecuteBidderRequestStage(t *testing.T) { } } +func getTransmitUFPDActivityConfig(componentName string, allow bool) *config.AccountPrivacy { + return &config.AccountPrivacy{ + AllowActivities: &config.AllowActivities{ + TransmitUserFPD: buildDefaultActivityConfig(componentName, allow), + }, + } +} + +func buildDefaultActivityConfig(componentName string, allow bool) config.Activity { + return config.Activity{ + Default: ptrutil.ToPtr(true), + Rules: []config.ActivityRule{ + { + Allow: allow, + Condition: config.ActivityCondition{ + ComponentName: []string{componentName}, + ComponentType: []string{"general"}, + }, + }, + }, + } +} + func TestExecuteRawBidderResponseStage(t *testing.T) { foobarModuleCtx := &moduleContexts{ctxs: map[string]hookstage.ModuleContext{"foobar": nil}} account := &config.Account{} @@ -1390,6 +1426,10 @@ func TestExecuteRawBidderResponseStage(t *testing.T) { for _, test := range testCases { t.Run(test.description, func(ti *testing.T) { exec := NewHookExecutor(test.givenPlanBuilder, EndpointAuction, &metricsConfig.NilMetricsEngine{}) + + privacyConfig := getTransmitUFPDActivityConfig("foo", false) + ac := privacy.NewActivityControl(privacyConfig) + exec.SetActivityControl(ac) exec.SetAccount(test.givenAccount) reject := exec.ExecuteRawBidderResponseStage(&test.givenBidderResponse, "the-bidder") @@ -1669,6 +1709,10 @@ func TestExecuteAllProcessedBidResponsesStage(t *testing.T) { for _, test := range testCases { t.Run(test.description, func(t *testing.T) { exec := NewHookExecutor(test.givenPlanBuilder, EndpointAuction, &metricsConfig.NilMetricsEngine{}) + + privacyConfig := getTransmitUFPDActivityConfig("foo", false) + ac := privacy.NewActivityControl(privacyConfig) + exec.SetActivityControl(ac) exec.SetAccount(test.givenAccount) exec.ExecuteAllProcessedBidResponsesStage(test.givenBiddersResponse) @@ -1918,6 +1962,10 @@ func TestExecuteAuctionResponseStage(t *testing.T) { for _, test := range testCases { t.Run(test.description, func(t *testing.T) { exec := NewHookExecutor(test.givenPlanBuilder, EndpointAuction, &metricsConfig.NilMetricsEngine{}) + + privacyConfig := getTransmitUFPDActivityConfig("foo", false) + ac := privacy.NewActivityControl(privacyConfig) + exec.SetActivityControl(ac) exec.SetAccount(test.givenAccount) exec.ExecuteAuctionResponseStage(test.givenResponse) diff --git a/hooks/hookstage/bidderrequest.go b/hooks/hookstage/bidderrequest.go index af480c5410c..05f3574c8bf 100644 --- a/hooks/hookstage/bidderrequest.go +++ b/hooks/hookstage/bidderrequest.go @@ -27,3 +27,17 @@ type BidderRequestPayload struct { Request *openrtb_ext.RequestWrapper Bidder string } + +func (brp *BidderRequestPayload) GetBidderRequestPayload() *openrtb_ext.RequestWrapper { + return brp.Request +} + +func (brp *BidderRequestPayload) SetBidderRequestPayload(br *openrtb_ext.RequestWrapper) { + brp.Request = br +} + +// RequestUpdater allows reading and writing a bid request +type RequestUpdater interface { + GetBidderRequestPayload() *openrtb_ext.RequestWrapper + SetBidderRequestPayload(br *openrtb_ext.RequestWrapper) +} diff --git a/hooks/hookstage/processedauctionrequest.go b/hooks/hookstage/processedauctionrequest.go index f420561310c..02638dccc20 100644 --- a/hooks/hookstage/processedauctionrequest.go +++ b/hooks/hookstage/processedauctionrequest.go @@ -2,7 +2,6 @@ package hookstage import ( "context" - "github.com/prebid/prebid-server/v2/openrtb_ext" ) @@ -28,3 +27,11 @@ type ProcessedAuctionRequest interface { type ProcessedAuctionRequestPayload struct { Request *openrtb_ext.RequestWrapper } + +func (parp *ProcessedAuctionRequestPayload) GetBidderRequestPayload() *openrtb_ext.RequestWrapper { + return parp.Request +} + +func (parp *ProcessedAuctionRequestPayload) SetBidderRequestPayload(br *openrtb_ext.RequestWrapper) { + parp.Request = br +} diff --git a/ortb/clone.go b/ortb/clone.go index c831aae21b5..2fe9be68f74 100644 --- a/ortb/clone.go +++ b/ortb/clone.go @@ -2,6 +2,7 @@ package ortb import ( "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/v2/openrtb_ext" "github.com/prebid/prebid-server/v2/util/ptrutil" "github.com/prebid/prebid-server/v2/util/sliceutil" ) @@ -399,3 +400,33 @@ func CloneDOOH(s *openrtb2.DOOH) *openrtb2.DOOH { return &c } + +// cloneBidderReq - clones bidder request and replaces req.User and req.Device and req.Source with new copies +func CloneBidderReq(req *openrtb2.BidRequest) *openrtb_ext.RequestWrapper { + if req == nil { + return nil + } + + // bidder request may be modified differently per bidder based on privacy configs + // new request should be created for each bidder request + // pointer fields like User and Device should be cloned and set back to the request copy + newReq := ptrutil.Clone(req) + + if req.User != nil { + userCopy := CloneUser(req.User) + newReq.User = userCopy + } + + if req.Device != nil { + deviceCopy := CloneDevice(req.Device) + newReq.Device = deviceCopy + } + + if req.Source != nil { + sourceCopy := CloneSource(req.Source) + newReq.Source = sourceCopy + } + + reqWrapper := &openrtb_ext.RequestWrapper{BidRequest: newReq} + return reqWrapper +} diff --git a/ortb/clone_test.go b/ortb/clone_test.go index 820c24397f4..1afc269240b 100644 --- a/ortb/clone_test.go +++ b/ortb/clone_test.go @@ -1127,6 +1127,58 @@ func TestCloneDOOH(t *testing.T) { }) } +func TestCloneBidderReq(t *testing.T) { + t.Run("nil", func(t *testing.T) { + result := CloneBidderReq(nil) + assert.Nil(t, result) + }) + + t.Run("empty", func(t *testing.T) { + given := &openrtb2.BidRequest{} + result := CloneBidderReq(given) + assert.Equal(t, given, result.BidRequest) + assert.NotSame(t, given, result) + }) + + t.Run("populated", func(t *testing.T) { + given := &openrtb2.BidRequest{ + ID: "anyID", + User: &openrtb2.User{ID: "testUserId"}, + Device: &openrtb2.Device{Carrier: "testCarrier"}, + Source: &openrtb2.Source{TID: "testTID"}, + } + result := CloneBidderReq(given) + assert.Equal(t, given, result.BidRequest) + assert.NotSame(t, given, result.BidRequest, "pointer") + assert.NotSame(t, given.User, result.User, "user") + assert.NotSame(t, given.Device, result.Device, "device") + assert.NotSame(t, given.Source, result.Source, "source") + }) + + t.Run("assumptions", func(t *testing.T) { + assert.ElementsMatch(t, discoverPointerFields(reflect.TypeOf(openrtb2.BidRequest{})), + []string{ + "Device", + "User", + "Source", + "Imp", + "Site", + "App", + "DOOH", + "WSeat", + "BSeat", + "Cur", + "WLang", + "WLangB", + "BCat", + "BAdv", + "BApp", + "Regs", + "Ext", + }) + }) +} + // discoverPointerFields returns the names of all fields of an object that are // pointers and would need to be cloned. This method is specific to types which can // appear within an OpenRTB data model object.