diff --git a/adapters/ortbbidder/constant.go b/adapters/ortbbidder/constant.go index 27a617bf4cd..158019d393d 100644 --- a/adapters/ortbbidder/constant.go +++ b/adapters/ortbbidder/constant.go @@ -31,6 +31,6 @@ const ( // constants to set values in adapter response const ( currencyKey = "Currency" - typeBidKey = "Bid" + typedbidKey = "Bid" bidsKey = "Bids" ) diff --git a/adapters/ortbbidder/errors.go b/adapters/ortbbidder/errors.go deleted file mode 100644 index af64cbd21f3..00000000000 --- a/adapters/ortbbidder/errors.go +++ /dev/null @@ -1,27 +0,0 @@ -package ortbbidder - -import ( - "errors" - "fmt" - - "github.com/prebid/prebid-server/v2/errortypes" -) - -// list of constant errors -var ( - errImpMissing error = errors.New("imp object not found in request") - errNilBidderParamCfg error = errors.New("found nil bidderParamsConfig") -) - -// newBadInputError returns the error of type bad-input -func newBadInputError(message string, args ...any) error { - return &errortypes.BadServerResponse{ - Message: fmt.Sprintf(message, args...), - } -} - -func newBadServerResponseError(message string, args ...any) error { - return &errortypes.BadServerResponse{ - Message: fmt.Sprintf(message, args...), - } -} diff --git a/adapters/ortbbidder/multi_request_builder.go b/adapters/ortbbidder/multi_request_builder.go index 1257feb47ff..3f6a1d1f3bf 100644 --- a/adapters/ortbbidder/multi_request_builder.go +++ b/adapters/ortbbidder/multi_request_builder.go @@ -5,6 +5,7 @@ import ( "github.com/prebid/openrtb/v20/openrtb2" "github.com/prebid/prebid-server/v2/adapters" + "github.com/prebid/prebid-server/v2/adapters/ortbbidder/util" "github.com/prebid/prebid-server/v2/util/jsonutil" ) @@ -17,7 +18,7 @@ type multiRequestBuilder struct { // parseRequest parse the incoming request and populates intermediate fields required for building requestData object func (rb *multiRequestBuilder) parseRequest(request *openrtb2.BidRequest) (err error) { if len(request.Imp) == 0 { - return errImpMissing + return util.ErrImpMissing } //get rawrequests without impression objects @@ -56,7 +57,7 @@ func (rb *multiRequestBuilder) makeRequest() (requestData []*adapters.RequestDat //step 1: clone request if requestCloneRequired { if newRequest, err = cloneRequest(rb.rawRequest); err != nil { - errs = append(errs, newBadInputError(err.Error())) + errs = append(errs, util.NewBadInputError(err.Error())) continue } } @@ -67,7 +68,7 @@ func (rb *multiRequestBuilder) makeRequest() (requestData []*adapters.RequestDat //step 3: get endpoint if endpoint, err = rb.getEndpoint(bidderParams); err != nil { - errs = append(errs, newBadInputError(err.Error())) + errs = append(errs, util.NewBadInputError(err.Error())) continue } @@ -81,7 +82,7 @@ func (rb *multiRequestBuilder) makeRequest() (requestData []*adapters.RequestDat } //step 5: append new request data if requestData, err = appendRequestData(requestData, newRequest, endpoint, []string{imp[idKey].(string)}); err != nil { - errs = append(errs, newBadInputError(err.Error())) + errs = append(errs, util.NewBadInputError(err.Error())) } } return requestData, errs diff --git a/adapters/ortbbidder/multi_request_builder_test.go b/adapters/ortbbidder/multi_request_builder_test.go index 637413be8dd..37f616e33e5 100644 --- a/adapters/ortbbidder/multi_request_builder_test.go +++ b/adapters/ortbbidder/multi_request_builder_test.go @@ -10,6 +10,7 @@ import ( "github.com/prebid/openrtb/v20/openrtb2" "github.com/prebid/prebid-server/v2/adapters" "github.com/prebid/prebid-server/v2/adapters/ortbbidder/bidderparams" + "github.com/prebid/prebid-server/v2/adapters/ortbbidder/util" "github.com/stretchr/testify/assert" ) @@ -35,7 +36,7 @@ func TestMultiRequestBuilderParseRequest(t *testing.T) { }, }, want: want{ - err: errImpMissing, + err: util.ErrImpMissing, rawRequest: nil, imps: nil, }, @@ -214,7 +215,7 @@ func TestMultiRequestBuilderMakeRequest(t *testing.T) { want: want{ requestData: nil, - errs: []error{newBadInputError("failed to replace macros in endpoint, err:template: endpointTemplate:1:2: " + + errs: []error{util.NewBadInputError("failed to replace macros in endpoint, err:template: endpointTemplate:1:2: " + "executing \"endpointTemplate\" at : error calling errorFunc: intentional error")}, }, }, diff --git a/adapters/ortbbidder/ortbbidder.go b/adapters/ortbbidder/ortbbidder.go index 5008f23cdb5..df8b175c772 100644 --- a/adapters/ortbbidder/ortbbidder.go +++ b/adapters/ortbbidder/ortbbidder.go @@ -10,6 +10,7 @@ import ( "github.com/prebid/prebid-server/v2/adapters/ortbbidder/bidderparams" "github.com/prebid/prebid-server/v2/adapters/ortbbidder/util" "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/errortypes" "github.com/prebid/prebid-server/v2/openrtb_ext" "github.com/prebid/prebid-server/v2/util/jsonutil" ) @@ -62,7 +63,7 @@ func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server co // MakeRequests prepares oRTB bidder-specific request information using which prebid server make call(s) to bidder. func (o *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { if o.bidderParamsConfig == nil { - return nil, []error{newBadInputError(errNilBidderParamCfg.Error())} + return nil, []error{util.NewBadInputError(util.ErrNilBidderParamCfg.Error())} } requestBuilder := newRequestBuilder( @@ -72,7 +73,7 @@ func (o *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapte o.bidderParamsConfig.GetRequestParams(o.bidderName.String())) if err := requestBuilder.parseRequest(request); err != nil { - return nil, []error{newBadInputError(err.Error())} + return nil, []error{util.NewBadInputError(err.Error())} } return requestBuilder.makeRequest() @@ -88,25 +89,24 @@ func (o *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.R return nil, []error{err} } - response, err := o.makeBids(request, responseData.Body) - if err != nil { - return nil, []error{newBadServerResponseError(err.Error())} - } - - return response, nil + return o.makeBids(request, responseData.Body) } // makeBids converts the bidderResponseBytes to a BidderResponse // It retrieves response parameters, creates a response builder, parses the response, and builds the response. // Finally, it converts the response builder's internal representation to an AdapterResponse and returns it. -func (o *adapter) makeBids(request *openrtb2.BidRequest, bidderResponseBytes json.RawMessage) (*adapters.BidderResponse, error) { +func (o *adapter) makeBids(request *openrtb2.BidRequest, bidderResponseBytes json.RawMessage) (*adapters.BidderResponse, []error) { responseParmas := o.bidderParamsConfig.GetResponseParams(o.bidderName.String()) rb := newResponseBuilder(responseParmas, request) - err := rb.setPrebidBidderResponse(bidderResponseBytes) - if err != nil { - return nil, err + errs := rb.setPrebidBidderResponse(bidderResponseBytes) + if errortypes.ContainsFatalError(errs) { + return nil, errs } - return rb.buildAdapterResponse() + bidderResponse, err := rb.buildAdapterResponse() + if err != nil { + errs = append(errs, err) + } + return bidderResponse, errs } diff --git a/adapters/ortbbidder/ortbbidder_test.go b/adapters/ortbbidder/ortbbidder_test.go index d0b0dc9ba4e..0606ec82c82 100644 --- a/adapters/ortbbidder/ortbbidder_test.go +++ b/adapters/ortbbidder/ortbbidder_test.go @@ -10,6 +10,7 @@ import ( "github.com/prebid/openrtb/v20/openrtb2" "github.com/prebid/prebid-server/v2/adapters" "github.com/prebid/prebid-server/v2/adapters/ortbbidder/bidderparams" + "github.com/prebid/prebid-server/v2/adapters/ortbbidder/util" "github.com/prebid/prebid-server/v2/config" "github.com/prebid/prebid-server/v2/errortypes" "github.com/prebid/prebid-server/v2/openrtb_ext" @@ -148,7 +149,7 @@ func TestMakeRequests(t *testing.T) { bidderCfg: bidderparams.NewBidderConfig(), }, want: want{ - errors: []error{newBadInputError(errImpMissing.Error())}, + errors: []error{util.NewBadInputError(util.ErrImpMissing.Error())}, }, }, { @@ -162,7 +163,7 @@ func TestMakeRequests(t *testing.T) { bidderCfg: nil, }, want: want{ - errors: []error{newBadInputError("found nil bidderParamsConfig")}, + errors: []error{util.NewBadInputError("found nil bidderParamsConfig")}, }, }, { @@ -619,7 +620,7 @@ func TestMakeBids(t *testing.T) { bc := bidderparams.NewBidderConfig() bc.BidderConfigMap["owortb_testbidder"] = &bidderparams.Config{ ResponseParams: map[string]bidderparams.BidderParamMapper{ - "bidtype": {Location: "seatbid.#.bid.#.ext.bidtype"}, + "bidType": {Location: "seatbid.#.bid.#.ext.bidtype"}, "currency": {Location: "ext.currency"}, }, } diff --git a/adapters/ortbbidder/resolver/bidVideo_resolver.go b/adapters/ortbbidder/resolver/bidVideo_resolver.go new file mode 100644 index 00000000000..95fa8f93d02 --- /dev/null +++ b/adapters/ortbbidder/resolver/bidVideo_resolver.go @@ -0,0 +1,152 @@ +package resolver + +import ( + "github.com/prebid/prebid-server/v2/adapters/ortbbidder/util" + "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/prebid/prebid-server/v2/util/jsonutil" +) + +// bidVideoResolver determines the duration of the bid by retrieving the video field using the bidder param location. +// The determined video field is subsequently assigned to adapterresponse.typedbid.bidvideo +type bidVideoResolver struct { + paramResolver +} + +func (b *bidVideoResolver) retrieveFromBidderParamLocation(responseNode map[string]any, path string) (any, error) { + value, found := util.GetValueFromLocation(responseNode, path) + if !found { + return nil, NewDefaultValueError("no value sent by bidder at [%s] for [bid.ext.prebid.video]", path) + } + video, err := validateBidVideo(value) + if err != nil { + return nil, NewValidationFailedError("invalid value sent by bidder at [%s] for [bid.ext.prebid.video]", path) + } + return video, nil +} + +func validateBidVideo(value any) (any, error) { + bidVideoBytes, err := jsonutil.Marshal(value) + if err != nil { + return nil, err + } + + var bidVideo openrtb_ext.ExtBidPrebidVideo + err = jsonutil.UnmarshalValid(bidVideoBytes, &bidVideo) + if err != nil { + return nil, err + } + + var bidVideoMap map[string]any + err = jsonutil.UnmarshalValid(bidVideoBytes, &bidVideoMap) + if err != nil { + return nil, err + } + return bidVideoMap, nil +} + +func (b *bidVideoResolver) setValue(adapterBid map[string]any, value any) error { + adapterBid[bidVideoKey] = value + return nil +} + +// bidVideoDurationResolver determines the duration of the bid based on the following hierarchy: +// 1. It first attempts to retrieve the bid type from the response.seat.bid.dur location. +// 2. If not found, it then tries to retrieve the duration using the bidder param location. +// The determined bid duration is subsequently assigned to adapterresponse.typedbid.bidvideo.dur +type bidVideoDurationResolver struct { + paramResolver +} + +func (b *bidVideoDurationResolver) getFromORTBObject(bid map[string]any) (any, error) { + value, ok := bid[ortbFieldDuration] + if !ok { + return nil, NewDefaultValueError("no value sent by bidder at [bid.dur] for [bid.ext.prebid.video.duration]") + } + + duration, ok := validateNumber[int64](value) + if !ok { + return nil, NewValidationFailedError("invalid value sent by bidder at [bid.dur] for [bid.ext.prebid.video.duration]") + } + + if duration == 0 { + return nil, NewDefaultValueError("default value sent by bidder at [bid.dur] for [bid.ext.prebid.video.duration]") + } + return duration, nil +} + +func (b *bidVideoDurationResolver) retrieveFromBidderParamLocation(responseNode map[string]any, path string) (any, error) { + value, found := util.GetValueFromLocation(responseNode, path) + if !found { + return nil, NewDefaultValueError("no value sent by bidder at [%s] for [bid.ext.prebid.video.duration]", path) + } + duration, ok := validateNumber[int64](value) + if !ok { + return nil, NewValidationFailedError("invalid value sent by bidder at [%s] for [bid.ext.prebid.video.duration]", path) + } + return duration, nil +} + +func (b *bidVideoDurationResolver) setValue(adapterBid map[string]any, value any) error { + return setKeyValueInBidVideo(adapterBid, bidVideoDurationKey, value) +} + +// bidVideoPrimaryCategoryResolver determines the primary-category of the bid based on the following hierarchy: +// 1. It first attempts to retrieve the bid category from the response.seat.bid.cat[0] location. +// 2. If not found, it then tries to retrieve the primary category using the bidder param location. +// The determined category is subsequently assigned to adapterresponse.typedbid.bidvideo.primary_category +type bidVideoPrimaryCategoryResolver struct { + paramResolver +} + +func (b *bidVideoPrimaryCategoryResolver) getFromORTBObject(bid map[string]any) (any, error) { + value, found := bid[ortbFieldCategory] + if !found { + return nil, NewDefaultValueError("no value sent by bidder at [bid.cat] for [bid.ext.prebid.video.primary_category]") + } + + categories, ok := value.([]any) + if !ok { + return nil, NewValidationFailedError("invalid value sent by bidder at [bid.cat] for [bid.ext.prebid.video.primary_category]") + } + + if len(categories) == 0 { + return nil, NewDefaultValueError("default value sent by bidder at [bid.cat] for [bid.ext.prebid.video.primary_category]") + } + + category, _ := categories[0].(string) + if len(category) == 0 { + return nil, NewValidationFailedError("invalid value sent by bidder at [bid.cat[0]] for [bid.ext.prebid.video.primary_category]") + } + + return category, nil +} + +func (b *bidVideoPrimaryCategoryResolver) retrieveFromBidderParamLocation(responseNode map[string]any, path string) (any, error) { + value, found := util.GetValueFromLocation(responseNode, path) + if !found { + return nil, NewDefaultValueError("no value sent by bidder at [%s] for [bid.ext.prebid.video.primary_category]", path) + } + category, ok := value.(string) + if !ok { + return nil, NewValidationFailedError("invalid value sent by bidder at [%s] for [bid.ext.prebid.video.primary_category]", path) + } + return category, nil +} + +func (b *bidVideoPrimaryCategoryResolver) setValue(adapterBid map[string]any, value any) error { + return setKeyValueInBidVideo(adapterBid, bidVideoPrimaryCategoryKey, value) +} + +func setKeyValueInBidVideo(adapterBid map[string]any, key string, value any) error { + video, found := adapterBid[bidVideoKey] + if !found { + video = map[string]any{} + adapterBid[bidVideoKey] = video + } + videoTyped, ok := video.(map[string]any) + if !ok || videoTyped == nil { + return NewValidationFailedError("failed to set key:[%s] in BidVideo, error:[incorrect data type]", key) + } + videoTyped[key] = value + return nil +} diff --git a/adapters/ortbbidder/resolver/bidVideo_resolver_test.go b/adapters/ortbbidder/resolver/bidVideo_resolver_test.go new file mode 100644 index 00000000000..332c4520d56 --- /dev/null +++ b/adapters/ortbbidder/resolver/bidVideo_resolver_test.go @@ -0,0 +1,568 @@ +package resolver + +import ( + "reflect" + "testing" + + "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +func TestBidVideoRetrieveFromLocation(t *testing.T) { + resolver := &bidVideoResolver{} + testCases := []struct { + name string + responseNode map[string]any + path string + expectedValue any + expectedError bool + }{ + { + name: "Found bidVideo in location", + responseNode: map[string]any{ + "seatbid": []any{ + map[string]any{ + "bid": []any{ + map[string]any{ + "duration": 100.0, + "ext": map[string]any{ + "video": map[string]any{ + "duration": 11.0, + "primary_category": "sport", + "extra_key": "extra_value", + }, + }, + }, + }, + }, + }, + }, + path: "seatbid.0.bid.0.ext.video", + expectedValue: map[string]any{ + "duration": 11.0, + "primary_category": "sport", + "extra_key": "extra_value", + }, + expectedError: false, + }, + { + name: "bidVideo found but few fields are invalid", + responseNode: map[string]any{ + "seatbid": []any{ + map[string]any{ + "bid": []any{ + map[string]any{ + "duration": 100.0, + "ext": map[string]any{ + "video": map[string]any{ + "duration": "11", // invalid + "primary_category": "sport", + "extra_key": "extra_value", + }, + }, + }, + }, + }, + }, + }, + path: "seatbid.0.bid.0.ext.video", + expectedValue: nil, + expectedError: true, + }, + { + name: "bidVideo not found in location", + responseNode: map[string]any{ + "seatbid": []any{ + map[string]any{}, + }, + }, + path: "seatbid.0.bid.0.ext.video", + expectedValue: nil, + expectedError: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + value, err := resolver.retrieveFromBidderParamLocation(tc.responseNode, tc.path) + assert.Equal(t, tc.expectedValue, value) + assert.Equal(t, tc.expectedError, err != nil) + }) + } +} + +func TestValidateBidVideo(t *testing.T) { + testCases := []struct { + name string + video any + expectedVideo any + expectedError bool + }{ + { + name: "Valid video map", + video: map[string]any{ + "duration": 30.0, + "primary_category": "sports", + "extra_key": "extra_value", + }, + expectedVideo: map[string]any{ + "duration": 30.0, + "primary_category": "sports", + "extra_key": "extra_value", + }, + expectedError: false, + }, + { + name: "Invalid duration type", + video: map[string]any{ + "duration": "30", + "primary_category": "sports", + }, + expectedVideo: nil, + expectedError: true, + }, + { + name: "Invalid primary category type", + video: map[string]any{ + "duration": 30.0, + "primary_category": 123, + }, + expectedVideo: nil, + expectedError: true, + }, + { + name: "Invalid type (not a map)", + video: make(chan int), + expectedVideo: nil, + expectedError: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + validatedVideo, err := validateBidVideo(tc.video) + assert.Equal(t, tc.expectedVideo, validatedVideo) + assert.Equal(t, tc.expectedError, err != nil) + }) + } +} + +func TestBidVideoSetValue(t *testing.T) { + resolver := &bidVideoResolver{} + testCases := []struct { + name string + adapterBid map[string]any + value any + expectedAdapter map[string]any + }{ + { + name: "Set bidVideo value", + adapterBid: map[string]any{}, + value: map[string]any{ + "duration": 30, + "primary_category": "IAB-1", + }, + expectedAdapter: map[string]any{ + "BidVideo": map[string]any{ + "duration": 30, + "primary_category": "IAB-1", + }, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _ = resolver.setValue(tc.adapterBid, tc.value) + assert.Equal(t, tc.expectedAdapter, tc.adapterBid) + }) + } +} + +func TestBidVideoDurationGetFromORTBObject(t *testing.T) { + resolver := &bidVideoDurationResolver{} + testCases := []struct { + name string + responseNode map[string]any + expectedValue any + expectedError bool + }{ + { + name: "Not found dur in location", + responseNode: map[string]any{}, + expectedValue: nil, + expectedError: true, + }, + { + name: "Found dur in location", + responseNode: map[string]any{ + "dur": 11.0, + }, + expectedValue: int64(11), + expectedError: false, + }, + { + name: "Found default value for dur", + responseNode: map[string]any{ + "dur": 0.0, + }, + expectedValue: nil, + expectedError: true, + }, + { + name: "Found dur in location but type is invalid", + responseNode: map[string]any{ + "dur": "invalid", + }, + expectedValue: nil, + expectedError: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + value, err := resolver.getFromORTBObject(tc.responseNode) + assert.Equal(t, tc.expectedValue, value) + assert.Equal(t, tc.expectedError, err != nil) + }) + } +} + +func TestBidVideoDurarionRetrieveFromLocation(t *testing.T) { + resolver := &bidVideoDurationResolver{} + testCases := []struct { + name string + responseNode map[string]any + path string + expectedValue any + expectedError bool + }{ + { + name: "Found dur in location", + responseNode: map[string]any{ + "seatbid": []any{ + map[string]any{ + "bid": []any{ + map[string]any{ + "duration": 100.0, + }, + }, + }, + }, + }, + path: "seatbid.0.bid.0.duration", + expectedValue: int64(100), + expectedError: false, + }, + { + name: "Found dur in location but type is invalid", + responseNode: map[string]any{ + "seatbid": []any{ + map[string]any{ + "bid": []any{ + map[string]any{ + "duration": 100, + }, + }, + }, + }, + }, + path: "seatbid.0.bid.0.duration", + expectedValue: nil, + expectedError: true, + }, + { + name: "dur not found in location", + responseNode: map[string]any{}, + path: "seat", + expectedValue: nil, + expectedError: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + value, err := resolver.retrieveFromBidderParamLocation(tc.responseNode, tc.path) + assert.Equal(t, tc.expectedValue, value) + assert.Equal(t, tc.expectedError, err != nil) + }) + } +} + +func TestSetValueBidVideoDuration(t *testing.T) { + resolver := &bidVideoDurationResolver{} + testCases := []struct { + name string + adapterBid map[string]any + value any + expectedAdapter map[string]any + expectedError bool + }{ + { + name: "Set video duration when video is absent", + adapterBid: map[string]any{}, + value: 10, + expectedAdapter: map[string]any{ + "BidVideo": map[string]any{ + bidVideoDurationKey: 10, + }, + }, + expectedError: false, + }, + { + name: "Set video duration when video is present", + adapterBid: map[string]any{ + "BidVideo": map[string]any{ + bidVideoPrimaryCategoryKey: "IAB-1", + }, + }, + value: 10, + expectedAdapter: map[string]any{ + "BidVideo": map[string]any{ + "duration": 10, + bidVideoPrimaryCategoryKey: "IAB-1", + }, + }, + expectedError: false, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := resolver.setValue(tc.adapterBid, tc.value) + assert.Equal(t, tc.expectedError, result != nil) + assert.Equal(t, tc.expectedAdapter, tc.adapterBid) + }) + } +} + +func TestBidVideoPrimaryCategoryRetrieveFromLocation(t *testing.T) { + resolver := &bidVideoPrimaryCategoryResolver{} + testCases := []struct { + name string + responseNode map[string]any + path string + expectedValue any + expectedError bool + }{ + { + name: "Found category in location", + responseNode: map[string]any{ + "cat": []any{"IAB-1", "IAB-2"}, + }, + path: "cat.1", + expectedValue: "IAB-2", + expectedError: false, + }, + { + name: "Found category in location but type is invalid", + responseNode: map[string]any{ + "cat": []any{"IAB-1", 100}, + }, + path: "cat.1", + expectedValue: nil, + expectedError: true, + }, + { + name: "Category not found in location", + responseNode: map[string]any{}, + path: "seat", + expectedValue: nil, + expectedError: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + value, err := resolver.retrieveFromBidderParamLocation(tc.responseNode, tc.path) + assert.Equal(t, tc.expectedValue, value) + assert.Equal(t, tc.expectedError, err != nil) + }) + } +} + +func TestBidVideoPrimaryCategoryGetFromORTBObject(t *testing.T) { + resolver := &bidVideoPrimaryCategoryResolver{} + testCases := []struct { + name string + responseNode map[string]any + expectedValue any + expectedError bool + }{ + { + name: "Found category in location", + responseNode: map[string]any{ + "cat": []any{"IAB-1", "IAB-2"}, + }, + expectedValue: "IAB-1", + expectedError: false, + }, + { + name: "Found empty category in location", + responseNode: map[string]any{ + "cat": []any{}, + }, + expectedValue: nil, + expectedError: true, + }, + { + name: "Not found category in location", + responseNode: map[string]any{ + "field": []any{}, + }, + expectedValue: nil, + expectedError: true, + }, + { + name: "Found category in location but type is invalid", + responseNode: map[string]any{ + "cat": "invalid", + }, + expectedValue: nil, + expectedError: true, + }, + { + name: "Found category in location but first category type is invalid", + responseNode: map[string]any{ + "cat": []any{1, 2}, + }, + expectedValue: nil, + expectedError: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + value, err := resolver.getFromORTBObject(tc.responseNode) + assert.Equal(t, tc.expectedValue, value) + assert.Equal(t, tc.expectedError, err != nil) + }) + } +} + +func TestSetValuePrimaryCategory(t *testing.T) { + resolver := &bidVideoPrimaryCategoryResolver{} + testCases := []struct { + name string + adapterBid map[string]any + value any + expectedAdapter map[string]any + expectedError bool + }{ + { + name: "Set video key-value when video is absent", + adapterBid: map[string]any{}, + value: "IAB-1", + expectedAdapter: map[string]any{ + "BidVideo": map[string]any{ + bidVideoPrimaryCategoryKey: "IAB-1", + }, + }, + expectedError: false, + }, + { + name: "Set video key-value when video is present", + adapterBid: map[string]any{ + "BidVideo": map[string]any{ + "duration": 30, + }, + }, + value: "IAB-1", + expectedAdapter: map[string]any{ + "BidVideo": map[string]any{ + "duration": 30, + bidVideoPrimaryCategoryKey: "IAB-1", + }, + }, + expectedError: false, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := resolver.setValue(tc.adapterBid, tc.value) + assert.Equal(t, tc.expectedError, err != nil) + assert.Equal(t, tc.expectedAdapter, tc.adapterBid) + }) + } +} + +func TestSetKeyValueInBidVideo(t *testing.T) { + testCases := []struct { + name string + adapterBid map[string]any + key string + value any + expectedAdapter map[string]any + expectedError bool + }{ + { + name: "Set video key-value when video is absent", + adapterBid: map[string]any{}, + key: "duration", + value: 30, + expectedAdapter: map[string]any{ + "BidVideo": map[string]any{ + "duration": 30, + }, + }, + expectedError: false, + }, + { + name: "Set video key-value when video is present", + adapterBid: map[string]any{ + "BidVideo": map[string]any{}, + }, + key: "duration", + value: 30, + expectedAdapter: map[string]any{ + "BidVideo": map[string]any{ + "duration": 30, + }, + }, + expectedError: false, + }, + { + name: "Override existing video key-value", + adapterBid: map[string]any{ + "BidVideo": map[string]any{ + "duration": 15, + }, + }, + key: "duration", + value: 30, + expectedAdapter: map[string]any{ + "BidVideo": map[string]any{ + "duration": 30, + }, + }, + expectedError: false, + }, + { + name: "Invalid video type", + adapterBid: map[string]any{ + "BidVideo": "invalid", + }, + key: "duration", + value: 30, + expectedAdapter: map[string]any{"BidVideo": "invalid"}, + expectedError: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := setKeyValueInBidVideo(tc.adapterBid, tc.key, tc.value) + assert.Equal(t, tc.expectedError, err != nil) + assert.Equal(t, tc.expectedAdapter, tc.adapterBid) + }) + } +} + +// TestExtBidPrebidVideo notifies us of any changes in the openrtb_ext.ExtBidPrebidVideo struct. +// If a new field is added in openrtb_ext.ExtBidPrebidVideo, then add the support to resolve the new field and update the test case. +// If the data type of an existing field changes then update the resolver of the respective field. +func TestExtBidPrebidVideoFields(t *testing.T) { + // Expected field count and types + expectedFields := map[string]reflect.Type{ + "Duration": reflect.TypeOf(0), + "PrimaryCategory": reflect.TypeOf(""), + "VASTTagID": reflect.TypeOf(""), // not expected to be set by adapter + } + + structType := reflect.TypeOf(openrtb_ext.ExtBidPrebidVideo{}) + err := ValidateStructFields(expectedFields, structType) + if err != nil { + t.Error(err) + } +} diff --git a/adapters/ortbbidder/resolver/bidmeta_resolver.go b/adapters/ortbbidder/resolver/bidmeta_resolver.go new file mode 100644 index 00000000000..3d919b5b2a9 --- /dev/null +++ b/adapters/ortbbidder/resolver/bidmeta_resolver.go @@ -0,0 +1,479 @@ +package resolver + +import ( + "github.com/prebid/prebid-server/v2/adapters/ortbbidder/util" + "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/prebid/prebid-server/v2/util/jsonutil" +) + +// bidMetaResolver retrieves the meta object of the bid using the bidder param location. +// The determined bidMeta is subsequently assigned to adapterresponse.typedbid.bidmeta +type bidMetaResolver struct { + paramResolver +} + +func (b *bidMetaResolver) retrieveFromBidderParamLocation(responseNode map[string]any, path string) (any, error) { + value, found := util.GetValueFromLocation(responseNode, path) + if !found { + return nil, NewDefaultValueError("no value sent by bidder at [%s] for [bid.ext.prebid.meta]", path) + } + bidMeta, err := validateBidMeta(value) + if err != nil { + return nil, NewValidationFailedError("invalid value sent by bidder at [%s] for [bid.ext.prebid.meta]", path) + } + return bidMeta, nil +} + +func validateBidMeta(value any) (any, error) { + bidMetaBytes, err := jsonutil.Marshal(value) + if err != nil { + return nil, err + } + + var bidMeta openrtb_ext.ExtBidPrebidMeta + err = jsonutil.UnmarshalValid(bidMetaBytes, &bidMeta) + if err != nil { + return nil, err + } + + var bidMetaMap map[string]any + err = jsonutil.UnmarshalValid(bidMetaBytes, &bidMetaMap) + if err != nil { + return nil, err + } + return bidMetaMap, nil +} + +func (b *bidMetaResolver) setValue(adapterBid map[string]any, value any) error { + adapterBid[bidMetaKey] = value + return nil +} + +// bidMetaAdvDomainsResolver retrieves the advertiserDomains of the bid using the bidder param location. +// The determined advertiserDomains is subsequently assigned to adapterresponse.typedbid.bidmeta.advertiserDomains +type bidMetaAdvDomainsResolver struct { + paramResolver +} + +func (b *bidMetaAdvDomainsResolver) retrieveFromBidderParamLocation(responseNode map[string]any, path string) (any, error) { + value, ok := util.GetValueFromLocation(responseNode, path) + if !ok { + return nil, NewDefaultValueError("no value sent by bidder at [%s] for [bid.ext.prebid.meta.advertiserDomains]", path) + } + + adomains, ok := validateDataTypeSlice[string](value) + if !ok { + return nil, NewValidationFailedError("invalid value sent by bidder at [%s] for [bid.ext.prebid.meta.advertiserDomains]", path) + } + return adomains, nil +} + +func (b *bidMetaAdvDomainsResolver) setValue(adapterBid map[string]any, value any) error { + return setKeyValueInBidMeta(adapterBid, bidMetaAdvertiserDomainsKey, value) +} + +// bidMetaAdvIDResolver retrieves the advertiserId of the bid using the bidder param location. +// The determined advertiserId is subsequently assigned to adapterresponse.typedbid.bidmeta.advertiserId +type bidMetaAdvIDResolver struct { + paramResolver +} + +func (b *bidMetaAdvIDResolver) retrieveFromBidderParamLocation(responseNode map[string]any, path string) (any, error) { + value, found := util.GetValueFromLocation(responseNode, path) + if !found { + return nil, NewDefaultValueError("no value sent by bidder at [%s] for [bid.ext.prebid.meta.advertiserId]", path) + } + advId, ok := validateNumber[int](value) + if !ok { + return nil, NewValidationFailedError("invalid value sent by bidder at [%s] for [bid.ext.prebid.meta.advertiserId]", path) + } + return advId, nil +} + +func (b *bidMetaAdvIDResolver) setValue(adapterBid map[string]any, value any) error { + return setKeyValueInBidMeta(adapterBid, bidMetaAdvertiserIdKey, value) +} + +// bidMetaAdvNameResolver retrieves the advertiserName of the bid using the bidder param location. +// The determined advertiserName is subsequently assigned to adapterresponse.typedbid.bidmeta.AdvertiserName +type bidMetaAdvNameResolver struct { + paramResolver +} + +func (b *bidMetaAdvNameResolver) retrieveFromBidderParamLocation(responseNode map[string]any, path string) (any, error) { + value, found := util.GetValueFromLocation(responseNode, path) + if !found { + return nil, NewDefaultValueError("no value sent by bidder at [%s] for [bid.ext.prebid.meta.advertiserName]", path) + } + advName, ok := validateString(value) + if !ok { + return nil, NewValidationFailedError("invalid value sent by bidder at [%s] for [bid.ext.prebid.meta.advertiserName]", path) + + } + return advName, nil +} + +func (b *bidMetaAdvNameResolver) setValue(adapterBid map[string]any, value any) error { + return setKeyValueInBidMeta(adapterBid, bidMetaAdvertiserNameKey, value) +} + +// bidMetaAgencyIDResolver retrieves the AgencyID of the bid using the bidder param location. +// The determined AgencyID is subsequently assigned to adapterresponse.typedbid.bidmeta.AgencyID +type bidMetaAgencyIDResolver struct { + paramResolver +} + +func (b *bidMetaAgencyIDResolver) retrieveFromBidderParamLocation(responseNode map[string]any, path string) (any, error) { + value, found := util.GetValueFromLocation(responseNode, path) + if !found { + return nil, NewDefaultValueError("no value sent by bidder at [%s] for [bid.ext.prebid.meta.agencyID]", path) + } + agencyId, ok := validateNumber[int](value) + if !ok { + return nil, NewValidationFailedError("invalid value sent by bidder at [%s] for [bid.ext.prebid.meta.agencyID]", path) + + } + return agencyId, nil +} + +func (b *bidMetaAgencyIDResolver) setValue(adapterBid map[string]any, value any) error { + return setKeyValueInBidMeta(adapterBid, bidMetaAgencyIdKey, value) +} + +// bidMetaAgencyNameResolver retrieves the AgencyName of the bid using the bidder param location. +// The determined AgencyName is subsequently assigned to adapterresponse.typedbid.bidmeta.AgencyName +type bidMetaAgencyNameResolver struct { + paramResolver +} + +func (b *bidMetaAgencyNameResolver) retrieveFromBidderParamLocation(responseNode map[string]any, path string) (any, error) { + value, found := util.GetValueFromLocation(responseNode, path) + if !found { + return nil, NewDefaultValueError("no value sent by bidder at [%s] for [bid.ext.prebid.meta.agencyName]", path) + } + agencyName, ok := validateString(value) + if !ok { + return nil, NewValidationFailedError("invalid value sent by bidder at [%s] for [bid.ext.prebid.meta.agencyName]", path) + + } + return agencyName, nil +} + +func (b *bidMetaAgencyNameResolver) setValue(adapterBid map[string]any, value any) error { + return setKeyValueInBidMeta(adapterBid, bidMetaAgencyNameKey, value) +} + +// bidMetaBrandIDResolver retrieves the BrandID of the bid using the bidder param location. +// The determined BrandID is subsequently assigned to adapterresponse.typedbid.bidmeta.BrandID +type bidMetaBrandIDResolver struct { + paramResolver +} + +func (b *bidMetaBrandIDResolver) retrieveFromBidderParamLocation(responseNode map[string]any, path string) (any, error) { + value, found := util.GetValueFromLocation(responseNode, path) + if !found { + return nil, NewDefaultValueError("no value sent by bidder at [%s] for [bid.ext.prebid.meta.brandID]", path) + } + brandId, ok := validateNumber[int](value) + if !ok { + return nil, NewValidationFailedError("invalid value sent by bidder at [%s] for [bid.ext.prebid.meta.brandID]", path) + + } + return brandId, nil +} + +func (b *bidMetaBrandIDResolver) setValue(adapterBid map[string]any, value any) error { + return setKeyValueInBidMeta(adapterBid, bidMetaBrandIdKey, value) +} + +// bidMetaBrandNameResolver retrieves the BrandName of the bid using the bidder param location. +// The determined BrandName is subsequently assigned to adapterresponse.typedbid.bidmeta.BrandName +type bidMetaBrandNameResolver struct { + paramResolver +} + +func (b *bidMetaBrandNameResolver) retrieveFromBidderParamLocation(responseNode map[string]any, path string) (any, error) { + value, found := util.GetValueFromLocation(responseNode, path) + if !found { + return nil, NewDefaultValueError("no value sent by bidder at [%s] for [bid.ext.prebid.meta.brandName]", path) + } + brandName, ok := validateString(value) + if !ok { + return nil, NewValidationFailedError("invalid value sent by bidder at [%s] for [bid.ext.prebid.meta.brandName]", path) + + } + return brandName, nil +} + +func (b *bidMetaBrandNameResolver) setValue(adapterBid map[string]any, value any) error { + return setKeyValueInBidMeta(adapterBid, bidMetaBrandNameKey, value) +} + +// bidMetaDChainResolver retrieves the Dchain of the bid using the bidder param location. +// The determined Dchain is subsequently assigned to adapterresponse.typedbid.bidmeta.DChain +type bidMetaDChainResolver struct { + paramResolver +} + +func (b *bidMetaDChainResolver) retrieveFromBidderParamLocation(responseNode map[string]any, path string) (any, error) { + value, found := util.GetValueFromLocation(responseNode, path) + if !found { + return nil, NewDefaultValueError("no value sent by bidder at [%s] for [bid.ext.prebid.meta.dchain]", path) + } + dChain, ok := validateMap(value) + if !ok { + return nil, NewValidationFailedError("invalid value sent by bidder at [%s] for [bid.ext.prebid.meta.dchain]", path) + + } + return dChain, nil +} + +func (b *bidMetaDChainResolver) setValue(adapterBid map[string]any, value any) error { + return setKeyValueInBidMeta(adapterBid, bidMetaDChainKey, value) +} + +// bidMetaDemandSourceResolver retrieves the DemandSource of the bid using the bidder param location. +// The determined DemandSource is subsequently assigned to adapterresponse.typedbid.bidmeta.DemandSource +type bidMetaDemandSourceResolver struct { + paramResolver +} + +func (b *bidMetaDemandSourceResolver) retrieveFromBidderParamLocation(responseNode map[string]any, path string) (any, error) { + value, found := util.GetValueFromLocation(responseNode, path) + if !found { + return nil, NewDefaultValueError("no value sent by bidder at [%s] for [bid.ext.prebid.meta.demandSource]", path) + } + demandSource, ok := validateString(value) + if !ok { + return nil, NewValidationFailedError("invalid value sent by bidder at [%s] for [bid.ext.prebid.meta.demandSource]", path) + + } + return demandSource, nil +} + +func (b *bidMetaDemandSourceResolver) setValue(adapterBid map[string]any, value any) error { + return setKeyValueInBidMeta(adapterBid, bidMetaDemandSourceKey, value) +} + +// bidMetaMediaTypeResolver retrieves the MediaType of the bid using the bidder param location. +// The determined MediaType is subsequently assigned to adapterresponse.typedbid.bidmeta.MediaType +type bidMetaMediaTypeResolver struct { + paramResolver +} + +func (b *bidMetaMediaTypeResolver) retrieveFromBidderParamLocation(responseNode map[string]any, path string) (any, error) { + value, found := util.GetValueFromLocation(responseNode, path) + if !found { + return nil, NewDefaultValueError("no value sent by bidder at [%s] for [bid.ext.prebid.meta.mediaType]", path) + } + mediaType, ok := validateString(value) + if !ok { + return nil, NewValidationFailedError("invalid value sent by bidder at [%s] for [bid.ext.prebid.meta.mediaType]", path) + + } + return mediaType, nil +} + +func (b *bidMetaMediaTypeResolver) setValue(adapterBid map[string]any, value any) error { + return setKeyValueInBidMeta(adapterBid, bidMetaMediaTypeKey, value) +} + +// bidMetaNetworkIDResolver retrieves the NetworkID of the bid using the bidder param location. +// The determined NetworkID is subsequently assigned to adapterresponse.typedbid.bidmeta.NetworkID +type bidMetaNetworkIDResolver struct { + paramResolver +} + +func (b *bidMetaNetworkIDResolver) retrieveFromBidderParamLocation(responseNode map[string]any, path string) (any, error) { + value, found := util.GetValueFromLocation(responseNode, path) + if !found { + return nil, NewDefaultValueError("no value sent by bidder at [%s] for [bid.ext.prebid.meta.networkId]", path) + } + networkId, ok := validateNumber[int](value) + if !ok { + return nil, NewValidationFailedError("invalid value sent by bidder at [%s] for [bid.ext.prebid.meta.networkId]", path) + + } + return networkId, nil +} + +func (b *bidMetaNetworkIDResolver) setValue(adapterBid map[string]any, value any) error { + return setKeyValueInBidMeta(adapterBid, bidMetaNetworkIdKey, value) +} + +// bidMetaNetworkNameResolver retrieves the NetworkName of the bid using the bidder param location. +// The determined NetworkName is subsequently assigned to adapterresponse.typedbid.bidmeta.NetworkName +type bidMetaNetworkNameResolver struct { + paramResolver +} + +func (b *bidMetaNetworkNameResolver) retrieveFromBidderParamLocation(responseNode map[string]any, path string) (any, error) { + value, found := util.GetValueFromLocation(responseNode, path) + if !found { + return nil, NewDefaultValueError("no value sent by bidder at [%s] for [bid.ext.prebid.meta.networkName]", path) + } + networkName, ok := validateString(value) + if !ok { + return nil, NewValidationFailedError("invalid value sent by bidder at [%s] for [bid.ext.prebid.meta.networkName]", path) + + } + return networkName, nil +} + +func (b *bidMetaNetworkNameResolver) setValue(adapterBid map[string]any, value any) error { + return setKeyValueInBidMeta(adapterBid, bidMetaNetworkNameKey, value) +} + +// bidMetaPrimaryCategoryIDResolver retrieves the PrimaryCategory of the bid using the bidder param location. +// The determined PrimaryCategory is subsequently assigned to adapterresponse.typedbid.bidmeta.PrimaryCategory +type bidMetaPrimaryCategoryIDResolver struct { + paramResolver +} + +func (b *bidMetaPrimaryCategoryIDResolver) retrieveFromBidderParamLocation(responseNode map[string]any, path string) (any, error) { + value, found := util.GetValueFromLocation(responseNode, path) + if !found { + return nil, NewDefaultValueError("no value sent by bidder at [%s] for [bid.ext.prebid.meta.primaryCategory]", path) + } + categoryId, ok := validateString(value) + if !ok { + return nil, NewValidationFailedError("invalid value sent by bidder at [%s] for [bid.ext.prebid.meta.primaryCategory]", path) + + } + return categoryId, nil +} + +func (b *bidMetaPrimaryCategoryIDResolver) setValue(adapterBid map[string]any, value any) error { + return setKeyValueInBidMeta(adapterBid, bidMetaPrimaryCatIdKey, value) +} + +// bidMetaRendererNameResolver retrieves the RendererName of the bid using the bidder param location. +// The determined RendererName is subsequently assigned to adapterresponse.typedbid.bidmeta.RendererName +type bidMetaRendererNameResolver struct { + paramResolver +} + +func (b *bidMetaRendererNameResolver) retrieveFromBidderParamLocation(responseNode map[string]any, path string) (any, error) { + value, found := util.GetValueFromLocation(responseNode, path) + if !found { + return nil, NewDefaultValueError("no value sent by bidder at [%s] for [bid.ext.prebid.meta.rendererName]", path) + } + rendererName, ok := validateString(value) + if !ok { + return nil, NewValidationFailedError("invalid value sent by bidder at [%s] for [bid.ext.prebid.meta.rendererName]", path) + + } + return rendererName, nil +} + +func (b *bidMetaRendererNameResolver) setValue(adapterBid map[string]any, value any) error { + return setKeyValueInBidMeta(adapterBid, bidMetaRendererNameKey, value) +} + +// bidMetaRendererVersionResolver retrieves the RendererVersion of the bid using the bidder param location. +// The determined RendererVersion is subsequently assigned to adapterresponse.typedbid.bidmeta.RendererVersion +type bidMetaRendererVersionResolver struct { + paramResolver +} + +func (b *bidMetaRendererVersionResolver) retrieveFromBidderParamLocation(responseNode map[string]any, path string) (any, error) { + value, found := util.GetValueFromLocation(responseNode, path) + if !found { + return nil, NewDefaultValueError("no value sent by bidder at [%s] for [bid.ext.prebid.meta.rendererVersion]", path) + } + rendererVersion, ok := validateString(value) + if !ok { + return nil, NewValidationFailedError("invalid value sent by bidder at [%s] for [bid.ext.prebid.meta.rendererVersion]", path) + + } + return rendererVersion, nil +} + +func (b *bidMetaRendererVersionResolver) setValue(adapterBid map[string]any, value any) error { + return setKeyValueInBidMeta(adapterBid, bidMetaRendererVersionKey, value) +} + +// bidMetaRendererDataResolver retrieves the RendererData of the bid using the bidder param location. +// The determined RendererData is subsequently assigned to adapterresponse.typedbid.bidmeta.RendererData +type bidMetaRendererDataResolver struct { + paramResolver +} + +func (b *bidMetaRendererDataResolver) retrieveFromBidderParamLocation(responseNode map[string]any, path string) (any, error) { + value, found := util.GetValueFromLocation(responseNode, path) + if !found { + return nil, NewDefaultValueError("no value sent by bidder at [%s] for [bid.ext.prebid.meta.rendererData]", path) + } + rendererData, ok := validateMap(value) + if !ok { + return nil, NewValidationFailedError("invalid value sent by bidder at [%s] for [bid.ext.prebid.meta.rendererData]", path) + + } + return rendererData, nil +} + +func (b *bidMetaRendererDataResolver) setValue(adapterBid map[string]any, value any) error { + return setKeyValueInBidMeta(adapterBid, bidMetaRenderedDataKey, value) +} + +// bidMetaRendererUrlResolver retrieves the RendererUrl of the bid using the bidder param location. +// The determined RendererUrl is subsequently assigned to adapterresponse.typedbid.bidmeta.RendererUrl +type bidMetaRendererUrlResolver struct { + paramResolver +} + +func (b *bidMetaRendererUrlResolver) retrieveFromBidderParamLocation(responseNode map[string]any, path string) (any, error) { + value, found := util.GetValueFromLocation(responseNode, path) + if !found { + return nil, NewDefaultValueError("no value sent by bidder at [%s] for [bid.ext.prebid.meta.rendererUrl]", path) + } + rendererUrl, ok := validateString(value) + if !ok { + return nil, NewValidationFailedError("invalid value sent by bidder at [%s] for [bid.ext.prebid.meta.rendererUrl]", path) + + } + return rendererUrl, nil +} + +func (b *bidMetaRendererUrlResolver) setValue(adapterBid map[string]any, value any) error { + return setKeyValueInBidMeta(adapterBid, bidMetaRenderedUrlKey, value) +} + +// bidMetaSecondaryCategoryIDsResolver retrieves the secondary-category ids of the bid using the bidder param location. +// The determined secondary-category id are subsequently assigned to adapterresponse.typedbid.bidmeta.secondaryCatIds +type bidMetaSecondaryCategoryIDsResolver struct { + paramResolver +} + +func (b *bidMetaSecondaryCategoryIDsResolver) retrieveFromBidderParamLocation(responseNode map[string]any, path string) (any, error) { + value, found := util.GetValueFromLocation(responseNode, path) + if !found { + return nil, NewDefaultValueError("no value sent by bidder at [%s] for [bid.ext.prebid.meta.secondaryCategoryIds]", path) + } + secondaryCategories, ok := validateDataTypeSlice[string](value) + if !ok { + return nil, NewValidationFailedError("invalid value sent by bidder at [%s] for [bid.ext.prebid.meta.secondaryCategoryIds]", path) + + } + return secondaryCategories, nil +} + +func (b *bidMetaSecondaryCategoryIDsResolver) setValue(adapterBid map[string]any, value any) error { + return setKeyValueInBidMeta(adapterBid, bidMetaSecondaryCatIdKey, value) +} + +// setKeyValueInBidMeta sets the key and value in bidMeta object +// it creates the bidMeta object if required. +func setKeyValueInBidMeta(adapterBid map[string]any, key string, value any) error { + meta, found := adapterBid[bidMetaKey] + if !found { + meta = map[string]any{} + adapterBid[bidMetaKey] = meta + } + typedMeta, ok := meta.(map[string]any) + if !ok || typedMeta == nil { + return NewValidationFailedError("failed to set key:[%s] in BidMeta, error:[incorrect data type]", key) + } + typedMeta[key] = value + return nil +} diff --git a/adapters/ortbbidder/resolver/bidmeta_resolver_test.go b/adapters/ortbbidder/resolver/bidmeta_resolver_test.go new file mode 100644 index 00000000000..529a4192e0e --- /dev/null +++ b/adapters/ortbbidder/resolver/bidmeta_resolver_test.go @@ -0,0 +1,1913 @@ +package resolver + +import ( + "encoding/json" + "reflect" + "testing" + + "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +func TestBidMetaRetrieveFromLocation(t *testing.T) { + testCases := []struct { + name string + ortbResponse map[string]any + path string + expectedValue any + expectedError bool + }{ + { + name: "Found in location", + ortbResponse: map[string]any{ + "cur": "USD", + "seatbid": []any{ + map[string]any{ + "bid": []any{ + map[string]any{ + "id": "123", + "ext": map[string]any{ + "metaObject": map[string]any{ + "advertiserDomains": []any{"abc.com", "xyz.com"}, + "brandId": 1.0, + }, + }, + }, + }, + }, + }, + }, + path: "seatbid.0.bid.0.ext.metaObject", + expectedValue: map[string]any{ + "advertiserDomains": []any{"abc.com", "xyz.com"}, + "brandId": 1.0, + }, + expectedError: false, + }, + { + name: "Found invalid meta object in location", + ortbResponse: map[string]any{ + "cur": "USD", + "seatbid": []any{ + map[string]any{ + "bid": []any{ + map[string]any{ + "id": "123", + "ext": map[string]any{ + "metaObject": map[string]any{ + "advertiserDomains": "abc.com", + "brandId": 1.0, + }, + }, + }, + }, + }, + }, + }, + path: "seatbid.0.bid.0.ext.metaObject", + expectedValue: nil, + expectedError: true, + }, + { + name: "Not found in location", + ortbResponse: map[string]any{ + "cur": "USD", + "seatbid": []any{ + map[string]any{ + "bid": []any{ + map[string]any{ + "id": "123", + }, + }, + }, + }, + }, + path: "seatbid.0.bid.0.ext.nonexistent", + expectedValue: nil, + expectedError: true, + }, + } + resolver := &bidMetaResolver{} + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + value, err := resolver.retrieveFromBidderParamLocation(tc.ortbResponse, tc.path) + assert.Equal(t, tc.expectedValue, value) + assert.Equal(t, tc.expectedError, err != nil) + }) + } +} + +func TestValidateBidMeta(t *testing.T) { + tests := []struct { + name string + value any + expected any + expectedError bool + }{ + { + name: "Metadata with all valid fields", + value: map[string]any{ + bidMetaSecondaryCatIdKey: []any{"music", "sports"}, + bidMetaAdvertiserIdKey: 123.0, + bidMetaDChainKey: map[string]any{ + "field": "value", + }, + "customField": "customValue", + }, + expected: map[string]any{ + bidMetaSecondaryCatIdKey: []any{"music", "sports"}, + bidMetaAdvertiserIdKey: 123.0, + bidMetaDChainKey: map[string]any{ + "field": "value", + }, + "customField": "customValue", + }, + expectedError: false, + }, + { + name: "Metadata with wrong type", + value: map[string]any{ + bidMetaAdvertiserDomainsKey: "example.com", // should be a slice + bidMetaAdvertiserIdKey: "123", // should be an float + }, + expected: nil, + expectedError: true, + }, + { + name: "Invalid type for value", + value: make(chan int), + expected: nil, + expectedError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := validateBidMeta(tt.value) + assert.Equal(t, tt.expectedError, err != nil) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestBidMetaSetValue(t *testing.T) { + resolver := &bidMetaResolver{} + testCases := []struct { + name string + typeBid map[string]any + value any + expectedTypeBid map[string]any + }{ + { + name: "set value ", + typeBid: map[string]any{ + "id": "123", + }, + value: map[string]any{ + "any-key": "any-val", + }, + expectedTypeBid: map[string]any{ + "id": "123", + "BidMeta": map[string]any{ + "any-key": "any-val", + }, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _ = resolver.setValue(tc.typeBid, tc.value) + assert.Equal(t, tc.expectedTypeBid, tc.typeBid) + }) + } +} + +func TestBidMetaAdvDomainsRetrieveFromLocation(t *testing.T) { + testCases := []struct { + name string + ortbResponse map[string]any + path string + expectedValue any + expectedError bool + }{ + { + name: "Found in location", + ortbResponse: map[string]any{ + "cur": "USD", + "seatbid": []any{ + map[string]any{ + "bid": []any{ + map[string]any{ + "id": "123", + "ext": map[string]any{ + "adomains": []any{"abc.com", "xyz.com"}, + }, + }, + }, + }, + }, + }, + path: "seatbid.0.bid.0.ext.adomains", + expectedValue: []string{"abc.com", "xyz.com"}, + expectedError: false, + }, + { + name: "Found in location but data type is invalid", + ortbResponse: map[string]any{ + "cur": "USD", + "seatbid": []any{ + map[string]any{ + "bid": []any{ + map[string]any{ + "id": "123", + "ext": map[string]any{ + "adomains": []string{"abc.com", "xyz.com"}, + }, + }, + }, + }, + }, + }, + path: "seatbid.0.bid.0.ext.adomains", + expectedValue: nil, + expectedError: true, + }, + { + name: "Not found in location", + ortbResponse: map[string]any{ + "cur": "USD", + "seatbid": []any{ + map[string]any{ + "bid": []any{ + map[string]any{ + "id": "123", + }, + }, + }, + }, + }, + path: "seatbid.0.bid.0.ext.nonexistent", + expectedValue: nil, + expectedError: true, + }, + } + resolver := &bidMetaAdvDomainsResolver{} + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + value, err := resolver.retrieveFromBidderParamLocation(tc.ortbResponse, tc.path) + assert.Equal(t, tc.expectedValue, value) + assert.Equal(t, tc.expectedError, err != nil) + }) + } +} + +func TestBidMetaAdvDomainsResolverSetValue(t *testing.T) { + resolver := &bidMetaAdvDomainsResolver{} + testCases := []struct { + name string + typeBid map[string]any + value any + expectedTypeBid map[string]any + }{ + { + name: "set value when meta object is present", + typeBid: map[string]any{ + "id": "123", + "BidMeta": map[string]any{}, + }, + value: "xyz.com", + expectedTypeBid: map[string]any{ + "id": "123", + "BidMeta": map[string]any{ + "advertiserDomains": "xyz.com", + }, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _ = resolver.setValue(tc.typeBid, tc.value) + assert.Equal(t, tc.expectedTypeBid, tc.typeBid) + }) + } +} + +func TestBidMetaAdvIdRetrieveFromLocation(t *testing.T) { + testCases := []struct { + name string + ortbResponse map[string]any + path string + expectedValue any + expectedError bool + }{ + { + name: "Found in location", + ortbResponse: map[string]any{ + "cur": "USD", + "seatbid": []any{ + map[string]any{ + "bid": []any{ + map[string]any{ + "id": "123", + "ext": map[string]any{ + "advid": 10.0, + }, + }, + }, + }, + }, + }, + path: "seatbid.0.bid.0.ext.advid", + expectedValue: 10, + expectedError: false, + }, + { + name: "Found in location but data type is other than float", + ortbResponse: map[string]any{ + "cur": "USD", + "seatbid": []any{ + map[string]any{ + "bid": []any{ + map[string]any{ + "id": "123", + "ext": map[string]any{ + "advid": 10, + }, + }, + }, + }, + }, + }, + path: "seatbid.0.bid.0.ext.advid", + expectedValue: nil, + expectedError: true, + }, + { + name: "Not found in location", + ortbResponse: map[string]any{ + "cur": "USD", + "seatbid": []any{ + map[string]any{ + "bid": []any{ + map[string]any{ + "id": "123", + }, + }, + }, + }, + }, + path: "seatbid.0.bid.0.ext.nonexistent", + expectedValue: nil, + expectedError: true, + }, + } + resolver := &bidMetaAdvIDResolver{} + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + value, err := resolver.retrieveFromBidderParamLocation(tc.ortbResponse, tc.path) + assert.Equal(t, tc.expectedValue, value) + assert.Equal(t, tc.expectedError, err != nil) + }) + } +} + +func TestBidMetaAdvIdResolverSetValue(t *testing.T) { + resolver := &bidMetaAdvIDResolver{} + testCases := []struct { + name string + typeBid map[string]any + value any + expectedTypeBid map[string]any + }{ + { + name: "set value when meta object is absent", + typeBid: map[string]any{ + "id": "123", + }, + value: 1, + expectedTypeBid: map[string]any{ + "id": "123", + "BidMeta": map[string]any{ + "advertiserId": 1, + }, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _ = resolver.setValue(tc.typeBid, tc.value) + assert.Equal(t, tc.expectedTypeBid, tc.typeBid) + }) + } +} + +func TestBidMetaAdvNameRetrieveFromLocation(t *testing.T) { + testCases := []struct { + name string + ortbResponse map[string]any + path string + expectedValue any + expectedError bool + }{ + { + name: "Found in location", + ortbResponse: map[string]any{ + "cur": "USD", + "advname": "Acme Corp", + }, + path: "advname", + expectedValue: "Acme Corp", + expectedError: false, + }, + { + name: "Found in location but data type is other than string", + ortbResponse: map[string]any{ + "ext": map[string]any{ + "advname": 123, + }, + }, + path: "ext.advname", + expectedValue: nil, + expectedError: true, + }, + { + name: "Not found in location", + ortbResponse: map[string]any{ + "cur": "USD", + }, + path: "seatbid.0.bid.0.ext.nonexistent", + expectedValue: nil, + expectedError: true, + }, + } + resolver := &bidMetaAdvNameResolver{} + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + value, err := resolver.retrieveFromBidderParamLocation(tc.ortbResponse, tc.path) + assert.Equal(t, tc.expectedValue, value) + assert.Equal(t, tc.expectedError, err != nil) + }) + } +} + +func TestBidMetaAdvNameResolverSetValue(t *testing.T) { + resolver := &bidMetaAdvNameResolver{} + testCases := []struct { + name string + typeBid map[string]any + value any + expectedTypeBid map[string]any + }{ + { + name: "set value when meta object is present", + typeBid: map[string]any{ + "id": "123", + "BidMeta": map[string]any{}, + }, + value: "Acme Corp", + expectedTypeBid: map[string]any{ + "id": "123", + "BidMeta": map[string]any{ + "advertiserName": "Acme Corp", + }, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + resolver.setValue(tc.typeBid, tc.value) + assert.Equal(t, tc.expectedTypeBid, tc.typeBid) + }) + } +} + +func TestBidMetaAgencyIDRetrieveFromLocation(t *testing.T) { + testCases := []struct { + name string + ortbResponse map[string]any + path string + expectedValue any + expectedError bool + }{ + { + name: "Found in location", + ortbResponse: map[string]any{ + "cur": "USD", + "ext": map[string]any{ + "agencyid": 10.0, + }, + }, + path: "ext.agencyid", + expectedValue: 10, + expectedError: false, + }, + { + name: "Found in location but data type is other than float", + ortbResponse: map[string]any{ + "cur": "USD", + "agencyid": 10, + }, + path: "agencyid", + expectedValue: nil, + expectedError: true, + }, + { + name: "Not found in location", + ortbResponse: map[string]any{ + "cur": "USD", + "seatbid": []any{}, + }, + path: "ext.nonexistent", + expectedValue: nil, + expectedError: true, + }, + } + resolver := &bidMetaAgencyIDResolver{} + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + value, err := resolver.retrieveFromBidderParamLocation(tc.ortbResponse, tc.path) + assert.Equal(t, tc.expectedValue, value) + assert.Equal(t, tc.expectedError, err != nil) + }) + } +} + +func TestBidMetaAgencyIDResolverSetValue(t *testing.T) { + resolver := &bidMetaAgencyIDResolver{} + testCases := []struct { + name string + typeBid map[string]any + value any + expectedTypeBid map[string]any + }{ + { + name: "set value when meta object is absent", + typeBid: map[string]any{ + "id": "123", + }, + value: 1, + expectedTypeBid: map[string]any{ + "id": "123", + "BidMeta": map[string]any{ + "agencyId": 1, + }, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + resolver.setValue(tc.typeBid, tc.value) + assert.Equal(t, tc.expectedTypeBid, tc.typeBid) + }) + } +} + +func TestBidMetaAgencyNameRetrieveFromLocation(t *testing.T) { + resolver := &bidMetaAgencyNameResolver{} + testCases := []struct { + name string + ortbResponse map[string]any + path string + expectedValue any + expectedError bool + }{ + { + name: "Found in location", + ortbResponse: map[string]any{ + "cur": "USD", + "ext": map[string]any{ + "agencyName": "TestAgency", + }, + }, + path: "ext.agencyName", + expectedValue: "TestAgency", + expectedError: false, + }, + { + name: "Found in location but data type is other than string", + ortbResponse: map[string]any{ + "cur": "USD", + "ext": map[string]any{ + "agencyName": 12345, + }, + }, + path: "ext.agencyName", + expectedValue: nil, + expectedError: true, + }, + { + name: "Not found in location", + ortbResponse: map[string]any{}, + path: "ext.nonexistent", + expectedValue: nil, + expectedError: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + value, err := resolver.retrieveFromBidderParamLocation(tc.ortbResponse, tc.path) + assert.Equal(t, tc.expectedValue, value) + assert.Equal(t, tc.expectedError, err != nil) + }) + } +} + +func TestBidMetaAgencyNameResolverSetValue(t *testing.T) { + resolver := &bidMetaAgencyNameResolver{} + testCases := []struct { + name string + typeBid map[string]any + value any + expectedTypeBid map[string]any + }{ + { + name: "set agency name value", + typeBid: map[string]any{ + "id": "123", + }, + value: "TestAgency", + expectedTypeBid: map[string]any{ + "id": "123", + "BidMeta": map[string]any{ + "agencyName": "TestAgency", + }, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + resolver.setValue(tc.typeBid, tc.value) + assert.Equal(t, tc.expectedTypeBid, tc.typeBid) + }) + } +} + +func TestBidMetaBrandIDRetrieveFromLocation(t *testing.T) { + testCases := []struct { + name string + ortbResponse map[string]any + path string + expectedValue any + expectedError bool + }{ + { + name: "Found in location", + ortbResponse: map[string]any{ + "cur": "USD", + "seatbid": []any{ + map[string]any{ + "bid": []any{ + map[string]any{ + "id": "123", + "ext": map[string]any{ + "brandid": 10.0, + }, + }, + }, + }, + }, + }, + path: "seatbid.0.bid.0.ext.brandid", + expectedValue: 10, + expectedError: false, + }, + { + name: "Found in location but data type is other than float", + ortbResponse: map[string]any{ + "cur": "USD", + "seatbid": []any{ + map[string]any{ + "bid": []any{ + map[string]any{ + "id": "123", + "ext": map[string]any{ + "brandid": 10, + }, + }, + }, + }, + }, + }, + path: "seatbid.0.bid.0.ext.brandid", + expectedValue: nil, + expectedError: true, + }, + { + name: "Not found in location", + ortbResponse: map[string]any{ + "cur": "USD", + "seatbid": []any{ + map[string]any{ + "bid": []any{ + map[string]any{ + "id": "123", + }, + }, + }, + }, + }, + path: "seatbid.0.bid.0.ext.nonexistent", + expectedValue: nil, + expectedError: true, + }, + } + resolver := &bidMetaBrandIDResolver{} + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + value, err := resolver.retrieveFromBidderParamLocation(tc.ortbResponse, tc.path) + assert.Equal(t, tc.expectedValue, value) + assert.Equal(t, tc.expectedError, err != nil) + }) + } +} + +func TestBidMetaBrandIDResolverSetValue(t *testing.T) { + resolver := &bidMetaBrandIDResolver{} + testCases := []struct { + name string + typeBid map[string]any + value any + expectedTypeBid map[string]any + }{ + { + name: "set value when meta object is present", + typeBid: map[string]any{ + "id": "123", + "BidMeta": map[string]any{}, + }, + value: 1, + expectedTypeBid: map[string]any{ + "id": "123", + "BidMeta": map[string]any{ + "brandId": 1, + }, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + resolver.setValue(tc.typeBid, tc.value) + assert.Equal(t, tc.expectedTypeBid, tc.typeBid) + }) + } +} + +func TestBidMetaBrandNameRetrieveFromLocation(t *testing.T) { + resolver := &bidMetaBrandNameResolver{} + testCases := []struct { + name string + ortbResponse map[string]any + path string + expectedValue any + expectedError bool + }{ + { + name: "Found in location", + ortbResponse: map[string]any{ + "cur": "USD", + "ext": map[string]any{ + "brandname": "TestBrand", + }, + }, + path: "ext.brandname", + expectedValue: "TestBrand", + expectedError: false, + }, + { + name: "Found in location but data type is other than string", + ortbResponse: map[string]any{ + "cur": "USD", + "ext": map[string]any{ + "brandname": 10, + }, + }, + path: "ext.brandname", + expectedValue: nil, + expectedError: true, + }, + { + name: "Not found in location", + ortbResponse: map[string]any{ + "cur": "USD", + }, + path: "seatbid.0.bid.0.ext.nonexistent", + expectedValue: nil, + expectedError: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + value, err := resolver.retrieveFromBidderParamLocation(tc.ortbResponse, tc.path) + assert.Equal(t, tc.expectedValue, value) + assert.Equal(t, tc.expectedError, err != nil) + }) + } +} + +func TestBidMetaBrandNameResolverSetValue(t *testing.T) { + resolver := &bidMetaBrandNameResolver{} + testCases := []struct { + name string + typeBid map[string]any + value any + expectedTypeBid map[string]any + }{ + { + name: "set value when meta object is present", + typeBid: map[string]any{ + "id": "123", + "BidMeta": map[string]any{}, + }, + value: "BrandName", + expectedTypeBid: map[string]any{ + "id": "123", + "BidMeta": map[string]any{ + "brandName": "BrandName", + }, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + resolver.setValue(tc.typeBid, tc.value) + assert.Equal(t, tc.expectedTypeBid, tc.typeBid) + }) + } +} + +func TestBidMetaDChainRetrieveFromLocation(t *testing.T) { + resolver := &bidMetaDChainResolver{} + testCases := []struct { + name string + ortbResponse map[string]any + path string + expectedValue any + expectedError bool + }{ + { + name: "Found in location", + ortbResponse: map[string]any{ + "cur": "USD", + "ext": map[string]any{ + "dchain": map[string]any{ + "segment": map[string]any{ + "s": "", + "t": 1, + }, + }, + }, + }, + path: "ext.dchain", + expectedValue: map[string]any{ + "segment": map[string]any{ + "s": "", + "t": 1, + }, + }, + expectedError: false, + }, + { + name: "Found in location but data type is other than json.RawMessage", + ortbResponse: map[string]any{ + "cur": "USD", + "ext": map[string]any{ + "dchain": "invalidJSON", + }, + }, + path: "ext.dchain", + expectedValue: nil, + expectedError: true, + }, + { + name: "Not found in location", + ortbResponse: map[string]any{}, + path: "seatbid.0.bid.0.ext.nonexistent", + expectedValue: nil, + expectedError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + value, err := resolver.retrieveFromBidderParamLocation(tc.ortbResponse, tc.path) + assert.Equal(t, tc.expectedValue, value) + assert.Equal(t, tc.expectedError, err != nil) + }) + } +} + +func TestBidMetaDChainResolverSetValue(t *testing.T) { + resolver := &bidMetaDChainResolver{} + testCases := []struct { + name string + typeBid map[string]any + value json.RawMessage + expectedTypeBid map[string]any + }{ + { + name: "Set DChain value when meta object is present", + typeBid: map[string]any{ + "id": "123", + "BidMeta": map[string]any{}, + }, + value: json.RawMessage(`{"segment":[{"s":"","t":1}]}`), + expectedTypeBid: map[string]any{ + "id": "123", + "BidMeta": map[string]any{ + "dchain": json.RawMessage(`{"segment":[{"s":"","t":1}]}`), + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + resolver.setValue(tc.typeBid, tc.value) + assert.Equal(t, tc.expectedTypeBid, tc.typeBid) + }) + } +} + +func TestBidMetaDemandSourceRetrieveFromLocation(t *testing.T) { + resolver := &bidMetaDemandSourceResolver{} + testCases := []struct { + name string + ortbResponse map[string]any + path string + expectedValue any + expectedError bool + }{ + { + name: "Found in location", + ortbResponse: map[string]any{ + "cur": "USD", + "seatbid": []any{ + map[string]any{ + "bid": []any{ + map[string]any{ + "id": "123", + }, + }, + "ext": map[string]any{ + "demandSource": "Direct", + }, + }, + }, + }, + path: "seatbid.0.ext.demandSource", + expectedValue: "Direct", + expectedError: false, + }, + { + name: "Found in location but data type is other than string", + ortbResponse: map[string]any{ + "cur": "USD", + "seatbid": []any{ + map[string]any{ + "bid": []any{ + map[string]any{ + "id": "123", + "ext": map[string]any{ + "demandSource": 100, + }, + }, + }, + }, + }, + }, + path: "seatbid.0.bid.0.ext.demandSource", + expectedValue: nil, + expectedError: true, + }, + { + name: "Not found in location", + ortbResponse: map[string]any{ + "cur": "USD", + "seatbid": []any{ + map[string]any{ + "bid": []any{ + map[string]any{ + "id": "123", + }, + }, + }, + }, + }, + path: "seatbid.0.bid.0.ext.nonexistent", + expectedValue: nil, + expectedError: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + value, err := resolver.retrieveFromBidderParamLocation(tc.ortbResponse, tc.path) + assert.Equal(t, tc.expectedValue, value) + assert.Equal(t, tc.expectedError, err != nil) + }) + } +} + +func TestBidMetaDemandSourceResolverSetValue(t *testing.T) { + resolver := &bidMetaDemandSourceResolver{} + testCases := []struct { + name string + typeBid map[string]any + value any + expectedTypeBid map[string]any + }{ + { + name: "set demand source", + typeBid: map[string]any{ + "id": "123", + }, + value: "Direct", + expectedTypeBid: map[string]any{ + "id": "123", + "BidMeta": map[string]any{ + "demandSource": "Direct", + }, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + resolver.setValue(tc.typeBid, tc.value) + assert.Equal(t, tc.expectedTypeBid, tc.typeBid) + }) + } +} + +func TestBidMetaMediaTypeRetrieveFromLocation(t *testing.T) { + resolver := &bidMetaMediaTypeResolver{} + testCases := []struct { + name string + ortbResponse map[string]any + path string + expectedValue any + expectedError bool + }{ + { + name: "Found in location", + ortbResponse: map[string]any{ + "cur": "USD", + "seatbid": []any{ + map[string]any{ + "bid": []any{ + map[string]any{ + "id": "123", + "ext": map[string]any{ + "bidder": map[string]any{ + "mediaType": "banner", + }, + }, + }, + }, + }, + }, + }, + path: "seatbid.0.bid.0.ext.bidder.mediaType", + expectedValue: "banner", + expectedError: false, + }, + { + name: "Found in location but data type is other than string", + ortbResponse: map[string]any{ + "cur": "USD", + "seatbid": []any{ + map[string]any{ + "bid": []any{ + map[string]any{ + "id": "123", + "ext": map[string]any{ + "mediaType": 10, + }, + }, + }, + }, + }, + }, + path: "seatbid.0.bid.0.ext.mediaType", + expectedValue: nil, + expectedError: true, + }, + { + name: "Not found in location", + ortbResponse: map[string]any{ + "cur": "USD", + "seatbid": []any{ + map[string]any{ + "bid": []any{ + map[string]any{ + "id": "123", + }, + }, + }, + }, + }, + path: "seatbid.0.bid.0.ext.nonexistent", + expectedValue: nil, + expectedError: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + value, err := resolver.retrieveFromBidderParamLocation(tc.ortbResponse, tc.path) + assert.Equal(t, tc.expectedValue, value) + assert.Equal(t, tc.expectedError, err != nil) + }) + } +} + +func TestBidMetaMediaTypeResolverSetValue(t *testing.T) { + resolver := &bidMetaMediaTypeResolver{} + testCases := []struct { + name string + typeBid map[string]any + value any + expectedTypeBid map[string]any + }{ + { + name: "set media type", + typeBid: map[string]any{ + "id": "123", + "BidMeta": map[string]any{ + "mediaType": "video", + }, + }, + value: "banner", + expectedTypeBid: map[string]any{ + "id": "123", + "BidMeta": map[string]any{ + "mediaType": "banner", + }, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + resolver.setValue(tc.typeBid, tc.value) + assert.Equal(t, tc.expectedTypeBid, tc.typeBid) + }) + } +} + +func TestBidMetaNetworkIDRetrieveFromLocation(t *testing.T) { + resolver := &bidMetaNetworkIDResolver{} + testCases := []struct { + name string + ortbResponse map[string]any + path string + expectedValue any + expectedError bool + }{ + { + name: "Found in location", + ortbResponse: map[string]any{ + "cur": "USD", + "seatbid": []any{ + map[string]any{ + "bid": []any{ + map[string]any{ + "id": "123", + "ext": map[string]any{ + "networkID": 100.0, + }, + }, + }, + }, + }, + }, + path: "seatbid.0.bid.0.ext.networkID", + expectedValue: 100, + expectedError: false, + }, + { + name: "Found in location but data type is other than int", + ortbResponse: map[string]any{ + "cur": "USD", + "seatbid": []any{ + map[string]any{ + "bid": []any{ + map[string]any{ + "id": "123", + "ext": map[string]any{ + "networkID": "wrongType", + }, + }, + }, + }, + }, + }, + path: "seatbid.0.bid.0.ext.networkID", + expectedValue: nil, + expectedError: true, + }, + { + name: "Not found in location", + ortbResponse: map[string]any{ + "cur": "USD", + "seatbid": []any{}, + }, + path: "seatbid.0.bid.0.ext.nonexistent", + expectedValue: nil, + expectedError: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + value, err := resolver.retrieveFromBidderParamLocation(tc.ortbResponse, tc.path) + assert.Equal(t, tc.expectedValue, value) + assert.Equal(t, tc.expectedError, err != nil) + }) + } +} + +func TestBidMetaNetworkIDResolverSetValue(t *testing.T) { + resolver := &bidMetaNetworkIDResolver{} + testCases := []struct { + name string + typeBid map[string]any + value any + expectedTypeBid map[string]any + }{ + { + name: "set networkid value", + typeBid: map[string]any{ + "id": "123", + }, + value: 100, + expectedTypeBid: map[string]any{ + "id": "123", + "BidMeta": map[string]any{ + "networkId": 100, + }, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + resolver.setValue(tc.typeBid, tc.value) + assert.Equal(t, tc.expectedTypeBid, tc.typeBid) + }) + } +} + +func TestBidMetaNetworkNameRetrieveFromLocation(t *testing.T) { + resolver := &bidMetaNetworkNameResolver{} + testCases := []struct { + name string + ortbResponse map[string]any + path string + expectedValue any + expectedError bool + }{ + { + name: "Found in location", + ortbResponse: map[string]any{ + "cur": "USD", + "networkName": "TestNetwork", + "seatbid": []any{ + map[string]any{ + "bid": []any{}, + }, + }, + }, + path: "networkName", + expectedValue: "TestNetwork", + expectedError: false, + }, + { + name: "Found in location but data type is other than string", + ortbResponse: map[string]any{ + "cur": "USD", + "networkName": 10, + "seatbid": []any{ + map[string]any{ + "bid": []any{}, + }, + }, + }, + path: "networkName", + expectedValue: nil, + expectedError: true, + }, + { + name: "Not found in location", + ortbResponse: map[string]any{ + "cur": "USD", + }, + path: "seatbid.0.bid.0.ext.nonexistent", + expectedValue: nil, + expectedError: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + value, err := resolver.retrieveFromBidderParamLocation(tc.ortbResponse, tc.path) + assert.Equal(t, tc.expectedValue, value) + assert.Equal(t, tc.expectedError, err != nil) + }) + } +} + +func TestBidMetaNetworkNameResolverSetValue(t *testing.T) { + resolver := &bidMetaNetworkNameResolver{} + testCases := []struct { + name string + typeBid map[string]any + value any + expectedTypeBid map[string]any + }{ + { + name: "set network name value", + typeBid: map[string]any{ + "id": "123", + }, + value: "TestNetwork", + expectedTypeBid: map[string]any{ + "id": "123", + "BidMeta": map[string]any{ + "networkName": "TestNetwork", + }, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + resolver.setValue(tc.typeBid, tc.value) + assert.Equal(t, tc.expectedTypeBid, tc.typeBid) + }) + } +} + +func TestBidMetaPrimaryCatIdRetrieveFromLocation(t *testing.T) { + resolver := &bidMetaPrimaryCategoryIDResolver{} + testCases := []struct { + name string + ortbResponse map[string]any + path string + expectedValue any + expectedError bool + }{ + { + name: "Found in location", + ortbResponse: map[string]any{ + "cur": "USD", + "primaryCatId": "testCategory", + }, + path: "primaryCatId", + expectedValue: "testCategory", + expectedError: false, + }, + { + name: "Found in location but data type is other than string", + ortbResponse: map[string]any{ + "cur": "USD", + "primaryCatId": 12345, + }, + path: "primaryCatId", + expectedValue: nil, + expectedError: true, + }, + { + name: "Not found in location", + ortbResponse: map[string]any{ + "cur": "USD", + }, + path: "nonexistent", + expectedValue: nil, + expectedError: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + value, err := resolver.retrieveFromBidderParamLocation(tc.ortbResponse, tc.path) + assert.Equal(t, tc.expectedValue, value) + assert.Equal(t, tc.expectedError, err != nil) + }) + } +} + +func TestBidMetaPrimaryCatIdResolverSetValue(t *testing.T) { + resolver := &bidMetaPrimaryCategoryIDResolver{} + testCases := []struct { + name string + typeBid map[string]any + value any + expectedTypeBid map[string]any + }{ + { + name: "set primaryCatId value", + typeBid: map[string]any{ + "id": "123", + "BidMeta": map[string]any{}, + }, + value: "testCategory", + expectedTypeBid: map[string]any{ + "id": "123", + "BidMeta": map[string]any{ + "primaryCatId": "testCategory", + }, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + resolver.setValue(tc.typeBid, tc.value) + assert.Equal(t, tc.expectedTypeBid, tc.typeBid) + }) + } +} + +func TestBidMetaRendererNameRetrieveFromLocation(t *testing.T) { + resolver := &bidMetaRendererNameResolver{} + testCases := []struct { + name string + ortbResponse map[string]any + path string + expectedValue any + expectedError bool + }{ + { + name: "Found in location", + ortbResponse: map[string]any{ + "cur": "USD", + "ext": map[string]any{ + "rendererName": "testRenderer", + }, + }, + path: "ext.rendererName", + expectedValue: "testRenderer", + expectedError: false, + }, + { + name: "Found in location but data type is other than string", + ortbResponse: map[string]any{ + "cur": "USD", + "ext": map[string]any{ + "rendererName": 12345, + }, + }, + path: "ext.rendererName", + expectedValue: nil, + expectedError: true, + }, + { + name: "Not found in location", + ortbResponse: map[string]any{}, + path: "seatbid.0.bid.0.ext.nonexistent", + expectedValue: nil, + expectedError: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + value, err := resolver.retrieveFromBidderParamLocation(tc.ortbResponse, tc.path) + assert.Equal(t, tc.expectedValue, value) + assert.Equal(t, tc.expectedError, err != nil) + }) + } +} + +func TestBidMetaRendererNameResolverSetValue(t *testing.T) { + resolver := &bidMetaRendererNameResolver{} + testCases := []struct { + name string + typeBid map[string]any + value any + expectedTypeBid map[string]any + }{ + { + name: "set rendered name value", + typeBid: map[string]any{ + "id": "123", + }, + value: "testRenderer", + expectedTypeBid: map[string]any{ + "id": "123", + "BidMeta": map[string]any{ + "rendererName": "testRenderer", + }, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + resolver.setValue(tc.typeBid, tc.value) + assert.Equal(t, tc.expectedTypeBid, tc.typeBid) + }) + } +} + +func TestBidMetaRendererVersionRetrieveFromLocation(t *testing.T) { + resolver := &bidMetaRendererVersionResolver{} + testCases := []struct { + name string + ortbResponse map[string]any + path string + expectedValue any + expectedError bool + }{ + { + name: "Found in location", + ortbResponse: map[string]any{ + "cur": "USD", + "ext": map[string]any{ + "rendererVersion": "1.0.0", + }, + }, + path: "ext.rendererVersion", + expectedValue: "1.0.0", + expectedError: false, + }, + { + name: "Found in location but data type is other than string", + ortbResponse: map[string]any{ + "cur": "USD", + "ext": map[string]any{ + "rendererVersion": 12345, + }, + }, + path: "ext.rendererVersion", + expectedValue: nil, + expectedError: true, + }, + { + name: "Not found in location", + ortbResponse: map[string]any{}, + path: "seatbid.0.bid.0.ext.nonexistent", + expectedValue: nil, + expectedError: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + value, err := resolver.retrieveFromBidderParamLocation(tc.ortbResponse, tc.path) + assert.Equal(t, tc.expectedValue, value) + assert.Equal(t, tc.expectedError, err != nil) + }) + } +} + +func TestBidMetaRendererVersionResolverSetValue(t *testing.T) { + resolver := &bidMetaRendererVersionResolver{} + testCases := []struct { + name string + typeBid map[string]any + value any + expectedTypeBid map[string]any + }{ + { + name: "set renderer version value", + typeBid: map[string]any{ + "id": "123", + }, + value: "1.0.0", + expectedTypeBid: map[string]any{ + "id": "123", + "BidMeta": map[string]any{ + "rendererVersion": "1.0.0", + }, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + resolver.setValue(tc.typeBid, tc.value) + assert.Equal(t, tc.expectedTypeBid, tc.typeBid) + }) + } +} + +func TestBidMetaRendererDataRetrieveFromLocation(t *testing.T) { + resolver := &bidMetaRendererDataResolver{} + testCases := []struct { + name string + ortbResponse map[string]any + path string + expectedValue any + expectedError bool + }{ + { + name: "Found in location", + ortbResponse: map[string]any{ + "cur": "USD", + "ext": map[string]any{ + "rendererData": map[string]any{"key": "value"}, + }, + }, + path: "ext.rendererData", + expectedValue: map[string]any{"key": "value"}, + expectedError: false, + }, + { + name: "Found in location but data type is other than json.RawMessage", + ortbResponse: map[string]any{ + "cur": "USD", + "ext": map[string]any{ + "rendererData": 12345, + }, + }, + path: "ext.rendererData", + expectedValue: nil, + expectedError: true, + }, + { + name: "Not found in location", + ortbResponse: map[string]any{}, + path: "seatbid.0.bid.0.ext.nonexistent", + expectedValue: nil, + expectedError: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + value, err := resolver.retrieveFromBidderParamLocation(tc.ortbResponse, tc.path) + assert.Equal(t, tc.expectedValue, value) + assert.Equal(t, tc.expectedError, err != nil) + }) + } +} + +func TestBidMetaRendererDataResolverSetValue(t *testing.T) { + resolver := &bidMetaRendererDataResolver{} + testCases := []struct { + name string + typeBid map[string]any + value json.RawMessage + expectedTypeBid map[string]any + }{ + { + name: "set renderer data value", + typeBid: map[string]any{ + "id": "123", + }, + value: json.RawMessage(`{"key":"value"}`), + expectedTypeBid: map[string]any{ + "id": "123", + "BidMeta": map[string]any{ + "rendererData": json.RawMessage(`{"key":"value"}`), + }, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + resolver.setValue(tc.typeBid, tc.value) + assert.Equal(t, tc.expectedTypeBid, tc.typeBid) + }) + } +} + +func TestBidMetaRendererUrlRetrieveFromLocation(t *testing.T) { + resolver := &bidMetaRendererUrlResolver{} + testCases := []struct { + name string + ortbResponse map[string]any + path string + expectedValue any + expectedError bool + }{ + { + name: "Found in location", + ortbResponse: map[string]any{ + "cur": "USD", + "ext": map[string]any{ + "rendererUrl": "https://example.com/renderer", + }, + }, + path: "ext.rendererUrl", + expectedValue: "https://example.com/renderer", + expectedError: false, + }, + { + name: "Found in location but data type is other than string", + ortbResponse: map[string]any{ + "cur": "USD", + "ext": map[string]any{ + "rendererUrl": 12345, + }, + }, + path: "ext.rendererUrl", + expectedValue: nil, + expectedError: true, + }, + { + name: "Not found in location", + ortbResponse: map[string]any{}, + path: "seatbid.0.bid.0.ext.nonexistent", + expectedValue: nil, + expectedError: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + value, err := resolver.retrieveFromBidderParamLocation(tc.ortbResponse, tc.path) + assert.Equal(t, tc.expectedValue, value) + assert.Equal(t, tc.expectedError, err != nil) + }) + } +} + +func TestBidMetaRendererUrlResolverSetValue(t *testing.T) { + resolver := &bidMetaRendererUrlResolver{} + testCases := []struct { + name string + typeBid map[string]any + value string + expectedTypeBid map[string]any + }{ + { + name: "set renderer URL value", + typeBid: map[string]any{ + "id": "123", + }, + value: "https://example.com/renderer", + expectedTypeBid: map[string]any{ + "id": "123", + "BidMeta": map[string]any{ + "rendererUrl": "https://example.com/renderer", + }, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + resolver.setValue(tc.typeBid, tc.value) + assert.Equal(t, tc.expectedTypeBid, tc.typeBid) + }) + } +} + +func TestBidMetaSecCatIdsRetrieveFromLocation(t *testing.T) { + resolver := &bidMetaSecondaryCategoryIDsResolver{} + testCases := []struct { + name string + ortbResponse map[string]any + path string + expectedValue any + expectedError bool + }{ + { + name: "Found in location", + ortbResponse: map[string]any{ + "cur": "USD", + "ext": map[string]any{ + "secondaryCatIds": []any{"cat1", "cat2"}, + }, + }, + path: "ext.secondaryCatIds", + expectedValue: []string{"cat1", "cat2"}, + expectedError: false, + }, + { + name: "Found in location but data type is other than []string", + ortbResponse: map[string]any{ + "cur": "USD", + "ext": map[string]any{ + "secondaryCatIds": "not a slice", + }, + }, + path: "ext.secondaryCatIds", + expectedValue: nil, + expectedError: true, + }, + { + name: "Not found in location", + ortbResponse: map[string]any{}, + path: "ext.nonexistent", + expectedValue: nil, + expectedError: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + value, err := resolver.retrieveFromBidderParamLocation(tc.ortbResponse, tc.path) + assert.Equal(t, tc.expectedValue, value) + assert.Equal(t, tc.expectedError, err != nil) + }) + } +} + +func TestBidMetaSecondaryCatIdsResolverSetValue(t *testing.T) { + resolver := &bidMetaSecondaryCategoryIDsResolver{} + testCases := []struct { + name string + typeBid map[string]any + value []string + expectedTypeBid map[string]any + }{ + { + name: "set secondary category IDs", + typeBid: map[string]any{ + "id": "123", + }, + value: []string{"cat1", "cat2"}, + expectedTypeBid: map[string]any{ + "id": "123", + "BidMeta": map[string]any{ + "secondaryCatIds": []string{"cat1", "cat2"}, + }, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + resolver.setValue(tc.typeBid, tc.value) + assert.Equal(t, tc.expectedTypeBid, tc.typeBid) + }) + } +} + +func TestSetKeyValueInBidMeta(t *testing.T) { + tests := []struct { + name string + adapterBid map[string]any + key string + value any + expectedBid map[string]any + expectedError bool + }{ + { + name: "Set new key-value pair when meta object is absent", + adapterBid: map[string]any{}, + key: "testKey", + value: "testValue", + expectedBid: map[string]any{ + "BidMeta": map[string]any{ + "testKey": "testValue", + }, + }, + expectedError: false, + }, + { + name: "Update existing key-value pair in meta object", + adapterBid: map[string]any{ + "BidMeta": map[string]any{ + "existingKey": "existingValue", + }, + }, + key: "existingKey", + value: "newValue", + expectedBid: map[string]any{ + "BidMeta": map[string]any{ + "existingKey": "newValue", + }, + }, + expectedError: false, + }, + { + name: "Fail to set value when meta object is invalid", + adapterBid: map[string]any{ + "BidMeta": "", + }, + key: "testKey", + value: "testValue", + expectedBid: map[string]any{ + "BidMeta": "", + }, + expectedError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := setKeyValueInBidMeta(tt.adapterBid, tt.key, tt.value) + assert.Equal(t, tt.expectedError, err != nil) + assert.Equal(t, tt.expectedBid, tt.adapterBid) + }) + } +} + +// TestExtBidPrebidMetaFields notifies us of any changes in the openrtb_ext.ExtBidPrebidMeta struct. +// If a new field is added in openrtb_ext.ExtBidPrebidMeta, then add the support to resolve the new field and update the test case. +// If the data type of an existing field changes then update the resolver of the respective field. +func TestExtBidPrebidMetaFields(t *testing.T) { + // Expected field count and types + expectedFields := map[string]reflect.Type{ + "AdapterCode": reflect.TypeOf(""), // not expected to be set by adapter + "AdvertiserDomains": reflect.TypeOf([]string{}), + "AdvertiserID": reflect.TypeOf(0), + "AdvertiserName": reflect.TypeOf(""), + "AgencyID": reflect.TypeOf(0), + "AgencyName": reflect.TypeOf(""), + "BrandID": reflect.TypeOf(0), + "BrandName": reflect.TypeOf(""), + "DChain": reflect.TypeOf(json.RawMessage{}), + "DemandSource": reflect.TypeOf(""), + "MediaType": reflect.TypeOf(""), + "NetworkID": reflect.TypeOf(0), + "NetworkName": reflect.TypeOf(""), + "PrimaryCategoryID": reflect.TypeOf(""), + "RendererName": reflect.TypeOf(""), + "RendererVersion": reflect.TypeOf(""), + "RendererData": reflect.TypeOf(json.RawMessage{}), + "RendererUrl": reflect.TypeOf(""), + "SecondaryCategoryIDs": reflect.TypeOf([]string{}), + } + structType := reflect.TypeOf(openrtb_ext.ExtBidPrebidMeta{}) + err := ValidateStructFields(expectedFields, structType) + if err != nil { + t.Error(err) + } +} diff --git a/adapters/ortbbidder/resolver/bidtype_resolver.go b/adapters/ortbbidder/resolver/bidtype_resolver.go index 4703ae97056..f8b6b3bbbd9 100644 --- a/adapters/ortbbidder/resolver/bidtype_resolver.go +++ b/adapters/ortbbidder/resolver/bidtype_resolver.go @@ -5,19 +5,10 @@ import ( "github.com/buger/jsonparser" "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/adapters/ortbbidder/util" "github.com/prebid/prebid-server/v2/openrtb_ext" ) -// Constants used for look up in ortb response -const ( - mtypeKey = "mtype" - bidTypeKey = "BidType" - currencyKey = "Currency" - curKey = "cur" - admKey = "adm" - impIdKey = "impid" -) - var ( videoRegex = regexp.MustCompile(` 1 { - return openrtb_ext.BidType("") + return openrtb_ext.BidType(""), NewValidationFailedError("found multi-format request, unable to auto detect value for [bid.ext.prebid.type]") } - return mediaType + return mediaType, nil } func convertToBidType(mtype openrtb2.MarkupType) openrtb_ext.BidType { // change name diff --git a/adapters/ortbbidder/resolver/bidtype_resolver_test.go b/adapters/ortbbidder/resolver/bidtype_resolver_test.go index a3129f3b6e2..ac0e7484fa9 100644 --- a/adapters/ortbbidder/resolver/bidtype_resolver_test.go +++ b/adapters/ortbbidder/resolver/bidtype_resolver_test.go @@ -16,7 +16,7 @@ func TestBidtypeResolverGetFromORTBObject(t *testing.T) { name string bid map[string]any expectedValue any - expectedFound bool + expectedError bool }{ { name: "mtype found in bid", @@ -24,7 +24,15 @@ func TestBidtypeResolverGetFromORTBObject(t *testing.T) { "mtype": 2.0, }, expectedValue: openrtb_ext.BidTypeVideo, - expectedFound: true, + expectedError: false, + }, + { + name: "mtype found in bid but its zero", + bid: map[string]any{ + "mtype": 0.0, + }, + expectedValue: nil, + expectedError: true, }, { name: "mtype found in bid - invalid type", @@ -32,35 +40,117 @@ func TestBidtypeResolverGetFromORTBObject(t *testing.T) { "mtype": "vide0", }, expectedValue: nil, - expectedFound: false, + expectedError: true, }, { name: "mtype found in bid - invalid value", bid: map[string]any{ - "mtype": 11, + "mtype": 11.0, }, expectedValue: nil, - expectedFound: false, + expectedError: true, }, { name: "mtype not found in bid", bid: map[string]any{}, expectedValue: nil, - expectedFound: false, + expectedError: true, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - value, found := resolver.getFromORTBObject(tc.bid) + value, err := resolver.getFromORTBObject(tc.bid) assert.Equal(t, tc.expectedValue, value) - assert.Equal(t, tc.expectedFound, found) + assert.Equal(t, tc.expectedError, err != nil) }) } }) } +func TestBidTypeResolverRetrieveFromBidderParamLocation(t *testing.T) { + testCases := []struct { + name string + ortbResponse map[string]any + path string + expectedValue any + expectedError bool + }{ + { + name: "Found in location", + ortbResponse: map[string]any{ + "cur": "USD", + "seatbid": []any{ + map[string]any{ + "bid": []any{ + map[string]any{ + "id": "123", + "ext": map[string]any{ + "mtype": "video", + }, + }, + }, + }, + }, + }, + path: "seatbid.0.bid.0.ext.mtype", + expectedValue: openrtb_ext.BidType("video"), + expectedError: false, + }, + { + name: "Found invalid bidtype in location", + ortbResponse: map[string]any{ + "cur": "USD", + "seatbid": []any{ + map[string]any{ + "bid": []any{ + map[string]any{ + "id": "123", + "ext": map[string]any{ + "mtype": 1, + }, + }, + }, + }, + }, + }, + path: "seatbid.0.bid.0.ext.mtype", + expectedValue: nil, + expectedError: true, + }, + { + name: "Not found in location", + ortbResponse: map[string]any{ + "cur": "USD", + "seatbid": []any{ + map[string]any{ + "bid": []any{ + map[string]any{ + "id": "123", + "ext": map[string]any{ + "mtype": openrtb_ext.BidType("video"), + }, + }, + }, + }, + }, + }, + path: "seatbid.0.bid.0.ext.nonexistent", + expectedValue: nil, + expectedError: true, + }, + } + resolver := &bidTypeResolver{} + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + value, err := resolver.retrieveFromBidderParamLocation(tc.ortbResponse, tc.path) + assert.Equal(t, tc.expectedValue, value) + assert.Equal(t, tc.expectedError, err != nil) + }) + } +} + func TestBidTypeResolverAutoDetect(t *testing.T) { resolver := &bidTypeResolver{} @@ -70,7 +160,7 @@ func TestBidTypeResolverAutoDetect(t *testing.T) { bid map[string]any request *openrtb2.BidRequest expectedValue any - expectedFound bool + expectedError bool }{ { name: "Auto detect from imp - Video", @@ -87,7 +177,7 @@ func TestBidTypeResolverAutoDetect(t *testing.T) { }, }, expectedValue: openrtb_ext.BidTypeVideo, - expectedFound: true, + expectedError: false, }, { name: "Auto detect from imp - banner", @@ -104,7 +194,7 @@ func TestBidTypeResolverAutoDetect(t *testing.T) { }, }, expectedValue: openrtb_ext.BidTypeBanner, - expectedFound: true, + expectedError: false, }, { name: "Auto detect from imp - native", @@ -121,7 +211,7 @@ func TestBidTypeResolverAutoDetect(t *testing.T) { }, }, expectedValue: openrtb_ext.BidTypeNative, - expectedFound: true, + expectedError: false, }, { name: "Auto detect from imp - multi format", @@ -139,7 +229,7 @@ func TestBidTypeResolverAutoDetect(t *testing.T) { }, }, expectedValue: openrtb_ext.BidType(""), - expectedFound: true, + expectedError: true, }, { name: "Auto detect with Video Adm", @@ -147,7 +237,7 @@ func TestBidTypeResolverAutoDetect(t *testing.T) { "adm": "", }, expectedValue: openrtb_ext.BidTypeVideo, - expectedFound: true, + expectedError: false, }, { name: "Auto detect with Native Adm", @@ -155,7 +245,7 @@ func TestBidTypeResolverAutoDetect(t *testing.T) { "adm": "{\"native\":{\"link\":{},\"assets\":[]}}", }, expectedValue: openrtb_ext.BidTypeNative, - expectedFound: true, + expectedError: false, }, { name: "Auto detect with Banner Adm", @@ -163,13 +253,13 @@ func TestBidTypeResolverAutoDetect(t *testing.T) { "adm": "
Some HTML content
", }, expectedValue: openrtb_ext.BidTypeBanner, - expectedFound: true, + expectedError: false, }, { name: "Auto detect with no Adm", bid: map[string]any{}, expectedValue: nil, - expectedFound: false, + expectedError: true, }, { name: "Auto detect with empty Adm", @@ -177,18 +267,63 @@ func TestBidTypeResolverAutoDetect(t *testing.T) { "adm": "", }, expectedValue: nil, - expectedFound: false, + expectedError: true, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - value, found := resolver.autoDetect(tc.request, tc.bid) + value, err := resolver.autoDetect(tc.request, tc.bid) assert.Equal(t, tc.expectedValue, value) - assert.Equal(t, tc.expectedFound, found) + assert.Equal(t, tc.expectedError, err != nil) }) } }) } +func TestGetMediaTypeFromImp(t *testing.T) { + testCases := []struct { + name string + impressions []openrtb2.Imp + impID string + expectedMediaType openrtb_ext.BidType + expectedError bool + }{ + { + name: "Found matching impID", + impressions: []openrtb2.Imp{ + {ID: "imp1"}, + {ID: "imp2", Banner: &openrtb2.Banner{}}, + }, + impID: "imp2", + expectedMediaType: openrtb_ext.BidType("banner"), + }, + { + name: "ImpID not found", + impressions: []openrtb2.Imp{ + {ID: "imp1"}, + {ID: "imp2"}, + }, + impID: "imp3", + expectedMediaType: openrtb_ext.BidType(""), + expectedError: true, + }, + { + name: "Empty impressions slice", + impressions: nil, + impID: "imp1", + expectedMediaType: openrtb_ext.BidType(""), + expectedError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mediaType, err := getMediaTypeFromImp(tc.impressions, tc.impID) + assert.Equal(t, tc.expectedMediaType, mediaType) + assert.Equal(t, tc.expectedError, err != nil) + }) + } +} + func TestMtypeResolverSetValue(t *testing.T) { resolver := &bidTypeResolver{} diff --git a/adapters/ortbbidder/resolver/constant.go b/adapters/ortbbidder/resolver/constant.go new file mode 100644 index 00000000000..d18d2abd6f0 --- /dev/null +++ b/adapters/ortbbidder/resolver/constant.go @@ -0,0 +1,79 @@ +package resolver + +type parameter string + +func (s parameter) String() string { + return string(s) +} + +// constant parameters defined in response-param.json file +const ( + bidType parameter = "bidType" + bidDealPriority parameter = "bidDealPriority" + bidVideo parameter = "bidVideo" + bidVideoDuration parameter = "bidVideoDuration" + bidVideoPrimaryCategory parameter = "bidVideoPrimaryCategory" + fledgeAuctionConfig parameter = "fledgeAuctionConfig" + bidMeta parameter = "bidMeta" + bidMetaAdvertiserDomains parameter = "bidMetaAdvertiserDomains" + bidMetaAdvertiserId parameter = "bidMetaAdvertiserId" + bidMetaAdvertiserName parameter = "bidMetaAdvertiserName" + bidMetaAgencyId parameter = "bidMetaAgencyId" + bidMetaAgencyName parameter = "bidMetaAgencyName" + bidMetaBrandId parameter = "bidMetaBrandId" + bidMetaBrandName parameter = "bidMetaBrandName" + bidMetaDChain parameter = "bidMetaDchain" + bidMetaDemandSource parameter = "bidMetaDemandSource" + bidMetaMediaType parameter = "bidMetaMediaType" + bidMetaNetworkId parameter = "bidMetaNetworkId" + bidMetaNetworkName parameter = "bidMetaNetworkName" + bidMetaPrimaryCatId parameter = "bidMetaPrimaryCatId" + bidMetaRendererName parameter = "bidMetaRendererName" + bidMetaRendererVersion parameter = "bidMetaRendererVersion" + bidMetaRenderedData parameter = "bidMetaRendererData" + bidMetaRenderedUrl parameter = "bidMetaRendererUrl" + bidMetaSecondaryCatId parameter = "bidMetaSecondaryCatIds" +) + +// constants used to look up for standard ortb fields in bidResponse +const ( + ortbFieldMtype = "mtype" + ortbFieldDuration = "dur" + ortbFieldCurrency = "cur" + ortbFieldAdM = "adm" + ortbFieldCategory = "cat" + ortbFieldImpId = "impid" + ortbFieldBidder = "bidder" + ortbFieldAdapter = "adapter" + ortbFieldConfig = "config" +) + +// constants used to set keys in the BidderResponse map +const ( + bidVideoPrimaryCategoryKey = "primary_category" + bidVideoDurationKey = "duration" + bidVideoKey = "BidVideo" + bidTypeKey = "BidType" + currencyKey = "Currency" + fledgeAuctionConfigKey = "FledgeAuctionConfigs" + bidDealPriorityKey = "DealPriority" + bidMetaKey = "BidMeta" + bidMetaAdvertiserDomainsKey = "advertiserDomains" + bidMetaAdvertiserIdKey = "advertiserId" + bidMetaAdvertiserNameKey = "advertiserName" + bidMetaAgencyIdKey = "agencyId" + bidMetaAgencyNameKey = "agencyName" + bidMetaBrandIdKey = "brandId" + bidMetaBrandNameKey = "brandName" + bidMetaDChainKey = "dchain" + bidMetaDemandSourceKey = "demandSource" + bidMetaMediaTypeKey = "mediaType" + bidMetaNetworkIdKey = "networkId" + bidMetaNetworkNameKey = "networkName" + bidMetaPrimaryCatIdKey = "primaryCatId" + bidMetaRendererNameKey = "rendererName" + bidMetaRendererVersionKey = "rendererVersion" + bidMetaRenderedDataKey = "rendererData" + bidMetaRenderedUrlKey = "rendererUrl" + bidMetaSecondaryCatIdKey = "secondaryCatIds" +) diff --git a/adapters/ortbbidder/resolver/dealPriority_resolver.go b/adapters/ortbbidder/resolver/dealPriority_resolver.go new file mode 100644 index 00000000000..db784f74208 --- /dev/null +++ b/adapters/ortbbidder/resolver/dealPriority_resolver.go @@ -0,0 +1,28 @@ +package resolver + +import ( + "github.com/prebid/prebid-server/v2/adapters/ortbbidder/util" +) + +// bidDealPriorityResolver retrieves the priority of the deal bid using the bidder param location. +// The determined dealPriority is subsequently assigned to adapterresponse.typedbid.dealPriority +type bidDealPriorityResolver struct { + paramResolver +} + +func (b *bidDealPriorityResolver) retrieveFromBidderParamLocation(responseNode map[string]any, path string) (any, error) { + value, found := util.GetValueFromLocation(responseNode, path) + if !found { + return nil, NewDefaultValueError("no value sent by bidder at [%s] for [bid.ext.prebid.dealpriority]", path) + } + val, ok := validateNumber[int](value) + if !ok { + return nil, NewValidationFailedError("invalid value sent by bidder at [%s] for [bid.ext.prebid.dealpriority]", path) + } + return val, nil +} + +func (b *bidDealPriorityResolver) setValue(adapterBid map[string]any, value any) (err error) { + adapterBid[bidDealPriorityKey] = value + return +} diff --git a/adapters/ortbbidder/resolver/dealPriority_resolver_test.go b/adapters/ortbbidder/resolver/dealPriority_resolver_test.go new file mode 100644 index 00000000000..e57ccf04026 --- /dev/null +++ b/adapters/ortbbidder/resolver/dealPriority_resolver_test.go @@ -0,0 +1,78 @@ +package resolver + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBidDealPriorityFromLocation(t *testing.T) { + resolver := &bidDealPriorityResolver{} + testCases := []struct { + name string + responseNode map[string]any + path string + expectedValue any + expectedError bool + }{ + { + name: "Found dealPriority in location", + responseNode: map[string]any{ + "cur": "USD", + "dp": 10.0, + }, + path: "dp", + expectedValue: 10, + expectedError: false, + }, + { + name: "Found invalid dealPriority in location", + responseNode: map[string]any{ + "cur": "USD", + "dp": "invalid", + }, + path: "dp", + expectedValue: nil, + expectedError: true, + }, + { + name: "Not found dealPriority in location", + responseNode: map[string]any{}, + path: "seat", + expectedValue: nil, + expectedError: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + value, err := resolver.retrieveFromBidderParamLocation(tc.responseNode, tc.path) + assert.Equal(t, tc.expectedValue, value) + assert.Equal(t, tc.expectedError, err != nil) + }) + } +} + +func TestBidDealPriorityResolverSetValue(t *testing.T) { + resolver := &bidDealPriorityResolver{} + testCases := []struct { + name string + adapterBid map[string]any + value any + expectedAdapter map[string]any + }{ + { + name: "Set deal priority value", + adapterBid: map[string]any{}, + value: 10, + expectedAdapter: map[string]any{ + bidDealPriorityKey: 10, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + resolver.setValue(tc.adapterBid, tc.value) + assert.Equal(t, tc.expectedAdapter, tc.adapterBid) + }) + } +} diff --git a/adapters/ortbbidder/resolver/errortypes.go b/adapters/ortbbidder/resolver/errortypes.go new file mode 100644 index 00000000000..752399ee99a --- /dev/null +++ b/adapters/ortbbidder/resolver/errortypes.go @@ -0,0 +1,68 @@ +package resolver + +import "fmt" + +// severity represents the severity level of a error. +type severity int + +const ( + severityFatal severity = iota // use this to discard the entire bid response + severityWarning // use this to include errors in responseExt.warnings + severityIgnore // use this to exclude errors from responseExt.warnings +) + +// coder is used to indicate the severity of an error. +type coder interface { + // Severity returns the severity level of the error. + Severity() severity +} + +// defaultValueError is used to flag that a default value was found. +type defaultValueError struct { + Message string +} + +// Error returns the error message. +func (err *defaultValueError) Error() string { + return err.Message +} + +// Severity returns the severity level of the error. +func (err *defaultValueError) Severity() severity { + return severityIgnore +} + +// validationFailedError is used to flag that the value validation failed. +type validationFailedError struct { + Message string // Message contains the error message. +} + +// Error returns the error message for ValidationFailedError. +func (err *validationFailedError) Error() string { + return err.Message +} + +// Severity returns the severity level for ValidationFailedError. +func (err *validationFailedError) Severity() severity { + return severityWarning +} + +// IsWarning returns true if an error is labeled with a Severity of SeverityWarning. +func IsWarning(err error) bool { + s, ok := err.(coder) + return ok && s.Severity() == severityWarning +} + +// NewDefaultValueError creates a new DefaultValueError with a formatted message. +func NewDefaultValueError(message string, args ...any) error { + return &defaultValueError{ + Message: fmt.Sprintf(message, args...), + } +} + +// NewValidationFailedError creates a new ValidationFailedError with a formatted message. +func NewValidationFailedError(message string, args ...any) error { + return &validationFailedError{ + Message: fmt.Sprintf(message, args...), + } +} diff --git a/adapters/ortbbidder/resolver/errortypes_test.go b/adapters/ortbbidder/resolver/errortypes_test.go new file mode 100644 index 00000000000..e9f1dfef481 --- /dev/null +++ b/adapters/ortbbidder/resolver/errortypes_test.go @@ -0,0 +1,56 @@ +package resolver + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestValidationFailedError(t *testing.T) { + t.Run("validationFailedError", func(t *testing.T) { + err := validationFailedError{Message: "any validation message"} + assert.Equal(t, "any validation message", err.Error()) + assert.Equal(t, severityWarning, err.Severity()) + }) +} + +func TestDefaultValueError(t *testing.T) { + t.Run("defaultValueError", func(t *testing.T) { + err := defaultValueError{Message: "any validation message"} + assert.Equal(t, "any validation message", err.Error()) + assert.Equal(t, severityIgnore, err.Severity()) + }) +} + +func TestIsWarning(t *testing.T) { + type args struct { + err error + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "input err is of severity warning", + args: args{ + err: NewValidationFailedError("error"), + }, + want: true, + }, + { + name: "input err is of severity ignore", + args: args{ + err: NewDefaultValueError("error"), + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsWarning(tt.args.err); got != tt.want { + t.Errorf("IsWarning() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/adapters/ortbbidder/resolver/fledgeconfig_resolver.go b/adapters/ortbbidder/resolver/fledgeconfig_resolver.go new file mode 100644 index 00000000000..cc5a1e6a17e --- /dev/null +++ b/adapters/ortbbidder/resolver/fledgeconfig_resolver.go @@ -0,0 +1,45 @@ +package resolver + +import ( + "github.com/prebid/prebid-server/v2/adapters/ortbbidder/util" + "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/prebid/prebid-server/v2/util/jsonutil" +) + +// fledgeResolver retrieves the fledge auction config of the bidresponse using the bidder param location. +// The determined fledge config is subsequently assigned to adapterresponse.FledgeAuctionConfigs +type fledgeResolver struct { + paramResolver +} + +func (f *fledgeResolver) retrieveFromBidderParamLocation(responseNode map[string]any, path string) (any, error) { + value, found := util.GetValueFromLocation(responseNode, path) + if !found { + return nil, NewDefaultValueError("no value sent by bidder at [%s] for [response.ext.fledgeAuctionConfig]", path) + } + fledgeCfg, err := validateFledgeConfig(value) + if err != nil { + return nil, NewValidationFailedError("invalid value sent by bidder at [%s] for [response.ext.fledgeAuctionConfig]", path) + } + return fledgeCfg, nil +} + +func validateFledgeConfig(value any) (any, error) { + fledgeCfgBytes, err := jsonutil.Marshal(value) + if err != nil { + return nil, err + } + + var fledgeCfg []*openrtb_ext.FledgeAuctionConfig + err = jsonutil.UnmarshalValid(fledgeCfgBytes, &fledgeCfg) + if err != nil { + return nil, err + } + + return fledgeCfg, nil +} + +func (f *fledgeResolver) setValue(adapterBid map[string]any, value any) error { + adapterBid[fledgeAuctionConfigKey] = value + return nil +} diff --git a/adapters/ortbbidder/resolver/fledgeconfig_resolver_test.go b/adapters/ortbbidder/resolver/fledgeconfig_resolver_test.go new file mode 100644 index 00000000000..ec19c710aff --- /dev/null +++ b/adapters/ortbbidder/resolver/fledgeconfig_resolver_test.go @@ -0,0 +1,195 @@ +package resolver + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +func TestFledgeConfigRetrieveFromLocation(t *testing.T) { + resolver := &fledgeResolver{} + testCases := []struct { + name string + responseNode map[string]any + path string + expectedValue any + expectedError bool + }{ + { + name: "Found fledgeConfig in location", + responseNode: map[string]any{ + "cur": "USD", + "ext": map[string]any{ + "fledgeCfg": []any{ + map[string]any{ + "impid": "imp_1", + "bidder": "magnite", + "config": map[string]any{ + "key": "value", + }, + }, + }, + }, + }, + path: "ext.fledgeCfg", + expectedValue: []*openrtb_ext.FledgeAuctionConfig{ + { + ImpId: "imp_1", + Bidder: "magnite", + Config: json.RawMessage(`{"key":"value"}`), + }, + }, + expectedError: false, + }, + { + name: "Found invalid fledgeConfig in location", + responseNode: map[string]any{ + "cur": "USD", + "ext": map[string]any{ + "fledgeCfg": []any{ + map[string]any{ + "impid": 1, + "bidder": "magnite", + "config": map[string]any{ + "key": "value", + }, + }, + }, + }, + }, + path: "ext.fledgeCfg", + expectedValue: nil, + expectedError: true, + }, + { + name: "Not found fledge config in location", + responseNode: map[string]any{}, + path: "seat", + expectedValue: nil, + expectedError: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + value, err := resolver.retrieveFromBidderParamLocation(tc.responseNode, tc.path) + assert.Equal(t, tc.expectedValue, value) + assert.Equal(t, tc.expectedError, err != nil) + }) + } +} + +func TestValidateFledgeConfigs(t *testing.T) { + testCases := []struct { + name string + input any + expectedOutput any + expectedError bool + }{ + { + name: "Valid fledge configs", + input: []any{ + map[string]any{ + "impid": "123", + "bidder": "exampleBidder", + "adapter": "exampleAdapter", + "config": map[string]any{ + "key1": "value1", + "key2": "value2", + }, + }, + }, + expectedOutput: []*openrtb_ext.FledgeAuctionConfig{ + { + ImpId: "123", + Bidder: "exampleBidder", + Adapter: "exampleAdapter", + Config: json.RawMessage(`{"key1":"value1","key2":"value2"}`), + }, + }, + expectedError: false, + }, + { + name: "Invalid fledge config with non-map entry", + input: []any{ + map[string]any{ + "impid": "123", + "bidder": "exampleBidder", + "adapter": "exampleAdapter", + "config": map[string]any{ + "key1": "value1", + "key2": "value2", + }, + }, + "invalidEntry", + }, + expectedOutput: nil, + expectedError: true, + }, + { + name: "nil fledge configs", + input: nil, + expectedOutput: []*openrtb_ext.FledgeAuctionConfig(nil), + expectedError: false, + }, + { + name: "Non-slice input", + input: make(chan int), + expectedOutput: nil, + expectedError: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + output, err := validateFledgeConfig(tc.input) + assert.Equal(t, tc.expectedOutput, output) + assert.Equal(t, tc.expectedError, err != nil) + }) + } +} + +func TestFledgeConfigSetValue(t *testing.T) { + resolver := &fledgeResolver{} + testCases := []struct { + name string + adapterBid map[string]any + value any + expectedAdapter map[string]any + }{ + { + name: "Set fledge config value", + adapterBid: map[string]any{}, + value: []map[string]any{ + { + "impid": "123", + "bidder": "exampleBidder", + "adapter": "exampleAdapter", + "config": map[string]any{ + "key1": "value1", + "key2": "value2", + }, + }, + }, + expectedAdapter: map[string]any{ + fledgeAuctionConfigKey: []map[string]any{ + { + "impid": "123", + "bidder": "exampleBidder", + "adapter": "exampleAdapter", + "config": map[string]any{ + "key1": "value1", + "key2": "value2", + }, + }, + }, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + resolver.setValue(tc.adapterBid, tc.value) + assert.Equal(t, tc.expectedAdapter, tc.adapterBid) + }) + } +} diff --git a/adapters/ortbbidder/resolver/param_resolver.go b/adapters/ortbbidder/resolver/param_resolver.go index 7e319931c97..16f89eb6422 100644 --- a/adapters/ortbbidder/resolver/param_resolver.go +++ b/adapters/ortbbidder/resolver/param_resolver.go @@ -2,33 +2,43 @@ package resolver import ( "github.com/prebid/openrtb/v20/openrtb2" - "github.com/prebid/prebid-server/v2/adapters/ortbbidder/util" -) - -type parameter string - -func (s parameter) String() string { - return string(s) -} - -const ( - BidType parameter = "bidtype" - Duration parameter = "duration" - BidMeta parameter = "bidmeta" - Fledge parameter = "fledge" ) var ( resolvers = resolverMap{ - BidType: &bidTypeResolver{}, + fledgeAuctionConfig: &fledgeResolver{}, + bidType: &bidTypeResolver{}, + bidDealPriority: &bidDealPriorityResolver{}, + bidVideo: &bidVideoResolver{}, + bidVideoDuration: &bidVideoDurationResolver{}, + bidVideoPrimaryCategory: &bidVideoPrimaryCategoryResolver{}, + bidMeta: &bidMetaResolver{}, + bidMetaAdvertiserDomains: &bidMetaAdvDomainsResolver{}, + bidMetaAdvertiserId: &bidMetaAdvIDResolver{}, + bidMetaAdvertiserName: &bidMetaAdvNameResolver{}, + bidMetaAgencyId: &bidMetaAgencyIDResolver{}, + bidMetaAgencyName: &bidMetaAgencyNameResolver{}, + bidMetaBrandId: &bidMetaBrandIDResolver{}, + bidMetaBrandName: &bidMetaBrandNameResolver{}, + bidMetaDChain: &bidMetaDChainResolver{}, + bidMetaDemandSource: &bidMetaDemandSourceResolver{}, + bidMetaMediaType: &bidMetaMediaTypeResolver{}, + bidMetaNetworkId: &bidMetaNetworkIDResolver{}, + bidMetaNetworkName: &bidMetaNetworkNameResolver{}, + bidMetaPrimaryCatId: &bidMetaPrimaryCategoryIDResolver{}, + bidMetaRendererName: &bidMetaRendererNameResolver{}, + bidMetaRendererVersion: &bidMetaRendererVersionResolver{}, + bidMetaRenderedData: &bidMetaRendererDataResolver{}, + bidMetaRenderedUrl: &bidMetaRendererUrlResolver{}, + bidMetaSecondaryCatId: &bidMetaSecondaryCategoryIDsResolver{}, } ) type resolver interface { - getFromORTBObject(sourceNode map[string]any) (any, bool) - retrieveFromBidderParamLocation(responseNode map[string]any, path string) (any, bool) - autoDetect(request *openrtb2.BidRequest, sourceNode map[string]any) (any, bool) - setValue(targetNode map[string]any, value any) + getFromORTBObject(sourceNode map[string]any) (any, error) + retrieveFromBidderParamLocation(responseNode map[string]any, path string) (any, error) + autoDetect(request *openrtb2.BidRequest, sourceNode map[string]any) (any, error) + setValue(targetNode map[string]any, value any) error } type resolverMap map[parameter]resolver @@ -46,13 +56,25 @@ func New(request *openrtb2.BidRequest, bidderResponse map[string]any) *paramReso } } +func (r *paramResolver) retrieveFromBidderParamLocation(responseNode map[string]any, path string) (any, error) { + return nil, nil +} + +func (r *paramResolver) getFromORTBObject(bid map[string]any) (any, error) { + return nil, nil +} + +func (r *paramResolver) autoDetect(request *openrtb2.BidRequest, bid map[string]any) (any, error) { + return nil, nil +} + // Resolve fetches a parameter value from sourceNode or bidderResponse and sets it in targetNode. // The order of lookup is as follows: // 1) ORTB standard field // 2) Location from JSON file (bidder params) // 3) Auto-detection // If the value is found, it is set in the targetNode. -func (pr *paramResolver) Resolve(sourceNode, targetNode map[string]any, path string, param parameter) { +func (pr *paramResolver) Resolve(sourceNode, targetNode map[string]any, path string, param parameter) (errs []error) { if sourceNode == nil || targetNode == nil || pr.bidderResponse == nil { return } @@ -61,26 +83,64 @@ func (pr *paramResolver) Resolve(sourceNode, targetNode map[string]any, path str return } - // get the value from the ORTB object - value, found := resolver.getFromORTBObject(sourceNode) - if !found { - // get the value from the bidder response using the location - value, found = resolver.retrieveFromBidderParamLocation(pr.bidderResponse, path) - if !found { - // auto detect value - value, found = resolver.autoDetect(pr.request, sourceNode) - if !found { - return + value, err := resolver.getFromORTBObject(sourceNode) // get the value from the ORTB object + if err != nil { + errs = append(errs, err) + } + + if value == nil { + value, err = resolver.retrieveFromBidderParamLocation(pr.bidderResponse, path) // get the value from the bidder response using the location + if err != nil { + errs = append(errs, err) + } + if value == nil { + value, err = resolver.autoDetect(pr.request, sourceNode) // auto detect value + if err != nil { + errs = append(errs, err) + } + if value == nil { + return errs // return if value not found } } } - resolver.setValue(targetNode, value) + err = resolver.setValue(targetNode, value) + if err != nil { + errs = append(errs, err) + } + return errs } -// valueResolver is a generic resolver to get values from the response node using location -type valueResolver struct{} +// list of parameters to be resolved at typedBid level. +// order of elements matters since child parameter's (BidMetaAdvertiserDomains) value overrides the parent parameter's (BidMeta.AdvertiserDomains) value. +var TypedBidParams = []parameter{ + bidType, + bidDealPriority, + bidVideo, + bidVideoDuration, + bidVideoPrimaryCategory, + bidMeta, + bidMetaAdvertiserDomains, + bidMetaAdvertiserId, + bidMetaAdvertiserName, + bidMetaAgencyId, + bidMetaAgencyName, + bidMetaBrandId, + bidMetaBrandName, + bidMetaDChain, + bidMetaDemandSource, + bidMetaMediaType, + bidMetaNetworkId, + bidMetaNetworkName, + bidMetaPrimaryCatId, + bidMetaRendererName, + bidMetaRendererVersion, + bidMetaRenderedData, + bidMetaRenderedUrl, + bidMetaSecondaryCatId, +} -func (r *valueResolver) retrieveFromBidderParamLocation(responseNode map[string]any, path string) (any, bool) { - return util.GetValueFromLocation(responseNode, path) +// list of parameters to be resolved at response level. +var ResponseParams = []parameter{ + fledgeAuctionConfig, } diff --git a/adapters/ortbbidder/resolver/param_resolver_test.go b/adapters/ortbbidder/resolver/param_resolver_test.go index a7714898bfa..6f73f458e8d 100644 --- a/adapters/ortbbidder/resolver/param_resolver_test.go +++ b/adapters/ortbbidder/resolver/param_resolver_test.go @@ -17,6 +17,7 @@ func TestResolveTypeBid(t *testing.T) { location string paramName parameter expectedTypeBid map[string]any + expectedErrs []error request *openrtb2.BidRequest }{ { @@ -45,7 +46,7 @@ func TestResolveTypeBid(t *testing.T) { map[string]any{ "id": "123", "ext": map[string]any{ - "bidtype": openrtb_ext.BidType("video"), + "bidtype": "video", }, }, }, @@ -53,7 +54,7 @@ func TestResolveTypeBid(t *testing.T) { }, }, location: "seatbid.0.bid.0.ext.bidtype", - paramName: "bidtype", + paramName: "bidType", expectedTypeBid: nil, }, { @@ -61,7 +62,7 @@ func TestResolveTypeBid(t *testing.T) { bid: map[string]any{ "id": "123", "ext": map[string]any{ - "bidtype": openrtb_ext.BidType("video"), + "bidtype": "video", }, }, typeBid: map[string]any{ @@ -78,7 +79,7 @@ func TestResolveTypeBid(t *testing.T) { map[string]any{ "id": "123", "ext": map[string]any{ - "bidtype": openrtb_ext.BidType("video"), + "bidtype": "video", }, }, }, @@ -95,7 +96,7 @@ func TestResolveTypeBid(t *testing.T) { }, }, { - name: "Get paramName from the ortb bid object", + name: "Get param from the ortb bid object", bid: map[string]any{ "id": "123", "mtype": float64(2), @@ -120,7 +121,7 @@ func TestResolveTypeBid(t *testing.T) { }, }, location: "seatbid.0.bid.0.ext.bidtype", - paramName: "bidtype", + paramName: "bidType", expectedTypeBid: map[string]any{ "Bid": map[string]any{ "id": "123", @@ -130,7 +131,48 @@ func TestResolveTypeBid(t *testing.T) { }, }, { - name: "Get paramName from the bidder paramName location", + name: "fail to get param from the ortb bid object, fallback to get from bidder param location", + bid: map[string]any{ + "id": "123", + "mtype": "a", + }, + typeBid: map[string]any{ + "Bid": map[string]any{ + "id": "123", + "mtype": float64(0), + }, + }, + bidderResponse: map[string]any{ + "cur": "USD", + "seatbid": []any{ + map[string]any{ + "bid": []any{ + map[string]any{ + "id": "123", + "mtype": float64(0), + "ext": map[string]any{ + "bidtype": "banner", + }, + }, + }, + }, + }, + }, + location: "seatbid.0.bid.0.ext.bidtype", + paramName: "bidType", + expectedTypeBid: map[string]any{ + "Bid": map[string]any{ + "id": "123", + "mtype": float64(0), + }, + "BidType": openrtb_ext.BidType("banner"), + }, + expectedErrs: []error{ + NewValidationFailedError("invalid value sent by bidder at [bid.mtype] for [bid.ext.prebid.type]"), + }, + }, + { + name: "Get param from the bidder paramName location", bid: map[string]any{ "id": "123", "ext": map[string]any{ @@ -153,7 +195,7 @@ func TestResolveTypeBid(t *testing.T) { map[string]any{ "id": "123", "ext": map[string]any{ - "bidtype": openrtb_ext.BidType("video"), + "bidtype": "video", }, }, }, @@ -161,7 +203,7 @@ func TestResolveTypeBid(t *testing.T) { }, }, location: "seatbid.0.bid.0.ext.bidtype", - paramName: "bidtype", + paramName: "bidType", expectedTypeBid: map[string]any{ "Bid": map[string]any{ "id": "123", @@ -171,9 +213,12 @@ func TestResolveTypeBid(t *testing.T) { }, "BidType": openrtb_ext.BidType("video"), }, + expectedErrs: []error{ + NewDefaultValueError("no value sent by bidder at [bid.mtype] for [bid.ext.prebid.type]"), + }, }, { - name: "Auto detect", + name: "fail to detect from location, fallback to Auto detect", bid: map[string]any{ "id": "123", "adm": "", @@ -185,7 +230,8 @@ func TestResolveTypeBid(t *testing.T) { }, }, bidderResponse: map[string]any{ - "cur": "USD", + "cur": "USD", + "bidtype": 1, "seatbid": []any{ map[string]any{ "bid": []any{ @@ -197,8 +243,8 @@ func TestResolveTypeBid(t *testing.T) { }, }, }, - location: "seatbid.0.bid.0.ext.bidtype", - paramName: "bidtype", + location: "bidtype", + paramName: "bidType", expectedTypeBid: map[string]any{ "Bid": map[string]any{ "id": "123", @@ -206,75 +252,122 @@ func TestResolveTypeBid(t *testing.T) { }, "BidType": openrtb_ext.BidType("video"), }, + expectedErrs: []error{ + NewDefaultValueError("no value sent by bidder at [bid.mtype] for [bid.ext.prebid.type]"), + NewValidationFailedError("invalid value sent by bidder at [bidtype] for [bid.ext.prebid.type]"), + }, }, - // Todo add auto detec logic test case when it is implemented - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - pr := New(tc.request, tc.bidderResponse) - pr.Resolve(tc.bid, tc.typeBid, tc.location, tc.paramName) - assert.Equal(t, tc.expectedTypeBid, tc.typeBid) - }) - } -} - -func TestRetrieveFromBidderParamLocation(t *testing.T) { - testCases := []struct { - name string - ortbResponse map[string]any - path string - expectedValue any - expectedFound bool - }{ { - name: "Found in location", - ortbResponse: map[string]any{ + name: "Auto detect", + bid: map[string]any{ + "id": "123", + "adm": "", + }, + typeBid: map[string]any{ + "Bid": map[string]any{ + "id": "123", + "adm": "", + }, + }, + bidderResponse: map[string]any{ "cur": "USD", "seatbid": []any{ map[string]any{ "bid": []any{ map[string]any{ - "id": "123", - "ext": map[string]any{ - "mtype": openrtb_ext.BidType("video"), - }, + "id": "123", + "adm": "", }, }, }, }, }, - path: "seatbid.0.bid.0.ext.mtype", - expectedValue: openrtb_ext.BidType("video"), - expectedFound: true, + location: "seatbid.0.bid.0.ext.bidtype", + paramName: "bidType", + expectedTypeBid: map[string]any{ + "Bid": map[string]any{ + "id": "123", + "adm": "", + }, + "BidType": openrtb_ext.BidType("video"), + }, + expectedErrs: []error{ + NewDefaultValueError("no value sent by bidder at [bid.mtype] for [bid.ext.prebid.type]"), + NewDefaultValueError("no value sent by bidder at [seatbid.0.bid.0.ext.bidtype] for [bid.ext.prebid.type]"), + }, }, { - name: "Not found in location", - ortbResponse: map[string]any{ + name: "Failed to Auto detect", + bid: map[string]any{ + "id": "123", + }, + typeBid: map[string]any{ + "Bid": map[string]any{ + "id": "123", + }, + }, + bidderResponse: map[string]any{ "cur": "USD", "seatbid": []any{ map[string]any{ "bid": []any{ map[string]any{ "id": "123", - "ext": map[string]any{ - "mtype": openrtb_ext.BidType("video"), - }, }, }, }, }, }, - path: "seatbid.0.bid.0.ext.nonexistent", - expectedValue: nil, - expectedFound: false, + location: "seatbid.0.bid.0.ext.bidtype", + paramName: "bidType", + expectedTypeBid: map[string]any{ + "Bid": map[string]any{ + "id": "123", + }, + }, + expectedErrs: []error{ + NewDefaultValueError("no value sent by bidder at [bid.mtype] for [bid.ext.prebid.type]"), + NewDefaultValueError("no value sent by bidder at [seatbid.0.bid.0.ext.bidtype] for [bid.ext.prebid.type]"), + NewValidationFailedError("invalid value sent by bidder at [bid.impid] for [bid.ext.prebid.type]"), + }, }, } - resolver := &valueResolver{} for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - value, found := resolver.retrieveFromBidderParamLocation(tc.ortbResponse, tc.path) - assert.Equal(t, tc.expectedValue, value) - assert.Equal(t, tc.expectedFound, found) + pr := New(tc.request, tc.bidderResponse) + errs := pr.Resolve(tc.bid, tc.typeBid, tc.location, tc.paramName) + assert.Equal(t, tc.expectedTypeBid, tc.typeBid) + assert.Equal(t, tc.expectedErrs, errs) + }) + } +} + +func TestDefaultvalueResolver(t *testing.T) { + tests := []struct { + name string + wantValue any + wantErr error + }{ + { + name: "test default values", + wantValue: nil, + wantErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := ¶mResolver{} + value, found := r.retrieveFromBidderParamLocation(map[string]any{}, "any.path") + assert.Equal(t, tt.wantErr, found) + assert.Equal(t, tt.wantValue, value) + + value, found = r.getFromORTBObject(map[string]any{}) + assert.Equal(t, tt.wantErr, found) + assert.Equal(t, tt.wantValue, value) + + value, found = r.autoDetect(&openrtb2.BidRequest{}, map[string]any{}) + assert.Equal(t, tt.wantErr, found) + assert.Equal(t, tt.wantValue, value) }) } } diff --git a/adapters/ortbbidder/resolver/testutil.go b/adapters/ortbbidder/resolver/testutil.go new file mode 100644 index 00000000000..34ef735a49e --- /dev/null +++ b/adapters/ortbbidder/resolver/testutil.go @@ -0,0 +1,28 @@ +package resolver + +import ( + "fmt" + "reflect" +) + +func ValidateStructFields(expectedFields map[string]reflect.Type, structType reflect.Type) error { + fieldCount := structType.NumField() + + // Check if the number of fields matches the expected count + if fieldCount != len(expectedFields) { + return fmt.Errorf("Expected %d fields, but got %d fields", len(expectedFields), fieldCount) + } + + // Check if the field types match the expected types + for i := 0; i < fieldCount; i++ { + field := structType.Field(i) + expectedType, ok := expectedFields[field.Name] + if !ok { + return fmt.Errorf("Unexpected field: %s", field.Name) + } + if field.Type != expectedType { + return fmt.Errorf("Field %s: expected type %v, but got %v", field.Name, expectedType, field.Type) + } + } + return nil +} diff --git a/adapters/ortbbidder/resolver/testutil_test.go b/adapters/ortbbidder/resolver/testutil_test.go new file mode 100644 index 00000000000..90d83906832 --- /dev/null +++ b/adapters/ortbbidder/resolver/testutil_test.go @@ -0,0 +1,78 @@ +package resolver + +import ( + "reflect" + "testing" + + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +func TestValidateStructFields(t *testing.T) { + type args struct { + expectedFields map[string]reflect.Type + structType reflect.Type + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "mismatch between count of fields", + args: args{ + expectedFields: map[string]reflect.Type{}, + structType: reflect.TypeOf(openrtb_ext.ExtBidPrebidVideo{}), + }, + wantErr: true, + }, + { + name: "found unexpected field", + args: args{ + expectedFields: map[string]reflect.Type{ + "Duration_1": reflect.TypeOf(0.0), + "field2": reflect.TypeOf(""), + "field3": reflect.TypeOf(""), + }, + structType: reflect.TypeOf(openrtb_ext.ExtBidPrebidVideo{ + Duration: 0, + }), + }, + wantErr: true, + }, + { + name: "found field with incorrect data type", + args: args{ + expectedFields: map[string]reflect.Type{ + "Duration": reflect.TypeOf(0.0), + "PrimaryCategory": reflect.TypeOf(""), + "VASTTagID": reflect.TypeOf(""), + }, + structType: reflect.TypeOf(openrtb_ext.ExtBidPrebidVideo{ + Duration: 0, + }), + }, + wantErr: true, + }, + { + name: "found valid fields", + args: args{ + expectedFields: map[string]reflect.Type{ + "Duration": reflect.TypeOf(0), + "PrimaryCategory": reflect.TypeOf(""), + "VASTTagID": reflect.TypeOf(""), + }, + structType: reflect.TypeOf(openrtb_ext.ExtBidPrebidVideo{ + Duration: 0, + }), + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := ValidateStructFields(tt.args.expectedFields, tt.args.structType); (err != nil) != tt.wantErr { + t.Errorf("ValidateStructFields() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/adapters/ortbbidder/resolver/validator.go b/adapters/ortbbidder/resolver/validator.go new file mode 100644 index 00000000000..6990e08cdb5 --- /dev/null +++ b/adapters/ortbbidder/resolver/validator.go @@ -0,0 +1,39 @@ +package resolver + +func validateNumber[T int | int64 | float64](value any) (T, bool) { + v, ok := value.(float64) + if !ok { + var zero T + return zero, false + } + return T(v), true +} + +func validateString(value any) (string, bool) { + v, ok := value.(string) + if len(v) == 0 { + return v, false + } + return v, ok +} + +func validateDataTypeSlice[T any](value any) ([]T, bool) { + typedValues, ok := value.([]any) + if !ok { + return nil, false + } + + values := make([]T, 0, len(typedValues)) + for _, v := range typedValues { + value, ok := v.(T) + if ok { + values = append(values, value) + } + } + return values, len(values) != 0 +} + +func validateMap(value any) (map[string]any, bool) { + v, ok := value.(map[string]any) + return v, ok +} diff --git a/adapters/ortbbidder/resolver/validator_test.go b/adapters/ortbbidder/resolver/validator_test.go new file mode 100644 index 00000000000..373a173cd86 --- /dev/null +++ b/adapters/ortbbidder/resolver/validator_test.go @@ -0,0 +1,126 @@ +package resolver + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestValidateInt(t *testing.T) { + tests := []struct { + input any + expected int + ok bool + }{ + {input: 42.0, expected: 42, ok: true}, + {input: 42.9, expected: 42, ok: true}, + {input: "42", expected: 0, ok: false}, + {input: nil, expected: 0, ok: false}, + } + for _, test := range tests { + result, ok := validateNumber[int](test.input) + if result != test.expected || ok != test.ok { + t.Errorf("validateInt(%v) = (%d, %v), want (%d, %v)", test.input, result, ok, test.expected, test.ok) + } + } +} +func TestValidateInt64(t *testing.T) { + tests := []struct { + input any + expected int64 + ok bool + }{ + {input: 42.0, expected: 42, ok: true}, + {input: 42.9, expected: 42, ok: true}, + {input: "42", expected: 0, ok: false}, + {input: nil, expected: 0, ok: false}, + {input: 42, expected: 0, ok: false}, + } + for _, test := range tests { + result, ok := validateNumber[int64](test.input) + if result != test.expected || ok != test.ok { + t.Errorf("validateInt64(%v) = (%d, %v), want (%d, %v)", test.input, result, ok, test.expected, test.ok) + } + } +} + +func TestValidateString(t *testing.T) { + tests := []struct { + input any + expected string + ok bool + }{ + {input: "hello", expected: "hello", ok: true}, + {input: "", expected: "", ok: false}, + {input: 42, expected: "", ok: false}, + {input: nil, expected: "", ok: false}, + } + for _, test := range tests { + result, ok := validateString(test.input) + if result != test.expected || ok != test.ok { + t.Errorf("validateString(%v) = (%q, %v), want (%q, %v)", test.input, result, ok, test.expected, test.ok) + } + } +} + +func TestValidateMap(t *testing.T) { + tests := []struct { + input any + expected map[string]any + ok bool + }{ + {input: map[string]any{"key": "value"}, expected: map[string]any{"key": "value"}, ok: true}, + {input: `{"key": "value"}`, expected: nil, ok: false}, + {input: nil, expected: nil, ok: false}, + } + for _, test := range tests { + result, ok := validateMap(test.input) + assert.Equal(t, test.expected, result, "mismatched result") + assert.Equal(t, test.ok, ok, "mismatched status") + } +} + +func TestValidateDataTypeSlice(t *testing.T) { + stringTests := []struct { + name string + input any + expected []string + ok bool + }{ + { + name: "valid string slice", + input: []any{"a", "b", "c"}, + expected: []string{"a", "b", "c"}, + ok: true, + }, + { + name: "int value in string slice", + input: []any{"a", 2, "c"}, + expected: []string{"a", "c"}, + ok: true, + }, + { + name: "int slice with string dataType", + input: []any{1, 2, 3}, + expected: []string{}, + ok: false, + }, + { + name: "invalid slice", + input: "not a slice", + expected: nil, + ok: false, + }, + { + name: "nil slice", + input: nil, + expected: nil, + ok: false, + }, + } + for _, test := range stringTests { + result, ok := validateDataTypeSlice[string](test.input) + assert.Equalf(t, test.expected, result, "mismatch result: %s", test.name) + assert.Equalf(t, test.ok, ok, "mismatched status flag: %s", test.name) + } +} diff --git a/adapters/ortbbidder/response_builder.go b/adapters/ortbbidder/response_builder.go index e1b06e4636b..6b90f3edbda 100644 --- a/adapters/ortbbidder/response_builder.go +++ b/adapters/ortbbidder/response_builder.go @@ -3,6 +3,7 @@ package ortbbidder import ( "encoding/json" + "github.com/buger/jsonparser" "github.com/prebid/openrtb/v20/openrtb2" "github.com/prebid/prebid-server/v2/adapters" "github.com/prebid/prebid-server/v2/adapters/ortbbidder/bidderparams" @@ -16,22 +17,28 @@ type responseBuilder struct { adapterRespone map[string]any // Response in the prebid format. responseParams map[string]bidderparams.BidderParamMapper // Bidder response parameters. request *openrtb2.BidRequest // Bid request. + isDebugEnabled bool // flag to determine if requestExt.prebid.debug is enabled. } func newResponseBuilder(responseParams map[string]bidderparams.BidderParamMapper, request *openrtb2.BidRequest) *responseBuilder { + var isDebugEnabled bool + if request != nil { + isDebugEnabled, _ = jsonparser.GetBoolean(request.Ext, "prebid", "debug") + } return &responseBuilder{ responseParams: responseParams, request: request, + isDebugEnabled: isDebugEnabled, } } // setPrebidBidderResponse determines and construct adapters.BidderResponse and adapters.TypedBid object with the help // of response parameter mappings defined in static/bidder-response-params -func (rb *responseBuilder) setPrebidBidderResponse(bidderResponseBytes json.RawMessage) error { +func (rb *responseBuilder) setPrebidBidderResponse(bidderResponseBytes json.RawMessage) (errs []error) { err := jsonutil.UnmarshalValid(bidderResponseBytes, &rb.bidderResponse) if err != nil { - return err + return []error{util.NewBadServerResponseError(err.Error())} } // Create a new ParamResolver with the bidder response. paramResolver := resolver.New(rb.request, rb.bidderResponse) @@ -39,50 +46,53 @@ func (rb *responseBuilder) setPrebidBidderResponse(bidderResponseBytes json.RawM adapterResponse := map[string]any{ currencyKey: rb.bidderResponse[ortbCurrencyKey], } - // Resolve the adapter response level parameters. - paramMapper := rb.responseParams[resolver.Fledge.String()] - paramResolver.Resolve(rb.bidderResponse, adapterResponse, paramMapper.Location, resolver.Fledge) - + for _, param := range resolver.ResponseParams { + bidderParam := rb.responseParams[param.String()] + resolverErrors := paramResolver.Resolve(rb.bidderResponse, adapterResponse, bidderParam.Location, param) + errs = collectWarningMessages(errs, resolverErrors, param.String(), rb.isDebugEnabled) + } // Extract the seat bids from the bidder response. seatBids, ok := rb.bidderResponse[seatBidKey].([]any) if !ok { - return newBadServerResponseError("invalid seatbid array found in response, seatbids:[%v]", rb.bidderResponse[seatBidKey]) + return []error{util.NewBadServerResponseError("invalid seatbid array found in response, seatbids:[%v]", rb.bidderResponse[seatBidKey])} } - // Initialize the list of type bids. - typeBids := make([]any, 0) + // Initialize the list of typed bids. + typedBids := make([]any, 0) for seatIndex, seatBid := range seatBids { seatBid, ok := seatBid.(map[string]any) if !ok { - return newBadServerResponseError("invalid seatbid found in seatbid array, seatbid:[%v]", seatBids[seatIndex]) + return []error{util.NewBadServerResponseError("invalid seatbid found in seatbid array, seatbid:[%v]", seatBids[seatIndex])} } bids, ok := seatBid[bidKey].([]any) if !ok { - return newBadServerResponseError("invalid bid array found in seatbid, bids:[%v]", seatBid[bidKey]) + return []error{util.NewBadServerResponseError("invalid bid array found in seatbid, bids:[%v]", seatBid[bidKey])} } for bidIndex, bid := range bids { bid, ok := bid.(map[string]any) if !ok { - return newBadServerResponseError("invalid bid found in bids array, bid:[%v]", bids[bidIndex]) + return []error{util.NewBadServerResponseError("invalid bid found in bids array, bid:[%v]", bids[bidIndex])} } - // Initialize the type bid with the bid. - typeBid := map[string]any{ - typeBidKey: bid, + // Initialize the typed bid with the bid. + typedBid := map[string]any{ + typedbidKey: bid, } - // Resolve the type bid level parameters. - paramMapper := rb.responseParams[resolver.BidType.String()] - location := util.ReplaceLocationMacro(paramMapper.Location, []int{seatIndex, bidIndex}) - paramResolver.Resolve(bid, typeBid, location, resolver.BidType) - - // Add the type bid to the list of type bids. - typeBids = append(typeBids, typeBid) + // Resolve the typed bid level parameters. + for _, param := range resolver.TypedBidParams { + paramMapper := rb.responseParams[param.String()] + location := util.ReplaceLocationMacro(paramMapper.Location, []int{seatIndex, bidIndex}) + resolverErrors := paramResolver.Resolve(bid, typedBid, location, param) + errs = collectWarningMessages(errs, resolverErrors, param.String(), rb.isDebugEnabled) + } + // Add the type bid to the list of typed bids. + typedBids = append(typedBids, typedBid) } } // Add the type bids to the adapter response. - adapterResponse[bidsKey] = typeBids + adapterResponse[bidsKey] = typedBids // Set the adapter response in the response builder. rb.adapterRespone = adapterResponse - return nil + return errs } // buildAdapterResponse converts the responseBuilder's adapter response to a prebid format. @@ -91,12 +101,27 @@ func (rb *responseBuilder) buildAdapterResponse() (resp *adapters.BidderResponse var adapterResponeBytes json.RawMessage adapterResponeBytes, err = jsonutil.Marshal(rb.adapterRespone) if err != nil { - return + return nil, util.NewBadServerResponseError(err.Error()) } err = jsonutil.UnmarshalValid(adapterResponeBytes, &resp) if err != nil { - return nil, err + return nil, util.NewBadServerResponseError(err.Error()) } return } + +// collectWarningMessages appends warning messages from resolverErrors to the errs slice. +// If debugging is disabled, it appends a generic warning message and returns immediately. +func collectWarningMessages(errs, resolverErrors []error, parameter string, isDebugEnabled bool) []error { + for _, err := range resolverErrors { + if resolver.IsWarning(err) { + if !isDebugEnabled { + errs = append(errs, util.NewWarning("Potential issue encountered while setting the response parameter [%s]", parameter)) + return errs + } + errs = append(errs, util.NewWarning(err.Error())) + } + } + return errs +} diff --git a/adapters/ortbbidder/response_builder_test.go b/adapters/ortbbidder/response_builder_test.go index 5af101f42f6..7c598a812c5 100644 --- a/adapters/ortbbidder/response_builder_test.go +++ b/adapters/ortbbidder/response_builder_test.go @@ -1,11 +1,15 @@ package ortbbidder import ( + "errors" + "reflect" "testing" "github.com/prebid/openrtb/v20/openrtb2" "github.com/prebid/prebid-server/v2/adapters" "github.com/prebid/prebid-server/v2/adapters/ortbbidder/bidderparams" + "github.com/prebid/prebid-server/v2/adapters/ortbbidder/resolver" + "github.com/prebid/prebid-server/v2/adapters/ortbbidder/util" "github.com/prebid/prebid-server/v2/errortypes" "github.com/prebid/prebid-server/v2/openrtb_ext" "github.com/stretchr/testify/assert" @@ -89,7 +93,7 @@ func TestBuildAdapterResponse(t *testing.T) { }, }, expectedResponse: nil, - expectedError: &errortypes.FailedToUnmarshal{ + expectedError: &errortypes.BadServerResponse{ Message: "cannot unmarshal adapters.BidderResponse.Bids: decode slice: expect [ or n, but found {", }, }, @@ -105,7 +109,7 @@ func TestBuildAdapterResponse(t *testing.T) { }, }, expectedResponse: nil, - expectedError: &errortypes.FailedToMarshal{Message: "chan int is unsupported type"}, + expectedError: &errortypes.BadServerResponse{Message: "chan int is unsupported type"}, }, } for _, tc := range testCases { @@ -125,8 +129,9 @@ func TestSetPrebidBidderResponse(t *testing.T) { name string bidderResponse map[string]any bidderResponseBytes []byte + isDebugEnabled bool responseParams map[string]bidderparams.BidderParamMapper - expectedError error + expectedError []error expectedResponse map[string]any }{ { @@ -137,7 +142,7 @@ func TestSetPrebidBidderResponse(t *testing.T) { Location: "cur", }, }, - expectedError: &errortypes.FailedToUnmarshal{Message: "expect ] in the end, but found \x00"}, + expectedError: []error{&errortypes.BadServerResponse{Message: "expect ] in the end, but found \x00"}}, }, { name: "Invalid seatbid object in response", @@ -147,7 +152,7 @@ func TestSetPrebidBidderResponse(t *testing.T) { Location: "cur", }, }, - expectedError: &errortypes.BadServerResponse{Message: "invalid seatbid array found in response, seatbids:[invalid]"}, + expectedError: []error{&errortypes.BadServerResponse{Message: "invalid seatbid array found in response, seatbids:[invalid]"}}, }, { name: "Invalid seatbid is seatbid arrays", @@ -157,7 +162,7 @@ func TestSetPrebidBidderResponse(t *testing.T) { Location: "cur", }, }, - expectedError: &errortypes.BadServerResponse{Message: "invalid seatbid found in seatbid array, seatbid:[invalid]"}, + expectedError: []error{&errortypes.BadServerResponse{Message: "invalid seatbid found in seatbid array, seatbid:[invalid]"}}, }, { name: "Invalid bid in seatbid", @@ -167,7 +172,7 @@ func TestSetPrebidBidderResponse(t *testing.T) { Location: "cur", }, }, - expectedError: &errortypes.BadServerResponse{Message: "invalid bid array found in seatbid, bids:[invalid]"}, + expectedError: []error{&errortypes.BadServerResponse{Message: "invalid bid array found in seatbid, bids:[invalid]"}}, }, { name: "Invalid bid in bids array", @@ -177,13 +182,30 @@ func TestSetPrebidBidderResponse(t *testing.T) { Location: "cur", }, }, - expectedError: &errortypes.BadServerResponse{Message: "invalid bid found in bids array, bid:[invalid]"}, + expectedError: []error{&errortypes.BadServerResponse{Message: "invalid bid found in bids array, bid:[invalid]"}}, }, { - name: "Valid bidder respone, no bidder params", + name: "Valid bidder respone, no bidder params, debug is disabled in request", bidderResponseBytes: []byte(`{"id":"bid-resp-id","cur":"USD","seatbid":[{"seat":"test_bidder","bid":[{"id":"123"}]}]}`), responseParams: map[string]bidderparams.BidderParamMapper{}, - expectedError: nil, + expectedError: []error{util.NewWarning("Potential issue encountered while setting the response parameter [bidType]")}, + expectedResponse: map[string]any{ + "Currency": "USD", + "Bids": []any{ + map[string]any{ + "Bid": map[string]any{ + "id": "123", + }, + }, + }, + }, + }, + { + name: "Valid bidder respone, no bidder params, debug is enabled in request", + bidderResponseBytes: []byte(`{"id":"bid-resp-id","cur":"USD","seatbid":[{"seat":"test_bidder","bid":[{"id":"123"}]}]}`), + responseParams: map[string]bidderparams.BidderParamMapper{}, + isDebugEnabled: true, + expectedError: []error{util.NewWarning("invalid value sent by bidder at [bid.impid] for [bid.ext.prebid.type]")}, expectedResponse: map[string]any{ "Currency": "USD", "Bids": []any{ @@ -214,32 +236,47 @@ func TestSetPrebidBidderResponse(t *testing.T) { }, }, { - name: "Valid bidder respone, with bidder params", + name: "Valid bidder respone, with single bidder param - bidType", bidderResponseBytes: []byte(`{"id":"bid-resp-id","cur":"USD","seatbid":[{"seat":"test_bidder","bid":[{"id":"123","ext": {"bidtype": "video"}}]}]}`), - bidderResponse: map[string]any{ - "cur": "USD", - seatBidKey: []any{ + responseParams: map[string]bidderparams.BidderParamMapper{ + "currency": { + Location: "cur", + }, + "bidType": { + Location: "seatbid.#.bid.#.ext.bidtype", + }, + }, + expectedError: nil, + expectedResponse: map[string]any{ + "Currency": "USD", + "Bids": []any{ map[string]any{ - "bid": []any{ - map[string]any{ - "id": "123", - "ext": map[string]any{ - "bidtype": "video", - }, + "Bid": map[string]any{ + "id": "123", + "ext": map[string]any{ + "bidtype": "video", }, }, + "BidType": openrtb_ext.BidType("video"), }, }, }, + }, + { + name: "failed to set the adapter-response level param - fledgeConfig", + bidderResponseBytes: []byte(`{"fledge":"","id":"bid-resp-id","cur":"USD","seatbid":[{"seat":"test_bidder","bid":[{"id":"123","ext": {"bidtype": "video"}}]}]}`), responseParams: map[string]bidderparams.BidderParamMapper{ "currency": { Location: "cur", }, - "bidtype": { - Location: "seatbid.#.bid.#.ext.bidtype", + "fledgeAuctionConfig": { + Location: "fledge", }, }, - expectedError: nil, + expectedError: []error{ + util.NewWarning("Potential issue encountered while setting the response parameter [fledgeAuctionConfig]"), + util.NewWarning("Potential issue encountered while setting the response parameter [bidType]"), + }, expectedResponse: map[string]any{ "Currency": "USD", "Bids": []any{ @@ -250,22 +287,179 @@ func TestSetPrebidBidderResponse(t *testing.T) { "bidtype": "video", }, }, - "BidType": "video", + }, + }, + }, + }, + { + name: "Valid bidder respone, with multiple bidder params", + bidderResponseBytes: []byte(`{"id":"bid-resp-id","cur":"USD","seatbid":[{"seat":"test_bidder","ext":{"dp":2},"bid":[{"id":"123","cat":["music"],"ext":{"bidtype":"video","advertiserId":"5"` + + `,"networkId":5,"duration":10,"meta_object":{"advertiserDomains":["xyz.com"],"mediaType":"video"}}}]}]}`), + responseParams: map[string]bidderparams.BidderParamMapper{ + "currency": {Location: "cur"}, + "bidType": {Location: "seatbid.#.bid.#.ext.bidtype"}, + "bidDealPriority": {Location: "seatbid.#.ext.dp"}, + "bidVideoDuration": {Location: "seatbid.#.bid.#.ext.duration"}, + "bidMeta": {Location: "seatbid.#.bid.#.ext.meta_object"}, + "bidMetaAdvertiserId": {Location: "seatbid.#.bid.#.ext.advertiserId"}, + "bidMetaNetworkId": {Location: "seatbid.#.bid.#.ext.networkId"}, + }, + expectedError: []error{util.NewWarning("Potential issue encountered while setting the response parameter [bidMetaAdvertiserId]")}, + expectedResponse: map[string]any{ + "Currency": "USD", + "Bids": []any{ + map[string]any{ + "Bid": map[string]any{ + "id": "123", + "cat": []any{"music"}, + "ext": map[string]any{ + "bidtype": "video", + "advertiserId": "5", + "networkId": 5.0, + "duration": 10.0, + "meta_object": map[string]any{ + "advertiserDomains": []any{"xyz.com"}, + "mediaType": "video", + }, + }, + }, + "BidType": openrtb_ext.BidType("video"), + "BidVideo": map[string]any{ + "primary_category": "music", + "duration": int64(10), + }, + "DealPriority": 2, + "BidMeta": map[string]any{ + "advertiserDomains": []any{"xyz.com"}, + "mediaType": "video", + "networkId": int(5), + }, }, }, }, }, } - for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { rb := &responseBuilder{ bidderResponse: tc.bidderResponse, responseParams: tc.responseParams, + isDebugEnabled: tc.isDebugEnabled, } err := rb.setPrebidBidderResponse(tc.bidderResponseBytes) assert.Equal(t, tc.expectedError, err) - assert.Equal(t, tc.expectedResponse, rb.adapterRespone) + assert.Equal(t, tc.expectedResponse, rb.adapterRespone, "mismatched adapterRespone") + }) + } +} + +// TestTypedBidFields notifies us of any changes in the adapters.TypedBid struct. +// If a new field is added in adapters.TypedBid, then add the support to resolve the new field and update the test case. +// If the data type of an existing field changes then update the resolver of the respective field. +func TestTypedBidFields(t *testing.T) { + expectedFields := map[string]reflect.Type{ + "Bid": reflect.TypeOf(&openrtb2.Bid{}), + "BidMeta": reflect.TypeOf(&openrtb_ext.ExtBidPrebidMeta{}), + "BidType": reflect.TypeOf(openrtb_ext.BidTypeBanner), + "BidVideo": reflect.TypeOf(&openrtb_ext.ExtBidPrebidVideo{}), + "BidTargets": reflect.TypeOf(map[string]string{}), + "DealPriority": reflect.TypeOf(0), + "Seat": reflect.TypeOf(openrtb_ext.BidderName("")), + } + + structType := reflect.TypeOf(adapters.TypedBid{}) + err := resolver.ValidateStructFields(expectedFields, structType) + if err != nil { + t.Error(err) + } +} + +// TestBidderResponseFields notifies us of any changes in the adapters.BidderResponse struct. +// If a new field is added in adapters.BidderResponse, then add the support to resolve the new field and update the test case. +// If the data type of an existing field changes then update the resolver of the respective field. +func TestBidderResponseFields(t *testing.T) { + expectedFields := map[string]reflect.Type{ + "Currency": reflect.TypeOf(""), + "Bids": reflect.TypeOf([]*adapters.TypedBid{nil}), + "FledgeAuctionConfigs": reflect.TypeOf([]*openrtb_ext.FledgeAuctionConfig{}), + "FastXMLMetrics": reflect.TypeOf(&openrtb_ext.FastXMLMetrics{}), + } + structType := reflect.TypeOf(adapters.BidderResponse{}) + err := resolver.ValidateStructFields(expectedFields, structType) + if err != nil { + t.Error(err) + } +} + +func TestCollectWarningMessages(t *testing.T) { + type args struct { + errs []error + resolverErrors []error + parameter string + isDebugEnabled bool + } + tests := []struct { + name string + args args + want []error + }{ + { + name: "No resolver errors", + args: args{ + errs: []error{}, + resolverErrors: []error{}, + parameter: "param1", + isDebugEnabled: false, + }, + want: []error{}, + }, + { + name: "Resolver errors with warnings and debugging enabled", + args: args{ + errs: []error{}, + resolverErrors: []error{ + resolver.NewValidationFailedError("Warning 1"), + resolver.NewValidationFailedError("Warning 2"), + }, + parameter: "param2", + isDebugEnabled: true, + }, + want: []error{ + util.NewWarning("Warning 1"), + util.NewWarning("Warning 2"), + }, + }, + { + name: "Resolver errors with warnings and debugging disabled", + args: args{ + errs: []error{}, + resolverErrors: []error{ + resolver.NewValidationFailedError("Warning 1"), + }, + parameter: "param3", + isDebugEnabled: false, + }, + want: []error{ + util.NewWarning("Potential issue encountered while setting the response parameter [param3]"), + }, + }, + { + name: "Resolver errors without warnings", + args: args{ + errs: []error{}, + resolverErrors: []error{ + errors.New("Non-warning error"), + }, + parameter: "param4", + isDebugEnabled: false, + }, + want: []error{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := collectWarningMessages(tt.args.errs, tt.args.resolverErrors, tt.args.parameter, tt.args.isDebugEnabled) + assert.Equal(t, tt.want, got) }) } } diff --git a/adapters/ortbbidder/single_request_builder.go b/adapters/ortbbidder/single_request_builder.go index 6b7b703648f..3486ced5e98 100644 --- a/adapters/ortbbidder/single_request_builder.go +++ b/adapters/ortbbidder/single_request_builder.go @@ -5,6 +5,7 @@ import ( "github.com/prebid/openrtb/v20/openrtb2" "github.com/prebid/prebid-server/v2/adapters" + "github.com/prebid/prebid-server/v2/adapters/ortbbidder/util" "github.com/prebid/prebid-server/v2/util/jsonutil" ) @@ -30,7 +31,7 @@ func (rb *singleRequestBuilder) parseRequest(request *openrtb2.BidRequest) (err imps, ok := rb.newRequest[impKey].([]any) if !ok { - return errImpMissing + return util.ErrImpMissing } for index, imp := range imps { imp, ok := imp.(map[string]any) @@ -51,7 +52,7 @@ func (rb *singleRequestBuilder) parseRequest(request *openrtb2.BidRequest) (err // it create single RequestData object for all impressions. func (rb *singleRequestBuilder) makeRequest() (requestData []*adapters.RequestData, errs []error) { if len(rb.imps) == 0 { - errs = append(errs, newBadInputError(errImpMissing.Error())) + errs = append(errs, util.NewBadInputError(util.ErrImpMissing.Error())) return } @@ -62,8 +63,8 @@ func (rb *singleRequestBuilder) makeRequest() (requestData []*adapters.RequestDa //step 1: get endpoint if endpoint, err = rb.getEndpoint(getImpExtBidderParams(rb.imps[0])); err != nil { - errs = append(errs, newBadInputError(err.Error())) - return nil, errs + errs = append(errs, util.NewBadInputError(err.Error())) + return } //step 2: replace parameters @@ -75,7 +76,7 @@ func (rb *singleRequestBuilder) makeRequest() (requestData []*adapters.RequestDa //step 3: append new request data if requestData, err = appendRequestData(requestData, rb.newRequest, endpoint, rb.impIDs); err != nil { - errs = append(errs, newBadInputError(err.Error())) + errs = append(errs, util.NewBadInputError(err.Error())) } - return requestData, errs + return } diff --git a/adapters/ortbbidder/single_request_builder_test.go b/adapters/ortbbidder/single_request_builder_test.go index 50e02ddeea1..3b66cc257df 100644 --- a/adapters/ortbbidder/single_request_builder_test.go +++ b/adapters/ortbbidder/single_request_builder_test.go @@ -10,6 +10,7 @@ import ( "github.com/prebid/openrtb/v20/openrtb2" "github.com/prebid/prebid-server/v2/adapters" "github.com/prebid/prebid-server/v2/adapters/ortbbidder/bidderparams" + "github.com/prebid/prebid-server/v2/adapters/ortbbidder/util" "github.com/stretchr/testify/assert" ) @@ -36,7 +37,7 @@ func TestSingleRequestBuilderParseRequest(t *testing.T) { }, }, want: want{ - err: errImpMissing, + err: util.ErrImpMissing, rawRequest: json.RawMessage(`{"id":"id","imp":null}`), imps: nil, newRequest: map[string]any{ @@ -111,7 +112,7 @@ func TestSingleRequestBuilderMakeRequest(t *testing.T) { }, want: want{ requestData: nil, - errs: []error{newBadInputError(errImpMissing.Error())}, + errs: []error{util.NewBadInputError(util.ErrImpMissing.Error())}, }, }, { @@ -213,7 +214,7 @@ func TestSingleRequestBuilderMakeRequest(t *testing.T) { }, want: want{ requestData: nil, - errs: []error{newBadInputError("failed to replace macros in endpoint, err:template: endpointTemplate:1:2: " + + errs: []error{util.NewBadInputError("failed to replace macros in endpoint, err:template: endpointTemplate:1:2: " + "executing \"endpointTemplate\" at : error calling errorFunc: intentional error")}, }, }, diff --git a/adapters/ortbbidder/util/errors.go b/adapters/ortbbidder/util/errors.go new file mode 100644 index 00000000000..d9443132f2f --- /dev/null +++ b/adapters/ortbbidder/util/errors.go @@ -0,0 +1,32 @@ +package util + +import ( + "errors" + "fmt" + + "github.com/prebid/prebid-server/v2/errortypes" +) + +// list of constant errors +var ( + ErrImpMissing error = errors.New("imp object not found in request") + ErrNilBidderParamCfg error = errors.New("found nil bidderParamsConfig") +) + +func NewBadInputError(message string, args ...any) error { + return &errortypes.BadInput{ + Message: fmt.Sprintf(message, args...), + } +} + +func NewBadServerResponseError(message string, args ...any) error { + return &errortypes.BadServerResponse{ + Message: fmt.Sprintf(message, args...), + } +} + +func NewWarning(message string, args ...any) error { + return &errortypes.Warning{ + Message: fmt.Sprintf(message, args...), + } +} diff --git a/adapters/ortbbidder/util/errors_test.go b/adapters/ortbbidder/util/errors_test.go new file mode 100644 index 00000000000..15b25a0027d --- /dev/null +++ b/adapters/ortbbidder/util/errors_test.go @@ -0,0 +1,95 @@ +package util + +import ( + "testing" + + "github.com/prebid/prebid-server/v2/errortypes" + "github.com/stretchr/testify/assert" +) + +func TestNewBadInputError(t *testing.T) { + type args struct { + message string + args []any + } + tests := []struct { + name string + args args + wantErr error + }{ + { + name: "bad input error with params", + args: args{ + message: "bad input error [%s]", + args: []any{"field"}, + }, + wantErr: &errortypes.BadInput{ + Message: "bad input error [field]", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := NewBadInputError(tt.args.message, tt.args.args...) + assert.Equal(t, tt.wantErr, err) + }) + } +} + +func TestNewBadServerResponseError(t *testing.T) { + type args struct { + message string + args []any + } + tests := []struct { + name string + args args + wantErr error + }{ + { + name: "bad serevr error with params", + args: args{ + message: "bad input error [%s]", + args: []any{"field"}, + }, + wantErr: &errortypes.BadServerResponse{ + Message: "bad input error [field]", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := NewBadServerResponseError(tt.args.message, tt.args.args...) + assert.Equal(t, tt.wantErr, err) + }) + } +} + +func TestNewWarning(t *testing.T) { + type args struct { + message string + args []any + } + tests := []struct { + name string + args args + wantErr error + }{ + { + name: "warning with params", + args: args{ + message: "bad error [%s] : [%d]", + args: []any{"field", 10}, + }, + wantErr: &errortypes.Warning{ + Message: "bad error [field] : [10]", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := NewWarning(tt.args.message, tt.args.args...) + assert.Equal(t, tt.wantErr, err) + }) + } +} diff --git a/adapters/ortbbidder/util/util.go b/adapters/ortbbidder/util/util.go index 9a40ba49e8f..71cfff29f50 100644 --- a/adapters/ortbbidder/util/util.go +++ b/adapters/ortbbidder/util/util.go @@ -107,21 +107,20 @@ func getNode(requestNode map[string]any, key string) any { } // getValueFromLocation retrieves a value from a map based on a specified location. -// getValueFromLocation retrieves a value from a map based on a specified location. -func GetValueFromLocation(souce interface{}, path string) (interface{}, bool) { +func GetValueFromLocation(souce any, path string) (any, bool) { location := strings.Split(path, ".") var ( ok bool - next interface{} = souce + next any = souce ) for _, loc := range location { switch nxt := next.(type) { - case map[string]interface{}: + case map[string]any: next, ok = nxt[loc] if !ok { return nil, false } - case []interface{}: + case []any: index, err := strconv.Atoi(loc) if err != nil { return nil, false diff --git a/adapters/ortbbidder/util/util_test.go b/adapters/ortbbidder/util/util_test.go index c5773fd5134..259f003ec13 100644 --- a/adapters/ortbbidder/util/util_test.go +++ b/adapters/ortbbidder/util/util_test.go @@ -492,6 +492,13 @@ func TestGetValueFromLocation(t *testing.T) { expectedValue: nil, ok: false, }, + { + name: "Value is present but node-element is not list", + node: node, + path: "seatbid.bid", + expectedValue: nil, + ok: false, + }, { name: "Value is not present in node due to invalid index", node: jsonToMap(`{"seatbid":[{"bid":[{"ext":{"mtype":"video"}}]}]}`), diff --git a/endpoints/openrtb2/ctv/adpod/dynamic_adpod.go b/endpoints/openrtb2/ctv/adpod/dynamic_adpod.go index 36ce90c0601..442dcbcf981 100644 --- a/endpoints/openrtb2/ctv/adpod/dynamic_adpod.go +++ b/endpoints/openrtb2/ctv/adpod/dynamic_adpod.go @@ -43,6 +43,8 @@ func NewDynamicAdpod(pubId string, imp openrtb2.Imp, adpodExt openrtb_ext.ExtVid minPodDuration := imp.Video.MinDuration maxPodDuration := imp.Video.MaxDuration + exclusion := getExclusionConfigs(imp.Video.PodID, reqAdpodExt) + if adpodExt.AdPod == nil { adpodExt = openrtb_ext.ExtVideoAdPod{ Offset: ptrutil.ToPtr(0), @@ -55,6 +57,15 @@ func NewDynamicAdpod(pubId string, imp openrtb2.Imp, adpodExt openrtb_ext.ExtVid IABCategoryExclusionPercent: ptrutil.ToPtr(100), }, } + + if exclusion.AdvertiserDomainExclusion { + adpodExt.AdPod.AdvertiserExclusionPercent = ptrutil.ToPtr(0) + } + + if exclusion.IABCategoryExclusion { + adpodExt.AdPod.IABCategoryExclusionPercent = ptrutil.ToPtr(0) + } + maxPodDuration = imp.Video.PodDur } @@ -64,6 +75,7 @@ func NewDynamicAdpod(pubId string, imp openrtb2.Imp, adpodExt openrtb_ext.ExtVid Type: Dynamic, ReqAdpodExt: reqAdpodExt, MetricsEngine: metricsEngine, + Exclusion: exclusion, }, Imp: imp, VideoExt: &adpodExt, diff --git a/endpoints/openrtb2/ctv/adpod/structured_adpod.go b/endpoints/openrtb2/ctv/adpod/structured_adpod.go index 92d457f70df..da08ffcfa66 100644 --- a/endpoints/openrtb2/ctv/adpod/structured_adpod.go +++ b/endpoints/openrtb2/ctv/adpod/structured_adpod.go @@ -23,15 +23,17 @@ type Slot struct { ImpId string Index int TotalBids int + NoBid bool } -func NewStructuredAdpod(pubId string, metricsEngine metrics.MetricsEngine, reqAdpodExt *openrtb_ext.ExtRequestAdPod) *structuredAdpod { +func NewStructuredAdpod(podId string, pubId string, metricsEngine metrics.MetricsEngine, reqAdpodExt *openrtb_ext.ExtRequestAdPod) *structuredAdpod { adpod := structuredAdpod{ AdpodCtx: AdpodCtx{ PubId: pubId, Type: Structured, ReqAdpodExt: reqAdpodExt, MetricsEngine: metricsEngine, + Exclusion: getExclusionConfigs(podId, reqAdpodExt), }, ImpBidMap: make(map[string][]*types.Bid), WinningBid: make(map[string]types.Bid), @@ -97,14 +99,16 @@ func (sa *structuredAdpod) HoldAuction() { // Select Winning bids for i := range slots { bids := sa.ImpBidMap[slots[i].ImpId] - // Add validations on len of array and index chosen - // Validate the array length and index chosen - if len(bids) > slots[i].Index { - selectedBid := bids[slots[i].Index] - selectedBid.Status = constant.StatusWinningBid - sa.WinningBid[slots[i].ImpId] = *selectedBid + if len(bids) == 0 { + continue } + slot := slots[i] + if slot.NoBid { + continue + } + bids[slot.Index].Status = constant.StatusWinningBid + sa.WinningBid[slot.ImpId] = *bids[slot.Index] } } @@ -167,6 +171,10 @@ func (sa *structuredAdpod) addDomains(domains []string) { } func (sa *structuredAdpod) isCategoryAlreadySelected(bid *types.Bid) bool { + if !sa.Exclusion.IABCategoryExclusion { + return false + } + if bid == nil || bid.Cat == nil { return false } @@ -185,6 +193,10 @@ func (sa *structuredAdpod) isCategoryAlreadySelected(bid *types.Bid) bool { } func (sa *structuredAdpod) isDomainAlreadySelected(bid *types.Bid) bool { + if !sa.Exclusion.AdvertiserDomainExclusion { + return false + } + if bid == nil || bid.ADomain == nil { return false } @@ -254,14 +266,19 @@ func (sa *structuredAdpod) selectBidForSlot(slots []Slot) { selectedSlot.Index = bidIndex slots[slotIndex] = selectedSlot } else if sa.isCategoryAlreadySelected(selectedBid) || sa.isDomainAlreadySelected(selectedBid) { + noBidSlot := true // Get bid for current slot for which category is not overlapping for i := selectedSlot.Index + 1; i < len(slotBids); i++ { if !sa.isCategoryAlreadySelected(slotBids[i]) && !sa.isDomainAlreadySelected(slotBids[i]) { selectedSlot.Index = i + noBidSlot = false break } } + // Update no bid status + selectedSlot.NoBid = noBidSlot + // Update selected Slot in slots array slots[slotIndex] = selectedSlot } diff --git a/endpoints/openrtb2/ctv/adpod/structured_adpod_test.go b/endpoints/openrtb2/ctv/adpod/structured_adpod_test.go index 3115a0a4aef..c821977b56f 100644 --- a/endpoints/openrtb2/ctv/adpod/structured_adpod_test.go +++ b/endpoints/openrtb2/ctv/adpod/structured_adpod_test.go @@ -873,7 +873,70 @@ func TestStructuredAdpodPerformAuctionAndExclusion(t *testing.T) { }, }, }, + { + name: "price_based_auction_with_one_slot_no_bid", + fields: fields{ + AdpodCtx: AdpodCtx{ + Type: Structured, + Exclusion: Exclusion{ + IABCategoryExclusion: true, + AdvertiserDomainExclusion: true, + }, + }, + ImpBidMap: map[string][]*types.Bid{ + "imp1": { + { + Bid: &openrtb2.Bid{ + Price: 6, + Cat: []string{"IAB-1"}, + }, + DealTierSatisfied: false, + Seat: "pubmatic", + }, + { + Bid: &openrtb2.Bid{ + Price: 4, + Cat: []string{"IAB-1"}, + }, + DealTierSatisfied: false, + Seat: "appnexux", + }, + }, + "imp2": { + { + Bid: &openrtb2.Bid{ + Price: 4, + Cat: []string{"IAB-1"}, + }, + DealTierSatisfied: false, + Seat: "index", + }, + { + Bid: &openrtb2.Bid{ + Price: 2, + Cat: []string{"IAB-1"}, + }, + DealTierSatisfied: false, + Seat: "pubmatic", + }, + }, + }, + WinningBid: make(map[string]types.Bid), + }, + wantWinningBid: map[string]types.Bid{ + "imp1": { + Bid: &openrtb2.Bid{ + Price: 6, + Cat: []string{"IAB-1"}, + }, + DealTierSatisfied: false, + Seat: "pubmatic", + Status: constant.StatusWinningBid, + }, + }, + }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { sa := &structuredAdpod{ diff --git a/endpoints/openrtb2/ctv/adpod/util.go b/endpoints/openrtb2/ctv/adpod/util.go index ee5bc67f843..d87ec32c1e7 100644 --- a/endpoints/openrtb2/ctv/adpod/util.go +++ b/endpoints/openrtb2/ctv/adpod/util.go @@ -102,3 +102,29 @@ func createMapFromSlice(slice []string) map[string]bool { } return resultMap } + +func getExclusionConfigs(podId string, adpodExt *openrtb_ext.ExtRequestAdPod) Exclusion { + var exclusion Exclusion + + if adpodExt != nil && adpodExt.Exclusion != nil { + var iabCategory, advertiserDomain bool + for i := range adpodExt.Exclusion.IABCategory { + if adpodExt.Exclusion.IABCategory[i] == podId { + iabCategory = true + break + } + } + + for i := range adpodExt.Exclusion.AdvertiserDomain { + if adpodExt.Exclusion.AdvertiserDomain[i] == podId { + advertiserDomain = true + break + } + } + + exclusion.IABCategoryExclusion = iabCategory + exclusion.AdvertiserDomainExclusion = advertiserDomain + } + + return exclusion +} diff --git a/endpoints/openrtb2/ctv/adpod/util_test.go b/endpoints/openrtb2/ctv/adpod/util_test.go index b41d84cecaa..db6599152c1 100644 --- a/endpoints/openrtb2/ctv/adpod/util_test.go +++ b/endpoints/openrtb2/ctv/adpod/util_test.go @@ -89,6 +89,79 @@ func TestConvertToV25VideoRequest(t *testing.T) { } } +func TestGetExclusionConfigs(t *testing.T) { + tests := []struct { + name string + podId string + adpodExt *openrtb_ext.ExtRequestAdPod + expected Exclusion + }{ + { + name: "Nil_adpodExt", + podId: "testPodId", + adpodExt: nil, + expected: Exclusion{}, + }, + { + name: "Nil_Exclusion", + podId: "testPodId", + adpodExt: &openrtb_ext.ExtRequestAdPod{ + Exclusion: nil, + }, + expected: Exclusion{}, + }, + { + name: "IABCategory_exclusion_present", + podId: "testPodId", + adpodExt: &openrtb_ext.ExtRequestAdPod{ + Exclusion: &openrtb_ext.AdpodExclusion{ + IABCategory: []string{"testPodId"}, + AdvertiserDomain: []string{"otherPodId"}, + }, + }, + expected: Exclusion{ + IABCategoryExclusion: true, + AdvertiserDomainExclusion: false, + }, + }, + { + name: "AdvertiserDomain_exclusion_present", + podId: "testPodId", + adpodExt: &openrtb_ext.ExtRequestAdPod{ + Exclusion: &openrtb_ext.AdpodExclusion{ + IABCategory: []string{"otherPodId"}, + AdvertiserDomain: []string{"testPodId"}, + }, + }, + expected: Exclusion{ + IABCategoryExclusion: false, + AdvertiserDomainExclusion: true, + }, + }, + { + name: "No_exclusion_config_provided", + podId: "testPodId", + adpodExt: &openrtb_ext.ExtRequestAdPod{ + Exclusion: &openrtb_ext.AdpodExclusion{ + IABCategory: []string{"otherPodId"}, + AdvertiserDomain: []string{"anotherPodId"}, + }, + }, + expected: Exclusion{ + IABCategoryExclusion: false, + AdvertiserDomainExclusion: false, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := getExclusionConfigs(tt.podId, tt.adpodExt) + assert.Equal(t, tt.expected, result) + }) + } +} + func TestConvertNBRCTOAPRC(t *testing.T) { type args struct { noBidReason *openrtb3.NoBidReason @@ -158,4 +231,4 @@ func TestConvertNBRCTOAPRC(t *testing.T) { } }) } -} +} \ No newline at end of file diff --git a/endpoints/openrtb2/ctv_auction.go b/endpoints/openrtb2/ctv_auction.go index 7c6fa5c89e1..d79a571c032 100644 --- a/endpoints/openrtb2/ctv_auction.go +++ b/endpoints/openrtb2/ctv_auction.go @@ -358,7 +358,7 @@ func (deps *ctvEndpointDeps) createStructuredAdpodCtx(imp openrtb2.Imp) { podContext, ok := deps.podCtx[imp.Video.PodID] if !ok { - podContext = adpod.NewStructuredAdpod(deps.labels.PubID, deps.metricsEngine, deps.reqExt) + podContext = adpod.NewStructuredAdpod(imp.Video.PodID, deps.labels.PubID, deps.metricsEngine, deps.reqExt) } podContext.AddImpressions(imp) diff --git a/endpoints/openrtb2/ctv_auction_test.go b/endpoints/openrtb2/ctv_auction_test.go index 7a22f2dcf9f..a808354d232 100644 --- a/endpoints/openrtb2/ctv_auction_test.go +++ b/endpoints/openrtb2/ctv_auction_test.go @@ -297,7 +297,7 @@ func TestCreateAdPodBidResponse(t *testing.T) { } func TestCreateAdPodBidResponseSeatNonBid(t *testing.T) { - structuredAdpod := adpod.NewStructuredAdpod("test-pub", &metrics.MetricsEngineMock{}, nil) + structuredAdpod := adpod.NewStructuredAdpod("test-pod", "test-pub", &metrics.MetricsEngineMock{}, nil) structuredAdpod.WinningBid = map[string]types.Bid{ "imp1": { Bid: &openrtb2.Bid{ diff --git a/modules/pubmatic/openwrap/adapters/bidders.go b/modules/pubmatic/openwrap/adapters/bidders.go index c6b4ab0255a..678dc601ec8 100644 --- a/modules/pubmatic/openwrap/adapters/bidders.go +++ b/modules/pubmatic/openwrap/adapters/bidders.go @@ -704,3 +704,29 @@ func builderPGAMSSP(params BidderParameters) (json.RawMessage, error) { jsonStr.WriteByte('}') return jsonStr.Bytes(), nil } + +func builderAidem(params BidderParameters) (json.RawMessage, error) { + jsonStr := bytes.Buffer{} + jsonStr.WriteByte('{') + + fields := []string{BidderParamAidemPublisherId, BidderParamAidemSiteId, BidderParamAidemPlacementId} + mandatoryFields := map[string]bool{BidderParamAidemPublisherId: true, BidderParamAidemSiteId: true} + + for _, field := range fields { + if value, ok := getString(params.FieldMap[field]); ok { + fmt.Fprintf(&jsonStr, `"%s":"%s",`, field, value) + } else if mandatoryFields[field] { + return nil, fmt.Errorf(errMandatoryParameterMissingFormat, params.AdapterName, field) + } + } + + // len=0 (no mandatory params present) + if jsonStr.Len() == 1 { + return nil, fmt.Errorf(errMandatoryParameterMissingFormat, params.AdapterName, "'publisherId' and 'siteId'") + } + + trimComma(&jsonStr) + jsonStr.WriteByte('}') + + return jsonStr.Bytes(), nil +} diff --git a/modules/pubmatic/openwrap/adapters/bidders_test.go b/modules/pubmatic/openwrap/adapters/bidders_test.go index e81ec4ba637..08c01d55a5c 100644 --- a/modules/pubmatic/openwrap/adapters/bidders_test.go +++ b/modules/pubmatic/openwrap/adapters/bidders_test.go @@ -3060,3 +3060,62 @@ func Test_builderPGAMSSP(t *testing.T) { }) } } + +func Test_builderAidem(t *testing.T) { + type args struct { + params BidderParameters + } + tests := []struct { + name string + args args + want json.RawMessage + wantErr bool + }{ + { + name: "Valid Scenerio rateLimit is present along with all other parameters", + args: args{params: BidderParameters{FieldMap: JSONObject{"placementId": "ABCDEF", "siteId": "ABCDEF", "publisherId": "5890", "rateLimit": 0.6}}}, + want: json.RawMessage(`{"placementId": "ABCDEF", "siteId": "ABCDEF", "publisherId": "5890"}`), + wantErr: false, + }, + { + name: "Valid Scenerio rateLimit is absent along with all other parameters", + args: args{params: BidderParameters{FieldMap: JSONObject{"placementId": "ABCDEF", "siteId": "ABCDEF", "publisherId": "5890"}}}, + want: json.RawMessage(`{"placementId": "ABCDEF", "siteId": "ABCDEF", "publisherId": "5890"}`), + wantErr: false, + }, + { + name: "Invalid Scenerio (None Of publisherId or siteId) is present", + args: args{params: BidderParameters{FieldMap: JSONObject{}}}, + want: json.RawMessage(``), + wantErr: true, + }, + { + name: "Invalid Scenerio (Only publisherId) is present", + args: args{params: BidderParameters{FieldMap: JSONObject{"publisherId": "5890"}}}, + want: json.RawMessage(``), + wantErr: true, + }, + { + name: "Invalid Scenerio (Only siteId) is present", + args: args{params: BidderParameters{FieldMap: JSONObject{"siteId": "abcd"}}}, + want: json.RawMessage(``), + wantErr: true, + }, + { + name: "Invalid Scenerio (Only placementId) is present", + args: args{params: BidderParameters{FieldMap: JSONObject{"placementId": "abcd"}}}, + want: json.RawMessage(``), + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := builderAidem(tt.args.params) + if (err != nil) != tt.wantErr { + t.Errorf("builderAidem() error = %v, wantErr %v", err, tt.wantErr) + return + } + AssertJSON(t, tt.want, got) + }) + } +} diff --git a/modules/pubmatic/openwrap/adapters/builder.go b/modules/pubmatic/openwrap/adapters/builder.go index 62d3aed7081..23ce21b68b7 100644 --- a/modules/pubmatic/openwrap/adapters/builder.go +++ b/modules/pubmatic/openwrap/adapters/builder.go @@ -58,6 +58,7 @@ func initBidderBuilderFactory() { string(openrtb_ext.BidderRise): builderRise, string(openrtb_ext.BidderKargo): builderKargo, string(openrtb_ext.BidderPGAMSsp): builderPGAMSSP, + string(openrtb_ext.BidderAidem): builderAidem, } } diff --git a/modules/pubmatic/openwrap/adapters/constant.go b/modules/pubmatic/openwrap/adapters/constant.go index a36b2126e46..c487c7c4053 100644 --- a/modules/pubmatic/openwrap/adapters/constant.go +++ b/modules/pubmatic/openwrap/adapters/constant.go @@ -37,4 +37,7 @@ const ( BidderParamRisePublisherID = "publisher_id" BidderKaroPlacementID = "placementId" BidderKargoAdSlotID = "adSlotID" + BidderParamAidemPublisherId = "publisherId" + BidderParamAidemSiteId = "siteId" + BidderParamAidemPlacementId = "placementId" ) diff --git a/modules/pubmatic/openwrap/adapters/tests/s2s_bidders.json b/modules/pubmatic/openwrap/adapters/tests/s2s_bidders.json index 7e479822034..12cefbbf095 100644 --- a/modules/pubmatic/openwrap/adapters/tests/s2s_bidders.json +++ b/modules/pubmatic/openwrap/adapters/tests/s2s_bidders.json @@ -120,5 +120,21 @@ "placementId": "123456" } } + }, + { + "name": "aidem - publisherId&siteId", + "args" : { + "adapterName": "aidem", + "requestJSON": { + "publisherId": "123456", + "siteId":"abcdefg" + } + }, + "want": { + "expectedJSON": { + "publisherId": "123456", + "siteId":"abcdefg" + } + } } ] \ No newline at end of file diff --git a/modules/pubmatic/openwrap/applovinmax.go b/modules/pubmatic/openwrap/applovinmax.go index 751a4753414..cbadc1e38b1 100644 --- a/modules/pubmatic/openwrap/applovinmax.go +++ b/modules/pubmatic/openwrap/applovinmax.go @@ -301,8 +301,11 @@ func getAppPublisherID(requestBody []byte) string { func getProfileID(requestBody []byte) string { if profileId, err := jsonparser.GetInt(requestBody, "ext", "prebid", "bidderparams", "pubmatic", "wrapper", "profileid"); err == nil { - a := strconv.Itoa(int(profileId)) - return a + profIDStr := strconv.Itoa(int(profileId)) + return profIDStr + } + if profIDStr, err := jsonparser.GetString(requestBody, "app", "id"); err == nil { + return profIDStr } return "" } diff --git a/modules/pubmatic/openwrap/applovinmax_test.go b/modules/pubmatic/openwrap/applovinmax_test.go index fb9709d1281..81ca46a2f49 100644 --- a/modules/pubmatic/openwrap/applovinmax_test.go +++ b/modules/pubmatic/openwrap/applovinmax_test.go @@ -1045,3 +1045,49 @@ func TestModifyRequestBody(t *testing.T) { }) } } + +func TestGetProfileID(t *testing.T) { + type args struct { + requestBody []byte + } + tests := []struct { + name string + args args + want string + }{ + { + name: "profileID present in request.ext.prebid.bidderparams.pubmatic.wrapper", + args: args{ + requestBody: []byte(`{"app":{"bundle":"com.pubmatic.openbid.app","name":"Sample","publisher":{"id":"156276"},"storeurl":"https://itunes.apple.com/us/app/pubmatic-sdk-app/id1175273098","ver":"1.0"},"at":1,"device":{"carrier":"MYTEL","connectiontype":2,"devicetype":4,"ext":{"atts":2},"geo":{"city":"Queens","country":"USA","dma":"501","ipservice":3,"lat":40.7429,"lon":-73.9392,"long":-73.9392,"metro":"501","region":"ny","type":2,"zip":"11101"},"h":2400,"hwv":"ruby","ifa":"497a10d6-c4dd-4e04-a986-c32b7180d462","ip":"38.158.207.171","js":1,"language":"en_US","make":"xiaomi","model":"22101316c","os":"android","osv":"13.0.0","ppi":440,"pxratio":2.75,"ua":"Mozilla/5.0 (Linux; Android 13; 22101316C Build/TP1A.220624.014; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/120.0.6099.230 Mobile Safari/537.36","w":1080},"ext":{"prebid":{"bidderparams":{"pubmatic":{"wrapper":{"profileid":1234}}}}},"id":"95d6643c-3da6-40a2-b9ca-12279393ffbf","imp":[{"banner":{"format":[{"w":728,"h":90},{"w":320,"h":50}],"api":[5,2],"w":700,"h":900},"clickbrowser":0,"displaymanager":"OpenBid_SDK","displaymanagerver":"1.4.0","ext":{"reward":1},"id":"imp176227948","secure":0,"tagid":"OpenWrapBidderBannerAdUnit"}],"regs":{"ext":{"gdpr":0}},"source":{"ext":{"omidpn":"PubMatic","omidpv":"1.2.11-Pubmatic"}},"user":{"data":[{"id":"1","name":"Publisher Passed","segment":[{"signal":"{\"id\":\"95d6643c-3da6-40a2-b9ca-12279393ffbf\",\"at\":1,\"tmax\":500,\"cur\":[\"USD\"],\"imp\":[{\"id\":\"imp176227948\",\"clickbrowser\":0,\"displaymanager\":\"PubMatic_OpenBid_SDK\",\"displaymanagerver\":\"1.4.0\",\"tagid\":\"/43743431/DMDemo\",\"secure\":0,\"banner\":{\"pos\":7,\"format\":[{\"w\":300,\"h\":250}],\"api\":[5,6,7]},\"instl\":1}],\"app\":{\"name\":\"OpenWrapperSample\",\"bundle\":\"com.pubmatic.openbid.app\",\"storeurl\":\"https://itunes.apple.com/us/app/pubmatic-sdk-app/id1175273098?appnexus_banner_fixedbid=1&fixedbid=1\",\"ver\":\"1.0\",\"publisher\":{\"id\":\"5890\"}},\"device\":{\"ext\":{\"atts\":0},\"geo\":{\"type\":1,\"lat\":37.421998333333335,\"lon\":-122.08400000000002},\"pxratio\":2.625,\"mccmnc\":\"310-260\",\"lmt\":0,\"ifa\":\"07c387f2-e030-428f-8336-42f682150759\",\"connectiontype\":6,\"carrier\":\"Android\",\"js\":1,\"ua\":\"Mozilla/5.0(Linux;Android9;AndroidSDKbuiltforx86Build/PSR1.180720.075;wv)AppleWebKit/537.36(KHTML,likeGecko)Version/4.0Chrome/69.0.3497.100MobileSafari/537.36\",\"make\":\"Google\",\"model\":\"AndroidSDKbuiltforx86\",\"os\":\"Android\",\"osv\":\"9\",\"h\":1794,\"w\":1080,\"language\":\"en-US\",\"devicetype\":4},\"source\":{\"ext\":{\"omidpn\":\"PubMatic\",\"omidpv\":\"1.2.11-Pubmatic\"}},\"user\":{},\"ext\":{\"wrapper\":{\"ssauction\":0,\"sumry_disable\":0,\"profileid\":58135,\"versionid\":1,\"clientconfig\":1}}}"}]}],"ext":{"consent":"CP2KIMAP2KIgAEPgABBYJGNX_H__bX9j-Xr3"}}}`), + }, + want: "1234", + }, + { + name: "profileID not present in request.ext.prebid.bidderparams.pubmatic.wrapper but present in request.app.id", + args: args{ + requestBody: []byte(`{"app":{"bundle":"com.pubmatic.openbid.app","name":"Sample","publisher":{"id":"156276"},"id":"13137","storeurl":"https://itunes.apple.com/us/app/pubmatic-sdk-app/id1175273098","ver":"1.0"},"at":1,"device":{"carrier":"MYTEL","connectiontype":2,"devicetype":4,"ext":{"atts":2},"geo":{"city":"Queens","country":"USA","dma":"501","ipservice":3,"lat":40.7429,"lon":-73.9392,"long":-73.9392,"metro":"501","region":"ny","type":2,"zip":"11101"},"h":2400,"hwv":"ruby","ifa":"497a10d6-c4dd-4e04-a986-c32b7180d462","ip":"38.158.207.171","js":1,"language":"en_US","make":"xiaomi","model":"22101316c","os":"android","osv":"13.0.0","ppi":440,"pxratio":2.75,"ua":"Mozilla/5.0 (Linux; Android 13; 22101316C Build/TP1A.220624.014; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/120.0.6099.230 Mobile Safari/537.36","w":1080},"ext":{"prebid":{"bidderparams":{"pubmatic":{"wrapper":{}}}}},"id":"95d6643c-3da6-40a2-b9ca-12279393ffbf","imp":[{"banner":{"format":[{"w":728,"h":90},{"w":320,"h":50}],"api":[5,2],"w":700,"h":900},"clickbrowser":0,"displaymanager":"OpenBid_SDK","displaymanagerver":"1.4.0","ext":{"reward":1},"id":"imp176227948","secure":0,"tagid":"OpenWrapBidderBannerAdUnit"}],"regs":{"ext":{"gdpr":0}},"source":{"ext":{"omidpn":"PubMatic","omidpv":"1.2.11-Pubmatic"}},"user":{"data":[{"id":"1","name":"Publisher Passed","segment":[{"signal":"{\"id\":\"95d6643c-3da6-40a2-b9ca-12279393ffbf\",\"at\":1,\"tmax\":500,\"cur\":[\"USD\"],\"imp\":[{\"id\":\"imp176227948\",\"clickbrowser\":0,\"displaymanager\":\"PubMatic_OpenBid_SDK\",\"displaymanagerver\":\"1.4.0\",\"tagid\":\"/43743431/DMDemo\",\"secure\":0,\"banner\":{\"pos\":7,\"format\":[{\"w\":300,\"h\":250}],\"api\":[5,6,7]},\"instl\":1}],\"app\":{\"name\":\"OpenWrapperSample\",\"bundle\":\"com.pubmatic.openbid.app\",\"storeurl\":\"https://itunes.apple.com/us/app/pubmatic-sdk-app/id1175273098?appnexus_banner_fixedbid=1&fixedbid=1\",\"ver\":\"1.0\",\"publisher\":{\"id\":\"5890\"}},\"device\":{\"ext\":{\"atts\":0},\"geo\":{\"type\":1,\"lat\":37.421998333333335,\"lon\":-122.08400000000002},\"pxratio\":2.625,\"mccmnc\":\"310-260\",\"lmt\":0,\"ifa\":\"07c387f2-e030-428f-8336-42f682150759\",\"connectiontype\":6,\"carrier\":\"Android\",\"js\":1,\"ua\":\"Mozilla/5.0(Linux;Android9;AndroidSDKbuiltforx86Build/PSR1.180720.075;wv)AppleWebKit/537.36(KHTML,likeGecko)Version/4.0Chrome/69.0.3497.100MobileSafari/537.36\",\"make\":\"Google\",\"model\":\"AndroidSDKbuiltforx86\",\"os\":\"Android\",\"osv\":\"9\",\"h\":1794,\"w\":1080,\"language\":\"en-US\",\"devicetype\":4},\"source\":{\"ext\":{\"omidpn\":\"PubMatic\",\"omidpv\":\"1.2.11-Pubmatic\"}},\"user\":{},\"ext\":{\"wrapper\":{\"ssauction\":0,\"sumry_disable\":0,\"profileid\":58135,\"versionid\":1,\"clientconfig\":1}}}"}]}],"ext":{"consent":"CP2KIMAP2KIgAEPgABBYJGNX_H__bX9j-Xr3"}}}`), + }, + want: "13137", + }, + { + name: "profileID present in both request.ext.prebid.bidderparams.pubmatic.wrapper and request.app.id give priority to wrapper", + args: args{ + requestBody: []byte(`{"app":{"bundle":"com.pubmatic.openbid.app","name":"Sample","publisher":{"id":"156276"},"id":"13137","storeurl":"https://itunes.apple.com/us/app/pubmatic-sdk-app/id1175273098","ver":"1.0"},"at":1,"device":{"carrier":"MYTEL","connectiontype":2,"devicetype":4,"ext":{"atts":2},"geo":{"city":"Queens","country":"USA","dma":"501","ipservice":3,"lat":40.7429,"lon":-73.9392,"long":-73.9392,"metro":"501","region":"ny","type":2,"zip":"11101"},"h":2400,"hwv":"ruby","ifa":"497a10d6-c4dd-4e04-a986-c32b7180d462","ip":"38.158.207.171","js":1,"language":"en_US","make":"xiaomi","model":"22101316c","os":"android","osv":"13.0.0","ppi":440,"pxratio":2.75,"ua":"Mozilla/5.0 (Linux; Android 13; 22101316C Build/TP1A.220624.014; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/120.0.6099.230 Mobile Safari/537.36","w":1080},"ext":{"prebid":{"bidderparams":{"pubmatic":{"wrapper":{"profileid":1234}}}}},"id":"95d6643c-3da6-40a2-b9ca-12279393ffbf","imp":[{"banner":{"format":[{"w":728,"h":90},{"w":320,"h":50}],"api":[5,2],"w":700,"h":900},"clickbrowser":0,"displaymanager":"OpenBid_SDK","displaymanagerver":"1.4.0","ext":{"reward":1},"id":"imp176227948","secure":0,"tagid":"OpenWrapBidderBannerAdUnit"}],"regs":{"ext":{"gdpr":0}},"source":{"ext":{"omidpn":"PubMatic","omidpv":"1.2.11-Pubmatic"}},"user":{"data":[{"id":"1","name":"Publisher Passed","segment":[{"signal":"{\"id\":\"95d6643c-3da6-40a2-b9ca-12279393ffbf\",\"at\":1,\"tmax\":500,\"cur\":[\"USD\"],\"imp\":[{\"id\":\"imp176227948\",\"clickbrowser\":0,\"displaymanager\":\"PubMatic_OpenBid_SDK\",\"displaymanagerver\":\"1.4.0\",\"tagid\":\"/43743431/DMDemo\",\"secure\":0,\"banner\":{\"pos\":7,\"format\":[{\"w\":300,\"h\":250}],\"api\":[5,6,7]},\"instl\":1}],\"app\":{\"name\":\"OpenWrapperSample\",\"bundle\":\"com.pubmatic.openbid.app\",\"storeurl\":\"https://itunes.apple.com/us/app/pubmatic-sdk-app/id1175273098?appnexus_banner_fixedbid=1&fixedbid=1\",\"ver\":\"1.0\",\"publisher\":{\"id\":\"5890\"}},\"device\":{\"ext\":{\"atts\":0},\"geo\":{\"type\":1,\"lat\":37.421998333333335,\"lon\":-122.08400000000002},\"pxratio\":2.625,\"mccmnc\":\"310-260\",\"lmt\":0,\"ifa\":\"07c387f2-e030-428f-8336-42f682150759\",\"connectiontype\":6,\"carrier\":\"Android\",\"js\":1,\"ua\":\"Mozilla/5.0(Linux;Android9;AndroidSDKbuiltforx86Build/PSR1.180720.075;wv)AppleWebKit/537.36(KHTML,likeGecko)Version/4.0Chrome/69.0.3497.100MobileSafari/537.36\",\"make\":\"Google\",\"model\":\"AndroidSDKbuiltforx86\",\"os\":\"Android\",\"osv\":\"9\",\"h\":1794,\"w\":1080,\"language\":\"en-US\",\"devicetype\":4},\"source\":{\"ext\":{\"omidpn\":\"PubMatic\",\"omidpv\":\"1.2.11-Pubmatic\"}},\"user\":{},\"ext\":{\"wrapper\":{\"ssauction\":0,\"sumry_disable\":0,\"profileid\":58135,\"versionid\":1,\"clientconfig\":1}}}"}]}],"ext":{"consent":"CP2KIMAP2KIgAEPgABBYJGNX_H__bX9j-Xr3"}}}`), + }, + want: "1234", + }, + { + name: "profileID not present in both request.ext.prebid.bidderparams.pubmatic.wrapper and request.app.id", + args: args{ + requestBody: []byte(`{"app":{"bundle":"com.pubmatic.openbid.app","name":"Sample","publisher":{"id":"156276"},"storeurl":"https://itunes.apple.com/us/app/pubmatic-sdk-app/id1175273098","ver":"1.0"},"at":1,"device":{"carrier":"MYTEL","connectiontype":2,"devicetype":4,"ext":{"atts":2},"geo":{"city":"Queens","country":"USA","dma":"501","ipservice":3,"lat":40.7429,"lon":-73.9392,"long":-73.9392,"metro":"501","region":"ny","type":2,"zip":"11101"},"h":2400,"hwv":"ruby","ifa":"497a10d6-c4dd-4e04-a986-c32b7180d462","ip":"38.158.207.171","js":1,"language":"en_US","make":"xiaomi","model":"22101316c","os":"android","osv":"13.0.0","ppi":440,"pxratio":2.75,"ua":"Mozilla/5.0 (Linux; Android 13; 22101316C Build/TP1A.220624.014; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/120.0.6099.230 Mobile Safari/537.36","w":1080},"ext":{"prebid":{"bidderparams":{"pubmatic":{"wrapper":{}}}}},"id":"95d6643c-3da6-40a2-b9ca-12279393ffbf","imp":[{"banner":{"format":[{"w":728,"h":90},{"w":320,"h":50}],"api":[5,2],"w":700,"h":900},"clickbrowser":0,"displaymanager":"OpenBid_SDK","displaymanagerver":"1.4.0","ext":{"reward":1},"id":"imp176227948","secure":0,"tagid":"OpenWrapBidderBannerAdUnit"}],"regs":{"ext":{"gdpr":0}},"source":{"ext":{"omidpn":"PubMatic","omidpv":"1.2.11-Pubmatic"}},"user":{"data":[{"id":"1","name":"Publisher Passed","segment":[{"signal":"{\"id\":\"95d6643c-3da6-40a2-b9ca-12279393ffbf\",\"at\":1,\"tmax\":500,\"cur\":[\"USD\"],\"imp\":[{\"id\":\"imp176227948\",\"clickbrowser\":0,\"displaymanager\":\"PubMatic_OpenBid_SDK\",\"displaymanagerver\":\"1.4.0\",\"tagid\":\"/43743431/DMDemo\",\"secure\":0,\"banner\":{\"pos\":7,\"format\":[{\"w\":300,\"h\":250}],\"api\":[5,6,7]},\"instl\":1}],\"app\":{\"name\":\"OpenWrapperSample\",\"bundle\":\"com.pubmatic.openbid.app\",\"storeurl\":\"https://itunes.apple.com/us/app/pubmatic-sdk-app/id1175273098?appnexus_banner_fixedbid=1&fixedbid=1\",\"ver\":\"1.0\",\"publisher\":{\"id\":\"5890\"}},\"device\":{\"ext\":{\"atts\":0},\"geo\":{\"type\":1,\"lat\":37.421998333333335,\"lon\":-122.08400000000002},\"pxratio\":2.625,\"mccmnc\":\"310-260\",\"lmt\":0,\"ifa\":\"07c387f2-e030-428f-8336-42f682150759\",\"connectiontype\":6,\"carrier\":\"Android\",\"js\":1,\"ua\":\"Mozilla/5.0(Linux;Android9;AndroidSDKbuiltforx86Build/PSR1.180720.075;wv)AppleWebKit/537.36(KHTML,likeGecko)Version/4.0Chrome/69.0.3497.100MobileSafari/537.36\",\"make\":\"Google\",\"model\":\"AndroidSDKbuiltforx86\",\"os\":\"Android\",\"osv\":\"9\",\"h\":1794,\"w\":1080,\"language\":\"en-US\",\"devicetype\":4},\"source\":{\"ext\":{\"omidpn\":\"PubMatic\",\"omidpv\":\"1.2.11-Pubmatic\"}},\"user\":{},\"ext\":{\"wrapper\":{\"ssauction\":0,\"sumry_disable\":0,\"profileid\":58135,\"versionid\":1,\"clientconfig\":1}}}"}]}],"ext":{"consent":"CP2KIMAP2KIgAEPgABBYJGNX_H__bX9j-Xr3"}}}`), + }, + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := getProfileID(tt.args.requestBody) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/modules/pubmatic/openwrap/bidderparams/pubmatic.go b/modules/pubmatic/openwrap/bidderparams/pubmatic.go index ee7f25908d5..88bd6a156d5 100644 --- a/modules/pubmatic/openwrap/bidderparams/pubmatic.go +++ b/modules/pubmatic/openwrap/bidderparams/pubmatic.go @@ -27,15 +27,6 @@ func PreparePubMaticParamsV25(rctx models.RequestCtx, cache cache.Cache, bidRequ slots, slotMap, slotMappingInfo, _ := getSlotMeta(rctx, cache, bidRequest, imp, impExt, partnerID) - if rctx.IsTestRequest > 0 { - if len(slots) > 0 { - extImpPubMatic.AdSlot = slots[0] - } - params, err := json.Marshal(extImpPubMatic) - return extImpPubMatic.AdSlot, "", false, params, err - } - - hash := "" var err error var matchedSlot, matchedPattern string var isRegexSlot, isRegexKGP bool @@ -45,28 +36,23 @@ func PreparePubMaticParamsV25(rctx models.RequestCtx, cache cache.Cache, bidRequ isRegexKGP = true } - // simple+regex key match - for _, slot := range slots { - matchedSlot, matchedPattern = GetMatchingSlot(rctx, cache, slot, slotMap, slotMappingInfo, isRegexKGP, partnerID) - if matchedSlot != "" { - extImpPubMatic.AdSlot = matchedSlot - - if matchedPattern != "" { - isRegexSlot = true - // imp.TagID = hash - // TODO: handle kgpv case sensitivity in hashvaluemap - if slotMappingInfo.HashValueMap != nil { - if v, ok := slotMappingInfo.HashValueMap[matchedPattern]; ok { - extImpPubMatic.AdSlot = v - imp.TagID = hash // TODO, make imp pointer. But do other bidders accept hash as TagID? - } - } - } + if rctx.IsTestRequest == 1 { + matchedSlot, matchedPattern, isRegexSlot = getMatchingSlotAndPattern(rctx, cache, slots, slotMap, slotMappingInfo, isRegexKGP, isRegexSlot, partnerID, &extImpPubMatic, imp) + params, err := json.Marshal(extImpPubMatic) + return matchedSlot, matchedPattern, isRegexSlot, params, err + } - break + if rctx.IsTestRequest > 0 { + if len(slots) > 0 { + extImpPubMatic.AdSlot = slots[0] } + params, err := json.Marshal(extImpPubMatic) + return extImpPubMatic.AdSlot, "", false, params, err } + // simple+regex key match + matchedSlot, matchedPattern, isRegexSlot = getMatchingSlotAndPattern(rctx, cache, slots, slotMap, slotMappingInfo, isRegexKGP, isRegexSlot, partnerID, &extImpPubMatic, imp) + if paramMap := getSlotMappings(matchedSlot, matchedPattern, slotMap); paramMap != nil { if matchedPattern == "" { // use alternate names defined in DB for this slot if selection is non-regex @@ -135,3 +121,29 @@ func getImpExtPubMaticKeyWords(impExt models.ImpExtension, bidderCode string) [] } return nil } + +func getMatchingSlotAndPattern(rctx models.RequestCtx, cache cache.Cache, slots []string, slotMap map[string]models.SlotMapping, slotMappingInfo models.SlotMappingInfo, isRegexKGP, isRegexSlot bool, partnerID int, extImpPubMatic *openrtb_ext.ExtImpPubmatic, imp openrtb2.Imp) (string, string, bool) { + + hash := "" + var matchedSlot, matchedPattern string + for _, slot := range slots { + matchedSlot, matchedPattern = GetMatchingSlot(rctx, cache, slot, slotMap, slotMappingInfo, isRegexKGP, partnerID) + if matchedSlot != "" { + extImpPubMatic.AdSlot = matchedSlot + + if matchedPattern != "" { + isRegexSlot = true + // imp.TagID = hash + // TODO: handle kgpv case sensitivity in hashvaluemap + if slotMappingInfo.HashValueMap != nil { + if v, ok := slotMappingInfo.HashValueMap[matchedPattern]; ok { + extImpPubMatic.AdSlot = v + imp.TagID = hash // TODO, make imp pointer. But do other bidders accept hash as TagID? + } + } + } + break + } + } + return matchedSlot, matchedPattern, isRegexSlot +} diff --git a/modules/pubmatic/openwrap/bidderparams/pubmatic_test.go b/modules/pubmatic/openwrap/bidderparams/pubmatic_test.go index d51b35b486d..1ec0335b4af 100644 --- a/modules/pubmatic/openwrap/bidderparams/pubmatic_test.go +++ b/modules/pubmatic/openwrap/bidderparams/pubmatic_test.go @@ -291,8 +291,15 @@ func TestPreparePubMaticParamsV25(t *testing.T) { }, setup: func() { mockCache.EXPECT().GetMappingsFromCacheV25(gomock.Any(), gomock.Any()).Return(map[string]models.SlotMapping{ - "test": { + "/test_adunit1234@div1@200x300": { PartnerId: 1, + AdapterId: 1, + SlotName: "/Test_Adunit1234@Div1@200x300", + SlotMappings: map[string]interface{}{ + "site": "12313", + "adtag": "45343", + "slotName": "/Test_Adunit1234@DIV1@200x300", + }, }, }) mockCache.EXPECT().GetSlotToHashValueMapFromCacheV25(gomock.Any(), gomock.Any()).Return(models.SlotMappingInfo{ @@ -891,7 +898,6 @@ func TestPreparePubMaticParamsV25(t *testing.T) { }, }, }) - mockCache.EXPECT().GetSlotToHashValueMapFromCacheV25(gomock.Any(), gomock.Any()).Return(models.SlotMappingInfo{ OrderedSlotList: []string{"random"}, HashValueMap: map[string]string{ @@ -958,7 +964,6 @@ func TestPreparePubMaticParamsV25(t *testing.T) { }, }, }) - mockCache.EXPECT().GetSlotToHashValueMapFromCacheV25(gomock.Any(), gomock.Any()).Return(models.SlotMappingInfo{ OrderedSlotList: []string{"random"}, HashValueMap: map[string]string{ @@ -1025,7 +1030,6 @@ func TestPreparePubMaticParamsV25(t *testing.T) { }, }, }) - mockCache.EXPECT().GetSlotToHashValueMapFromCacheV25(gomock.Any(), gomock.Any()).Return(models.SlotMappingInfo{ OrderedSlotList: []string{"random"}, HashValueMap: map[string]string{ @@ -1041,6 +1045,175 @@ func TestPreparePubMaticParamsV25(t *testing.T) { wantErr: false, }, }, + { + name: "For test value 1", + args: args{ + rctx: models.RequestCtx{ + IsTestRequest: 1, + PubID: 5890, + ProfileID: 123, + DisplayID: 1, + PartnerConfigMap: map[int]map[string]string{ + 1: { + models.PREBID_PARTNER_NAME: "pubmatic", + models.BidderCode: "pubmatic", + models.TIMEOUT: "200", + models.KEY_GEN_PATTERN: "_AU_@_DIV_@_W_x_H_", + models.SERVER_SIDE_FLAG: "1", + }, + }, + }, + cache: mockCache, + impExt: models.ImpExtension{ + Bidder: map[string]*models.BidderExtension{ + "pubmatic": { + KeyWords: []models.KeyVal{ + { + Key: "test_key1", + Values: []string{"test_value1", "test_value2"}, + }, + { + Key: "test_key2", + Values: []string{"test_value1", "test_value2"}, + }, + }, + }, + }, + Wrapper: &models.ExtImpWrapper{ + Div: "Div1", + }, + }, + imp: getTestImp("/Test_Adunit1234", true, false), + partnerID: 1, + }, + setup: func() { + mockCache.EXPECT().GetMappingsFromCacheV25(gomock.Any(), gomock.Any()).Return(map[string]models.SlotMapping{ + "/test_adunit12345@div1@200x300": { + PartnerId: 1, + AdapterId: 1, + SlotName: "/Test_Adunit1234@Div1@200x300", + SlotMappings: map[string]interface{}{ + "site": "12313", + "adtag": "45343", + "slotName": "/Test_Adunit1234@DIV1@200x300", + }, + }, + }) + mockCache.EXPECT().GetSlotToHashValueMapFromCacheV25(gomock.Any(), gomock.Any()).Return(models.SlotMappingInfo{ + OrderedSlotList: []string{"*", ".*@.*@.*"}, + HashValueMap: map[string]string{ + ".*@.*@.*": "2aa34b52a9e941c1594af7565e599c8d", // Code should match the given slot name with this regex + }, + }) + mockCache.EXPECT().Get("psregex_5890_123_1_1_/Test_Adunit1234@Div1@200x300").Return(nil, false) + mockCache.EXPECT().Set("psregex_5890_123_1_1_/Test_Adunit1234@Div1@200x300", regexSlotEntry{SlotName: "/Test_Adunit1234@Div1@200x300", RegexPattern: ".*@.*@.*"}).Times(1) + }, + want: want{ + matchedSlot: "/Test_Adunit1234@Div1@200x300", + matchedPattern: ".*@.*@.*", + isRegexSlot: true, + params: []byte(`{"publisherId":"5890","adSlot":"2aa34b52a9e941c1594af7565e599c8d","wrapper":{"version":1,"profile":123},"keywords":[{"key":"test_key1","value":["test_value1","test_value2"]},{"key":"test_key2","value":["test_value1","test_value2"]}]}`), + wantErr: false, + }, + }, + { + name: "For_test_value_2_with_regex", + args: args{ + rctx: models.RequestCtx{ + IsTestRequest: 2, + PubID: 5890, + ProfileID: 123, + DisplayID: 1, + PartnerConfigMap: map[int]map[string]string{ + 1: { + models.PREBID_PARTNER_NAME: "pubmatic", + models.BidderCode: "pubmatic", + models.TIMEOUT: "200", + models.KEY_GEN_PATTERN: "_AU_@_DIV_@_W_x_H_", + models.SERVER_SIDE_FLAG: "1", + }, + }, + }, + cache: mockCache, + impExt: models.ImpExtension{ + Bidder: map[string]*models.BidderExtension{ + "pubmatic": { + KeyWords: []models.KeyVal{ + { + Key: "test_key1", + Values: []string{"test_value1", "test_value2"}, + }, + { + Key: "test_key2", + Values: []string{"test_value1", "test_value2"}, + }, + }, + }, + }, + Wrapper: &models.ExtImpWrapper{ + Div: "Div1", + }, + }, + imp: getTestImp("/Test_Adunit1234", true, false), + partnerID: 1, + }, + want: want{ + matchedSlot: "/Test_Adunit1234@Div1@200x300", + matchedPattern: "", + isRegexSlot: false, + params: []byte(`{"publisherId":"5890","adSlot":"/Test_Adunit1234@Div1@200x300","wrapper":{"version":1,"profile":123},"keywords":[{"key":"test_key1","value":["test_value1","test_value2"]},{"key":"test_key2","value":["test_value1","test_value2"]}]}`), + wantErr: false, + }, + }, + { + name: "For_test_value_2_with_non_regex", + args: args{ + rctx: models.RequestCtx{ + IsTestRequest: 2, + PubID: 5890, + ProfileID: 123, + DisplayID: 1, + PartnerConfigMap: map[int]map[string]string{ + 1: { + models.PREBID_PARTNER_NAME: "pubmatic", + models.BidderCode: "pubmatic", + models.TIMEOUT: "200", + models.KEY_GEN_PATTERN: "_AU_@_W_x_H_", + models.SERVER_SIDE_FLAG: "1", + }, + }, + }, + cache: mockCache, + impExt: models.ImpExtension{ + Bidder: map[string]*models.BidderExtension{ + "pubmatic": { + KeyWords: []models.KeyVal{ + { + Key: "test_key1", + Values: []string{"test_value1", "test_value2"}, + }, + { + Key: "test_key2", + Values: []string{"test_value1", "test_value2"}, + }, + }, + }, + }, + Wrapper: &models.ExtImpWrapper{ + Div: "Div1", + }, + }, + imp: getTestImp("/Test_Adunit1234", true, false), + partnerID: 1, + }, + want: want{ + matchedSlot: "/Test_Adunit1234@200x300", + matchedPattern: "", + isRegexSlot: false, + params: []byte(`{"publisherId":"5890","adSlot":"/Test_Adunit1234@200x300","wrapper":{"version":1,"profile":123},"keywords":[{"key":"test_key1","value":["test_value1","test_value2"]},{"key":"test_key2","value":["test_value1","test_value2"]}]}`), + wantErr: false, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1071,3 +1244,118 @@ func createSlotMapping(slotName string, mappings map[string]interface{}) models. OrderID: 0, } } + +func TestGetMatchingSlotAndPattern(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockCache := mock_cache.NewMockCache(ctrl) + + type args struct { + rctx models.RequestCtx + cache cache.Cache + slots []string + slotMap map[string]models.SlotMapping + slotMappingInfo models.SlotMappingInfo + isRegexKGP bool + isRegexSlot bool + partnerID int + extImpPubMatic *openrtb_ext.ExtImpPubmatic + imp openrtb2.Imp + } + type want struct { + matchedSlot string + matchedPattern string + isRegexSlot bool + } + tests := []struct { + name string + args args + setup func() + want want + }{ + { + name: "found_matced_regex_slot", + args: args{ + rctx: models.RequestCtx{ + PubID: 5890, + ProfileID: 123, + DisplayID: 1, + }, + partnerID: 1, + slots: []string{"AU123@Div1@728x90"}, + slotMappingInfo: models.SlotMappingInfo{ + OrderedSlotList: []string{"*", ".*@.*@.*"}, + HashValueMap: map[string]string{ + ".*@.*@.*": "2aa34b52a9e941c1594af7565e599c8d", // Code should match the given slot name with this regex + }, + }, + slotMap: map[string]models.SlotMapping{ + "AU123@Div1@728x90": { + SlotMappings: map[string]interface{}{ + "site": "123123", + "adtag": "45343", + }, + }, + }, + cache: mockCache, + isRegexKGP: true, + isRegexSlot: false, + extImpPubMatic: &openrtb_ext.ExtImpPubmatic{}, + imp: openrtb2.Imp{}, + }, + setup: func() { + mockCache.EXPECT().Get("psregex_5890_123_1_1_AU123@Div1@728x90").Return(nil, false) + mockCache.EXPECT().Set("psregex_5890_123_1_1_AU123@Div1@728x90", regexSlotEntry{SlotName: "AU123@Div1@728x90", RegexPattern: ".*@.*@.*"}).Times(1) + }, + want: want{ + matchedSlot: "AU123@Div1@728x90", + matchedPattern: ".*@.*@.*", + isRegexSlot: true, + }, + }, + { + name: "not_found_matced_regex_slot", + args: args{ + rctx: models.RequestCtx{ + PubID: 5890, + ProfileID: 123, + DisplayID: 1, + }, + partnerID: 1, + slots: []string{"AU123@Div1@728x90"}, + slotMap: map[string]models.SlotMapping{ + "AU123@Div1@728x90": { + SlotMappings: map[string]interface{}{ + "site": "123123", + "adtag": "45343", + }, + }, + }, + cache: mockCache, + isRegexKGP: true, + isRegexSlot: false, + extImpPubMatic: &openrtb_ext.ExtImpPubmatic{}, + imp: openrtb2.Imp{}, + }, + setup: func() { + mockCache.EXPECT().Get("psregex_5890_123_1_1_AU123@Div1@728x90").Return(nil, false) + }, + want: want{ + matchedSlot: "", + matchedPattern: "", + isRegexSlot: false, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.setup != nil { + tt.setup() + } + matchedSlot, matchedPattern, isRegexSlot := getMatchingSlotAndPattern(tt.args.rctx, tt.args.cache, tt.args.slots, tt.args.slotMap, tt.args.slotMappingInfo, tt.args.isRegexKGP, tt.args.isRegexSlot, tt.args.partnerID, tt.args.extImpPubMatic, tt.args.imp) + assert.Equal(t, tt.want.matchedSlot, matchedSlot) + assert.Equal(t, tt.want.matchedPattern, matchedPattern) + assert.Equal(t, tt.want.isRegexSlot, isRegexSlot) + }) + } +} diff --git a/modules/pubmatic/openwrap/entrypointhook.go b/modules/pubmatic/openwrap/entrypointhook.go index 1def350f515..e71ee03c77a 100644 --- a/modules/pubmatic/openwrap/entrypointhook.go +++ b/modules/pubmatic/openwrap/entrypointhook.go @@ -201,7 +201,17 @@ func GetRequestWrapper(payload hookstage.EntrypointPayload, result hookstage.Hoo fallthrough case models.EndpointVideo, models.EndpointORTB, models.EndpointVAST, models.EndpointJson: requestExtWrapper, err = models.GetRequestExtWrapper(payload.Body, "ext", "wrapper") - case models.EndpointWebS2S, models.EndpointAppLovinMax: + case models.EndpointAppLovinMax: + requestExtWrapper, err = models.GetRequestExtWrapper(payload.Body) + if requestExtWrapper.ProfileId == 0 { + profileIDStr := getProfileID(payload.Body) + if profileIDStr != "" { + if ProfileId, newErr := strconv.Atoi(profileIDStr); newErr == nil { + requestExtWrapper.ProfileId = ProfileId + } + } + } + case models.EndpointWebS2S: fallthrough default: requestExtWrapper, err = models.GetRequestExtWrapper(payload.Body) diff --git a/modules/pubmatic/openwrap/entrypointhook_test.go b/modules/pubmatic/openwrap/entrypointhook_test.go index b46fb7ddfae..b1b753297f4 100644 --- a/modules/pubmatic/openwrap/entrypointhook_test.go +++ b/modules/pubmatic/openwrap/entrypointhook_test.go @@ -674,6 +674,34 @@ func TestGetRequestWrapper(t *testing.T) { VersionId: 1, }, }, + { + name: "EndpointAppLovinMax", + args: args{ + payload: hookstage.EntrypointPayload{ + Body: []byte(`{"app":{"bundle":"com.pubmatic.openbid.app","name":"Sample","publisher":{"id":"156276"},"id":"13137","storeurl":"https://itunes.apple.com/us/app/pubmatic-sdk-app/id1175273098","ver":"1.0"},"at":1,"device":{"carrier":"MYTEL","connectiontype":2,"devicetype":4,"ext":{"atts":2},"geo":{"city":"Queens","country":"USA","dma":"501","ipservice":3,"lat":40.7429,"lon":-73.9392,"long":-73.9392,"metro":"501","region":"ny","type":2,"zip":"11101"},"h":2400,"hwv":"ruby","ifa":"497a10d6-c4dd-4e04-a986-c32b7180d462","ip":"38.158.207.171","js":1,"language":"en_US","make":"xiaomi","model":"22101316c","os":"android","osv":"13.0.0","ppi":440,"pxratio":2.75,"ua":"Mozilla/5.0 (Linux; Android 13; 22101316C Build/TP1A.220624.014; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/120.0.6099.230 Mobile Safari/537.36","w":1080},"ext":{"prebid":{"bidderparams":{"pubmatic":{"wrapper":{}}}}},"id":"95d6643c-3da6-40a2-b9ca-12279393ffbf","imp":[{"banner":{"format":[{"w":728,"h":90},{"w":320,"h":50}],"api":[5,2],"w":700,"h":900},"clickbrowser":0,"displaymanager":"OpenBid_SDK","displaymanagerver":"1.4.0","ext":{"reward":1},"id":"imp176227948","secure":0,"tagid":"OpenWrapBidderBannerAdUnit"}],"regs":{"ext":{"gdpr":0}},"source":{"ext":{"omidpn":"PubMatic","omidpv":"1.2.11-Pubmatic"}},"user":{"data":[{"id":"1","name":"Publisher Passed","segment":[{"signal":"{\"id\":\"95d6643c-3da6-40a2-b9ca-12279393ffbf\",\"at\":1,\"tmax\":500,\"cur\":[\"USD\"],\"imp\":[{\"id\":\"imp176227948\",\"clickbrowser\":0,\"displaymanager\":\"PubMatic_OpenBid_SDK\",\"displaymanagerver\":\"1.4.0\",\"tagid\":\"/43743431/DMDemo\",\"secure\":0,\"banner\":{\"pos\":7,\"format\":[{\"w\":300,\"h\":250}],\"api\":[5,6,7]},\"instl\":1}],\"app\":{\"name\":\"OpenWrapperSample\",\"bundle\":\"com.pubmatic.openbid.app\",\"storeurl\":\"https://itunes.apple.com/us/app/pubmatic-sdk-app/id1175273098?appnexus_banner_fixedbid=1&fixedbid=1\",\"ver\":\"1.0\",\"publisher\":{\"id\":\"5890\"}},\"device\":{\"ext\":{\"atts\":0},\"geo\":{\"type\":1,\"lat\":37.421998333333335,\"lon\":-122.08400000000002},\"pxratio\":2.625,\"mccmnc\":\"310-260\",\"lmt\":0,\"ifa\":\"07c387f2-e030-428f-8336-42f682150759\",\"connectiontype\":6,\"carrier\":\"Android\",\"js\":1,\"ua\":\"Mozilla/5.0(Linux;Android9;AndroidSDKbuiltforx86Build/PSR1.180720.075;wv)AppleWebKit/537.36(KHTML,likeGecko)Version/4.0Chrome/69.0.3497.100MobileSafari/537.36\",\"make\":\"Google\",\"model\":\"AndroidSDKbuiltforx86\",\"os\":\"Android\",\"osv\":\"9\",\"h\":1794,\"w\":1080,\"language\":\"en-US\",\"devicetype\":4},\"source\":{\"ext\":{\"omidpn\":\"PubMatic\",\"omidpv\":\"1.2.11-Pubmatic\"}},\"user\":{},\"ext\":{\"wrapper\":{\"ssauction\":0,\"sumry_disable\":0,\"profileid\":58135,\"versionid\":1,\"clientconfig\":1}}}"}]}],"ext":{"consent":"CP2KIMAP2KIgAEPgABBYJGNX_H__bX9j-Xr3"}}}`), + }, + result: hookstage.HookResult[hookstage.EntrypointPayload]{}, + endpoint: models.EndpointAppLovinMax, + }, + want: models.RequestExtWrapper{ + SSAuctionFlag: -1, + ProfileId: 13137, + }, + }, + { + name: "EndpointAppLovinMax give preferance to profileid in wrapper", + args: args{ + payload: hookstage.EntrypointPayload{ + Body: []byte(`{"app":{"bundle":"com.pubmatic.openbid.app","name":"Sample","publisher":{"id":"156276"},"id":"1234","storeurl":"https://itunes.apple.com/us/app/pubmatic-sdk-app/id1175273098","ver":"1.0"},"at":1,"device":{"carrier":"MYTEL","connectiontype":2,"devicetype":4,"ext":{"atts":2},"geo":{"city":"Queens","country":"USA","dma":"501","ipservice":3,"lat":40.7429,"lon":-73.9392,"long":-73.9392,"metro":"501","region":"ny","type":2,"zip":"11101"},"h":2400,"hwv":"ruby","ifa":"497a10d6-c4dd-4e04-a986-c32b7180d462","ip":"38.158.207.171","js":1,"language":"en_US","make":"xiaomi","model":"22101316c","os":"android","osv":"13.0.0","ppi":440,"pxratio":2.75,"ua":"Mozilla/5.0 (Linux; Android 13; 22101316C Build/TP1A.220624.014; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/120.0.6099.230 Mobile Safari/537.36","w":1080},"ext":{"prebid":{"bidderparams":{"pubmatic":{"wrapper":{"profileid":5890}}}}},"id":"95d6643c-3da6-40a2-b9ca-12279393ffbf","imp":[{"banner":{"format":[{"w":728,"h":90},{"w":320,"h":50}],"api":[5,2],"w":700,"h":900},"clickbrowser":0,"displaymanager":"OpenBid_SDK","displaymanagerver":"1.4.0","ext":{"reward":1},"id":"imp176227948","secure":0,"tagid":"OpenWrapBidderBannerAdUnit"}],"regs":{"ext":{"gdpr":0}},"source":{"ext":{"omidpn":"PubMatic","omidpv":"1.2.11-Pubmatic"}},"user":{"data":[{"id":"1","name":"Publisher Passed","segment":[{"signal":"{\"id\":\"95d6643c-3da6-40a2-b9ca-12279393ffbf\",\"at\":1,\"tmax\":500,\"cur\":[\"USD\"],\"imp\":[{\"id\":\"imp176227948\",\"clickbrowser\":0,\"displaymanager\":\"PubMatic_OpenBid_SDK\",\"displaymanagerver\":\"1.4.0\",\"tagid\":\"/43743431/DMDemo\",\"secure\":0,\"banner\":{\"pos\":7,\"format\":[{\"w\":300,\"h\":250}],\"api\":[5,6,7]},\"instl\":1}],\"app\":{\"name\":\"OpenWrapperSample\",\"bundle\":\"com.pubmatic.openbid.app\",\"storeurl\":\"https://itunes.apple.com/us/app/pubmatic-sdk-app/id1175273098?appnexus_banner_fixedbid=1&fixedbid=1\",\"ver\":\"1.0\",\"publisher\":{\"id\":\"5890\"}},\"device\":{\"ext\":{\"atts\":0},\"geo\":{\"type\":1,\"lat\":37.421998333333335,\"lon\":-122.08400000000002},\"pxratio\":2.625,\"mccmnc\":\"310-260\",\"lmt\":0,\"ifa\":\"07c387f2-e030-428f-8336-42f682150759\",\"connectiontype\":6,\"carrier\":\"Android\",\"js\":1,\"ua\":\"Mozilla/5.0(Linux;Android9;AndroidSDKbuiltforx86Build/PSR1.180720.075;wv)AppleWebKit/537.36(KHTML,likeGecko)Version/4.0Chrome/69.0.3497.100MobileSafari/537.36\",\"make\":\"Google\",\"model\":\"AndroidSDKbuiltforx86\",\"os\":\"Android\",\"osv\":\"9\",\"h\":1794,\"w\":1080,\"language\":\"en-US\",\"devicetype\":4},\"source\":{\"ext\":{\"omidpn\":\"PubMatic\",\"omidpv\":\"1.2.11-Pubmatic\"}},\"user\":{},\"ext\":{\"wrapper\":{\"ssauction\":0,\"sumry_disable\":0,\"profileid\":58135,\"versionid\":1,\"clientconfig\":1}}}"}]}],"ext":{"consent":"CP2KIMAP2KIgAEPgABBYJGNX_H__bX9j-Xr3"}}}`), + }, + result: hookstage.HookResult[hookstage.EntrypointPayload]{}, + endpoint: models.EndpointAppLovinMax, + }, + want: models.RequestExtWrapper{ + SSAuctionFlag: -1, + ProfileId: 5890, + }, + }, //TODO: Add test cases for other endpoints after migration(AMP, Video, VAST, JSON, InappVideo) } for _, tt := range tests { diff --git a/modules/pubmatic/openwrap/models/tracking.go b/modules/pubmatic/openwrap/models/tracking.go index 367171de04c..0c202f5d613 100644 --- a/modules/pubmatic/openwrap/models/tracking.go +++ b/modules/pubmatic/openwrap/models/tracking.go @@ -37,6 +37,7 @@ const ( TRKAdPodExist = "aps" TRKFloorType = "ft" TRKFloorModelVersion = "fmv" + TRKFloorProvider = "fp" TRKFloorSkippedFlag = "fskp" TRKFloorSource = "fsrc" TRKFloorValue = "fv" diff --git a/modules/pubmatic/openwrap/models/utils_test.go b/modules/pubmatic/openwrap/models/utils_test.go index bcd64e5d9f3..5e878cfaf19 100644 --- a/modules/pubmatic/openwrap/models/utils_test.go +++ b/modules/pubmatic/openwrap/models/utils_test.go @@ -1116,7 +1116,7 @@ func Test_getFloorsDetails(t *testing.T) { ModelVersion: "version 1", }, }, - FloorProvider: "provider", + FloorProvider: "providerA", }, PriceFloorLocation: openrtb_ext.FetchLocation, Enforcement: &openrtb_ext.PriceFloorEnforcement{ @@ -1131,7 +1131,7 @@ func Test_getFloorsDetails(t *testing.T) { FloorType: HardFloor, FloorSource: ptrutil.ToPtr(2), FloorModelVersion: "version 1", - FloorProvider: "provider", + FloorProvider: "providerA", }, }, { @@ -1148,7 +1148,7 @@ func Test_getFloorsDetails(t *testing.T) { ModelVersion: "version 1", }, }, - FloorProvider: "provider", + FloorProvider: "providerB", }, PriceFloorLocation: openrtb_ext.FetchLocation, Enforcement: &openrtb_ext.PriceFloorEnforcement{ @@ -1163,7 +1163,7 @@ func Test_getFloorsDetails(t *testing.T) { FloorType: HardFloor, FloorSource: ptrutil.ToPtr(2), FloorModelVersion: "version 1", - FloorProvider: "provider", + FloorProvider: "providerB", }, }, { @@ -1180,7 +1180,7 @@ func Test_getFloorsDetails(t *testing.T) { ModelVersion: "version 1", }, }, - FloorProvider: "provider", + FloorProvider: "providerC", }, PriceFloorLocation: openrtb_ext.FetchLocation, Enforcement: &openrtb_ext.PriceFloorEnforcement{ @@ -1195,7 +1195,7 @@ func Test_getFloorsDetails(t *testing.T) { FloorType: HardFloor, FloorSource: ptrutil.ToPtr(2), FloorModelVersion: "version 1", - FloorProvider: "provider", + FloorProvider: "providerC", FloorFetchStatus: ptrutil.ToPtr(2), }, }, diff --git a/modules/pubmatic/openwrap/tracker/create.go b/modules/pubmatic/openwrap/tracker/create.go index b5524bee716..5c1410ff3b7 100644 --- a/modules/pubmatic/openwrap/tracker/create.go +++ b/modules/pubmatic/openwrap/tracker/create.go @@ -270,6 +270,10 @@ func constructTrackerURL(rctx models.RequestCtx, tracker models.Tracker) string if len(tracker.FloorModelVersion) > 0 { v.Set(models.TRKFloorModelVersion, tracker.FloorModelVersion) } + if len(tracker.LoggerData.FloorProvider) > 0 { + v.Set(models.TRKFloorProvider, tracker.LoggerData.FloorProvider) + } + if tracker.FloorSource != nil { v.Set(models.TRKFloorSource, strconv.Itoa(*tracker.FloorSource)) } diff --git a/modules/pubmatic/openwrap/tracker/create_test.go b/modules/pubmatic/openwrap/tracker/create_test.go index 86384dc23d6..e49f4865bc9 100644 --- a/modules/pubmatic/openwrap/tracker/create_test.go +++ b/modules/pubmatic/openwrap/tracker/create_test.go @@ -371,6 +371,9 @@ func TestConstructTrackerURL(t *testing.T) { Secure: 1, SSAI: "mediatailor", CustomDimensions: "traffic=media;age=23", + LoggerData: models.LoggerData{ + FloorProvider: "PM", + }, PartnerInfo: models.Partner{ PartnerID: "AppNexus", BidderCode: "AppNexus1", @@ -390,7 +393,7 @@ func TestConstructTrackerURL(t *testing.T) { }, }, }, - want: "https://t.pubmatic.com/wt?adv=fb.com&af=banner&aps=0&au=adunit&bc=AppNexus1&bidid=6521&cds=traffic=media;age=23&di=420&dur=10&eg=4.3&en=2.5&fmv=test version&frv=2&fskp=0&fsrc=1&ft=1&fv=4.4&iid=98765&kgpv=adunit@300x250&orig=www.publisher.com&origbidid=6521&pdvid=1&pid=123&plt=1&pn=AppNexus&psz=300x250&pubid=12345&purl=www.abc.com&rwrd=1&sl=1&slot=1234_1234&ss=1&ssai=mediatailor&tgid=1&tst=0", + want: "https://t.pubmatic.com/wt?adv=fb.com&af=banner&aps=0&au=adunit&bc=AppNexus1&bidid=6521&cds=traffic=media;age=23&di=420&dur=10&eg=4.3&en=2.5&fmv=test version&fp=PM&frv=2&fskp=0&fsrc=1&ft=1&fv=4.4&iid=98765&kgpv=adunit@300x250&orig=www.publisher.com&origbidid=6521&pdvid=1&pid=123&plt=1&pn=AppNexus&psz=300x250&pubid=12345&purl=www.abc.com&rwrd=1&sl=1&slot=1234_1234&ss=1&ssai=mediatailor&tgid=1&tst=0", }, { name: "all_details_with_secure_enable_in_tracker", diff --git a/openrtb_ext/openwrap.go b/openrtb_ext/openwrap.go index 2b37f513953..5371c78cf44 100644 --- a/openrtb_ext/openwrap.go +++ b/openrtb_ext/openwrap.go @@ -79,6 +79,13 @@ type ExtRequestAdPod struct { AdvertiserExclusionWindow *int `json:"excladvwindow,omitempty"` //Duration in minute between pods where exclusive advertiser rule needs to be applied VideoAdDuration []int `json:"videoadduration,omitempty"` //Range of ad durations allowed in the response VideoAdDurationMatching OWVideoAdDurationMatchingPolicy `json:"videoaddurationmatching,omitempty"` //Flag indicating exact ad duration requirement. (default)empty/exact/round. + Exclusion *AdpodExclusion `json:"exclusion,omitempty"` //Exclusion parameters +} + +// AdpodExclusion holds AdPod specific exclusion parameters +type AdpodExclusion struct { + IABCategory []string `json:"iabcategory,omitempty"` + AdvertiserDomain []string `json:"advertiserdomain,omitempty"` } // VideoAdPod holds Video AdPod specific extension parameters at impression level diff --git a/static/bidder-info/owortb_testbidder.yaml b/static/bidder-info/owortb_testbidder.yaml index 0ca43c5ca99..376d554477e 100644 --- a/static/bidder-info/owortb_testbidder.yaml +++ b/static/bidder-info/owortb_testbidder.yaml @@ -9,4 +9,4 @@ capabilities: - video site: mediaTypes: - - video + - video \ No newline at end of file diff --git a/static/bidder-response-params/owortb_testbidder.json b/static/bidder-response-params/owortb_testbidder.json index 236c0323834..b3e1eb8d66c 100644 --- a/static/bidder-response-params/owortb_testbidder.json +++ b/static/bidder-response-params/owortb_testbidder.json @@ -4,10 +4,125 @@ "description": "A schema which validates params accepted by the testbidder (oRTB Integration)", "type": "object", "properties": { - "bidtype": { + "fledgeAuctionConfig": { + "type": "object", + "description": "Specifies fledge auction configurations", + "location": "ext.fledge" + }, + "bidType": { "type": "string", - "description": "bidtype", - "location": "seat.#.bid.#.ext.bidtype" + "description": "type of the bid. banner, video, audio and native are the only supported values.", + "location": "seatbid.#.bid.#.ext.bidtype" + }, + "bidDealPriority": { + "type": "integer", + "description": "priority of the deal bid", + "location": "seatbid.#.bid.#.dp" + }, + "bidVideo": { + "type": "object", + "description": "Specifies primary category and duration of the video bid", + "location": "seatbid.#.bid.#.ext.bidvideo" + }, + "bidVideoDuration": { + "type": "integer", + "description": "video duration of the bid", + "location": "seatbid.#.bid.#.ext.video.duration" + }, + "bidVideoPrimaryCategory": { + "type": "string", + "description": "primary IAB category of the bid", + "location": "seatbid.#.bid.#.ext.bidcategory.0" + }, + "bidMeta": { + "type": "object", + "description": "meta information of the bid", + "location": "seatbid.#.bid.#.ext.metaobject" + }, + "bidMetaAdvertiserDomains": { + "type": "string", + "description": "Domains for the landing page(s) aligning with the OpenRTB adomain field", + "location": "seatbid.#.bid.#.meta.domains" + }, + "bidMetaAdvertiserId": { + "type": "integer", + "description": "Bidder-specific advertiser id", + "location": "seatbid.#.bid.#.ext.advID" + }, + "bidMetaAdvertiserName" : { + "type": "string", + "description": "Bidder-specific advertiser name", + "location": "seatbid.#.bid.#.ext.advname" + }, + "bidMetaAgencyId" : { + "type": "integer", + "description": "Bidder-specific agency id", + "location": "seatbid.#.bid.#.ext.agency.id" + }, + "bidMetaAgencyName" : { + "type": "string", + "description": "Bidder-specific agency name", + "location": "seatbid.#.bid.#.ext.agency.name" + }, + "bidMetaBrandId": { + "type": "integer", + "description": "Bidder-specific brand id for advertisers with multiple brands", + "location": "seatbid.#.bid.#.ext.brandid" + }, + "bidMetaDchain": { + "type": "string", + "description": "Demand chain object", + "location": "seatbid.#.bid.#.ext.dchain" + }, + "bidMetaDemandSource": { + "type": "string", + "description": "Bidder-specific demand source", + "location": "seatbid.#.bid.#.ext.demand.source" + }, + "bidMetaMediaType": { + "type": "string", + "description": "media type of bid, either banner, audio, video, or native", + "location": "seatbid.#.bid.#.ext.bidtype" + }, + "bidMetaNetworkId": { + "type": "integer", + "description": "Bidder-specific network/DSP id", + "location": "seatbid.#.bid.#.ext.networkID" + }, + "bidMetaNetworkName": { + "type": "string", + "description": "Bidder-specific network/DSP name", + "location": "seatbid.#.bid.#.ext.networkName" + }, + "bidMetaPrimaryCatId": { + "type": "string", + "description": "Primary IAB category id", + "location": "seatbid.#.bid.#.cat.0" + }, + "bidMetaRendererName": { + "type": "string", + "description": "Name of the desired renderer for the creative", + "location": "seatbid.#.bid.#.renderer.name" + }, + "bidMetaRendererVersion": { + "type": "string", + "description": "Version of the desired renderer for the creative", + "location": "seatbid.#.bid.#.ext.renderer.version" + }, + "bidMetaRendererData": { + "type": "string", + "description": "Data of the custom renderer", + "location": "seatbid.#.bid.#.ext.renderer.data" + }, + "bidMetaRendererUrl": { + "type": "string", + "description": "Dynamic renderer URL for use in outstream rendering", + "location": "seatbid.#.bid.#.ext.renderer.url" + }, + "bidMetaSecondaryCatIds": { + "type": "array", + "description": "Secondary IAB category ids", + "location": "seatbid.#.bid.#.ext.meta.categories" } } } \ No newline at end of file diff --git a/static/bidder-response-params/owortb_testbidder_multi.json b/static/bidder-response-params/owortb_testbidder_multi.json new file mode 100644 index 00000000000..18984cd09d1 --- /dev/null +++ b/static/bidder-response-params/owortb_testbidder_multi.json @@ -0,0 +1,73 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "testbidder (oRTB Integration) Adapter Params", + "description": "A schema which validates params accepted by the testbidder (oRTB Integration)", + "type": "object", + "properties": { + "fledgeAuctionConfig": { + "type": "object", + "description": "Specifies fledge auction configurations", + "location": "ext.Fledge.config" + }, + "bidType": { + "type": "string", + "description": "type of the bid. banner, video, audio and native are the only supported values.", + "location": "seatbid.#.ext.bidtype" + }, + "bidDealPriority": { + "type": "integer", + "description": "priority of the deal bid", + "location": "seatbid.#.ext.deal" + }, + "bidVideoDuration": { + "type": "integer", + "description": "video duration of the bid", + "location": "seatbid.#.bid.#.ext.video.duration" + }, + "bidMetaAdvertiserDomains": { + "type": "string", + "description": "Domains for the landing page(s) aligning with the OpenRTB adomain field", + "location": "seatbid.#.bid.#.ext.advertiser.domains" + }, + "bidMetaAdvertiserId": { + "type": "integer", + "description": "Bidder-specific advertiser id", + "location": "seatbid.#.bid.#.ext.advertiser.id" + }, + "bidMetaAdvertiserName" : { + "type": "string", + "description": "Bidder-specific advertiser name", + "location": "seatbid.#.bid.#.ext.advertiser.name" + }, + "bidMetaBrandId": { + "type": "integer", + "description": "Bidder-specific brand id for advertisers with multiple brands", + "location": "seatbid.#.bid.#.ext.brandid" + }, + "bidMetaBrandName": { + "type": "integer", + "description": "Bidder-specific brand id for advertisers with multiple brands", + "location": "seatbid.#.bid.#.ext.brandName" + }, + "bidMetaDchain": { + "type": "string", + "description": "Demand chain object", + "location": "seatbid.#.bid.#.ext.dchain" + }, + "bidMetaPrimaryCatId": { + "type": "string", + "description": "Primary IAB category id", + "location": "seatbid.#.bid.#.ext.cat" + }, + "bidMetaRendererName": { + "type": "string", + "description": "Name of the desired renderer for the creative", + "location": "seatbid.#.bid.#.ext.renderer.name" + }, + "bidMetaSecondaryCatIds": { + "type": "array", + "description": "Secondary IAB category ids", + "location": "seatbid.#.bid.#.ext.categories" + } + } + } \ No newline at end of file