diff --git a/adapters/bidder.go b/adapters/bidder.go index b659f969f71..c18dde7c120 100644 --- a/adapters/bidder.go +++ b/adapters/bidder.go @@ -156,8 +156,6 @@ type ExtraRequestInfo struct { PbsEntryPoint metrics.RequestType GlobalPrivacyControlHeader string CurrencyConversions currency.Conversions - - BidderCoreName openrtb_ext.BidderName // OW specific: required for oRTB bidder } func NewExtraRequestInfo(c currency.Conversions) ExtraRequestInfo { diff --git a/adapters/ortbbidder/bidderparams/config.go b/adapters/ortbbidder/bidderparams/config.go index 1b75da9bf25..94f1f9ba9af 100644 --- a/adapters/ortbbidder/bidderparams/config.go +++ b/adapters/ortbbidder/bidderparams/config.go @@ -2,18 +2,7 @@ package bidderparams // BidderParamMapper contains property details like location type BidderParamMapper struct { - location []string -} - -// GetLocation returns the location of bidderParam -func (bpm *BidderParamMapper) GetLocation() []string { - return bpm.location -} - -// SetLocation sets the location in BidderParamMapper -// Do not modify the location of bidderParam unless you are writing unit test case -func (bpm *BidderParamMapper) SetLocation(location []string) { - bpm.location = location + Location string // do not update this parameter for each request, its being shared across all requests } // config contains mappings requestParams and responseParams @@ -27,14 +16,15 @@ type BidderConfig struct { bidderConfigMap map[string]*config } -// setRequestParams sets the bidder specific requestParams -func (bcfg *BidderConfig) setRequestParams(bidderName string, requestParams map[string]BidderParamMapper) { - if bcfg == nil { - return - } - if bcfg.bidderConfigMap == nil { - bcfg.bidderConfigMap = make(map[string]*config) +// NewBidderConfig initializes and returns the object of BidderConfig +func NewBidderConfig() *BidderConfig { + return &BidderConfig{ + bidderConfigMap: make(map[string]*config), } +} + +// SetRequestParams sets the bidder specific requestParams +func (bcfg *BidderConfig) SetRequestParams(bidderName string, requestParams map[string]BidderParamMapper) { if _, found := bcfg.bidderConfigMap[bidderName]; !found { bcfg.bidderConfigMap[bidderName] = &config{} } @@ -42,13 +32,13 @@ func (bcfg *BidderConfig) setRequestParams(bidderName string, requestParams map[ } // GetRequestParams returns bidder specific requestParams -func (bcfg *BidderConfig) GetRequestParams(bidderName string) (map[string]BidderParamMapper, bool) { - if bcfg == nil || len(bcfg.bidderConfigMap) == 0 { - return nil, false +func (bcfg *BidderConfig) GetRequestParams(bidderName string) map[string]BidderParamMapper { + if len(bcfg.bidderConfigMap) == 0 { + return nil } - bidderConfig, _ := bcfg.bidderConfigMap[bidderName] + bidderConfig := bcfg.bidderConfigMap[bidderName] if bidderConfig == nil { - return nil, false + return nil } - return bidderConfig.requestParams, true + return bidderConfig.requestParams } diff --git a/adapters/ortbbidder/bidderparams/config_test.go b/adapters/ortbbidder/bidderparams/config_test.go index 580689e6bf4..2dd4b09c585 100644 --- a/adapters/ortbbidder/bidderparams/config_test.go +++ b/adapters/ortbbidder/bidderparams/config_test.go @@ -14,55 +14,15 @@ func TestSetRequestParams(t *testing.T) { bidderName string requestParams map[string]BidderParamMapper } - + type want struct { + bidderCfg *BidderConfig + } tests := []struct { name string fields fields args args - want *BidderConfig + want want }{ - { - name: "bidderConfig_is_nil", - fields: fields{ - bidderConfig: nil, - }, - args: args{ - bidderName: "test", - requestParams: map[string]BidderParamMapper{ - "adunit": { - location: []string{"ext", "adunit"}, - }, - }, - }, - want: nil, - }, - { - name: "bidderConfigMap_is_nil", - fields: fields{ - bidderConfig: &BidderConfig{ - bidderConfigMap: nil, - }, - }, - args: args{ - bidderName: "test", - requestParams: map[string]BidderParamMapper{ - "adunit": { - location: []string{"ext", "adunit"}, - }, - }, - }, - want: &BidderConfig{ - bidderConfigMap: map[string]*config{ - "test": { - requestParams: map[string]BidderParamMapper{ - "adunit": { - location: []string{"ext", "adunit"}, - }, - }, - }, - }, - }, - }, { name: "bidderName_not_found", fields: fields{ @@ -74,16 +34,18 @@ func TestSetRequestParams(t *testing.T) { bidderName: "test", requestParams: map[string]BidderParamMapper{ "param-1": { - location: []string{"path"}, + Location: "path", }, }, }, - want: &BidderConfig{ - bidderConfigMap: map[string]*config{ - "test": { - requestParams: map[string]BidderParamMapper{ - "param-1": { - location: []string{"path"}, + want: want{ + bidderCfg: &BidderConfig{ + bidderConfigMap: map[string]*config{ + "test": { + requestParams: map[string]BidderParamMapper{ + "param-1": { + Location: "path", + }, }, }, }, @@ -98,7 +60,7 @@ func TestSetRequestParams(t *testing.T) { "test": { requestParams: map[string]BidderParamMapper{ "param-1": { - location: []string{"path-1"}, + Location: "path-1", }, }, }, @@ -109,16 +71,18 @@ func TestSetRequestParams(t *testing.T) { bidderName: "test", requestParams: map[string]BidderParamMapper{ "param-2": { - location: []string{"path-2"}, + Location: "path-2", }, }, }, - want: &BidderConfig{ - bidderConfigMap: map[string]*config{ - "test": { - requestParams: map[string]BidderParamMapper{ - "param-2": { - location: []string{"path-2"}, + want: want{ + bidderCfg: &BidderConfig{ + bidderConfigMap: map[string]*config{ + "test": { + requestParams: map[string]BidderParamMapper{ + "param-2": { + Location: "path-2", + }, }, }, }, @@ -128,8 +92,8 @@ func TestSetRequestParams(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - tt.fields.bidderConfig.setRequestParams(tt.args.bidderName, tt.args.requestParams) - assert.Equal(t, tt.want, tt.fields.bidderConfig, "mismatched bidderConfig") + tt.fields.bidderConfig.SetRequestParams(tt.args.bidderName, tt.args.requestParams) + assert.Equal(t, tt.want.bidderCfg, tt.fields.bidderConfig, "mismatched bidderConfig") }) } } @@ -143,7 +107,6 @@ func TestGetBidderRequestProperties(t *testing.T) { } type want struct { requestParams map[string]BidderParamMapper - found bool } tests := []struct { name string @@ -151,19 +114,6 @@ func TestGetBidderRequestProperties(t *testing.T) { args args want want }{ - { - name: "BidderConfig_is_nil", - fields: fields{ - biddersConfig: nil, - }, - args: args{ - bidderName: "test", - }, - want: want{ - requestParams: nil, - found: false, - }, - }, { name: "BidderConfigMap_is_nil", fields: fields{ @@ -176,7 +126,6 @@ func TestGetBidderRequestProperties(t *testing.T) { }, want: want{ requestParams: nil, - found: false, }, }, { @@ -193,7 +142,6 @@ func TestGetBidderRequestProperties(t *testing.T) { }, want: want{ requestParams: nil, - found: false, }, }, { @@ -210,7 +158,6 @@ func TestGetBidderRequestProperties(t *testing.T) { }, want: want{ requestParams: nil, - found: false, }, }, { @@ -221,7 +168,7 @@ func TestGetBidderRequestProperties(t *testing.T) { "test": { requestParams: map[string]BidderParamMapper{ "param-1": { - location: []string{"value-1"}, + Location: "value-1", }, }, }, @@ -234,88 +181,16 @@ func TestGetBidderRequestProperties(t *testing.T) { want: want{ requestParams: map[string]BidderParamMapper{ "param-1": { - location: []string{"value-1"}, + Location: "value-1", }, }, - found: true, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - params, found := tt.fields.biddersConfig.GetRequestParams(tt.args.bidderName) + params := tt.fields.biddersConfig.GetRequestParams(tt.args.bidderName) assert.Equal(t, tt.want.requestParams, params, "mismatched requestParams") - assert.Equal(t, tt.want.found, found, "mismatched found value") - }) - } -} - -func TestBidderParamMapperGetLocation(t *testing.T) { - tests := []struct { - name string - bpm BidderParamMapper - want []string - }{ - { - name: "location_is_nil", - bpm: BidderParamMapper{ - location: nil, - }, - want: nil, - }, - { - name: "location_is_non_empty", - bpm: BidderParamMapper{ - location: []string{"req", "ext"}, - }, - want: []string{"req", "ext"}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := tt.bpm.GetLocation() - assert.Equal(t, tt.want, got, "mismatched location") - }) - } -} - -func TestBidderParamMapperSetLocation(t *testing.T) { - type args struct { - location []string - } - tests := []struct { - name string - bpm BidderParamMapper - args args - want BidderParamMapper - }{ - { - name: "set_location", - bpm: BidderParamMapper{}, - args: args{ - location: []string{"req", "ext"}, - }, - want: BidderParamMapper{ - location: []string{"req", "ext"}, - }, - }, - { - name: "override_location", - bpm: BidderParamMapper{ - location: []string{"imp", "ext"}, - }, - args: args{ - location: []string{"req", "ext"}, - }, - want: BidderParamMapper{ - location: []string{"req", "ext"}, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.bpm.SetLocation(tt.args.location) - assert.Equal(t, tt.want, tt.bpm, "mismatched location") }) } } diff --git a/adapters/ortbbidder/bidderparams/parser.go b/adapters/ortbbidder/bidderparams/parser.go index 8d198464fcb..278629608b7 100644 --- a/adapters/ortbbidder/bidderparams/parser.go +++ b/adapters/ortbbidder/bidderparams/parser.go @@ -19,7 +19,7 @@ func LoadBidderConfig(dirPath string, isBidderAllowed func(string) bool) (*Bidde if err != nil { return nil, fmt.Errorf("error:[%s] dirPath:[%s]", err.Error(), dirPath) } - bidderConfigMap := &BidderConfig{bidderConfigMap: make(map[string]*config)} + bidderConfigMap := NewBidderConfig() for _, file := range files { bidderName, ok := strings.CutSuffix(file.Name(), ".json") if !ok { @@ -36,7 +36,7 @@ func LoadBidderConfig(dirPath string, isBidderAllowed func(string) bool) (*Bidde if err != nil { return nil, err } - bidderConfigMap.setRequestParams(bidderName, requestParams) + bidderConfigMap.SetRequestParams(bidderName, requestParams) } return bidderConfigMap, nil } @@ -78,7 +78,7 @@ func prepareRequestParams(bidderName string, requestParamsConfig map[string]any) return nil, fmt.Errorf("error:[incorrect_location_in_bidderparam] bidder:[%s] bidderParam:[%s]", bidderName, paramName) } requestParams[paramName] = BidderParamMapper{ - location: strings.Split(locationStr, "."), + Location: locationStr, } } return requestParams, nil diff --git a/adapters/ortbbidder/bidderparams/parser_test.go b/adapters/ortbbidder/bidderparams/parser_test.go index 54ae74deea0..3d543b9960d 100644 --- a/adapters/ortbbidder/bidderparams/parser_test.go +++ b/adapters/ortbbidder/bidderparams/parser_test.go @@ -119,7 +119,7 @@ func TestPrepareRequestParams(t *testing.T) { }, want: want{ requestParams: map[string]BidderParamMapper{ - "adunitid": {location: []string{"app", "adunitid"}}, + "adunitid": {Location: "app.adunitid"}, }, err: nil, }, @@ -144,8 +144,8 @@ func TestPrepareRequestParams(t *testing.T) { }, want: want{ requestParams: map[string]BidderParamMapper{ - "adunitid": {location: []string{"app", "adunitid"}}, - "slotname": {location: []string{"ext", "slot"}}, + "adunitid": {Location: "app.adunitid"}, + "slotname": {Location: "ext.slot"}, }, err: nil, }, @@ -249,8 +249,8 @@ func TestLoadBidderConfig(t *testing.T) { bidderConfigMap: map[string]*config{ "owortb_test": { requestParams: map[string]BidderParamMapper{ - "adunitid": {location: []string{"app", "adunit", "id"}}, - "slotname": {location: []string{"ext", "slotname"}}, + "adunitid": {Location: "app.adunit.id"}, + "slotname": {Location: "ext.slotname"}, }, }, }}, diff --git a/adapters/ortbbidder/constant.go b/adapters/ortbbidder/constant.go new file mode 100644 index 00000000000..3b56198636b --- /dev/null +++ b/adapters/ortbbidder/constant.go @@ -0,0 +1,20 @@ +package ortbbidder + +// constants required for oRTB adapter +const ( + impKey = "imp" + extKey = "ext" + bidderKey = "bidder" + appsiteKey = "appsite" + siteKey = "site" + appKey = "app" +) + +const ( + urlMacroPrefix = "{{." + urlMacroNoValue = "" + requestModeSingle = "single" + locationIndexMacro = "#" + endpointTemplate = "endpointTemplate" + templateOption = "missingkey=zero" +) diff --git a/adapters/ortbbidder/errors.go b/adapters/ortbbidder/errors.go new file mode 100644 index 00000000000..d1866947a9e --- /dev/null +++ b/adapters/ortbbidder/errors.go @@ -0,0 +1,21 @@ +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...), + } +} diff --git a/adapters/ortbbidder/multiRequestBuilder.go b/adapters/ortbbidder/multiRequestBuilder.go new file mode 100644 index 00000000000..b8733f8ca19 --- /dev/null +++ b/adapters/ortbbidder/multiRequestBuilder.go @@ -0,0 +1,82 @@ +package ortbbidder + +import ( + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/adapters" + "github.com/prebid/prebid-server/v2/util/jsonutil" +) + +// struct to build the multi requests each containing sinlge impression when requestMode="single" +type multiRequestBuilder struct { + requestBuilderImpl + imps []map[string]any +} + +// 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 + } + + //get rawrequests without impression objects + tmpImp := request.Imp[0:] + request.Imp = nil + if rb.rawRequest, err = jsonutil.Marshal(request); err != nil { + return err + } + // request.Imp = tmpImp[0:] //resetting is not required + + //cache impression from request + data, err := jsonutil.Marshal(tmpImp) + if err != nil { + return err + } + if err = jsonutil.Unmarshal(data, &rb.imps); err != nil { + return err + } + + return nil +} + +// makeRequest constructs the endpoint URL and maps the bidder-parameters in request to create the RequestData objects. +// it processes a request to generate 'N' RequestData objects, one for each of the 'N' impressions +func (rb *multiRequestBuilder) makeRequest() (requestData []*adapters.RequestData, errs []error) { + var ( + endpoint string + newRequest map[string]any + err error + requestCloneRequired bool + ) + + requestCloneRequired = true + + for index := range rb.imps { + //step 1: clone request + if requestCloneRequired { + if newRequest, err = cloneRequest(rb.rawRequest); err != nil { + errs = append(errs, newBadInputError(err.Error())) + continue + } + } + + //step 2: get impression extension + imp := rb.imps[index] + bidderParams := getImpExtBidderParams(imp) + + //step 3: get endpoint + if endpoint, err = rb.getEndpoint(bidderParams); err != nil { + errs = append(errs, newBadInputError(err.Error())) + continue + } + + //step 4: update the request object by mapping bidderParams at expected location. + newRequest[impKey] = []any{imp} + requestCloneRequired = setRequestParams(newRequest, bidderParams, rb.requestParams, []int{0}) + + //step 5: append new request data + if requestData, err = appendRequestData(requestData, newRequest, endpoint); err != nil { + errs = append(errs, newBadInputError(err.Error())) + } + } + return requestData, errs +} diff --git a/adapters/ortbbidder/multiRequestBuilder_test.go b/adapters/ortbbidder/multiRequestBuilder_test.go new file mode 100644 index 00000000000..2f3e178a0a1 --- /dev/null +++ b/adapters/ortbbidder/multiRequestBuilder_test.go @@ -0,0 +1,360 @@ +package ortbbidder + +import ( + "encoding/json" + "errors" + "net/http" + "testing" + "text/template" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/adapters" + "github.com/prebid/prebid-server/v2/adapters/ortbbidder/bidderparams" + "github.com/stretchr/testify/assert" +) + +func TestMultiRequestBuilderParseRequest(t *testing.T) { + type args struct { + request *openrtb2.BidRequest + } + type want struct { + err error + rawRequest json.RawMessage + imps []map[string]any + } + tests := []struct { + name string + args args + want want + }{ + { + name: "request_without_imps", + args: args{ + request: &openrtb2.BidRequest{ + ID: "id", + }, + }, + want: want{ + err: errImpMissing, + rawRequest: nil, + imps: nil, + }, + }, + { + name: "request_is_valid", + args: args{ + request: &openrtb2.BidRequest{ + ID: "id", + Imp: []openrtb2.Imp{ + { + ID: "imp_1", + }, + }, + }, + }, + want: want{ + err: nil, + rawRequest: json.RawMessage(`{"id":"id","imp":null}`), + imps: []map[string]any{{ + "id": "imp_1", + }}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reqBuilder := &multiRequestBuilder{} + err := reqBuilder.parseRequest(tt.args.request) + assert.Equalf(t, tt.want.err, err, "mismatched error") + assert.Equalf(t, string(tt.want.rawRequest), string(reqBuilder.rawRequest), "mismatched rawRequest") + assert.Equalf(t, tt.want.imps, reqBuilder.imps, "mismatched imps") + }) + } +} + +func TestMultiRequestBuilderMakeRequest(t *testing.T) { + type fields struct { + requestBuilder multiRequestBuilder + } + type want struct { + requestData []*adapters.RequestData + errs []error + } + tests := []struct { + name string + fields fields + want want + }{ + { + name: "nil_request", + fields: fields{ + requestBuilder: multiRequestBuilder{ + requestBuilderImpl: requestBuilderImpl{ + rawRequest: nil, + }, + }, + }, + want: want{ + requestData: nil, + errs: nil, + }, + }, + { + name: "no_imp_object_in_builder", + fields: fields{ + requestBuilder: multiRequestBuilder{ + requestBuilderImpl: requestBuilderImpl{ + rawRequest: json.RawMessage(`{}`), + }, + }, + }, + want: want{ + requestData: nil, + errs: nil, + }, + }, + { + name: "replace_macros_to_form_endpoint_url", + fields: fields{ + requestBuilder: multiRequestBuilder{ + requestBuilderImpl: requestBuilderImpl{ + hasMacrosInEndpoint: true, + rawRequest: json.RawMessage(`{"imp":[{"ext":{"bidder":{"ext":{"pubid":5890},"host":"localhost.com"}},"id":"imp_1"}]}`), + endpointTemplate: template.Must(template.New("endpointTemplate").Parse(`http://{{.host}}/publisher/{{.ext.pubid}}`)), + }, + imps: []map[string]any{ + { + "id": "imp_1", + "ext": map[string]any{ + "bidder": map[string]any{ + "ext": map[string]any{ + "pubid": 5890, + }, + "host": "localhost.com", + }, + }, + }, + }, + }, + }, + want: want{ + requestData: []*adapters.RequestData{ + { + Method: http.MethodPost, + Uri: "http://localhost.com/publisher/5890", + Body: json.RawMessage(`{"imp":[{"ext":{"bidder":{"ext":{"pubid":5890},"host":"localhost.com"}},"id":"imp_1"}]}`), + Headers: http.Header{ + "Content-Type": {"application/json;charset=utf-8"}, + "Accept": {"application/json"}, + }, + }, + }, + errs: nil, + }, + }, + { + name: "macros_value_absent_in_bidder_params", + fields: fields{ + requestBuilder: multiRequestBuilder{ + requestBuilderImpl: requestBuilderImpl{ + hasMacrosInEndpoint: true, + rawRequest: json.RawMessage(`{"imp":[{"ext":{},"id":"imp_1"}]}`), + endpointTemplate: template.Must(template.New("endpointTemplate").Option("missingkey=default").Parse(`http://{{.host}}/publisher/{{.pubid}}`)), + }, + imps: []map[string]any{ + { + "ext": map[string]any{}, + "id": "imp_1", + }, + }, + }, + }, + want: want{ + requestData: []*adapters.RequestData{ + { + Method: http.MethodPost, + Uri: "http:///publisher/", + Body: json.RawMessage(`{"imp":[{"ext":{},"id":"imp_1"}]}`), + Headers: http.Header{ + "Content-Type": {"application/json;charset=utf-8"}, + "Accept": {"application/json"}, + }, + }, + }, + errs: nil, + }, + }, + { + name: "buildEndpoint_returns_error", + fields: fields{ + requestBuilder: multiRequestBuilder{ + requestBuilderImpl: requestBuilderImpl{ + hasMacrosInEndpoint: true, + rawRequest: json.RawMessage(`{"imp":[{"ext":{},"id":"imp_1"}]}`), + endpointTemplate: func() *template.Template { + errorFunc := template.FuncMap{ + "errorFunc": func() (string, error) { + return "", errors.New("intentional error") + }, + } + t := template.Must(template.New("endpointTemplate").Funcs(errorFunc).Parse(`{{errorFunc}}`)) + return t + }(), + }, + imps: []map[string]any{ + { + "ext": map[string]any{}, + "id": "imp_1", + }, + }, + }, + }, + + want: want{ + requestData: nil, + errs: []error{newBadInputError("failed to replace macros in endpoint, err:template: endpointTemplate:1:2: " + + "executing \"endpointTemplate\" at : error calling errorFunc: intentional error")}, + }, + }, + { + name: "multi_imps_request", + fields: fields{ + requestBuilder: multiRequestBuilder{ + requestBuilderImpl: requestBuilderImpl{ + hasMacrosInEndpoint: true, + rawRequest: json.RawMessage(`{"imp":[{"ext":{"bidder":{"ext":{"pubid":1111},"host":"imp1.host.com"}},"id":"imp_1"},{"ext":{"bidder":{"ext":{"pubid":2222},"host":"imp2.host.com"}},"id":"imp_2"}]}`), + endpointTemplate: template.Must(template.New("endpointTemplate").Parse(`http://{{.host}}/publisher/{{.ext.pubid}}`)), + requestParams: func() map[string]bidderparams.BidderParamMapper { + hostMapper := bidderparams.BidderParamMapper{Location: "host"} + extMapper := bidderparams.BidderParamMapper{Location: "device"} + return map[string]bidderparams.BidderParamMapper{ + "host": hostMapper, + "ext": extMapper, + } + }(), + }, + imps: []map[string]any{ + { + "ext": map[string]any{ + "bidder": map[string]any{ + "ext": map[string]any{ + "pubid": 1111, + }, + "host": "imp1.host.com", + }, + }, + "id": "imp_1", + }, + { + "ext": map[string]any{ + "bidder": map[string]any{ + "ext": map[string]any{ + "pubid": 2222, + }, + "host": "imp2.host.com", + }, + }, + "id": "imp_2", + }, + }, + }, + }, + want: want{ + requestData: []*adapters.RequestData{ + { + Method: http.MethodPost, + Uri: "http://imp1.host.com/publisher/1111", + Body: json.RawMessage(`{"device":{"pubid":1111},"host":"imp1.host.com","imp":[{"ext":{"bidder":{}},"id":"imp_1"}]}`), + Headers: http.Header{ + "Content-Type": {"application/json;charset=utf-8"}, + "Accept": {"application/json"}, + }, + }, + { + Method: http.MethodPost, + Uri: "http://imp2.host.com/publisher/2222", + Body: json.RawMessage(`{"device":{"pubid":2222},"host":"imp2.host.com","imp":[{"ext":{"bidder":{}},"id":"imp_2"}]}`), + Headers: http.Header{ + "Content-Type": {"application/json;charset=utf-8"}, + "Accept": {"application/json"}, + }, + }, + }, + errs: nil, + }, + }, + { + name: "one_imp_updates_request_level_param_but_another_imp_expects_original_request_param", + fields: fields{ + requestBuilder: multiRequestBuilder{ + requestBuilderImpl: requestBuilderImpl{ + hasMacrosInEndpoint: true, + rawRequest: json.RawMessage(`{"imp":[{"ext":{"bidder":{"ext":{"pubid":1111}}},"id":"imp_1"},{"ext":{"bidder":{"ext":{"pubid":2222},"host":"imp2.host.com"}},"id":"imp_2"}]}`), + endpointTemplate: template.Must(template.New("endpointTemplate").Parse(`http://{{.host}}/publisher/{{.ext.pubid}}`)), + requestParams: func() map[string]bidderparams.BidderParamMapper { + hostMapper := bidderparams.BidderParamMapper{Location: "host"} + extMapper := bidderparams.BidderParamMapper{Location: "device"} + return map[string]bidderparams.BidderParamMapper{ + "host": hostMapper, + "ext": extMapper, + } + }(), + }, + imps: []map[string]any{ + { + "ext": map[string]any{ + "bidder": map[string]any{ + "ext": map[string]any{ + "pubid": 1111, + }, + }, + }, + "id": "imp_1", + }, + { + "ext": map[string]any{ + "bidder": map[string]any{ + "ext": map[string]any{ + "pubid": 2222, + }, + "host": "imp2.host.com", + }, + }, + "id": "imp_2", + }, + }, + }, + }, + want: want{ + requestData: []*adapters.RequestData{ + { + Method: http.MethodPost, + Uri: "http:///publisher/1111", + Body: json.RawMessage(`{"device":{"pubid":1111},"imp":[{"ext":{"bidder":{}},"id":"imp_1"}]}`), + Headers: http.Header{ + "Content-Type": {"application/json;charset=utf-8"}, + "Accept": {"application/json"}, + }, + }, + { + Method: http.MethodPost, + Uri: "http://imp2.host.com/publisher/2222", + Body: json.RawMessage(`{"device":{"pubid":2222},"host":"imp2.host.com","imp":[{"ext":{"bidder":{}},"id":"imp_2"}]}`), + Headers: http.Header{ + "Content-Type": {"application/json;charset=utf-8"}, + "Accept": {"application/json"}, + }, + }, + }, + errs: nil, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + requestData, errs := tt.fields.requestBuilder.makeRequest() + assert.Equalf(t, tt.want.requestData, requestData, "mismatched requestData") + assert.Equalf(t, tt.want.errs, errs, "mismatched errs") + }) + } +} diff --git a/adapters/ortbbidder/ortbbidder.go b/adapters/ortbbidder/ortbbidder.go index 4cc3f349f8a..223e855c248 100644 --- a/adapters/ortbbidder/ortbbidder.go +++ b/adapters/ortbbidder/ortbbidder.go @@ -3,14 +3,15 @@ package ortbbidder import ( "encoding/json" "fmt" - "net/http" "strings" + "text/template" "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/config" "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/prebid/prebid-server/v2/util/jsonutil" ) // adapter implements adapters.Bidder interface @@ -19,15 +20,12 @@ type adapter struct { bidderParamsConfig *bidderparams.BidderConfig } -const ( - RequestModeSingle string = "single" -) - // adapterInfo contains oRTB bidder specific info required in MakeRequests/MakeBids functions type adapterInfo struct { config.Adapter - extraInfo extraAdapterInfo - bidderName openrtb_ext.BidderName + extraInfo extraAdapterInfo + bidderName openrtb_ext.BidderName + endpointTemplate *template.Template } type extraAdapterInfo struct { RequestMode string `json:"requestMode"` @@ -42,78 +40,42 @@ func InitBidderParamsConfig(dirPath string) (err error) { return err } -// makeRequest converts openrtb2.BidRequest to adapters.RequestData, sets requestParams in request if required -func (o adapterInfo) makeRequest(request *openrtb2.BidRequest, requestParams map[string]bidderparams.BidderParamMapper) (*adapters.RequestData, error) { - if request == nil { - return nil, fmt.Errorf("found nil request") - } - requestBody, err := json.Marshal(request) - if err != nil { - return nil, fmt.Errorf("failed to marshal request %s", err.Error()) - } - requestBody, err = setRequestParams(requestBody, requestParams) - if err != nil { - return nil, err - } - return &adapters.RequestData{ - Method: http.MethodPost, - Uri: o.Endpoint, - Body: requestBody, - Headers: http.Header{ - "Content-Type": {"application/json;charset=utf-8"}, - "Accept": {"application/json"}, - }, - }, nil -} - // Builder returns an instance of oRTB adapter func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { extraAdapterInfo := extraAdapterInfo{} if len(config.ExtraAdapterInfo) > 0 { - err := json.Unmarshal([]byte(config.ExtraAdapterInfo), &extraAdapterInfo) + err := jsonutil.Unmarshal([]byte(config.ExtraAdapterInfo), &extraAdapterInfo) if err != nil { - return nil, fmt.Errorf("Failed to parse extra_info for bidder:[%s] err:[%s]", bidderName, err.Error()) + return nil, fmt.Errorf("failed to parse extra_info: %s", err.Error()) } } + template, err := template.New(endpointTemplate).Option(templateOption).Parse(config.Endpoint) + if err != nil || template == nil { + return nil, fmt.Errorf("failed to parse endpoint url template: %v", err) + } return &adapter{ - adapterInfo: adapterInfo{config, extraAdapterInfo, bidderName}, + adapterInfo: adapterInfo{config, extraAdapterInfo, bidderName, template}, bidderParamsConfig: g_bidderParamsConfig, }, nil } // 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 request == nil || requestInfo == nil { - return nil, []error{fmt.Errorf("Found either nil request or nil requestInfo")} - } if o.bidderParamsConfig == nil { - return nil, []error{fmt.Errorf("Found nil bidderParamsConfig")} - } - var errs []error - adapterInfo := o.adapterInfo - requestParams, _ := o.bidderParamsConfig.GetRequestParams(o.bidderName.String()) - - // bidder request supports single impression in single HTTP call. - if adapterInfo.extraInfo.RequestMode == RequestModeSingle { - requestData := make([]*adapters.RequestData, 0, len(request.Imp)) - requestCopy := *request - for _, imp := range request.Imp { - requestCopy.Imp = []openrtb2.Imp{imp} // requestCopy contains single impression - reqData, err := adapterInfo.makeRequest(&requestCopy, requestParams) - if err != nil { - errs = append(errs, err) - continue - } - requestData = append(requestData, reqData) - } - return requestData, errs + return nil, []error{newBadInputError(errNilBidderParamCfg.Error())} } - // bidder request supports multi impressions in single HTTP call. - requestData, err := adapterInfo.makeRequest(request, requestParams) - if err != nil { - return nil, []error{err} + + requestBuilder := newRequestBuilder( + o.adapterInfo.extraInfo.RequestMode, + o.Endpoint, + o.endpointTemplate, + o.bidderParamsConfig.GetRequestParams(o.bidderName.String())) + + if err := requestBuilder.parseRequest(request); err != nil { + return nil, []error{newBadInputError(err.Error())} } - return []*adapters.RequestData{requestData}, nil + + return requestBuilder.makeRequest() } // MakeBids prepares bidderResponse from the oRTB bidder server's http.Response @@ -127,7 +89,7 @@ func (o *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.R } var response openrtb2.BidResponse - if err := json.Unmarshal(responseData.Body, &response); err != nil { + if err := jsonutil.Unmarshal(responseData.Body, &response); err != nil { return nil, []error{err} } diff --git a/adapters/ortbbidder/ortbbidder_test.go b/adapters/ortbbidder/ortbbidder_test.go index b951efc4086..1b9397449bb 100644 --- a/adapters/ortbbidder/ortbbidder_test.go +++ b/adapters/ortbbidder/ortbbidder_test.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "testing" + "text/template" "github.com/prebid/openrtb/v20/openrtb2" "github.com/prebid/prebid-server/v2/adapters" @@ -18,10 +19,10 @@ import ( func TestMakeRequests(t *testing.T) { type args struct { - request *openrtb2.BidRequest - requestInfo *adapters.ExtraRequestInfo - adapterInfo adapterInfo - bidderCfgMap *bidderparams.BidderConfig + request *openrtb2.BidRequest + requestInfo *adapters.ExtraRequestInfo + adapterInfo adapterInfo + bidderCfg *bidderparams.BidderConfig } type want struct { requestData []*adapters.RequestData @@ -35,49 +36,53 @@ func TestMakeRequests(t *testing.T) { { name: "request_is_nil", args: args{ - bidderCfgMap: &bidderparams.BidderConfig{}, + bidderCfg: bidderparams.NewBidderConfig(), }, want: want{ - errors: []error{fmt.Errorf("Found either nil request or nil requestInfo")}, + errors: []error{newBadInputError(errImpMissing.Error())}, }, }, { - name: "requestInfo_is_nil", + name: "bidderParamsConfig_is_nil", args: args{ - bidderCfgMap: &bidderparams.BidderConfig{}, + request: &openrtb2.BidRequest{ + ID: "reqid", + Imp: []openrtb2.Imp{{ID: "imp1", TagID: "tag1"}}, + }, + adapterInfo: adapterInfo{config.Adapter{Endpoint: "http://test_bidder.com"}, extraAdapterInfo{RequestMode: "single"}, "testbidder", nil}, + bidderCfg: nil, }, want: want{ - errors: []error{fmt.Errorf("Found either nil request or nil requestInfo")}, + errors: []error{newBadInputError("found nil bidderParamsConfig")}, }, }, { - name: "multi_requestmode_to_form_requestdata", + name: "bidderParamsConfig_not_contains_bidder_param_data", args: args{ request: &openrtb2.BidRequest{ - ID: "reqid", - Imp: []openrtb2.Imp{ - {ID: "imp1", TagID: "tag1"}, - {ID: "imp2", TagID: "tag2"}, - }, - }, - requestInfo: &adapters.ExtraRequestInfo{ - BidderCoreName: openrtb_ext.BidderName("ortb_test_multi_requestmode"), + ID: "reqid", + Imp: []openrtb2.Imp{{ID: "imp1", TagID: "tag1"}}, }, - adapterInfo: adapterInfo{config.Adapter{Endpoint: "http://test_bidder.com"}, extraAdapterInfo{RequestMode: ""}, "testbidder"}, - bidderCfgMap: &bidderparams.BidderConfig{}, + adapterInfo: func() adapterInfo { + endpoint := "http://test_bidder.com" + template, _ := template.New("endpointTemplate").Parse(endpoint) + return adapterInfo{config.Adapter{Endpoint: endpoint}, extraAdapterInfo{RequestMode: "single"}, "testbidder", template} + }(), + bidderCfg: bidderparams.NewBidderConfig(), }, want: want{ requestData: []*adapters.RequestData{ { Method: http.MethodPost, Uri: "http://test_bidder.com", - Body: []byte(`{"id":"reqid","imp":[{"id":"imp1","tagid":"tag1"},{"id":"imp2","tagid":"tag2"}]}`), + Body: []byte(`{"id":"reqid","imp":[{"id":"imp1","tagid":"tag1"}]}`), Headers: http.Header{ "Content-Type": {"application/json;charset=utf-8"}, "Accept": {"application/json"}, }, }, }, + errors: nil, }, }, { @@ -90,11 +95,12 @@ func TestMakeRequests(t *testing.T) { {ID: "imp2", TagID: "tag2"}, }, }, - requestInfo: &adapters.ExtraRequestInfo{ - BidderCoreName: openrtb_ext.BidderName("ortb_test_single_requestmode"), - }, - adapterInfo: adapterInfo{config.Adapter{Endpoint: "http://test_bidder.com"}, extraAdapterInfo{RequestMode: "single"}, "testbidder"}, - bidderCfgMap: &bidderparams.BidderConfig{}, + adapterInfo: func() adapterInfo { + endpoint := "http://test_bidder.com" + template, _ := template.New("endpointTemplate").Parse(endpoint) + return adapterInfo{config.Adapter{Endpoint: endpoint}, extraAdapterInfo{RequestMode: "single"}, "testbidder", template} + }(), + bidderCfg: bidderparams.NewBidderConfig(), }, want: want{ requestData: []*adapters.RequestData{ @@ -120,33 +126,164 @@ func TestMakeRequests(t *testing.T) { }, }, { - name: "biddersConfigMap_is_nil", + name: "single_requestmode_validate_endpoint_macro", args: args{ request: &openrtb2.BidRequest{ - ID: "reqid", - Imp: []openrtb2.Imp{{ID: "imp1", TagID: "tag1"}}, + ID: "reqid", + Imp: []openrtb2.Imp{ + {ID: "imp1", TagID: "tag1", Ext: json.RawMessage(`{"bidder": {"host": "localhost.com"}}`)}, + {ID: "imp2", TagID: "tag2"}, + }, + }, + adapterInfo: func() adapterInfo { + endpoint := "http://{{.host}}" + template, _ := template.New("endpointTemplate").Parse(endpoint) + return adapterInfo{config.Adapter{Endpoint: endpoint}, extraAdapterInfo{RequestMode: "single"}, "testbidder", template} + }(), + bidderCfg: bidderparams.NewBidderConfig(), + }, + want: want{ + requestData: []*adapters.RequestData{ + { + Method: http.MethodPost, + Uri: "http://localhost.com", + Body: []byte(`{"id":"reqid","imp":[{"ext":{"bidder":{"host":"localhost.com"}},"id":"imp1","tagid":"tag1"}]}`), + Headers: http.Header{ + "Content-Type": {"application/json;charset=utf-8"}, + "Accept": {"application/json"}, + }, + }, + { + Method: http.MethodPost, + Uri: "http://", + Body: []byte(`{"id":"reqid","imp":[{"id":"imp2","tagid":"tag2"}]}`), + Headers: http.Header{ + "Content-Type": {"application/json;charset=utf-8"}, + "Accept": {"application/json"}, + }, + }, + }, + }, + }, + { + name: "multi_requestmode_to_form_requestdata", + args: args{ + request: &openrtb2.BidRequest{ + ID: "reqid", + Imp: []openrtb2.Imp{ + {ID: "imp1", TagID: "tag1"}, + {ID: "imp2", TagID: "tag2"}, + }, + }, + adapterInfo: func() adapterInfo { + endpoint := "http://test_bidder.com" + template, _ := template.New("endpointTemplate").Parse(endpoint) + return adapterInfo{config.Adapter{Endpoint: endpoint}, extraAdapterInfo{RequestMode: ""}, "testbidder", template} + }(), + bidderCfg: bidderparams.NewBidderConfig(), + }, + want: want{ + requestData: []*adapters.RequestData{ + { + Method: http.MethodPost, + Uri: "http://test_bidder.com", + Body: []byte(`{"id":"reqid","imp":[{"id":"imp1","tagid":"tag1"},{"id":"imp2","tagid":"tag2"}]}`), + Headers: http.Header{ + "Content-Type": {"application/json;charset=utf-8"}, + "Accept": {"application/json"}, + }, + }, + }, + }, + }, + { + name: "multi_requestmode_validate_endpoint_macros", + args: args{ + request: &openrtb2.BidRequest{ + ID: "reqid", + Imp: []openrtb2.Imp{ + {ID: "imp1", TagID: "tag1", Ext: json.RawMessage(`{"bidder": {"host": "localhost.com"}}`)}, + {ID: "imp2", TagID: "tag2"}, + }, + }, + adapterInfo: func() adapterInfo { + endpoint := "http://{{.host}}" + template, _ := template.New("endpointTemplate").Parse(endpoint) + return adapterInfo{config.Adapter{Endpoint: endpoint}, extraAdapterInfo{RequestMode: ""}, "testbidder", template} + }(), + bidderCfg: bidderparams.NewBidderConfig(), + }, + want: want{ + requestData: []*adapters.RequestData{ + { + Method: http.MethodPost, + Uri: "http://localhost.com", + Body: []byte(`{"id":"reqid","imp":[{"ext":{"bidder":{"host":"localhost.com"}},"id":"imp1","tagid":"tag1"},{"id":"imp2","tagid":"tag2"}]}`), + Headers: http.Header{ + "Content-Type": {"application/json;charset=utf-8"}, + "Accept": {"application/json"}, + }, + }, }, - requestInfo: &adapters.ExtraRequestInfo{ - BidderCoreName: openrtb_ext.BidderName("ortb_test_single_requestmode"), + }, + }, + { + name: "single_requestmode_add_request_params_in_request", + args: args{ + request: &openrtb2.BidRequest{ + ID: "reqid", + Imp: []openrtb2.Imp{ + {ID: "imp1", TagID: "tag1", Ext: json.RawMessage(`{"bidder": {"host": "localhost.com"}}`)}, + {ID: "imp2", TagID: "tag2", Ext: json.RawMessage(`{"bidder": {"zone": "testZone"}}`)}, + }, }, - adapterInfo: adapterInfo{config.Adapter{Endpoint: "http://test_bidder.com"}, extraAdapterInfo{RequestMode: "single"}, "testbidder"}, - bidderCfgMap: nil, + adapterInfo: func() adapterInfo { + endpoint := "http://{{.host}}" + template, _ := template.New("endpointTemplate").Parse(endpoint) + return adapterInfo{config.Adapter{Endpoint: endpoint}, extraAdapterInfo{RequestMode: "single"}, "testbidder", template} + }(), + bidderCfg: func() *bidderparams.BidderConfig { + cfg := bidderparams.NewBidderConfig() + cfg.SetRequestParams("testbidder", map[string]bidderparams.BidderParamMapper{ + "host": {Location: "server.host"}, + "zone": {Location: "ext.zone"}, + }) + return cfg + }(), }, want: want{ - errors: []error{fmt.Errorf("Found nil bidderParamsConfig")}, + requestData: []*adapters.RequestData{ + { + Method: http.MethodPost, + Uri: "http://localhost.com", + Body: []byte(`{"id":"reqid","imp":[{"ext":{"bidder":{}},"id":"imp1","tagid":"tag1"}],"server":{"host":"localhost.com"}}`), + Headers: http.Header{ + "Content-Type": {"application/json;charset=utf-8"}, + "Accept": {"application/json"}, + }, + }, + { + Method: http.MethodPost, + Uri: "http://", + Body: []byte(`{"ext":{"zone":"testZone"},"id":"reqid","imp":[{"ext":{"bidder":{}},"id":"imp2","tagid":"tag2"}]}`), + Headers: http.Header{ + "Content-Type": {"application/json;charset=utf-8"}, + "Accept": {"application/json"}, + }, + }, + }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - adapter := &adapter{adapterInfo: tt.args.adapterInfo, bidderParamsConfig: tt.args.bidderCfgMap} + adapter := &adapter{adapterInfo: tt.args.adapterInfo, bidderParamsConfig: tt.args.bidderCfg} requestData, errors := adapter.MakeRequests(tt.args.request, tt.args.requestInfo) assert.Equalf(t, tt.want.requestData, requestData, "mismatched requestData") assert.Equalf(t, tt.want.errors, errors, "mismatched errors") }) } } - func TestMakeBids(t *testing.T) { type args struct { request *openrtb2.BidRequest @@ -363,76 +500,6 @@ func TestJsonSamplesForMultiRequestMode(t *testing.T) { adapterstest.RunJSONBidderTest(t, "ortbbiddertest/owortb_generic_multi_requestmode", bidder) } -func Test_makeRequest(t *testing.T) { - type fields struct { - Adapter config.Adapter - } - type args struct { - request *openrtb2.BidRequest - requestParams map[string]bidderparams.BidderParamMapper - } - type want struct { - requestData *adapters.RequestData - err error - } - tests := []struct { - name string - fields fields - args args - want want - }{ - { - name: "valid_request", - fields: fields{ - Adapter: config.Adapter{Endpoint: "https://example.com"}, - }, - args: args{ - request: &openrtb2.BidRequest{ - ID: "123", - Imp: []openrtb2.Imp{{ID: "imp1"}}, - }, - requestParams: make(map[string]bidderparams.BidderParamMapper), - }, - want: want{ - requestData: &adapters.RequestData{ - Method: http.MethodPost, - Uri: "https://example.com", - Body: []byte(`{"id":"123","imp":[{"id":"imp1"}]}`), - Headers: http.Header{ - "Content-Type": {"application/json;charset=utf-8"}, - "Accept": {"application/json"}, - }, - }, - err: nil, - }, - }, - { - name: "nil_request", - fields: fields{ - Adapter: config.Adapter{Endpoint: "https://example.com"}, - }, - args: args{ - request: nil, - requestParams: make(map[string]bidderparams.BidderParamMapper), - }, - want: want{ - requestData: nil, - err: fmt.Errorf("found nil request"), - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - o := adapterInfo{ - Adapter: tt.fields.Adapter, - } - got, err := o.makeRequest(tt.args.request, tt.args.requestParams) - assert.Equal(t, tt.want.requestData, got, "mismatched requestData") - assert.Equal(t, tt.want.err, err, "mismatched error") - }) - } -} - func TestBuilder(t *testing.T) { InitBidderParamsConfig("../../static/bidder-params") type args struct { @@ -460,7 +527,22 @@ func TestBuilder(t *testing.T) { }, want: want{ bidder: nil, - err: fmt.Errorf("Failed to parse extra_info for bidder:[ortbbidder] err:[invalid character 'i' looking for beginning of value]"), + err: fmt.Errorf("failed to parse extra_info: expect { or n, but found i"), + }, + }, + { + name: "fails_to_parse_template_endpoint", + args: args{ + bidderName: "ortbbidder", + config: config.Adapter{ + ExtraAdapterInfo: "{}", + Endpoint: "http://{{.Host}", + }, + server: config.Server{}, + }, + want: want{ + bidder: nil, + err: fmt.Errorf("failed to parse endpoint url template: template: endpointTemplate:1: bad character U+007D '}'"), }, }, { @@ -482,6 +564,10 @@ func TestBuilder(t *testing.T) { ExtraAdapterInfo: `{"requestMode":"single"}`, }, bidderName: "ortbbidder", + endpointTemplate: func() *template.Template { + template, _ := template.New("endpointTemplate").Option("missingkey=zero").Parse("") + return template + }(), }, bidderParamsConfig: g_bidderParamsConfig, }, @@ -504,6 +590,10 @@ func TestBuilder(t *testing.T) { ExtraAdapterInfo: ``, }, bidderName: "ortbbidder", + endpointTemplate: func() *template.Template { + template, _ := template.New("endpointTemplate").Option("missingkey=zero").Parse("") + return template + }(), }, bidderParamsConfig: g_bidderParamsConfig, }, diff --git a/adapters/ortbbidder/requestBuilder.go b/adapters/ortbbidder/requestBuilder.go new file mode 100644 index 00000000000..e784c0cbc5c --- /dev/null +++ b/adapters/ortbbidder/requestBuilder.go @@ -0,0 +1,87 @@ +package ortbbidder + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + "text/template" + + "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/macros" + "github.com/prebid/prebid-server/v2/util/jsonutil" +) + +// requestBuilder is an interface containing parseRequest, makeRequest functions +type requestBuilder interface { + parseRequest(*openrtb2.BidRequest) error + makeRequest() ([]*adapters.RequestData, []error) +} + +type requestBuilderImpl struct { + endpoint string + endpointTemplate *template.Template + requestParams map[string]bidderparams.BidderParamMapper + hasMacrosInEndpoint bool + rawRequest json.RawMessage +} + +// newRequestBuilder returns the request-builder based on requestMode argument +func newRequestBuilder(requestMode, endpoint string, endpointTemplate *template.Template, requestParams map[string]bidderparams.BidderParamMapper) requestBuilder { + requestBuilder := requestBuilderImpl{ + endpoint: endpoint, + endpointTemplate: endpointTemplate, + requestParams: requestParams, + hasMacrosInEndpoint: strings.Contains(endpoint, urlMacroPrefix), + } + if requestMode == requestModeSingle { + return &multiRequestBuilder{ + requestBuilderImpl: requestBuilder, + } + } + return &singleRequestBuilder{ + requestBuilderImpl: requestBuilder, + } +} + +// getEndpoint returns the endpoint-url, if required replaces macros +func (rb *requestBuilderImpl) getEndpoint(values map[string]any) (string, error) { + if !rb.hasMacrosInEndpoint { + return rb.endpoint, nil + } + uri, err := macros.ResolveMacros(rb.endpointTemplate, values) + if err != nil { + return uri, fmt.Errorf("failed to replace macros in endpoint, err:%s", err.Error()) + } + uri = strings.ReplaceAll(uri, urlMacroNoValue, "") + return uri, err +} + +func cloneRequest(request json.RawMessage) (map[string]any, error) { + req := map[string]any{} + err := jsonutil.Unmarshal(request, &req) + if err != nil { + return nil, err + } + return req, nil +} + +// appendRequestData creates new RequestData using request and uri then appends it to requestData passed as argument +func appendRequestData(requestData []*adapters.RequestData, request map[string]any, uri string) ([]*adapters.RequestData, error) { + rawRequest, err := jsonutil.Marshal(request) + if err != nil { + return requestData, err + } + requestData = append(requestData, &adapters.RequestData{ + Method: http.MethodPost, + Uri: uri, + Body: rawRequest, + Headers: http.Header{ + "Content-Type": {"application/json;charset=utf-8"}, + "Accept": {"application/json"}, + }, + }) + return requestData, nil +} diff --git a/adapters/ortbbidder/requestBuilder_test.go b/adapters/ortbbidder/requestBuilder_test.go new file mode 100644 index 00000000000..da7240919f6 --- /dev/null +++ b/adapters/ortbbidder/requestBuilder_test.go @@ -0,0 +1,287 @@ +package ortbbidder + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "testing" + "text/template" + + "github.com/prebid/prebid-server/v2/adapters" + "github.com/prebid/prebid-server/v2/adapters/ortbbidder/bidderparams" + "github.com/stretchr/testify/assert" +) + +func TestNewRequestBuilder(t *testing.T) { + type args struct { + requestMode string + endpoint string + endpointTemplate *template.Template + requestParams map[string]bidderparams.BidderParamMapper + } + tests := []struct { + name string + args args + want requestBuilder + }{ + { + name: "singleRequestMode", + args: args{ + requestMode: requestModeSingle, + endpoint: "http://localhost/publisher", + }, + want: &multiRequestBuilder{ + requestBuilderImpl: requestBuilderImpl{ + endpoint: "http://localhost/publisher", + }, + }, + }, + + { + name: "multiRequestMode", + args: args{ + requestMode: requestModeSingle, + endpoint: "http://{{.host}}/publisher", + }, + want: &multiRequestBuilder{ + requestBuilderImpl: requestBuilderImpl{ + endpoint: "http://{{.host}}/publisher", + hasMacrosInEndpoint: true, + }, + }, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := newRequestBuilder(tt.args.requestMode, tt.args.endpoint, tt.args.endpointTemplate, tt.args.requestParams) + assert.Equalf(t, tt.want, got, "mismacthed requestbuilder") + }) + } +} + +func TestGetEndpoint(t *testing.T) { + type fields struct { + endpoint string + hasMacrosInEndpoint bool + endpointTemplate *template.Template + } + type args struct { + bidderParams map[string]any + } + type want struct { + err error + endpoint string + } + tests := []struct { + name string + fields fields + args args + want want + }{ + { + name: "macros_present_but_hasMacrosInEndpoint_is_false", + fields: fields{ + endpoint: "http://{{.host}}/publisher", + hasMacrosInEndpoint: false, + endpointTemplate: nil, + }, + args: args{ + bidderParams: map[string]any{}, + }, + want: want{ + endpoint: "http://{{.host}}/publisher", + }, + }, + { + name: "macros_present_and_bidder_params_not_present", + fields: fields{ + endpoint: "http://{{.host}}/publisher", + hasMacrosInEndpoint: true, + endpointTemplate: template.Must(template.New("endpointTemplate").Parse(`http://{{.host}}/publisher`)), + }, + args: args{ + bidderParams: map[string]any{}, + }, + want: want{ + endpoint: "http:///publisher", + }, + }, + { + name: "macros_present_and_bidder_params_present", + fields: fields{ + endpoint: "http://{{.host}}/publisher", + hasMacrosInEndpoint: true, + endpointTemplate: template.Must(template.New("endpointTemplate").Parse(`http://{{.host}}/publisher`)), + }, + args: args{ + bidderParams: map[string]any{ + "host": "localhost", + }, + }, + want: want{ + endpoint: "http://localhost/publisher", + }, + }, + { + name: "resolveMacros_returns_error", + fields: fields{ + endpoint: "http://{{.errorFunc}}/publisher", + hasMacrosInEndpoint: true, + endpointTemplate: func() *template.Template { + errorFunc := template.FuncMap{ + "errorFunc": func() (string, error) { + return "", errors.New("intentional error") + }, + } + template := template.Must(template.New("endpointTemplate").Funcs(errorFunc).Parse(`{{errorFunc}}`)) + return template + }(), + }, + args: args{ + bidderParams: map[string]any{}, + }, + want: want{ + endpoint: "", + err: fmt.Errorf("failed to replace macros in endpoint, err:template: endpointTemplate:1:2: " + + "executing \"endpointTemplate\" at : error calling errorFunc: intentional error"), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reqBuilder := &requestBuilderImpl{ + endpoint: tt.fields.endpoint, + hasMacrosInEndpoint: tt.fields.hasMacrosInEndpoint, + endpointTemplate: tt.fields.endpointTemplate, + } + endpoint, err := reqBuilder.getEndpoint(tt.args.bidderParams) + assert.Equalf(t, tt.want.endpoint, endpoint, "mismatched endpoint") + assert.Equalf(t, tt.want.err, err, "mismatched error") + }) + } +} + +func TestCloneRequest(t *testing.T) { + type args struct { + request json.RawMessage + } + type want struct { + requestNode map[string]any + err error + } + tests := []struct { + name string + args args + want want + }{ + { + name: "clone_request", + args: args{ + request: json.RawMessage(`{"id":"reqId","imps":[{"id":"impId"}]}`), + }, + want: want{ + requestNode: map[string]any{ + "id": "reqId", + "imps": []any{ + map[string]any{ + "id": "impId", + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + requstNode, err := cloneRequest(tt.args.request) + assert.Equal(t, tt.want.requestNode, requstNode, "mismatched requestnode") + assert.Equal(t, tt.want.err, err, "mismatched error") + }) + } +} + +func TestAppendRequestData(t *testing.T) { + type args struct { + requestData []*adapters.RequestData + request map[string]any + uri string + } + type want struct { + reqData []*adapters.RequestData + err error + } + tests := []struct { + name string + args args + want want + }{ + { + name: "append_request_data_to_nil_object", + args: args{ + requestData: nil, + request: map[string]any{ + "id": "reqId", + }, + uri: "http://endpoint.com", + }, + want: want{ + reqData: []*adapters.RequestData{{ + Method: http.MethodPost, + Uri: "http://endpoint.com", + Body: []byte(`{"id":"reqId"}`), + Headers: http.Header{ + "Content-Type": {"application/json;charset=utf-8"}, + "Accept": {"application/json"}, + }, + }}, + }, + }, + { + name: "append_request_data_to_non_empty_object", + args: args{ + requestData: []*adapters.RequestData{ + { + Method: http.MethodPost, + Uri: "http://endpoint.com", + Body: []byte(`{"id":"req_1"}`), + Headers: http.Header{ + "Content-Type": {"application/json;charset=utf-8"}, + "Accept": {"application/json"}, + }, + }, + }, + request: map[string]any{ + "id": "req_2", + }, + uri: "http://endpoint.com", + }, + want: want{ + reqData: []*adapters.RequestData{ + { + Method: http.MethodPost, + Uri: "http://endpoint.com", + Body: []byte(`{"id":"req_1"}`), + Headers: http.Header{ + "Content-Type": {"application/json;charset=utf-8"}, + "Accept": {"application/json"}, + }, + }, { + Method: http.MethodPost, + Uri: "http://endpoint.com", + Body: []byte(`{"id":"req_2"}`), + Headers: http.Header{ + "Content-Type": {"application/json;charset=utf-8"}, + "Accept": {"application/json"}, + }, + }}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := appendRequestData(tt.args.requestData, tt.args.request, tt.args.uri) + assert.Equal(t, tt.want.reqData, got, "mismatched request-data") + assert.Equal(t, tt.want.err, err, "mismatched error") + }) + } +} diff --git a/adapters/ortbbidder/requestParamMapper.go b/adapters/ortbbidder/requestParamMapper.go index 013db98c7c9..b453abd4bc4 100644 --- a/adapters/ortbbidder/requestParamMapper.go +++ b/adapters/ortbbidder/requestParamMapper.go @@ -1,71 +1,57 @@ package ortbbidder import ( - "encoding/json" - "fmt" + "strconv" + "strings" "github.com/prebid/prebid-server/v2/adapters/ortbbidder/bidderparams" ) -const ( - impKey = "imp" - extKey = "ext" - bidderKey = "bidder" - appsiteKey = "appsite" - siteKey = "site" - appKey = "app" -) - -// setRequestParams updates the requestBody based on the requestParams mapping details. -func setRequestParams(requestBody []byte, requestParams map[string]bidderparams.BidderParamMapper) ([]byte, error) { - if len(requestParams) == 0 { - return requestBody, nil - } - request := map[string]any{} - err := json.Unmarshal(requestBody, &request) - if err != nil { - return nil, err - } - imps, ok := request[impKey].([]any) - if !ok { - return nil, fmt.Errorf("error:[invalid_imp_found_in_requestbody], imp:[%v]", request[impKey]) - } +// setRequestParams updates the request object by mapping bidderParams at expected location. +func setRequestParams(request, params map[string]any, paramsMapper map[string]bidderparams.BidderParamMapper, paramIndices []int) bool { updatedRequest := false - for ind, imp := range imps { - request[impKey] = imp - imp, ok := imp.(map[string]any) - if !ok { - return nil, fmt.Errorf("error:[invalid_imp_found_in_implist], imp:[%v]", request[impKey]) - } - ext, ok := imp[extKey].(map[string]any) + for paramName, paramValue := range params { + paramMapper, ok := paramsMapper[paramName] if !ok { continue } - bidderParams, ok := ext[bidderKey].(map[string]any) - if !ok { - continue + // add index in path by replacing # macro + location := addIndicesInPath(paramMapper.Location, paramIndices) + // set the value in the request according to the mapping details + // remove the parameter from bidderParams after successful mapping + if setValue(request, location, paramValue) { + delete(params, paramName) + updatedRequest = true } - for paramName, paramValue := range bidderParams { - paramMapper, ok := requestParams[paramName] - if !ok { - continue - } - // set the value in the request according to the mapping details and remove the parameter. - if setValue(request, paramMapper.GetLocation(), paramValue) { - delete(bidderParams, paramName) - updatedRequest = true + } + return updatedRequest +} + +// addIndicesInPath updates the path by replacing # by arrayIndices +func addIndicesInPath(path string, indices []int) []string { + parts := strings.Split(path, ".") + j := 0 + for i, part := range parts { + if part == locationIndexMacro { + if j >= len(indices) { + break } + parts[i] = strconv.Itoa(indices[j]) + j++ } - imps[ind] = request[impKey] } - // update the impression list in the request - request[impKey] = imps - // if the request was modified, marshal it back to JSON. - if updatedRequest { - requestBody, err = json.Marshal(request) - if err != nil { - return nil, fmt.Errorf("error:[fail_to_update_request_body] msg:[%s]", err.Error()) - } + return parts +} + +// getImpExtBidderParams returns imp.ext.bidder parameters +func getImpExtBidderParams(imp map[string]any) map[string]any { + ext, ok := imp[extKey].(map[string]any) + if !ok { + return nil + } + bidderParams, ok := ext[bidderKey].(map[string]any) + if !ok { + return nil } - return requestBody, nil + return bidderParams } diff --git a/adapters/ortbbidder/requestParamMapper_test.go b/adapters/ortbbidder/requestParamMapper_test.go index 3aad8961e4b..6b19bd906c8 100644 --- a/adapters/ortbbidder/requestParamMapper_test.go +++ b/adapters/ortbbidder/requestParamMapper_test.go @@ -1,7 +1,6 @@ package ortbbidder import ( - "encoding/json" "testing" "github.com/prebid/prebid-server/v2/adapters/ortbbidder/bidderparams" @@ -10,12 +9,14 @@ import ( func TestSetRequestParams(t *testing.T) { type args struct { - requestBody []byte - mapper map[string]bidderparams.BidderParamMapper + request map[string]any + bidderParams map[string]any + paramsMapper map[string]bidderparams.BidderParamMapper + paramIndices []int } type want struct { - err string - requestBody []byte + request map[string]any + bidderParams map[string]any } tests := []struct { name string @@ -23,235 +24,210 @@ func TestSetRequestParams(t *testing.T) { want want }{ { - name: "empty_mapper", + name: "bidder_param_missing", args: args{ - requestBody: json.RawMessage(`{"imp":[{"ext":{"bidder":{}}}]}`), - }, - want: want{ - err: "", - requestBody: json.RawMessage(`{"imp":[{"ext":{"bidder":{}}}]}`), - }, - }, - { - name: "nil_requestbody", - args: args{ - requestBody: nil, - mapper: map[string]bidderparams.BidderParamMapper{ - "adunit": {}, + request: map[string]any{ + "id": "req_1", }, - }, - want: want{ - err: "unexpected end of JSON input", - }, - }, - { - name: "requestbody_has_invalid_imps", - args: args{ - requestBody: json.RawMessage(`{"imp":{"id":"1"}}`), - mapper: map[string]bidderparams.BidderParamMapper{ - "adunit": {}, + bidderParams: map[string]any{ + "param": "value", }, + paramsMapper: nil, }, want: want{ - err: "error:[invalid_imp_found_in_requestbody], imp:[map[id:1]]", - }, - }, - { - name: "missing_imp_ext", - args: args{ - requestBody: json.RawMessage(`{"imp":[{}]}`), - mapper: map[string]bidderparams.BidderParamMapper{ - "adunit": {}, + request: map[string]any{ + "id": "req_1", }, - }, - want: want{ - err: "", - requestBody: json.RawMessage(`{"imp":[{}]}`), - }, - }, - { - name: "missing_bidder_in_imp_ext", - args: args{ - requestBody: json.RawMessage(`{"imp":[{"ext":{}}]}`), - mapper: map[string]bidderparams.BidderParamMapper{ - "adunit": {}, + bidderParams: map[string]any{ + "param": "value", }, }, - want: want{ - err: "", - requestBody: json.RawMessage(`{"imp":[{"ext":{}}]}`), - }, }, { - name: "missing_bidderparams_in_imp_ext", + name: "request_level_param_set_successfully", args: args{ - requestBody: json.RawMessage(`{"imp":[{"ext":{"bidder":{}}}]}`), - mapper: map[string]bidderparams.BidderParamMapper{ - "adunit": {}, + request: map[string]any{ + "id": "req_1", }, - }, - want: want{ - err: "", - requestBody: json.RawMessage(`{"imp":[{"ext":{"bidder":{}}}]}`), - }, - }, - { - name: "mapper_not_contains_bidder_param_location", - args: args{ - requestBody: json.RawMessage(`{"imp":[{"ext":{"bidder":{"adunit":123}}}]}`), - mapper: func() map[string]bidderparams.BidderParamMapper { - bpm := bidderparams.BidderParamMapper{} - bpm.SetLocation([]string{"ext"}) + bidderParams: map[string]any{ + "param": "value", + }, + paramsMapper: func() map[string]bidderparams.BidderParamMapper { + mapper := bidderparams.BidderParamMapper{Location: "param"} return map[string]bidderparams.BidderParamMapper{ - "slot": bpm, + "param": mapper, } }(), + paramIndices: nil, }, want: want{ - err: "", - requestBody: json.RawMessage(`{"imp":[{"ext":{"bidder":{"adunit":123}}}]}`), + request: map[string]any{ + "param": "value", + "id": "req_1", + }, + bidderParams: map[string]any{}, }, }, { - name: "mapper_contains_bidder_param_location", + name: "imp_level_param_set_successfully", args: args{ - requestBody: json.RawMessage(`{"imp":[{"ext":{"bidder":{"adunit":123}}}]}`), - mapper: func() map[string]bidderparams.BidderParamMapper { - bpm := bidderparams.BidderParamMapper{} - bpm.SetLocation([]string{"ext", "adunit"}) + request: map[string]any{ + "id": "req_1", + "imp": []any{ + map[string]any{}, + }, + }, + bidderParams: map[string]any{ + "param": "value", + }, + paramsMapper: func() map[string]bidderparams.BidderParamMapper { + mapper := bidderparams.BidderParamMapper{Location: "imp.#.param"} return map[string]bidderparams.BidderParamMapper{ - "adunit": bpm, + "param": mapper, } }(), + paramIndices: []int{0}, }, want: want{ - err: "", - requestBody: json.RawMessage(`{"ext":{"adunit":123},"imp":[{"ext":{"bidder":{}}}]}`), + request: map[string]any{ + "id": "req_1", + "imp": []any{ + map[string]any{ + "param": "value", + }, + }, + }, + bidderParams: map[string]any{}, }, }, { - name: "do_not_delete_bidder_param_if_failed_to_set_value", + name: "attempt_to_set_imp_level_param_in_invalid_index_position", args: args{ - requestBody: json.RawMessage(`{"imp":[{"ext":{"bidder":{"adunit":123}}}]}`), - mapper: func() map[string]bidderparams.BidderParamMapper { - bpm := bidderparams.BidderParamMapper{} - bpm.SetLocation([]string{"req", "", ""}) + request: map[string]any{ + "id": "req_1", + "imp": []any{ + map[string]any{}, + }, + }, + bidderParams: map[string]any{ + "param": "value", + }, + paramsMapper: func() map[string]bidderparams.BidderParamMapper { + mapper := bidderparams.BidderParamMapper{Location: "imp.#.param"} return map[string]bidderparams.BidderParamMapper{ - "adunit": bpm, + "param": mapper, } }(), + paramIndices: []int{1}, }, want: want{ - err: "", - requestBody: json.RawMessage(`{"imp":[{"ext":{"bidder":{"adunit":123}}}]}`), + request: map[string]any{ + "id": "req_1", + "imp": []any{ + map[string]any{}, + }, + }, + bidderParams: map[string]any{ + "param": "value", + }, }, }, { - name: "set_multiple_bidder_params", + name: "attempt_to_set_imp_level_param_when_no_index_is_given", args: args{ - requestBody: json.RawMessage(`{"app":{"name":"sampleapp"},"imp":[{"tagid":"oldtagid","ext":{"bidder":{"paramWithoutLocation":"value","adunit":123,"slot":"test_slot","wrapper":{"pubid":5890,"profile":1}}}}]}`), - mapper: func() map[string]bidderparams.BidderParamMapper { - adunit := bidderparams.BidderParamMapper{} - adunit.SetLocation([]string{"adunit", "id"}) - slot := bidderparams.BidderParamMapper{} - slot.SetLocation([]string{"imp", "tagid"}) - wrapper := bidderparams.BidderParamMapper{} - wrapper.SetLocation([]string{"app", "ext"}) + request: map[string]any{ + "id": "req_1", + "imp": []any{ + map[string]any{}, + }, + }, + bidderParams: map[string]any{ + "param": "value", + }, + paramsMapper: func() map[string]bidderparams.BidderParamMapper { + mapper := bidderparams.BidderParamMapper{Location: "imp.#.param"} return map[string]bidderparams.BidderParamMapper{ - "adunit": adunit, - "slot": slot, - "wrapper": wrapper, + "param": mapper, } }(), + paramIndices: []int{}, }, want: want{ - err: "", - requestBody: json.RawMessage(`{"adunit":{"id":123},"app":{"ext":{"profile":1,"pubid":5890},"name":"sampleapp"},"imp":[{"ext":{"bidder":{"paramWithoutLocation":"value"}},"tagid":"test_slot"}]}`), + request: map[string]any{ + "id": "req_1", + "imp": []any{ + map[string]any{}, + }, + }, + bidderParams: map[string]any{ + "param": "value", + }, }, }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + setRequestParams(tt.args.request, tt.args.bidderParams, tt.args.paramsMapper, tt.args.paramIndices) + assert.Equal(t, tt.want.bidderParams, tt.args.bidderParams, "mismatched bidderparams") + assert.Equal(t, tt.want.request, tt.args.request, "mismatched request") + }) + } +} + +func TestGetImpExtBidderParams(t *testing.T) { + type args struct { + imp map[string]any + } + tests := []struct { + name string + args args + want map[string]any + }{ { - name: "conditional_mapping_set_app_object", + name: "ext_key_absent_in_imp", args: args{ - requestBody: json.RawMessage(`{"app":{"name":"sampleapp"},"imp":[{"tagid":"oldtagid","ext":{"bidder":{"paramWithoutLocation":"value","adunit":123,"slot":"test_slot","wrapper":{"pubid":5890,"profile":1}}}}]}`), - mapper: func() map[string]bidderparams.BidderParamMapper { - bpm := bidderparams.BidderParamMapper{} - bpm.SetLocation([]string{"appsite", "wrapper"}) - return map[string]bidderparams.BidderParamMapper{ - "wrapper": bpm, - } - }(), - }, - want: want{ - err: "", - requestBody: json.RawMessage(`{"app":{"name":"sampleapp","wrapper":{"profile":1,"pubid":5890}},"imp":[{"ext":{"bidder":{"adunit":123,"paramWithoutLocation":"value","slot":"test_slot"}},"tagid":"oldtagid"}]}`), + imp: map[string]any{}, }, + want: nil, }, { - name: "conditional_mapping_set_site_object", + name: "invalid_ext_key_in_imp", args: args{ - requestBody: json.RawMessage(`{"site":{"name":"sampleapp"},"imp":[{"tagid":"oldtagid","ext":{"bidder":{"paramWithoutLocation":"value","adunit":123,"slot":"test_slot","wrapper":{"pubid":5890,"profile":1}}}}]}`), - mapper: func() map[string]bidderparams.BidderParamMapper { - bpm := bidderparams.BidderParamMapper{} - bpm.SetLocation([]string{"appsite", "wrapper"}) - return map[string]bidderparams.BidderParamMapper{ - "wrapper": bpm, - } - }(), - }, - want: want{ - err: "", - requestBody: json.RawMessage(`{"imp":[{"ext":{"bidder":{"adunit":123,"paramWithoutLocation":"value","slot":"test_slot"}},"tagid":"oldtagid"}],"site":{"name":"sampleapp","wrapper":{"profile":1,"pubid":5890}}}`), + imp: map[string]any{ + "ext": "invalid", + }, }, + want: nil, }, { - name: "multi_imps_bidder_params_mapping", + name: "bidder_key_absent_in_imp_ext", args: args{ - requestBody: json.RawMessage(`{"app":{"name":"sampleapp"},"imp":[{"tagid":"tagid_1","ext":{"bidder":{"paramWithoutLocation":"value","adunit":111,"slot":"test_slot_1","wrapper":{"pubid":5890,"profile":1}}}},{"tagid":"tagid_2","ext":{"bidder":{"slot":"test_slot_2","adunit":222}}}]}`), - mapper: func() map[string]bidderparams.BidderParamMapper { - adunit := bidderparams.BidderParamMapper{} - adunit.SetLocation([]string{"adunit", "id"}) - slot := bidderparams.BidderParamMapper{} - slot.SetLocation([]string{"imp", "tagid"}) - wrapper := bidderparams.BidderParamMapper{} - wrapper.SetLocation([]string{"app", "ext"}) - return map[string]bidderparams.BidderParamMapper{ - "adunit": adunit, - "slot": slot, - "wrapper": wrapper, - } - }(), - }, - want: want{ - err: "", - requestBody: json.RawMessage(`{"adunit":{"id":222},"app":{"ext":{"profile":1,"pubid":5890},"name":"sampleapp"},"imp":[{"ext":{"bidder":{"paramWithoutLocation":"value"}},"tagid":"test_slot_1"},{"ext":{"bidder":{}},"tagid":"test_slot_2"}]}`), + imp: map[string]any{ + "ext": map[string]any{}, + }, }, + want: nil, }, { - name: "multi_imps_bidder_params_mapping_override_if_same_param_present", + name: "bidder_key_present_in_imp_ext", args: args{ - requestBody: json.RawMessage(`{"app":{"name":"sampleapp"},"imp":[{"tagid":"tagid_1","ext":{"bidder":{"paramWithoutLocation":"value","adunit":111}}},{"tagid":"tagid_2","ext":{"bidder":{"adunit":222}}}]}`), - mapper: func() map[string]bidderparams.BidderParamMapper { - bpm := bidderparams.BidderParamMapper{} - bpm.SetLocation([]string{"adunit", "id"}) - return map[string]bidderparams.BidderParamMapper{ - "adunit": bpm, - } - }(), + imp: map[string]any{ + "ext": map[string]any{ + "bidder": map[string]any{ + "param": "value", + }, + }, + }, }, - want: want{ - err: "", - requestBody: json.RawMessage(`{"adunit":{"id":222},"app":{"name":"sampleapp"},"imp":[{"ext":{"bidder":{"paramWithoutLocation":"value"}},"tagid":"tagid_1"},{"ext":{"bidder":{}},"tagid":"tagid_2"}]}`), + want: map[string]any{ + "param": "value", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := setRequestParams(tt.args.requestBody, tt.args.mapper) - assert.Equal(t, string(tt.want.requestBody), string(got), "mismatched request body") - assert.Equal(t, len(tt.want.err) == 0, err == nil, "mismatched error") - if err != nil { - assert.Equal(t, err.Error(), tt.want.err, "mismatched error string") - } + got := getImpExtBidderParams(tt.args.imp) + assert.Equal(t, tt.want, got, "mismatched bidder-params") }) } } diff --git a/adapters/ortbbidder/singleRequestBuilder.go b/adapters/ortbbidder/singleRequestBuilder.go new file mode 100644 index 00000000000..cf0fb79ed0e --- /dev/null +++ b/adapters/ortbbidder/singleRequestBuilder.go @@ -0,0 +1,75 @@ +package ortbbidder + +import ( + "fmt" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/adapters" + "github.com/prebid/prebid-server/v2/util/jsonutil" +) + +// struct to build the single request containing multi impressions when requestMode="multi" +type singleRequestBuilder struct { + requestBuilderImpl + newRequest map[string]any + imps []map[string]any +} + +// parseRequest parse the incoming request and populates intermediate fields required for building requestData object +func (rb *singleRequestBuilder) parseRequest(request *openrtb2.BidRequest) (err error) { + rb.rawRequest, err = jsonutil.Marshal(request) + if err != nil { + return err + } + + rb.newRequest, err = cloneRequest(rb.rawRequest) + if err != nil { + return err + } + + imps, ok := rb.newRequest[impKey].([]any) + if !ok { + return errImpMissing + } + for index, imp := range imps { + imp, ok := imp.(map[string]any) + if !ok { + return fmt.Errorf("invalid imp found at index:%d", index) + } + rb.imps = append(rb.imps, imp) + } + return +} + +// makeRequest constructs the endpoint URL and maps the bidder-parameters in request to create the RequestData objects. +// 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())) + return + } + + var ( + endpoint string + err error + ) + + //step 1: get endpoint + if endpoint, err = rb.getEndpoint(getImpExtBidderParams(rb.imps[0])); err != nil { + errs = append(errs, newBadInputError(err.Error())) + return nil, errs + } + + //step 2: replace parameters + // iterate through imps in reverse order to ensure setRequestParams prioritizes + // the parameters from imp[0].ext.bidder over those from imp[1..N].ext.bidder. + for index := len(rb.imps) - 1; index >= 0; index-- { + setRequestParams(rb.newRequest, getImpExtBidderParams(rb.imps[index]), rb.requestParams, []int{index}) + } + + //step 3: append new request data + if requestData, err = appendRequestData(requestData, rb.newRequest, endpoint); err != nil { + errs = append(errs, newBadInputError(err.Error())) + } + return requestData, errs +} diff --git a/adapters/ortbbidder/singleRequestBuilder_test.go b/adapters/ortbbidder/singleRequestBuilder_test.go new file mode 100644 index 00000000000..50e02ddeea1 --- /dev/null +++ b/adapters/ortbbidder/singleRequestBuilder_test.go @@ -0,0 +1,290 @@ +package ortbbidder + +import ( + "encoding/json" + "errors" + "net/http" + "testing" + "text/template" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/adapters" + "github.com/prebid/prebid-server/v2/adapters/ortbbidder/bidderparams" + "github.com/stretchr/testify/assert" +) + +func TestSingleRequestBuilderParseRequest(t *testing.T) { + type args struct { + request *openrtb2.BidRequest + } + type want struct { + err error + rawRequest json.RawMessage + newRequest map[string]any + imps []map[string]any + } + tests := []struct { + name string + args args + want want + }{ + { + name: "request_without_imps", + args: args{ + request: &openrtb2.BidRequest{ + ID: "id", + }, + }, + want: want{ + err: errImpMissing, + rawRequest: json.RawMessage(`{"id":"id","imp":null}`), + imps: nil, + newRequest: map[string]any{ + "id": "id", + "imp": nil, + }, + }, + }, + { + name: "request_is_valid", + args: args{ + request: &openrtb2.BidRequest{ + ID: "id", + Imp: []openrtb2.Imp{ + { + ID: "imp_1", + }, + }, + }, + }, + want: want{ + err: nil, + rawRequest: json.RawMessage(`{"id":"id","imp":[{"id":"imp_1"}]}`), + newRequest: map[string]any{ + "id": "id", + "imp": []any{map[string]any{ + "id": "imp_1", + }}, + }, + imps: []map[string]any{ + { + "id": "imp_1", + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reqBuilder := &singleRequestBuilder{} + err := reqBuilder.parseRequest(tt.args.request) + assert.Equalf(t, tt.want.err, err, "mismatched error") + assert.Equalf(t, string(tt.want.rawRequest), string(reqBuilder.rawRequest), "mismatched rawRequest") + assert.Equalf(t, tt.want.imps, reqBuilder.imps, "mismatched imps") + assert.Equalf(t, tt.want.newRequest, reqBuilder.newRequest, "mismatched newRequest") + }) + } +} + +func TestSingleRequestBuilderMakeRequest(t *testing.T) { + type fields struct { + requestBuilder singleRequestBuilder + } + type want struct { + requestData []*adapters.RequestData + errs []error + } + tests := []struct { + name string + fields fields + want want + }{ + { + name: "no_imps", + fields: fields{ + requestBuilder: singleRequestBuilder{ + requestBuilderImpl: requestBuilderImpl{ + rawRequest: nil, + }, + imps: nil, + }, + }, + want: want{ + requestData: nil, + errs: []error{newBadInputError(errImpMissing.Error())}, + }, + }, + { + name: "replace_macros_to_form_endpoint_url", + fields: fields{ + requestBuilder: singleRequestBuilder{ + requestBuilderImpl: requestBuilderImpl{ + hasMacrosInEndpoint: true, + rawRequest: json.RawMessage(`{"imp":[{"ext":{"bidder":{"ext":{"pubid":5890},"host":"localhost.com"}},"id":"imp_1"}]}`), + endpointTemplate: template.Must(template.New("endpointTemplate").Parse(`http://{{.host}}/publisher/{{.ext.pubid}}`)), + }, + newRequest: make(map[string]any), + imps: []map[string]any{ + { + "ext": map[string]any{ + "bidder": map[string]any{ + "ext": map[string]any{"pubid": 5890}, + "host": "localhost.com", + }, + }, + "id": "imp_1", + }, + }, + }, + }, + want: want{ + requestData: []*adapters.RequestData{ + { + Method: http.MethodPost, + Uri: "http://localhost.com/publisher/5890", + Body: json.RawMessage(`{"imp":[{"ext":{"bidder":{"ext":{"pubid":5890},"host":"localhost.com"}},"id":"imp_1"}]}`), + Headers: http.Header{ + "Content-Type": {"application/json;charset=utf-8"}, + "Accept": {"application/json"}, + }, + }, + }, + errs: nil, + }, + }, + { + name: "macros_value_absent_in_bidder_params", + fields: fields{ + requestBuilder: singleRequestBuilder{ + requestBuilderImpl: requestBuilderImpl{ + hasMacrosInEndpoint: true, + rawRequest: json.RawMessage(`{"imp":[{"ext":{},"id":"imp_1"}]}`), + endpointTemplate: template.Must(template.New("endpointTemplate").Option("missingkey=default").Parse(`http://{{.host}}/publisher/{{.pubid}}`)), + }, + newRequest: make(map[string]any), + imps: []map[string]any{ + { + "ext": map[string]any{}, + "id": "imp_1", + }, + }, + }, + }, + want: want{ + requestData: []*adapters.RequestData{ + { + Method: http.MethodPost, + Uri: "http:///publisher/", + Body: json.RawMessage(`{"imp":[{"ext":{},"id":"imp_1"}]}`), + Headers: http.Header{ + "Content-Type": {"application/json;charset=utf-8"}, + "Accept": {"application/json"}, + }, + }, + }, + errs: nil, + }, + }, + { + name: "buildEndpoint_returns_error", + fields: fields{ + requestBuilder: singleRequestBuilder{ + requestBuilderImpl: requestBuilderImpl{ + hasMacrosInEndpoint: true, + rawRequest: json.RawMessage(`{"imp":[{"ext":{},"id":"imp_1"}]}`), + endpointTemplate: func() *template.Template { + errorFunc := template.FuncMap{ + "errorFunc": func() (string, error) { + return "", errors.New("intentional error") + }, + } + t := template.Must(template.New("endpointTemplate").Funcs(errorFunc).Parse(`{{errorFunc}}`)) + return t + }(), + }, + newRequest: make(map[string]any), + imps: []map[string]any{ + { + "ext": map[string]any{}, + "id": "imp_1", + }, + }, + }, + }, + want: want{ + requestData: nil, + errs: []error{newBadInputError("failed to replace macros in endpoint, err:template: endpointTemplate:1:2: " + + "executing \"endpointTemplate\" at : error calling errorFunc: intentional error")}, + }, + }, + { + name: "multi_imps_request", + fields: fields{ + requestBuilder: singleRequestBuilder{ + requestBuilderImpl: requestBuilderImpl{ + hasMacrosInEndpoint: true, + rawRequest: json.RawMessage(`{"imp":[{"ext":{"bidder":{"ext":{"pubid":1111},"host":"imp1.host.com"}},"id":"imp_1"},{"ext":{"bidder":{"ext":{"pubid":2222},"host":"imp2.host.com"}},"id":"imp_2"}]}`), + endpointTemplate: template.Must(template.New("endpointTemplate").Parse(`http://{{.host}}/publisher/{{.ext.pubid}}`)), + requestParams: func() map[string]bidderparams.BidderParamMapper { + hostMapper := bidderparams.BidderParamMapper{Location: "host"} + extMapper := bidderparams.BidderParamMapper{Location: "device"} + return map[string]bidderparams.BidderParamMapper{ + "host": hostMapper, + "ext": extMapper, + } + }(), + }, + newRequest: make(map[string]any), + imps: []map[string]any{ + { + "ext": map[string]any{ + "bidder": map[string]any{ + "ext": map[string]any{ + "pubid": 1111, + }, + "host": "imp1.host.com", + }, + }, + "id": "imp_1", + }, + { + "ext": map[string]any{ + "bidder": map[string]any{ + "ext": map[string]any{ + "pubid": 2222, + }, + "host": "imp2.host.com", + }, + }, + "id": "imp_2", + }, + }, + }, + }, + want: want{ + requestData: []*adapters.RequestData{ + { + Method: http.MethodPost, + Uri: "http://imp1.host.com/publisher/1111", + Body: json.RawMessage(`{"device":{"pubid":1111},"host":"imp1.host.com","imp":[{"ext":{"bidder":{}},"id":"imp_1"},{"ext":{"bidder":{}},"id":"imp_2"}]}`), + Headers: http.Header{ + "Content-Type": {"application/json;charset=utf-8"}, + "Accept": {"application/json"}, + }, + }, + }, + errs: nil, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.fields.requestBuilder.newRequest != nil { + tt.fields.requestBuilder.newRequest[impKey] = tt.fields.requestBuilder.imps + } + requestData, errs := tt.fields.requestBuilder.makeRequest() + assert.Equalf(t, tt.want.requestData, requestData, "mismatched requestData") + assert.Equalf(t, tt.want.errs, errs, "mismatched errs") + }) + } +} diff --git a/adapters/ortbbidder/util.go b/adapters/ortbbidder/util.go index 5b2dccc67bf..1aa5881b073 100644 --- a/adapters/ortbbidder/util.go +++ b/adapters/ortbbidder/util.go @@ -1,5 +1,9 @@ package ortbbidder +import ( + "strconv" +) + /* setValue updates or creates a value in a node based on a specified location. The location is a string that specifies a path through the node hierarchy, @@ -8,56 +12,70 @@ create intermediate nodes as necessary if they do not exist. Arguments: - node: the root of the map in which to set the value -- locations: slice of strings indicating the path to set the value. +- location: slice of strings indicating the path to set the value. - value: The value to set at the specified location. Can be of any type. Example: - - location = imp.ext.adunitid; value = 123 ==> {"imp": {"ext" : {"adunitid":123}}} + - location = imp.0.ext.adunitid; value = 123 ==> {"imp": {"ext" : {"adunitid":123}}} */ -func setValue(node map[string]any, locations []string, value any) bool { - if value == nil || len(locations) == 0 { +func setValue(node map[string]any, location []string, value any) bool { + if node == nil || value == nil { return false } - - lastNodeIndex := len(locations) - 1 - currentNode := node - - for index, loc := range locations { - if len(loc) == 0 { // if location part is empty string + var nextNode any = node + lastNodeIndex := len(location) - 1 + for locIndex, loc := range location { + if len(loc) == 0 { + // if location is invalid return false } - if index == lastNodeIndex { // if it's the last part in location, set the value - currentNode[loc] = value - break - } - nextNode := getNode(currentNode, loc) - // not the last part, navigate deeper - if nextNode == nil { - // loc does not exist, set currentNode to a new node - newNode := make(map[string]any) - currentNode[loc] = newNode - currentNode = newNode - continue - } - // loc exists, set currentNode to nextNode - nextNodeTyped, ok := nextNode.(map[string]any) - if !ok { + switch nextNodeTyped := nextNode.(type) { + case map[string]any: + if locIndex == lastNodeIndex { + // set value at last index + nextNodeTyped[loc] = value + return true + } + nextNode = getNode(nextNodeTyped, loc) + if nextNode == nil { + // create a new node if the next node does not exist + newNode := make(map[string]any) + nextNodeTyped[loc] = newNode + nextNode = newNode + } + case []any: + // extract the array nodeIndex from the location to determine where to set the value + nodeIndex, err := strconv.Atoi(loc) + if err != nil || nodeIndex < 0 || nodeIndex >= len(nextNodeTyped) { + return false + } + if locIndex == lastNodeIndex { + nextNodeTyped[nodeIndex] = value + return true + } + nextNode = nextNodeTyped[nodeIndex] + if nextNode == nil { + // create a new node if the next node does not exist + newNode := make(map[string]any) + nextNodeTyped[nodeIndex] = newNode + nextNode = newNode + } + default: return false } - currentNode = nextNodeTyped } - return true + return false } -// getNode retrieves the value for a given key from a map with special handling for the "appsite" key -func getNode(nodes map[string]any, key string) any { +// getNode retrieves the value for a given key from a map with special handling for the "appsite", "imp" key +func getNode(requestNode map[string]any, key string) any { switch key { case appsiteKey: // if key is "appsite" and if nodes contains "site" object then return nodes["site"] else return nodes["app"] - if value, ok := nodes[siteKey]; ok { + if value, ok := requestNode[siteKey]; ok { return value } - return nodes[appKey] + return requestNode[appKey] } - return nodes[key] + return requestNode[key] } diff --git a/adapters/ortbbidder/util_test.go b/adapters/ortbbidder/util_test.go index 059303f6dfb..46792c7aa77 100644 --- a/adapters/ortbbidder/util_test.go +++ b/adapters/ortbbidder/util_test.go @@ -8,9 +8,9 @@ import ( func TestSetValue(t *testing.T) { type args struct { - node map[string]any - location []string - value any + requestNode map[string]any + location []string + value any } type want struct { node map[string]any @@ -24,9 +24,9 @@ func TestSetValue(t *testing.T) { { name: "set_nil_value", args: args{ - node: map[string]any{}, - location: []string{"key"}, - value: nil, + requestNode: map[string]any{}, + location: []string{"key"}, + value: nil, }, want: want{ status: false, @@ -36,9 +36,9 @@ func TestSetValue(t *testing.T) { { name: "set_value_in_empty_location", args: args{ - node: map[string]any{}, - location: []string{}, - value: 123, + requestNode: map[string]any{}, + location: []string{}, + value: 123, }, want: want{ status: false, @@ -48,9 +48,9 @@ func TestSetValue(t *testing.T) { { name: "set_value_in_invalid_location_modifies_node", args: args{ - node: map[string]any{}, - location: []string{"key", ""}, - value: 123, + requestNode: map[string]any{}, + location: []string{"key", ""}, + value: 123, }, want: want{ status: false, @@ -62,9 +62,9 @@ func TestSetValue(t *testing.T) { { name: "set_value_at_root_level_in_empty_node", args: args{ - node: map[string]any{}, - location: []string{"key"}, - value: 123, + requestNode: map[string]any{}, + location: []string{"key"}, + value: 123, }, want: want{ status: true, @@ -74,9 +74,9 @@ func TestSetValue(t *testing.T) { { name: "set_value_at_root_level_in_non-empty_node", args: args{ - node: map[string]any{"oldKey": "oldValue"}, - location: []string{"key"}, - value: 123, + requestNode: map[string]any{"oldKey": "oldValue"}, + location: []string{"key"}, + value: 123, }, want: want{ status: true, @@ -86,9 +86,9 @@ func TestSetValue(t *testing.T) { { name: "set_value_at_non-root_level_in_non-json_node", args: args{ - node: map[string]any{"rootKey": "rootValue"}, - location: []string{"rootKey", "key"}, - value: 123, + requestNode: map[string]any{"rootKey": "rootValue"}, + location: []string{"rootKey", "key"}, + value: 123, }, want: want{ status: false, @@ -98,7 +98,7 @@ func TestSetValue(t *testing.T) { { name: "set_value_at_non-root_level_in_json_node", args: args{ - node: map[string]any{"rootKey": map[string]any{ + requestNode: map[string]any{"rootKey": map[string]any{ "oldKey": "oldValue", }}, location: []string{"rootKey", "newKey"}, @@ -115,7 +115,7 @@ func TestSetValue(t *testing.T) { { name: "set_value_at_non-root_level_in_nested-json_node", args: args{ - node: map[string]any{"rootKey": map[string]any{ + requestNode: map[string]any{"rootKey": map[string]any{ "parentKey1": map[string]any{ "innerKey": "innerValue", }, @@ -136,7 +136,7 @@ func TestSetValue(t *testing.T) { { name: "override_existing_key's_value", args: args{ - node: map[string]any{"rootKey": map[string]any{ + requestNode: map[string]any{"rootKey": map[string]any{ "parentKey": map[string]any{ "innerKey": "innerValue", }, @@ -154,7 +154,7 @@ func TestSetValue(t *testing.T) { { name: "appsite_key_app_object_present", args: args{ - node: map[string]any{"app": map[string]any{ + requestNode: map[string]any{"app": map[string]any{ "parentKey": "oldValue", }}, location: []string{"appsite", "parentKey"}, @@ -170,7 +170,7 @@ func TestSetValue(t *testing.T) { { name: "appsite_key_site_object_present", args: args{ - node: map[string]any{"site": map[string]any{ + requestNode: map[string]any{"site": map[string]any{ "parentKey": "oldValue", }}, location: []string{"appsite", "parentKey"}, @@ -183,11 +183,154 @@ func TestSetValue(t *testing.T) { }}, }, }, + { + name: "request_has_list_of_interface", + args: args{ + requestNode: map[string]any{ + "id": "req_1", + "imp": []any{ + map[string]any{ + "id": "imp_1", + }, + }, + }, + location: []string{"imp", "0", "ext"}, + value: "value", + }, + want: want{ + status: true, + node: map[string]any{ + "id": "req_1", + "imp": []any{ + map[string]any{ + "id": "imp_1", + "ext": "value", + }, + }, + }, + }, + }, + { + name: "request_has_list_of_interface_with_multi_items", + args: args{ + requestNode: map[string]any{ + "id": "req_1", + "imp": []any{ + map[string]any{ + "id": "imp_1", + }, + map[string]any{ + "id": "imp_2", + }, + }, + }, + location: []string{"imp", "1", "ext"}, + value: "value", + }, + want: want{ + status: true, + node: map[string]any{ + "id": "req_1", + "imp": []any{ + map[string]any{ + "id": "imp_1", + }, + map[string]any{ + "id": "imp_2", + "ext": "value", + }, + }, + }, + }, + }, + { + name: "request_has_list_of_interface_with_multi_items_but_invalid_index_to_update", + args: args{ + requestNode: map[string]any{ + "id": "req_1", + "imp": []any{ + map[string]any{ + "id": "imp_1", + }, + }, + }, + location: []string{"imp", "3", "ext"}, + value: "value", + }, + want: want{ + status: false, + node: map[string]any{ + "id": "req_1", + "imp": []any{ + map[string]any{ + "id": "imp_1", + }, + }, + }, + }, + }, + { + name: "request_has_list_of_interface_with_multi_items_but_valid_index_to_update", + args: args{ + requestNode: map[string]any{ + "id": "req_1", + "imp": []any{ + map[string]any{ + "id": "imp_1", + }, + }, + }, + location: []string{"imp", "0"}, + value: map[string]any{ + "id": "updated_id", + }, + }, + want: want{ + status: true, + node: map[string]any{ + "id": "req_1", + "imp": []any{ + map[string]any{ + "id": "updated_id", + }, + }, + }, + }, + }, + { + name: "request_has_list_of_interface_where_new_node_need_to_be_created", + args: args{ + requestNode: map[string]any{ + "id": "req_1", + "imp": []any{ + nil, nil, + }, + }, + location: []string{"imp", "0", "ext"}, + value: map[string]any{ + "id": "updated_id", + }, + }, + want: want{ + status: true, + node: map[string]any{ + "id": "req_1", + "imp": []any{ + map[string]any{ + "ext": map[string]any{ + "id": "updated_id", + }, + }, + nil, + }, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := setValue(tt.args.node, tt.args.location, tt.args.value) - assert.Equalf(t, tt.want.node, tt.args.node, "SetValue failed to update node object") + got := setValue(tt.args.requestNode, tt.args.location, tt.args.value) + assert.Equalf(t, tt.want.node, tt.args.requestNode, "SetValue failed to update node object") assert.Equalf(t, tt.want.status, got, "SetValue returned invalid status") }) } @@ -195,8 +338,8 @@ func TestSetValue(t *testing.T) { func TestGetNode(t *testing.T) { type args struct { - nodes map[string]any - key string + requestNode map[string]any + key string } tests := []struct { name string @@ -206,7 +349,7 @@ func TestGetNode(t *testing.T) { { name: "appsite_key_present_when_app_object_present", args: args{ - nodes: map[string]any{"app": map[string]any{ + requestNode: map[string]any{"app": map[string]any{ "parentKey": "oldValue", }}, key: "appsite", @@ -216,7 +359,7 @@ func TestGetNode(t *testing.T) { { name: "appsite_key_present_when_site_object_present", args: args{ - nodes: map[string]any{"site": map[string]any{ + requestNode: map[string]any{"site": map[string]any{ "siteKey": "siteValue", }}, key: "appsite", @@ -226,17 +369,38 @@ func TestGetNode(t *testing.T) { { name: "appsite_key_absent", args: args{ - nodes: map[string]any{"device": map[string]any{ + requestNode: map[string]any{"device": map[string]any{ "deviceKey": "deviceVal", }}, key: "appsite", }, want: nil, }, + { + name: "imp_key_present", + args: args{ + requestNode: map[string]any{ + "device": map[string]any{ + "deviceKey": "deviceVal", + }, + "imp": []any{ + map[string]any{ + "id": "imp_1", + }, + }, + }, + key: "imp", + }, + want: []any{ + map[string]any{ + "id": "imp_1", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - node := getNode(tt.args.nodes, tt.args.key) + node := getNode(tt.args.requestNode, tt.args.key) assert.Equal(t, tt.want, node) }) } diff --git a/config/bidderinfo.go b/config/bidderinfo.go index d78f5722552..68e6a10a72e 100644 --- a/config/bidderinfo.go +++ b/config/bidderinfo.go @@ -426,6 +426,10 @@ func validateAdapterEndpoint(endpoint string, bidderName string, errs []error) [ if err != nil { return append(errs, fmt.Errorf("Invalid endpoint template: %s for adapter: %s. %v", endpoint, bidderName, err)) } + // OW specific : do not perform endpoint validation if bidder is an oRTB bidder + if strings.HasPrefix(bidderName, "owortb_") { + return nil + } // Resolve macros (if any) in the endpoint URL resolvedEndpoint, err := macros.ResolveMacros(endpointTemplate, testEndpointTemplateParams) if err != nil { diff --git a/exchange/exchange.go b/exchange/exchange.go index 2e42e685bf2..02afe83a620 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -759,7 +759,6 @@ func (e *exchange) getAllBids( reqInfo := adapters.NewExtraRequestInfo(conversions) reqInfo.PbsEntryPoint = bidderRequest.BidderLabels.RType reqInfo.GlobalPrivacyControlHeader = globalPrivacyControlHeader - reqInfo.BidderCoreName = bidderRequest.BidderCoreName // OW specific: required for oRTB bidder bidReqOptions := bidRequestOptions{ accountDebugAllowed: accountDebugAllowed, diff --git a/static/bidder-info/owortb_testbidder.yaml b/static/bidder-info/owortb_testbidder.yaml index a8175136b10..05e501aa50d 100644 --- a/static/bidder-info/owortb_testbidder.yaml +++ b/static/bidder-info/owortb_testbidder.yaml @@ -1,7 +1,7 @@ # sample bidder-info yaml for testbidder (oRTB Integration) maintainer: email: "header-bidding@pubmatic.com" -endpoint: "http://test.endpoint.com" +endpoint: "http://{{.host}}.endpoint.com/zone={{.zone}}" capabilities: app: mediaTypes: diff --git a/static/bidder-params/owortb_testbidder.json b/static/bidder-params/owortb_testbidder.json index b0ce3c53aed..2013927a55c 100644 --- a/static/bidder-params/owortb_testbidder.json +++ b/static/bidder-params/owortb_testbidder.json @@ -4,11 +4,61 @@ "description": "A schema which validates params accepted by the testbidder (oRTB Integration)", "type": "object", "properties": { - "adunitID": { + "adunit": { "type": "string", "description": "adunitID param", - "location": "ext.adunit.id" + "location": "id" + }, + "tagid": { + "type": "string", + "description": "tagid param", + "location": "imp.#.tagid" + }, + "zone": { + "type": "string", + "description": "zone param", + "location": "appsite.id" + }, + "maxduration": { + "type": "integer", + "description": "maxduration param", + "location": "imp.#.video.maxduration" + }, + "livestream": { + "type": "integer", + "description": "livestream param", + "location": "appsite.cnt.livestream" + }, + "url": { + "type": "string", + "description": "URL param setting in video-startdelay", + "location": "imp.#.video.startdelay" + }, + "randomKey": { + "type": "string", + "description": "randomKey param", + "location": "content.data" + }, + "host": { + "type": "string", + "description": "host param", + "location": "ext.server.host" + }, + "wrapper": { + "type": "object", + "description": "Specifies configuration for a publisher", + "properties": { + "profile": { + "type": "integer", + "description": "An ID which identifies the openwrap profile of publisher" + }, + "version": { + "type": "integer", + "description": "An ID which identifies version of the openwrap profile" + } + }, + "location": "device.ext.publisherWrapper" } }, - "required": ["adunitID"] + "required": ["adunit"] } \ No newline at end of file