diff --git a/.github/workflows/helpers/pull-request-utils.js b/.github/workflows/helpers/pull-request-utils.js index 73c80396473..de49e91a3ee 100644 --- a/.github/workflows/helpers/pull-request-utils.js +++ b/.github/workflows/helpers/pull-request-utils.js @@ -43,6 +43,10 @@ class diffHelper { let diff = {} for (const { filename, patch } of files) { if (this.fileNameFilter(filename)) { + if (!patch) { + console.log(`No patch found for file: ${filename}`) + continue + } const lines = patch.split("\n") if (lines.length === 1) { continue diff --git a/adapters/ortbbidder/bidderparams/config.go b/adapters/ortbbidder/bidderparams/config.go index 94f1f9ba9af..5f00f1fa960 100644 --- a/adapters/ortbbidder/bidderparams/config.go +++ b/adapters/ortbbidder/bidderparams/config.go @@ -5,40 +5,44 @@ type BidderParamMapper struct { Location string // do not update this parameter for each request, its being shared across all requests } -// config contains mappings requestParams and responseParams -type config struct { - requestParams map[string]BidderParamMapper - responseParams map[string]BidderParamMapper +// Config contains mappings RequestParams and ResponseParams +type Config struct { + RequestParams map[string]BidderParamMapper + ResponseParams map[string]BidderParamMapper } -// BidderConfig contains map of bidderName to its requestParams and responseParams +// BidderConfig contains map of bidderName to its RequestParams and ResponseParams type BidderConfig struct { - bidderConfigMap map[string]*config + BidderConfigMap map[string]*Config } // NewBidderConfig initializes and returns the object of BidderConfig func NewBidderConfig() *BidderConfig { return &BidderConfig{ - bidderConfigMap: make(map[string]*config), + 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{} +// GetRequestParams returns bidder specific ResponseParams +func (bcfg *BidderConfig) GetRequestParams(bidderName string) map[string]BidderParamMapper { + if len(bcfg.BidderConfigMap) == 0 { + return nil } - bcfg.bidderConfigMap[bidderName].requestParams = requestParams + bidderConfig := bcfg.BidderConfigMap[bidderName] + if bidderConfig == nil { + return nil + } + return bidderConfig.RequestParams } -// GetRequestParams returns bidder specific requestParams -func (bcfg *BidderConfig) GetRequestParams(bidderName string) map[string]BidderParamMapper { - if len(bcfg.bidderConfigMap) == 0 { +// GetResponseParams returns bidder specific ResponseParams +func (bcfg *BidderConfig) GetResponseParams(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 + return map[string]BidderParamMapper{} } - return bidderConfig.requestParams + return bidderConfig.ResponseParams } diff --git a/adapters/ortbbidder/bidderparams/config_test.go b/adapters/ortbbidder/bidderparams/config_test.go index 2dd4b09c585..d17362ed185 100644 --- a/adapters/ortbbidder/bidderparams/config_test.go +++ b/adapters/ortbbidder/bidderparams/config_test.go @@ -6,98 +6,6 @@ import ( "github.com/stretchr/testify/assert" ) -func TestSetRequestParams(t *testing.T) { - type fields struct { - bidderConfig *BidderConfig - } - type args struct { - bidderName string - requestParams map[string]BidderParamMapper - } - type want struct { - bidderCfg *BidderConfig - } - tests := []struct { - name string - fields fields - args args - want want - }{ - { - name: "bidderName_not_found", - fields: fields{ - bidderConfig: &BidderConfig{ - bidderConfigMap: map[string]*config{}, - }, - }, - args: args{ - bidderName: "test", - requestParams: map[string]BidderParamMapper{ - "param-1": { - Location: "path", - }, - }, - }, - want: want{ - bidderCfg: &BidderConfig{ - bidderConfigMap: map[string]*config{ - "test": { - requestParams: map[string]BidderParamMapper{ - "param-1": { - Location: "path", - }, - }, - }, - }, - }, - }, - }, - { - name: "bidderName_found", - fields: fields{ - bidderConfig: &BidderConfig{ - bidderConfigMap: map[string]*config{ - "test": { - requestParams: map[string]BidderParamMapper{ - "param-1": { - Location: "path-1", - }, - }, - }, - }, - }, - }, - args: args{ - bidderName: "test", - requestParams: map[string]BidderParamMapper{ - "param-2": { - Location: "path-2", - }, - }, - }, - want: want{ - bidderCfg: &BidderConfig{ - bidderConfigMap: map[string]*config{ - "test": { - requestParams: map[string]BidderParamMapper{ - "param-2": { - Location: "path-2", - }, - }, - }, - }, - }, - }, - }, - } - 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.bidderCfg, tt.fields.bidderConfig, "mismatched bidderConfig") - }) - } -} - func TestGetBidderRequestProperties(t *testing.T) { type fields struct { biddersConfig *BidderConfig @@ -106,7 +14,7 @@ func TestGetBidderRequestProperties(t *testing.T) { bidderName string } type want struct { - requestParams map[string]BidderParamMapper + RequestParams map[string]BidderParamMapper } tests := []struct { name string @@ -118,21 +26,21 @@ func TestGetBidderRequestProperties(t *testing.T) { name: "BidderConfigMap_is_nil", fields: fields{ biddersConfig: &BidderConfig{ - bidderConfigMap: nil, + BidderConfigMap: nil, }, }, args: args{ bidderName: "test", }, want: want{ - requestParams: nil, + RequestParams: nil, }, }, { name: "BidderName_absent_in_biddersConfigMap", fields: fields{ biddersConfig: &BidderConfig{ - bidderConfigMap: map[string]*config{ + BidderConfigMap: map[string]*Config{ "ortb": {}, }, }, @@ -141,14 +49,14 @@ func TestGetBidderRequestProperties(t *testing.T) { bidderName: "test", }, want: want{ - requestParams: nil, + RequestParams: nil, }, }, { - name: "BidderName_present_but_config_is_nil", + name: "BidderName_present_but_Config_is_nil", fields: fields{ biddersConfig: &BidderConfig{ - bidderConfigMap: map[string]*config{ + BidderConfigMap: map[string]*Config{ "ortb": nil, }, }, @@ -157,16 +65,16 @@ func TestGetBidderRequestProperties(t *testing.T) { bidderName: "test", }, want: want{ - requestParams: nil, + RequestParams: nil, }, }, { name: "BidderName_present_in_biddersConfigMap", fields: fields{ biddersConfig: &BidderConfig{ - bidderConfigMap: map[string]*config{ + BidderConfigMap: map[string]*Config{ "test": { - requestParams: map[string]BidderParamMapper{ + RequestParams: map[string]BidderParamMapper{ "param-1": { Location: "value-1", }, @@ -179,7 +87,7 @@ func TestGetBidderRequestProperties(t *testing.T) { bidderName: "test", }, want: want{ - requestParams: map[string]BidderParamMapper{ + RequestParams: map[string]BidderParamMapper{ "param-1": { Location: "value-1", }, @@ -190,7 +98,64 @@ func TestGetBidderRequestProperties(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { params := tt.fields.biddersConfig.GetRequestParams(tt.args.bidderName) - assert.Equal(t, tt.want.requestParams, params, "mismatched requestParams") + assert.Equal(t, tt.want.RequestParams, params, "mismatched RequestParams") + }) + } +} + +func TestGetResponseParams(t *testing.T) { + tests := []struct { + name string + bidderName string + BidderConfigMap map[string]*Config + expected map[string]BidderParamMapper + }{ + { + name: "Get response params for existing bidder", + bidderName: "existingBidder", + BidderConfigMap: map[string]*Config{ + "existingBidder": { + ResponseParams: map[string]BidderParamMapper{ + "param1": { + Location: "location", + }, + }, + }, + }, + expected: map[string]BidderParamMapper{ + "param1": { + Location: "location", + }, + }, + }, + { + name: "Get response params for non-existing bidder", + bidderName: "nonExistingBidder", + BidderConfigMap: map[string]*Config{ + "existingBidder": { + ResponseParams: map[string]BidderParamMapper{ + "param1": { + Location: "location", + }, + }, + }, + }, + expected: map[string]BidderParamMapper{}, + }, + { + name: "Get response params for empty bidder Config map", + bidderName: "anyBidder", + BidderConfigMap: map[string]*Config{}, + expected: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bcfg := &BidderConfig{ + BidderConfigMap: tt.BidderConfigMap, + } + got := bcfg.GetResponseParams(tt.bidderName) + assert.Equal(t, tt.expected, got) }) } } diff --git a/adapters/ortbbidder/bidderparams/parser.go b/adapters/ortbbidder/bidderparams/parser.go index 278629608b7..69195eb3e67 100644 --- a/adapters/ortbbidder/bidderparams/parser.go +++ b/adapters/ortbbidder/bidderparams/parser.go @@ -1,11 +1,19 @@ package bidderparams import ( - "encoding/json" "fmt" "os" "path/filepath" "strings" + + "github.com/prebid/prebid-server/v2/util/jsonutil" +) + +type ParamType int + +const ( + requestParams ParamType = iota + responseParams ) const ( @@ -14,31 +22,58 @@ const ( ) // LoadBidderConfig creates a bidderConfig from JSON files specified in dirPath directory. -func LoadBidderConfig(dirPath string, isBidderAllowed func(string) bool) (*BidderConfig, error) { +func LoadBidderConfig(requestParamsDirPath, responseParamsDirPath string, isBidderAllowed func(string) bool) (*BidderConfig, error) { + cfg := NewBidderConfig() + + err := loadFile(requestParamsDirPath, isBidderAllowed, cfg.BidderConfigMap, requestParams) + if err != nil { + return nil, fmt.Errorf("error handling request params: %w", err) + } + + err = loadFile(responseParamsDirPath, isBidderAllowed, cfg.BidderConfigMap, responseParams) + if err != nil { + return nil, fmt.Errorf("error handling response params: %w", err) + } + + return cfg, nil +} + +func loadFile(dirPath string, isBidderAllowed func(string) bool, bidderConfigMap map[string]*Config, paramType ParamType) error { files, err := os.ReadDir(dirPath) if err != nil { - return nil, fmt.Errorf("error:[%s] dirPath:[%s]", err.Error(), dirPath) + return fmt.Errorf("error:[%s] dirPath:[%s]", err.Error(), dirPath) } - bidderConfigMap := NewBidderConfig() for _, file := range files { bidderName, ok := strings.CutSuffix(file.Name(), ".json") if !ok { - return nil, fmt.Errorf("error:[invalid_json_file_name] filename:[%s]", file.Name()) + return fmt.Errorf("error:[invalid_json_file_name] filename:[%s]", file.Name()) } if !isBidderAllowed(bidderName) { continue } - requestParamsConfig, err := readFile(dirPath, file.Name()) + paramsConfig, err := readFile(dirPath, file.Name()) if err != nil { - return nil, fmt.Errorf("error:[fail_to_read_file] dir:[%s] filename:[%s] err:[%s]", dirPath, file.Name(), err.Error()) + return fmt.Errorf("error:[fail_to_read_file] dir:[%s] filename:[%s] err:[%s]", dirPath, file.Name(), err.Error()) } - requestParams, err := prepareRequestParams(bidderName, requestParamsConfig) + params, err := prepareParams(bidderName, paramsConfig) if err != nil { - return nil, err + return err + } + + if _, found := bidderConfigMap[bidderName]; !found { + bidderConfigMap[bidderName] = &Config{} + } + + switch paramType { + case requestParams: + bidderConfigMap[bidderName].RequestParams = params + case responseParams: + bidderConfigMap[bidderName].ResponseParams = params + default: + return fmt.Errorf("error:[invalid_param_type] paramType:[%d]", paramType) } - bidderConfigMap.SetRequestParams(bidderName, requestParams) } - return bidderConfigMap, nil + return nil } // readFile reads the file from directory and unmarshals it into the map[string]any @@ -49,21 +84,21 @@ func readFile(dirPath, file string) (map[string]any, error) { return nil, err } var contentMap map[string]any - err = json.Unmarshal(content, &contentMap) + err = jsonutil.UnmarshalValid(content, &contentMap) return contentMap, err } -// prepareRequestParams parse the requestParamsConfig and returns the requestParams -func prepareRequestParams(bidderName string, requestParamsConfig map[string]any) (map[string]BidderParamMapper, error) { - params, found := requestParamsConfig[propertiesKey] +// prepareParams parse the paramsConfig and returns the request/response params +func prepareParams(bidderName string, paramsConfig map[string]any) (map[string]BidderParamMapper, error) { + paramsProperties, found := paramsConfig[propertiesKey] if !found { return nil, nil } - paramsMap, ok := params.(map[string]any) + paramsMap, ok := paramsProperties.(map[string]any) if !ok { return nil, fmt.Errorf("error:[invalid_json_file_content_malformed_properties] bidderName:[%s]", bidderName) } - requestParams := make(map[string]BidderParamMapper, len(paramsMap)) + params := make(map[string]BidderParamMapper, len(paramsMap)) for paramName, paramValue := range paramsMap { paramValueMap, ok := paramValue.(map[string]any) if !ok { @@ -77,9 +112,9 @@ func prepareRequestParams(bidderName string, requestParamsConfig map[string]any) if !ok { return nil, fmt.Errorf("error:[incorrect_location_in_bidderparam] bidder:[%s] bidderParam:[%s]", bidderName, paramName) } - requestParams[paramName] = BidderParamMapper{ + params[paramName] = BidderParamMapper{ Location: locationStr, } } - return requestParams, nil + return params, nil } diff --git a/adapters/ortbbidder/bidderparams/parser_test.go b/adapters/ortbbidder/bidderparams/parser_test.go index 3d543b9960d..8314f53f359 100644 --- a/adapters/ortbbidder/bidderparams/parser_test.go +++ b/adapters/ortbbidder/bidderparams/parser_test.go @@ -3,9 +3,9 @@ package bidderparams import ( "fmt" "os" - "strings" "testing" + "github.com/prebid/prebid-server/v2/adapters/ortbbidder/util" "github.com/stretchr/testify/assert" ) @@ -15,7 +15,7 @@ func TestPrepareRequestParams(t *testing.T) { bidderName string } type want struct { - requestParams map[string]BidderParamMapper + RequestParams map[string]BidderParamMapper err error } tests := []struct { @@ -32,7 +32,7 @@ func TestPrepareRequestParams(t *testing.T) { bidderName: "testbidder", }, want: want{ - requestParams: nil, + RequestParams: nil, err: nil, }, }, @@ -46,7 +46,7 @@ func TestPrepareRequestParams(t *testing.T) { bidderName: "testbidder", }, want: want{ - requestParams: nil, + RequestParams: nil, err: fmt.Errorf("error:[invalid_json_file_content_malformed_properties] bidderName:[testbidder]"), }, }, @@ -62,7 +62,7 @@ func TestPrepareRequestParams(t *testing.T) { bidderName: "testbidder", }, want: want{ - requestParams: nil, + RequestParams: nil, err: fmt.Errorf("error:[invalid_json_file_content] bidder:[testbidder] bidderParam:[adunitid]"), }, }, @@ -80,7 +80,7 @@ func TestPrepareRequestParams(t *testing.T) { bidderName: "testbidder", }, want: want{ - requestParams: map[string]BidderParamMapper{}, + RequestParams: map[string]BidderParamMapper{}, err: nil, }, }, @@ -99,7 +99,7 @@ func TestPrepareRequestParams(t *testing.T) { bidderName: "testbidder", }, want: want{ - requestParams: nil, + RequestParams: nil, err: fmt.Errorf("error:[incorrect_location_in_bidderparam] bidder:[testbidder] bidderParam:[adunitid]"), }, }, @@ -118,7 +118,7 @@ func TestPrepareRequestParams(t *testing.T) { bidderName: "testbidder", }, want: want{ - requestParams: map[string]BidderParamMapper{ + RequestParams: map[string]BidderParamMapper{ "adunitid": {Location: "app.adunitid"}, }, err: nil, @@ -143,7 +143,7 @@ func TestPrepareRequestParams(t *testing.T) { bidderName: "testbidder", }, want: want{ - requestParams: map[string]BidderParamMapper{ + RequestParams: map[string]BidderParamMapper{ "adunitid": {Location: "app.adunitid"}, "slotname": {Location: "ext.slot"}, }, @@ -153,9 +153,10 @@ func TestPrepareRequestParams(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - requestParams, err := prepareRequestParams(tt.args.bidderName, tt.args.requestParamCfg) + RequestParams, err := prepareParams(tt.args.bidderName, tt.args.requestParamCfg) assert.Equalf(t, tt.want.err, err, "updateBidderParamsMapper returned unexpected error") - assert.Equalf(t, tt.want.requestParams, requestParams, "updateBidderParamsMapper returned unexpected mapper") + assert.Equalf(t, tt.want.RequestParams, RequestParams, "updateBidderParamsMapper returned unexpected mapper") + }) } } @@ -168,66 +169,112 @@ func TestLoadBidderConfig(t *testing.T) { tests := []struct { name string want want - setup func() (string, error) + setup func() (string, string, error) }{ { name: "read_directory_fail", want: want{ biddersConfigMap: nil, - err: "error:[open invalid-path: no such file or directory] dirPath:[invalid-path]", + err: "error handling request params: error:[open invalid-request-param-path: no such file or directory] dirPath:[invalid-request-param-path]", + }, + setup: func() (string, string, error) { + return "invalid-request-param-path", "invalid-response-param-path", nil }, - setup: func() (string, error) { return "invalid-path", nil }, }, { name: "found_file_without_.json_extension", want: want{ biddersConfigMap: nil, - err: "error:[invalid_json_file_name] filename:[example.txt]", + err: "error handling request params: error:[invalid_json_file_name] filename:[example.txt]", }, - setup: func() (string, error) { + setup: func() (string, string, error) { dirPath := t.TempDir() err := os.WriteFile(dirPath+"/example.txt", []byte("anything"), 0644) - return dirPath, err + return dirPath, "", err }, }, { - name: "oRTB_bidder_not_found", + name: "response params - read_directory_fail", want: want{ - biddersConfigMap: &BidderConfig{bidderConfigMap: make(map[string]*config)}, - err: "", + biddersConfigMap: nil, + err: "error handling response params: error:[open invalid-path: no such file or directory] dirPath:[invalid-path]", }, - setup: func() (string, error) { + setup: func() (string, string, error) { dirPath := t.TempDir() err := os.WriteFile(dirPath+"/example.json", []byte("anything"), 0644) - return dirPath, err + return dirPath, "invalid-path", err + }, + }, + { + name: "response params - found_file_without_.json_extension", + want: want{ + biddersConfigMap: nil, + err: "error handling response params: error:[invalid_json_file_name] filename:[example.txt]", + }, + setup: func() (string, string, error) { + requestDirPath := t.TempDir() + err := os.WriteFile(requestDirPath+"/example.json", []byte("anything"), 0644) + if err != nil { + return "", "", err + } + responseDirPath := t.TempDir() + err = os.WriteFile(responseDirPath+"/example.txt", []byte("anything"), 0644) + return requestDirPath, responseDirPath, err + }, + }, + { + name: "oRTB_bidder_not_found", + want: want{ + biddersConfigMap: &BidderConfig{BidderConfigMap: make(map[string]*Config)}, + err: "", + }, + setup: func() (string, string, error) { + requestDirPath := t.TempDir() + err := os.WriteFile(requestDirPath+"/example.json", []byte("anything"), 0644) + if err != nil { + return "", "", err + } + responseDirPath := t.TempDir() + err = os.WriteFile(responseDirPath+"/example.json", []byte("anything"), 0644) + return requestDirPath, responseDirPath, err }, }, { name: "oRTB_bidder_found_but_invalid_json_present", want: want{ biddersConfigMap: nil, - err: "error:[fail_to_read_file]", + err: "error handling request params: error:[fail_to_read_file]", }, - setup: func() (string, error) { - dirPath := t.TempDir() - err := os.WriteFile(dirPath+"/owortb_test.json", []byte("anything"), 0644) - return dirPath, err + setup: func() (string, string, error) { + requestDirPath := t.TempDir() + err := os.WriteFile(requestDirPath+"/owortb_test.json", []byte("anything"), 0644) + if err != nil { + return "", "", err + } + responseDirPath := t.TempDir() + err = os.WriteFile(responseDirPath+"/owortb_test.json", []byte("anything"), 0644) + return requestDirPath, responseDirPath, err }, }, { name: "oRTB_bidder_found_but_bidder-params_are_absent", want: want{ - biddersConfigMap: &BidderConfig{bidderConfigMap: map[string]*config{ + biddersConfigMap: &BidderConfig{BidderConfigMap: map[string]*Config{ "owortb_test": { - requestParams: nil, + RequestParams: nil, }, }}, err: "", }, - setup: func() (string, error) { - dirPath := t.TempDir() - err := os.WriteFile(dirPath+"/owortb_test.json", []byte("{}"), 0644) - return dirPath, err + setup: func() (string, string, error) { + requestDirPath := t.TempDir() + err := os.WriteFile(requestDirPath+"/owortb_test.json", []byte("{}"), 0644) + if err != nil { + return "", "", err + } + responseDirPath := t.TempDir() + err = os.WriteFile(responseDirPath+"/owortb_test.json", []byte("{}"), 0644) + return requestDirPath, responseDirPath, err }, }, { @@ -236,29 +283,37 @@ func TestLoadBidderConfig(t *testing.T) { biddersConfigMap: nil, err: "error:[invalid_json_file_content_malformed_properties] bidderName:[owortb_test]", }, - setup: func() (string, error) { - dirPath := t.TempDir() - err := os.WriteFile(dirPath+"/owortb_test.json", []byte(`{"properties":"invalid-properties"}`), 0644) - return dirPath, err + setup: func() (string, string, error) { + requestDirPath := t.TempDir() + err := os.WriteFile(requestDirPath+"/owortb_test.json", []byte(`{"properties":"invalid-properties"}`), 0644) + if err != nil { + return "", "", err + } + responseDirPath := t.TempDir() + err = os.WriteFile(responseDirPath+"/owortb_test.json", []byte(`{"properties":"invalid-properties"}`), 0644) + return requestDirPath, responseDirPath, err }, }, { name: "oRTB_bidder_found_and_valid_json_contents_present", want: want{ biddersConfigMap: &BidderConfig{ - bidderConfigMap: map[string]*config{ + BidderConfigMap: map[string]*Config{ "owortb_test": { - requestParams: map[string]BidderParamMapper{ + RequestParams: map[string]BidderParamMapper{ "adunitid": {Location: "app.adunit.id"}, "slotname": {Location: "ext.slotname"}, }, + ResponseParams: map[string]BidderParamMapper{ + "mtype": {Location: "seatbid.#.bid.#.ext.mtype"}, + }, }, }}, err: "", }, - setup: func() (string, error) { - dirPath := t.TempDir() - err := os.WriteFile(dirPath+"/owortb_test.json", []byte(` + setup: func() (string, string, error) { + requestDirPath := t.TempDir() + err := os.WriteFile(requestDirPath+"/owortb_test.json", []byte(` { "title":"ortb bidder", "properties": { @@ -273,20 +328,28 @@ func TestLoadBidderConfig(t *testing.T) { } } `), 0644) - return dirPath, err + if err != nil { + return "", "", err + } + responseDirPath := t.TempDir() + err = os.WriteFile(responseDirPath+"/owortb_test.json", []byte(`{ + "title":"ortb bidder", + "properties": { + "mtype": { + "type": "string", + "location": "seatbid.#.bid.#.ext.mtype" + } + } + }`), 0644) + return requestDirPath, responseDirPath, err }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - dirPath, err := tt.setup() + RequestParamsDirPath, ResponseParamsDirPath, err := tt.setup() assert.NoError(t, err, "setup returned unexpected error") - got, err := LoadBidderConfig(dirPath, func(bidderName string) bool { - if strings.HasPrefix(bidderName, "owortb_") { - return true - } - return false - }) + got, err := LoadBidderConfig(RequestParamsDirPath, ResponseParamsDirPath, util.IsORTBBidder) assert.Equal(t, tt.want.biddersConfigMap, got, "found incorrect mapper") assert.Equal(t, len(tt.want.err) == 0, err == nil, "mismatched error") if err != nil { diff --git a/adapters/ortbbidder/constant.go b/adapters/ortbbidder/constant.go index 81bb90b49d2..b9a2f124f36 100644 --- a/adapters/ortbbidder/constant.go +++ b/adapters/ortbbidder/constant.go @@ -19,3 +19,17 @@ const ( templateOption = "missingkey=zero" oRTBPrefix = "owortb_" ) + +// constants to retrieve values from oRTB request/response +const ( + seatBidKey = "seatbid" + bidKey = "bid" + ortbCurrencyKey = "cur" +) + +// constants to set values in adapter response +const ( + currencyKey = "Currency" + typeBidKey = "Bid" + bidsKey = "Bids" +) diff --git a/adapters/ortbbidder/errors.go b/adapters/ortbbidder/errors.go index d1866947a9e..af64cbd21f3 100644 --- a/adapters/ortbbidder/errors.go +++ b/adapters/ortbbidder/errors.go @@ -19,3 +19,9 @@ func newBadInputError(message string, args ...any) error { Message: fmt.Sprintf(message, args...), } } + +func newBadServerResponseError(message string, args ...any) error { + return &errortypes.BadServerResponse{ + Message: fmt.Sprintf(message, args...), + } +} diff --git a/adapters/ortbbidder/multiRequestBuilder.go b/adapters/ortbbidder/multi_request_builder.go similarity index 100% rename from adapters/ortbbidder/multiRequestBuilder.go rename to adapters/ortbbidder/multi_request_builder.go diff --git a/adapters/ortbbidder/multiRequestBuilder_test.go b/adapters/ortbbidder/multi_request_builder_test.go similarity index 100% rename from adapters/ortbbidder/multiRequestBuilder_test.go rename to adapters/ortbbidder/multi_request_builder_test.go diff --git a/adapters/ortbbidder/ortbbidder.go b/adapters/ortbbidder/ortbbidder.go index 5761552f54c..5008f23cdb5 100644 --- a/adapters/ortbbidder/ortbbidder.go +++ b/adapters/ortbbidder/ortbbidder.go @@ -3,12 +3,12 @@ package ortbbidder import ( "encoding/json" "fmt" - "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/adapters/ortbbidder/util" "github.com/prebid/prebid-server/v2/config" "github.com/prebid/prebid-server/v2/openrtb_ext" "github.com/prebid/prebid-server/v2/util/jsonutil" @@ -35,9 +35,9 @@ type extraAdapterInfo struct { var g_bidderParamsConfig *bidderparams.BidderConfig // InitBidderParamsConfig initializes a g_bidderParamsConfig instance from the files provided in dirPath. -func InitBidderParamsConfig(dirPath string) (err error) { - g_bidderParamsConfig, err = bidderparams.LoadBidderConfig(dirPath, IsORTBBidder) - return err +func InitBidderParamsConfig(requestParamsDirPath, responseParamsDirPath string) (err error) { + g_bidderParamsConfig, err = bidderparams.LoadBidderConfig(requestParamsDirPath, responseParamsDirPath, util.IsORTBBidder) + return } // Builder returns an instance of oRTB adapter @@ -88,63 +88,25 @@ func (o *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.R return nil, []error{err} } - var response openrtb2.BidResponse - if err := jsonutil.Unmarshal(responseData.Body, &response); err != nil { - return nil, []error{err} + response, err := o.makeBids(request, responseData.Body) + if err != nil { + return nil, []error{newBadServerResponseError(err.Error())} } - bidResponse := adapters.BidderResponse{ - Bids: make([]*adapters.TypedBid, 0), - } - for _, seatBid := range response.SeatBid { - for bidInd, bid := range seatBid.Bid { - bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ - Bid: &seatBid.Bid[bidInd], - BidType: getMediaTypeForBid(bid), - }) - } - } - return &bidResponse, nil + return response, nil } -// getMediaTypeForBid returns the BidType as per the bid.MType field -// bid.MType has high priority over bidExt.Prebid.Type -func getMediaTypeForBid(bid openrtb2.Bid) openrtb_ext.BidType { - var bidType openrtb_ext.BidType - if bid.MType > 0 { - bidType = getMediaTypeForBidFromMType(bid.MType) - } else { - if bid.Ext != nil { - var bidExt openrtb_ext.ExtBid - err := json.Unmarshal(bid.Ext, &bidExt) - if err == nil && bidExt.Prebid != nil { - bidType, _ = openrtb_ext.ParseBidType(string(bidExt.Prebid.Type)) - } - } - } - if bidType == "" { - // TODO : detect mediatype from bid.AdM and request.imp parameter - } - return bidType -} +// makeBids converts the bidderResponseBytes to a BidderResponse +// It retrieves response parameters, creates a response builder, parses the response, and builds the response. +// Finally, it converts the response builder's internal representation to an AdapterResponse and returns it. +func (o *adapter) makeBids(request *openrtb2.BidRequest, bidderResponseBytes json.RawMessage) (*adapters.BidderResponse, error) { + responseParmas := o.bidderParamsConfig.GetResponseParams(o.bidderName.String()) + rb := newResponseBuilder(responseParmas, request) -// getMediaTypeForBidFromMType returns the bidType from the MarkupType field -func getMediaTypeForBidFromMType(mtype openrtb2.MarkupType) openrtb_ext.BidType { - var bidType openrtb_ext.BidType - switch mtype { - case openrtb2.MarkupBanner: - bidType = openrtb_ext.BidTypeBanner - case openrtb2.MarkupVideo: - bidType = openrtb_ext.BidTypeVideo - case openrtb2.MarkupAudio: - bidType = openrtb_ext.BidTypeAudio - case openrtb2.MarkupNative: - bidType = openrtb_ext.BidTypeNative + err := rb.setPrebidBidderResponse(bidderResponseBytes) + if err != nil { + return nil, err } - return bidType -} -// IsORTBBidder returns true if the bidder is an oRTB bidder -func IsORTBBidder(bidderName string) bool { - return strings.HasPrefix(bidderName, oRTBPrefix) + return rb.buildAdapterResponse() } diff --git a/adapters/ortbbidder/ortbbidder_test.go b/adapters/ortbbidder/ortbbidder_test.go index 48588fa906b..3b7759c7a02 100644 --- a/adapters/ortbbidder/ortbbidder_test.go +++ b/adapters/ortbbidder/ortbbidder_test.go @@ -9,7 +9,6 @@ import ( "github.com/prebid/openrtb/v20/openrtb2" "github.com/prebid/prebid-server/v2/adapters" - "github.com/prebid/prebid-server/v2/adapters/adapterstest" "github.com/prebid/prebid-server/v2/adapters/ortbbidder/bidderparams" "github.com/prebid/prebid-server/v2/config" "github.com/prebid/prebid-server/v2/errortypes" @@ -17,6 +16,116 @@ import ( "github.com/stretchr/testify/assert" ) +func TestBuilder(t *testing.T) { + InitBidderParamsConfig("../../static/bidder-params", "../../static/bidder-response-params") + type args struct { + bidderName openrtb_ext.BidderName + config config.Adapter + server config.Server + } + type want struct { + err error + bidder adapters.Bidder + } + tests := []struct { + name string + args args + want want + }{ + { + name: "fails_to_parse_extra_info", + args: args{ + bidderName: "ortbbidder", + config: config.Adapter{ + ExtraAdapterInfo: "invalid-string", + }, + server: config.Server{}, + }, + want: want{ + bidder: nil, + 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 '}'"), + }, + }, + { + name: "bidder_with_requestType", + args: args{ + bidderName: "ortbbidder", + config: config.Adapter{ + ExtraAdapterInfo: `{"requestType":"single"}`, + }, + server: config.Server{}, + }, + want: want{ + bidder: &adapter{ + adapterInfo: adapterInfo{ + extraInfo: extraAdapterInfo{ + RequestType: "single", + }, + Adapter: config.Adapter{ + ExtraAdapterInfo: `{"requestType":"single"}`, + }, + bidderName: "ortbbidder", + endpointTemplate: func() *template.Template { + template, _ := template.New("endpointTemplate").Option("missingkey=zero").Parse("") + return template + }(), + }, + bidderParamsConfig: g_bidderParamsConfig, + }, + err: nil, + }, + }, + { + name: "bidder_without_requestType", + args: args{ + bidderName: "ortbbidder", + config: config.Adapter{ + ExtraAdapterInfo: "", + }, + server: config.Server{}, + }, + want: want{ + bidder: &adapter{ + adapterInfo: adapterInfo{ + Adapter: config.Adapter{ + ExtraAdapterInfo: ``, + }, + bidderName: "ortbbidder", + endpointTemplate: func() *template.Template { + template, _ := template.New("endpointTemplate").Option("missingkey=zero").Parse("") + return template + }(), + }, + bidderParamsConfig: g_bidderParamsConfig, + }, + err: nil, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Builder(tt.args.bidderName, tt.args.config, tt.args.server) + assert.Equal(t, tt.want.bidder, got, "mismatched bidder") + assert.Equal(t, tt.want.err, err, "mismatched error") + }) + } +} + func TestMakeRequests(t *testing.T) { type args struct { request *openrtb2.BidRequest @@ -244,10 +353,13 @@ func TestMakeRequests(t *testing.T) { }(), bidderCfg: func() *bidderparams.BidderConfig { cfg := bidderparams.NewBidderConfig() - cfg.SetRequestParams("testbidder", map[string]bidderparams.BidderParamMapper{ - "host": {Location: "server.host"}, - "zone": {Location: "ext.zone"}, - }) + cfg.BidderConfigMap["testbidder"] = &bidderparams.Config{ + RequestParams: map[string]bidderparams.BidderParamMapper{ + "host": {Location: "server.host"}, + "zone": {Location: "ext.zone"}, + }, + } + return cfg }(), }, @@ -284,385 +396,240 @@ func TestMakeRequests(t *testing.T) { }) } } + func TestMakeBids(t *testing.T) { - type args struct { - request *openrtb2.BidRequest - requestData *adapters.RequestData - responseData *adapters.ResponseData - } - type want struct { - response *adapters.BidderResponse - errors []error - } tests := []struct { - name string - args args - want want + name string + responseData *adapters.ResponseData + expectedResponse *adapters.BidderResponse + request *openrtb2.BidRequest + requestData *adapters.RequestData + expectedErrors []error + setup func() adapter }{ { - name: "responseData_is_nil", - args: args{ - responseData: nil, - }, - want: want{ - response: nil, - errors: nil, + name: "response data is nil", + expectedResponse: nil, + responseData: nil, + setup: func() adapter { + return adapter{ + bidderParamsConfig: &bidderparams.BidderConfig{}, + } }, }, { - name: "StatusNoContent_in_responseData", - args: args{ - responseData: &adapters.ResponseData{StatusCode: http.StatusNoContent}, - }, - want: want{ - response: nil, - errors: nil, - }, + name: "no content response data", + responseData: &adapters.ResponseData{StatusCode: http.StatusNoContent}, + setup: func() adapter { + return adapter{ + bidderParamsConfig: &bidderparams.BidderConfig{}, + } + }, + expectedResponse: nil, + expectedErrors: nil, }, { - name: "StatusBadRequest_in_responseData", - args: args{ - responseData: &adapters.ResponseData{StatusCode: http.StatusBadRequest}, - }, - want: want{ - response: nil, - errors: []error{&errortypes.BadInput{ - Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", http.StatusBadRequest), - }}, - }, + name: "status bad request in response data", + responseData: &adapters.ResponseData{StatusCode: http.StatusBadRequest}, + setup: func() adapter { + return adapter{ + bidderParamsConfig: &bidderparams.BidderConfig{}, + } + }, + expectedResponse: nil, + expectedErrors: []error{&errortypes.BadInput{ + Message: "Unexpected status code: 400. Run with request.debug = 1 for more info", + }}, }, { - name: "valid_response", - args: args{ - responseData: &adapters.ResponseData{ - StatusCode: http.StatusOK, - Body: []byte(`{"id":"bid-resp-id","seatbid":[{"seat":"test_bidder","bid":[{"id":"bid-1","mtype":2}]}]}`), - }, - }, - want: want{ - response: &adapters.BidderResponse{ - Bids: []*adapters.TypedBid{ - { - Bid: &openrtb2.Bid{ - ID: "bid-1", - MType: 2, - }, - BidType: "video", - }, - }, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - adapter := &adapter{} - response, errs := adapter.MakeBids(tt.args.request, tt.args.requestData, tt.args.responseData) - assert.Equalf(t, tt.want.response, response, "mismatched response") - assert.Equalf(t, tt.want.errors, errs, "mismatched errors") - }) - } -} + name: "status too many requests in response data", -func TestGetMediaTypeForBid(t *testing.T) { - type args struct { - bid openrtb2.Bid - } - type want struct { - bidType openrtb_ext.BidType - } - tests := []struct { - name string - args args - want want - }{ - { - name: "valid_banner_bid", - args: args{ - bid: openrtb2.Bid{ID: "1", MType: openrtb2.MarkupBanner}, - }, - want: want{ - bidType: openrtb_ext.BidTypeBanner, - }, - }, - { - name: "valid_video_bid", - args: args{ - bid: openrtb2.Bid{ID: "2", MType: openrtb2.MarkupVideo}, - }, - want: want{ - bidType: openrtb_ext.BidTypeVideo, - }, + responseData: &adapters.ResponseData{StatusCode: http.StatusTooManyRequests}, + setup: func() adapter { + return adapter{ + bidderParamsConfig: &bidderparams.BidderConfig{}, + } + }, + expectedResponse: nil, + expectedErrors: []error{&errortypes.BadServerResponse{ + Message: "Unexpected status code: 429. Run with request.debug = 1 for more info", + }}, }, { - name: "valid_audio_bid", - args: args{ - bid: openrtb2.Bid{ID: "3", MType: openrtb2.MarkupAudio}, - }, - want: want{ - bidType: openrtb_ext.BidTypeAudio, - }, + name: "malformed response data body", + + responseData: &adapters.ResponseData{ + StatusCode: http.StatusOK, + Body: []byte(`{"id":1,"seatbid":[{"seat":"test_bidder","bid":[{"id":"bid-1","bidtype":2}]}]`), + }, + setup: func() adapter { + return adapter{ + bidderParamsConfig: &bidderparams.BidderConfig{}, + } + }, + expectedResponse: nil, + expectedErrors: []error{&errortypes.BadServerResponse{ + Message: "expect }, but found \x00", + }}, }, { - name: "valid_native_bid", - args: args{ - bid: openrtb2.Bid{ID: "4", MType: openrtb2.MarkupNative}, - }, - want: want{ - bidType: openrtb_ext.BidTypeNative, + name: "error parsing bidder response", + responseData: &adapters.ResponseData{ + Body: []byte(`{"id":}`), + StatusCode: http.StatusOK, + }, + expectedResponse: nil, + expectedErrors: []error{&errortypes.BadServerResponse{Message: "unexpected value type: 0"}}, + setup: func() adapter { + return adapter{ + bidderParamsConfig: &bidderparams.BidderConfig{}, + } }, }, { - name: "invalid_bid_type", - args: args{ - bid: openrtb2.Bid{ID: "5", MType: 123}, - }, - want: want{ - bidType: "", + name: "invalid seatbid in response", + responseData: &adapters.ResponseData{ + Body: []byte(`{"id":1, "seatbid":"invalid"}`), + StatusCode: http.StatusOK, + }, + expectedResponse: nil, + expectedErrors: []error{&errortypes.BadServerResponse{Message: "invalid seatbid array found in response, seatbids:[invalid]"}}, + setup: func() adapter { + return adapter{ + bidderParamsConfig: &bidderparams.BidderConfig{}, + } }, }, { - name: "bid.MType_has_high_priority", - args: args{ - bid: openrtb2.Bid{ID: "5", MType: openrtb2.MarkupVideo, Ext: json.RawMessage(`{"prebid":{"type":"video"}}`)}, - }, - want: want{ - bidType: "video", + name: "invalid seat in seatbid array", + responseData: &adapters.ResponseData{ + Body: []byte(`{"id":1, "seatbid":["invalid"]}`), + StatusCode: http.StatusOK, + }, + expectedResponse: nil, + expectedErrors: []error{&errortypes.BadServerResponse{Message: "invalid seatbid found in seatbid array, seatbid:[invalid]"}}, + setup: func() adapter { + return adapter{ + bidderParamsConfig: &bidderparams.BidderConfig{}, + } }, }, { - name: "bid.ext.prebid.type_is_absent", - args: args{ - bid: openrtb2.Bid{ID: "5", Ext: json.RawMessage(`{"prebid":{}}`)}, - }, - want: want{ - bidType: "", + name: "invalid bid arrays in seatbid", + responseData: &adapters.ResponseData{ + Body: []byte(`{"id":1,"seatbid":[{"bid":"invalid"}]}`), + StatusCode: http.StatusOK, + }, + expectedResponse: nil, + expectedErrors: []error{&errortypes.BadServerResponse{Message: "invalid bid array found in seatbid, bids:[invalid]"}}, + setup: func() adapter { + return adapter{ + bidderParamsConfig: &bidderparams.BidderConfig{}, + } }, }, { - name: "bid.ext.prebid.type_json_unmarshal_fails", - args: args{ - bid: openrtb2.Bid{ID: "5", Ext: json.RawMessage(`{"prebid":{invalid-json}}`)}, - }, - want: want{ - bidType: "", + name: "invalid bid in bids array", + responseData: &adapters.ResponseData{ + Body: []byte(`{"id":1,"seatbid":[{"bid":["invalid"]}]}`), + StatusCode: http.StatusOK, + }, + expectedResponse: nil, + expectedErrors: []error{&errortypes.BadServerResponse{Message: "invalid bid found in bids array, bid:[invalid]"}}, + setup: func() adapter { + return adapter{ + bidderParamsConfig: &bidderparams.BidderConfig{}, + } }, }, { - name: "bid.ext.prebid.type_is_valid", - args: args{ - bid: openrtb2.Bid{ID: "5", Ext: json.RawMessage(`{"prebid":{"type":"banner"}}`)}, - }, - want: want{ - bidType: "banner", + name: "failure converting to adapter response", + responseData: &adapters.ResponseData{ + Body: []byte(`{"id":1,"seatbid":[{"bid":[{"id": 1, "mtype":2}]}]}`), + StatusCode: http.StatusOK, + }, + expectedResponse: nil, + expectedErrors: []error{&errortypes.BadServerResponse{Message: "cannot unmarshal ID: expects \" or n, but found 1"}}, + setup: func() adapter { + return adapter{ + bidderParamsConfig: &bidderparams.BidderConfig{}, + } }, }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bidType := getMediaTypeForBid(tt.args.bid) - assert.Equal(t, tt.want.bidType, bidType, "mismatched bidType") - }) - } -} - -func TestJsonSamplesForSinglerequestType(t *testing.T) { - oldMapper := g_bidderParamsConfig - defer func() { - g_bidderParamsConfig = oldMapper - }() - g_bidderParamsConfig = &bidderparams.BidderConfig{} - bidder, buildErr := Builder("single_request", - config.Adapter{ - Endpoint: "http://test_bidder.com", - ExtraAdapterInfo: `{"requestType":"single"}`, - }, config.Server{}) - if buildErr != nil { - t.Fatalf("Builder returned unexpected error %v", buildErr) - } - adapterstest.RunJSONBidderTest(t, "ortbbiddertest/single_request", bidder) -} - -func TestJsonSamplesForMultirequestType(t *testing.T) { - oldMapper := g_bidderParamsConfig - defer func() { - g_bidderParamsConfig = oldMapper - }() - g_bidderParamsConfig = &bidderparams.BidderConfig{} - bidder, buildErr := Builder("multi_request", - config.Adapter{ - Endpoint: "http://test_bidder.com", - ExtraAdapterInfo: `{"requestType":"multi"}`, - }, config.Server{}) - if buildErr != nil { - t.Fatalf("Builder returned unexpected error %v", buildErr) - } - adapterstest.RunJSONBidderTest(t, "ortbbiddertest/multi_request", bidder) -} - -func TestBuilder(t *testing.T) { - InitBidderParamsConfig("../../static/bidder-params") - type args struct { - bidderName openrtb_ext.BidderName - config config.Adapter - server config.Server - } - type want struct { - err error - bidder adapters.Bidder - } - tests := []struct { - name string - args args - want want - }{ { - name: "fails_to_parse_extra_info", - args: args{ - bidderName: "ortbbidder", - config: config.Adapter{ - ExtraAdapterInfo: "invalid-string", - }, - server: config.Server{}, - }, - want: want{ - bidder: nil, - 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 '}'"), - }, - }, - { - name: "bidder_with_requestType", - args: args{ - bidderName: "ortbbidder", - config: config.Adapter{ - ExtraAdapterInfo: `{"requestType":"single"}`, + name: "valid response - no bidder params", + responseData: &adapters.ResponseData{ + Body: []byte(`{"id":"1","cur":"USD","seatbid":[{"bid":[{"id":"1","mtype":2}]}]}`), + StatusCode: http.StatusOK, + }, + expectedResponse: &adapters.BidderResponse{ + Currency: "USD", + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ + ID: "1", + MType: 2, + }, + BidType: "video", + }, }, - server: config.Server{}, }, - want: want{ - bidder: &adapter{ + expectedErrors: nil, + setup: func() adapter { + bc := bidderparams.NewBidderConfig() + bc.BidderConfigMap["owortb_testbidder"] = &bidderparams.Config{ + ResponseParams: map[string]bidderparams.BidderParamMapper{}, + } + return adapter{ + bidderParamsConfig: bc, adapterInfo: adapterInfo{ - extraInfo: extraAdapterInfo{ - RequestType: "single", - }, - Adapter: config.Adapter{ - ExtraAdapterInfo: `{"requestType":"single"}`, - }, - bidderName: "ortbbidder", - endpointTemplate: func() *template.Template { - template, _ := template.New("endpointTemplate").Option("missingkey=zero").Parse("") - return template - }(), + bidderName: "owortb_testbidder", }, - bidderParamsConfig: g_bidderParamsConfig, - }, - err: nil, + } }, }, { - name: "bidder_without_requestType", - args: args{ - bidderName: "ortbbidder", - config: config.Adapter{ - ExtraAdapterInfo: "", - }, - server: config.Server{}, - }, - want: want{ - bidder: &adapter{ - adapterInfo: adapterInfo{ - Adapter: config.Adapter{ - ExtraAdapterInfo: ``, + name: "valid response - bidder params present", + responseData: &adapters.ResponseData{ + Body: []byte(`{"id":"1","cur":"","seatbid":[{"bid":[{"id":"1","ext":{"bidtype":"video"}}]}],"ext":{"currency":"USD"}}`), + StatusCode: http.StatusOK, + }, + expectedResponse: &adapters.BidderResponse{ + Currency: "", + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ + ID: "1", + Ext: json.RawMessage(`{"bidtype":"video"}`), }, - bidderName: "ortbbidder", - endpointTemplate: func() *template.Template { - template, _ := template.New("endpointTemplate").Option("missingkey=zero").Parse("") - return template - }(), + BidType: "video", }, - bidderParamsConfig: g_bidderParamsConfig, }, - err: nil, }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := Builder(tt.args.bidderName, tt.args.config, tt.args.server) - assert.Equal(t, tt.want.bidder, got, "mismatched bidder") - assert.Equal(t, tt.want.err, err, "mismatched error") - }) - } -} - -func TestInitBidderParamsConfig(t *testing.T) { - tests := []struct { - name string - dirPath string - wantErr bool - }{ - { - name: "test_InitBiddersConfigMap_success", - dirPath: "../../static/bidder-params/", - wantErr: false, - }, - { - name: "test_InitBiddersConfigMap_failure", - dirPath: "/invalid_directory/", - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := InitBidderParamsConfig(tt.dirPath) - assert.Equal(t, err != nil, tt.wantErr, "mismatched error") - }) - } -} - -func TestIsORTBBidder(t *testing.T) { - type args struct { - bidderName string - } - tests := []struct { - name string - args args - want bool - }{ - { - name: "ortb_bidder", - args: args{ - bidderName: "owortb_magnite", - }, - want: true, - }, - { - name: "non_ortb_bidder", - args: args{ - bidderName: "magnite", + expectedErrors: nil, + setup: func() adapter { + bc := bidderparams.NewBidderConfig() + bc.BidderConfigMap["owortb_testbidder"] = &bidderparams.Config{ + ResponseParams: map[string]bidderparams.BidderParamMapper{ + "bidtype": {Location: "seatbid.#.bid.#.ext.bidtype"}, + "currency": {Location: "ext.currency"}, + }, + } + return adapter{ + adapterInfo: adapterInfo{ + bidderName: "owortb_testbidder", + }, + bidderParamsConfig: bc, + } }, - want: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := IsORTBBidder(tt.args.bidderName) - assert.Equal(t, tt.want, got, "mismatched output of IsORTBBidder") + adapter := tt.setup() + got, err := adapter.MakeBids(tt.request, tt.requestData, tt.responseData) + assert.Equal(t, tt.expectedResponse, got, "response mismatch") + assert.Equal(t, tt.expectedErrors, err, "error mismatch") }) } } diff --git a/adapters/ortbbidder/requestBuilder.go b/adapters/ortbbidder/request_builder.go similarity index 100% rename from adapters/ortbbidder/requestBuilder.go rename to adapters/ortbbidder/request_builder.go diff --git a/adapters/ortbbidder/requestBuilder_test.go b/adapters/ortbbidder/request_builder_test.go similarity index 100% rename from adapters/ortbbidder/requestBuilder_test.go rename to adapters/ortbbidder/request_builder_test.go diff --git a/adapters/ortbbidder/requestParamMapper.go b/adapters/ortbbidder/requestparam_mapper.go similarity index 92% rename from adapters/ortbbidder/requestParamMapper.go rename to adapters/ortbbidder/requestparam_mapper.go index b453abd4bc4..3a9a826e360 100644 --- a/adapters/ortbbidder/requestParamMapper.go +++ b/adapters/ortbbidder/requestparam_mapper.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/prebid/prebid-server/v2/adapters/ortbbidder/bidderparams" + "github.com/prebid/prebid-server/v2/adapters/ortbbidder/util" ) // setRequestParams updates the request object by mapping bidderParams at expected location. @@ -19,7 +20,7 @@ func setRequestParams(request, params map[string]any, paramsMapper map[string]bi 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) { + if util.SetValue(request, location, paramValue) { delete(params, paramName) updatedRequest = true } diff --git a/adapters/ortbbidder/requestParamMapper_test.go b/adapters/ortbbidder/requestparam_mapper_test.go similarity index 100% rename from adapters/ortbbidder/requestParamMapper_test.go rename to adapters/ortbbidder/requestparam_mapper_test.go diff --git a/adapters/ortbbidder/resolver/bidtype_resolver.go b/adapters/ortbbidder/resolver/bidtype_resolver.go new file mode 100644 index 00000000000..4703ae97056 --- /dev/null +++ b/adapters/ortbbidder/resolver/bidtype_resolver.go @@ -0,0 +1,128 @@ +package resolver + +import ( + "regexp" + + "github.com/buger/jsonparser" + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +// Constants used for look up in ortb response +const ( + mtypeKey = "mtype" + bidTypeKey = "BidType" + currencyKey = "Currency" + curKey = "cur" + admKey = "adm" + impIdKey = "impid" +) + +var ( + videoRegex = regexp.MustCompile(` 1 { + return openrtb_ext.BidType("") + } + + return mediaType +} + +func convertToBidType(mtype openrtb2.MarkupType) openrtb_ext.BidType { // change name + var bidType openrtb_ext.BidType + switch mtype { + case openrtb2.MarkupBanner: + bidType = openrtb_ext.BidTypeBanner + case openrtb2.MarkupVideo: + bidType = openrtb_ext.BidTypeVideo + case openrtb2.MarkupAudio: + bidType = openrtb_ext.BidTypeAudio + case openrtb2.MarkupNative: + bidType = openrtb_ext.BidTypeNative + } + return bidType +} diff --git a/adapters/ortbbidder/resolver/bidtype_resolver_test.go b/adapters/ortbbidder/resolver/bidtype_resolver_test.go new file mode 100644 index 00000000000..a3129f3b6e2 --- /dev/null +++ b/adapters/ortbbidder/resolver/bidtype_resolver_test.go @@ -0,0 +1,287 @@ +package resolver + +import ( + "testing" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +func TestBidtypeResolverGetFromORTBObject(t *testing.T) { + resolver := &bidTypeResolver{} + + t.Run("getFromORTBObject", func(t *testing.T) { + testCases := []struct { + name string + bid map[string]any + expectedValue any + expectedFound bool + }{ + { + name: "mtype found in bid", + bid: map[string]any{ + "mtype": 2.0, + }, + expectedValue: openrtb_ext.BidTypeVideo, + expectedFound: true, + }, + { + name: "mtype found in bid - invalid type", + bid: map[string]any{ + "mtype": "vide0", + }, + expectedValue: nil, + expectedFound: false, + }, + { + name: "mtype found in bid - invalid value", + bid: map[string]any{ + "mtype": 11, + }, + expectedValue: nil, + expectedFound: false, + }, + { + name: "mtype not found in bid", + bid: map[string]any{}, + expectedValue: nil, + expectedFound: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + value, found := resolver.getFromORTBObject(tc.bid) + assert.Equal(t, tc.expectedValue, value) + assert.Equal(t, tc.expectedFound, found) + }) + } + }) + +} + +func TestBidTypeResolverAutoDetect(t *testing.T) { + resolver := &bidTypeResolver{} + + t.Run("autoDetect", func(t *testing.T) { + testCases := []struct { + name string + bid map[string]any + request *openrtb2.BidRequest + expectedValue any + expectedFound bool + }{ + { + name: "Auto detect from imp - Video", + bid: map[string]any{ + "adm": "", + "impid": "123", + }, + request: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + { + ID: "123", + Video: &openrtb2.Video{}, + }, + }, + }, + expectedValue: openrtb_ext.BidTypeVideo, + expectedFound: true, + }, + { + name: "Auto detect from imp - banner", + bid: map[string]any{ + "adm": "", + "impid": "123", + }, + request: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + { + ID: "123", + Banner: &openrtb2.Banner{}, + }, + }, + }, + expectedValue: openrtb_ext.BidTypeBanner, + expectedFound: true, + }, + { + name: "Auto detect from imp - native", + bid: map[string]any{ + "adm": "", + "impid": "123", + }, + request: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + { + ID: "123", + Native: &openrtb2.Native{}, + }, + }, + }, + expectedValue: openrtb_ext.BidTypeNative, + expectedFound: true, + }, + { + name: "Auto detect from imp - multi format", + bid: map[string]any{ + "adm": "", + "impid": "123", + }, + request: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + { + ID: "123", + Banner: &openrtb2.Banner{}, + Video: &openrtb2.Video{}, + }, + }, + }, + expectedValue: openrtb_ext.BidType(""), + expectedFound: true, + }, + { + name: "Auto detect with Video Adm", + bid: map[string]any{ + "adm": "", + }, + expectedValue: openrtb_ext.BidTypeVideo, + expectedFound: true, + }, + { + name: "Auto detect with Native Adm", + bid: map[string]any{ + "adm": "{\"native\":{\"link\":{},\"assets\":[]}}", + }, + expectedValue: openrtb_ext.BidTypeNative, + expectedFound: true, + }, + { + name: "Auto detect with Banner Adm", + bid: map[string]any{ + "adm": "
Some HTML content
", + }, + expectedValue: openrtb_ext.BidTypeBanner, + expectedFound: true, + }, + { + name: "Auto detect with no Adm", + bid: map[string]any{}, + expectedValue: nil, + expectedFound: false, + }, + { + name: "Auto detect with empty Adm", + bid: map[string]any{ + "adm": "", + }, + expectedValue: nil, + expectedFound: false, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + value, found := resolver.autoDetect(tc.request, tc.bid) + assert.Equal(t, tc.expectedValue, value) + assert.Equal(t, tc.expectedFound, found) + }) + } + }) +} +func TestMtypeResolverSetValue(t *testing.T) { + resolver := &bidTypeResolver{} + + t.Run("setValue", func(t *testing.T) { + testCases := []struct { + name string + typeBid map[string]any + value any + expectedTypeBid map[string]any + }{ + { + name: "Set value in adapter bid", + typeBid: map[string]any{ + "id": "123", + }, + value: openrtb_ext.BidTypeVideo, + expectedTypeBid: map[string]any{ + "id": "123", + "BidType": openrtb_ext.BidTypeVideo, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + resolver.setValue(tc.typeBid, tc.value) + assert.Equal(t, tc.expectedTypeBid, tc.typeBid) + }) + } + }) +} +func TestGetMediaTypeFromAdm(t *testing.T) { + tests := []struct { + name string + adm string + expected openrtb_ext.BidType + }{ + { + name: "Video Adm", + adm: "", + expected: openrtb_ext.BidTypeVideo, + }, + { + name: "Native Adm", + adm: "{\"native\":{\"link\":{},\"assets\":[]}}", + expected: openrtb_ext.BidTypeNative, + }, + { + name: "Banner Adm", + adm: "
Some HTML content
", + expected: openrtb_ext.BidTypeBanner, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := getMediaTypeFromAdm(tt.adm) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestGetMediaType(t *testing.T) { + tests := []struct { + name string + mtype openrtb2.MarkupType + expectedBidType openrtb_ext.BidType + }{ + { + name: "MarkupBanner", + mtype: openrtb2.MarkupBanner, + expectedBidType: openrtb_ext.BidTypeBanner, + }, + { + name: "MarkupVideo", + mtype: openrtb2.MarkupVideo, + expectedBidType: openrtb_ext.BidTypeVideo, + }, + { + name: "MarkupAudio", + mtype: openrtb2.MarkupAudio, + expectedBidType: openrtb_ext.BidTypeAudio, + }, + { + name: "MarkupNative", + mtype: openrtb2.MarkupNative, + expectedBidType: openrtb_ext.BidTypeNative, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := convertToBidType(tt.mtype) + assert.Equal(t, tt.expectedBidType, result) + }) + } +} diff --git a/adapters/ortbbidder/resolver/param_resolver.go b/adapters/ortbbidder/resolver/param_resolver.go new file mode 100644 index 00000000000..7e319931c97 --- /dev/null +++ b/adapters/ortbbidder/resolver/param_resolver.go @@ -0,0 +1,86 @@ +package resolver + +import ( + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/adapters/ortbbidder/util" +) + +type parameter string + +func (s parameter) String() string { + return string(s) +} + +const ( + BidType parameter = "bidtype" + Duration parameter = "duration" + BidMeta parameter = "bidmeta" + Fledge parameter = "fledge" +) + +var ( + resolvers = resolverMap{ + BidType: &bidTypeResolver{}, + } +) + +type resolver interface { + getFromORTBObject(sourceNode map[string]any) (any, bool) + retrieveFromBidderParamLocation(responseNode map[string]any, path string) (any, bool) + autoDetect(request *openrtb2.BidRequest, sourceNode map[string]any) (any, bool) + setValue(targetNode map[string]any, value any) +} + +type resolverMap map[parameter]resolver + +type paramResolver struct { + bidderResponse map[string]any + request *openrtb2.BidRequest +} + +// New returns a new instance of paramResolver. +func New(request *openrtb2.BidRequest, bidderResponse map[string]any) *paramResolver { + return ¶mResolver{ + bidderResponse: bidderResponse, + request: request, + } +} + +// Resolve fetches a parameter value from sourceNode or bidderResponse and sets it in targetNode. +// The order of lookup is as follows: +// 1) ORTB standard field +// 2) Location from JSON file (bidder params) +// 3) Auto-detection +// If the value is found, it is set in the targetNode. +func (pr *paramResolver) Resolve(sourceNode, targetNode map[string]any, path string, param parameter) { + if sourceNode == nil || targetNode == nil || pr.bidderResponse == nil { + return + } + resolver, ok := resolvers[param] + if !ok { + return + } + + // get the value from the ORTB object + value, found := resolver.getFromORTBObject(sourceNode) + if !found { + // get the value from the bidder response using the location + value, found = resolver.retrieveFromBidderParamLocation(pr.bidderResponse, path) + if !found { + // auto detect value + value, found = resolver.autoDetect(pr.request, sourceNode) + if !found { + return + } + } + } + + resolver.setValue(targetNode, value) +} + +// valueResolver is a generic resolver to get values from the response node using location +type valueResolver struct{} + +func (r *valueResolver) retrieveFromBidderParamLocation(responseNode map[string]any, path string) (any, bool) { + return util.GetValueFromLocation(responseNode, path) +} diff --git a/adapters/ortbbidder/resolver/param_resolver_test.go b/adapters/ortbbidder/resolver/param_resolver_test.go new file mode 100644 index 00000000000..a7714898bfa --- /dev/null +++ b/adapters/ortbbidder/resolver/param_resolver_test.go @@ -0,0 +1,280 @@ +package resolver + +import ( + "testing" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +func TestResolveTypeBid(t *testing.T) { + testCases := []struct { + name string + bid map[string]any + typeBid map[string]any + bidderResponse map[string]any + location string + paramName parameter + expectedTypeBid map[string]any + request *openrtb2.BidRequest + }{ + { + name: "bid is nil, typeBid is nil, Response is nil", + bid: nil, + typeBid: nil, + bidderResponse: nil, + location: "", + paramName: "", + expectedTypeBid: nil, + }, + { + name: "bid is present, typeBid is nil, Response is present", + bid: map[string]any{ + "id": "123", + "ext": map[string]any{ + "bidtype": openrtb_ext.BidType("video"), + }, + }, + typeBid: nil, + bidderResponse: map[string]any{ + "cur": "USD", + "seatbid": []any{ + map[string]any{ + "bid": []any{ + map[string]any{ + "id": "123", + "ext": map[string]any{ + "bidtype": openrtb_ext.BidType("video"), + }, + }, + }, + }, + }, + }, + location: "seatbid.0.bid.0.ext.bidtype", + paramName: "bidtype", + expectedTypeBid: nil, + }, + { + name: "Invalid paramName", + bid: map[string]any{ + "id": "123", + "ext": map[string]any{ + "bidtype": openrtb_ext.BidType("video"), + }, + }, + typeBid: map[string]any{ + "Bid": map[string]any{ + "id": "123", + "mtype": float64(2), + }, + }, + bidderResponse: map[string]any{ + "cur": "USD", + "seatbid": []any{ + map[string]any{ + "bid": []any{ + map[string]any{ + "id": "123", + "ext": map[string]any{ + "bidtype": openrtb_ext.BidType("video"), + }, + }, + }, + }, + }, + }, + location: "seatbid.0.bid.0.ext.bidtype", + paramName: "paramName1", + expectedTypeBid: map[string]any{ + "Bid": map[string]any{ + "id": "123", + "mtype": float64(2), + }, + }, + }, + { + name: "Get paramName from the ortb bid object", + bid: map[string]any{ + "id": "123", + "mtype": float64(2), + }, + typeBid: map[string]any{ + "Bid": map[string]any{ + "id": "123", + "mtype": float64(2), + }, + }, + bidderResponse: map[string]any{ + "cur": "USD", + "seatbid": []any{ + map[string]any{ + "bid": []any{ + map[string]any{ + "id": "123", + "mtype": float64(2), + }, + }, + }, + }, + }, + location: "seatbid.0.bid.0.ext.bidtype", + paramName: "bidtype", + expectedTypeBid: map[string]any{ + "Bid": map[string]any{ + "id": "123", + "mtype": float64(2), + }, + "BidType": openrtb_ext.BidType("video"), + }, + }, + { + name: "Get paramName from the bidder paramName location", + bid: map[string]any{ + "id": "123", + "ext": map[string]any{ + "bidtype": openrtb_ext.BidType("video"), + }, + }, + typeBid: map[string]any{ + "Bid": map[string]any{ + "id": "123", + "ext": map[string]any{ + "bidtype": openrtb_ext.BidType("video"), + }, + }, + }, + bidderResponse: map[string]any{ + "cur": "USD", + "seatbid": []any{ + map[string]any{ + "bid": []any{ + map[string]any{ + "id": "123", + "ext": map[string]any{ + "bidtype": openrtb_ext.BidType("video"), + }, + }, + }, + }, + }, + }, + location: "seatbid.0.bid.0.ext.bidtype", + paramName: "bidtype", + expectedTypeBid: map[string]any{ + "Bid": map[string]any{ + "id": "123", + "ext": map[string]any{ + "bidtype": openrtb_ext.BidType("video"), + }, + }, + "BidType": openrtb_ext.BidType("video"), + }, + }, + { + name: "Auto detect", + bid: map[string]any{ + "id": "123", + "adm": "", + }, + typeBid: map[string]any{ + "Bid": map[string]any{ + "id": "123", + "adm": "", + }, + }, + bidderResponse: map[string]any{ + "cur": "USD", + "seatbid": []any{ + map[string]any{ + "bid": []any{ + map[string]any{ + "id": "123", + "adm": "", + }, + }, + }, + }, + }, + location: "seatbid.0.bid.0.ext.bidtype", + paramName: "bidtype", + expectedTypeBid: map[string]any{ + "Bid": map[string]any{ + "id": "123", + "adm": "", + }, + "BidType": openrtb_ext.BidType("video"), + }, + }, + // Todo add auto detec logic test case when it is implemented + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + pr := New(tc.request, tc.bidderResponse) + pr.Resolve(tc.bid, tc.typeBid, tc.location, tc.paramName) + assert.Equal(t, tc.expectedTypeBid, tc.typeBid) + }) + } +} + +func TestRetrieveFromBidderParamLocation(t *testing.T) { + testCases := []struct { + name string + ortbResponse map[string]any + path string + expectedValue any + expectedFound bool + }{ + { + name: "Found in location", + ortbResponse: map[string]any{ + "cur": "USD", + "seatbid": []any{ + map[string]any{ + "bid": []any{ + map[string]any{ + "id": "123", + "ext": map[string]any{ + "mtype": openrtb_ext.BidType("video"), + }, + }, + }, + }, + }, + }, + path: "seatbid.0.bid.0.ext.mtype", + expectedValue: openrtb_ext.BidType("video"), + expectedFound: true, + }, + { + name: "Not found in location", + ortbResponse: map[string]any{ + "cur": "USD", + "seatbid": []any{ + map[string]any{ + "bid": []any{ + map[string]any{ + "id": "123", + "ext": map[string]any{ + "mtype": openrtb_ext.BidType("video"), + }, + }, + }, + }, + }, + }, + path: "seatbid.0.bid.0.ext.nonexistent", + expectedValue: nil, + expectedFound: false, + }, + } + resolver := &valueResolver{} + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + value, found := resolver.retrieveFromBidderParamLocation(tc.ortbResponse, tc.path) + assert.Equal(t, tc.expectedValue, value) + assert.Equal(t, tc.expectedFound, found) + }) + } +} diff --git a/adapters/ortbbidder/response_builder.go b/adapters/ortbbidder/response_builder.go new file mode 100644 index 00000000000..e1b06e4636b --- /dev/null +++ b/adapters/ortbbidder/response_builder.go @@ -0,0 +1,102 @@ +package ortbbidder + +import ( + "encoding/json" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/adapters" + "github.com/prebid/prebid-server/v2/adapters/ortbbidder/bidderparams" + "github.com/prebid/prebid-server/v2/adapters/ortbbidder/resolver" + "github.com/prebid/prebid-server/v2/adapters/ortbbidder/util" + "github.com/prebid/prebid-server/v2/util/jsonutil" +) + +type responseBuilder struct { + bidderResponse map[string]any // Raw response from the bidder. + adapterRespone map[string]any // Response in the prebid format. + responseParams map[string]bidderparams.BidderParamMapper // Bidder response parameters. + request *openrtb2.BidRequest // Bid request. +} + +func newResponseBuilder(responseParams map[string]bidderparams.BidderParamMapper, request *openrtb2.BidRequest) *responseBuilder { + return &responseBuilder{ + responseParams: responseParams, + request: request, + } +} + +// setPrebidBidderResponse determines and construct adapters.BidderResponse and adapters.TypedBid object with the help +// of response parameter mappings defined in static/bidder-response-params +func (rb *responseBuilder) setPrebidBidderResponse(bidderResponseBytes json.RawMessage) error { + + err := jsonutil.UnmarshalValid(bidderResponseBytes, &rb.bidderResponse) + if err != nil { + return err + } + // Create a new ParamResolver with the bidder response. + paramResolver := resolver.New(rb.request, rb.bidderResponse) + // Initialize the adapter response with the currency from the bidder response. + adapterResponse := map[string]any{ + currencyKey: rb.bidderResponse[ortbCurrencyKey], + } + + // Resolve the adapter response level parameters. + paramMapper := rb.responseParams[resolver.Fledge.String()] + paramResolver.Resolve(rb.bidderResponse, adapterResponse, paramMapper.Location, resolver.Fledge) + + // Extract the seat bids from the bidder response. + seatBids, ok := rb.bidderResponse[seatBidKey].([]any) + if !ok { + return newBadServerResponseError("invalid seatbid array found in response, seatbids:[%v]", rb.bidderResponse[seatBidKey]) + } + // Initialize the list of type bids. + typeBids := make([]any, 0) + for seatIndex, seatBid := range seatBids { + seatBid, ok := seatBid.(map[string]any) + if !ok { + return newBadServerResponseError("invalid seatbid found in seatbid array, seatbid:[%v]", seatBids[seatIndex]) + } + bids, ok := seatBid[bidKey].([]any) + if !ok { + return newBadServerResponseError("invalid bid array found in seatbid, bids:[%v]", seatBid[bidKey]) + } + for bidIndex, bid := range bids { + bid, ok := bid.(map[string]any) + if !ok { + return newBadServerResponseError("invalid bid found in bids array, bid:[%v]", bids[bidIndex]) + } + // Initialize the type bid with the bid. + typeBid := map[string]any{ + typeBidKey: bid, + } + // Resolve the type bid level parameters. + paramMapper := rb.responseParams[resolver.BidType.String()] + location := util.ReplaceLocationMacro(paramMapper.Location, []int{seatIndex, bidIndex}) + paramResolver.Resolve(bid, typeBid, location, resolver.BidType) + + // Add the type bid to the list of type bids. + typeBids = append(typeBids, typeBid) + } + } + // Add the type bids to the adapter response. + adapterResponse[bidsKey] = typeBids + // Set the adapter response in the response builder. + rb.adapterRespone = adapterResponse + return nil +} + +// buildAdapterResponse converts the responseBuilder's adapter response to a prebid format. +// Returns the BidderResponse and any error encountered during the conversion. +func (rb *responseBuilder) buildAdapterResponse() (resp *adapters.BidderResponse, err error) { + var adapterResponeBytes json.RawMessage + adapterResponeBytes, err = jsonutil.Marshal(rb.adapterRespone) + if err != nil { + return + } + + err = jsonutil.UnmarshalValid(adapterResponeBytes, &resp) + if err != nil { + return nil, err + } + return +} diff --git a/adapters/ortbbidder/response_builder_test.go b/adapters/ortbbidder/response_builder_test.go new file mode 100644 index 00000000000..5af101f42f6 --- /dev/null +++ b/adapters/ortbbidder/response_builder_test.go @@ -0,0 +1,271 @@ +package ortbbidder + +import ( + "testing" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/adapters" + "github.com/prebid/prebid-server/v2/adapters/ortbbidder/bidderparams" + "github.com/prebid/prebid-server/v2/errortypes" + "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +func TestNewResponseBuilder(t *testing.T) { + testCases := []struct { + name string + request *openrtb2.BidRequest + responseParams map[string]bidderparams.BidderParamMapper + expected *responseBuilder + }{ + { + name: "With non-nil responseParams", + responseParams: map[string]bidderparams.BidderParamMapper{ + "test": {}, + }, + request: &openrtb2.BidRequest{}, + expected: &responseBuilder{ + responseParams: map[string]bidderparams.BidderParamMapper{ + "test": {}, + }, + request: &openrtb2.BidRequest{}, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := newResponseBuilder(tc.responseParams, tc.request) + assert.Equal(t, tc.expected, result) + }) + } +} + +func TestBuildAdapterResponse(t *testing.T) { + testCases := []struct { + name string + adapterResponse map[string]any + expectedResponse *adapters.BidderResponse + expectedError error + }{ + { + name: "Valid adapter response", + adapterResponse: map[string]any{ + "Currency": "USD", + "Bids": []any{ + map[string]any{ + "Bid": map[string]any{ + "id": "123", + "mtype": 2, + }, + "BidType": "video", + }, + }, + }, + expectedResponse: &adapters.BidderResponse{ + Currency: "USD", + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ + ID: "123", + MType: 2, + }, + BidType: "video", + }, + }, + }, + expectedError: nil, + }, + { + name: "Invalid adapter response - conversion failed", + adapterResponse: map[string]any{ + "Currency": "USD", + "Bids": map[string]any{ + "Bid": map[string]any{ + "id": "123", + "mtype": "video", + }, + "BidType": "video", + }, + }, + expectedResponse: nil, + expectedError: &errortypes.FailedToUnmarshal{ + Message: "cannot unmarshal adapters.BidderResponse.Bids: decode slice: expect [ or n, but found {", + }, + }, + { + name: "Invalid adapter response - marshal failed", + adapterResponse: map[string]any{ + "Currency": 123, // Invalid type + "Bids": map[string]any{ + "Bid": map[string]any{ + "id": "123", + }, + "BidType": make(chan int), + }, + }, + expectedResponse: nil, + expectedError: &errortypes.FailedToMarshal{Message: "chan int is unsupported type"}, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + rb := &responseBuilder{ + adapterRespone: tc.adapterResponse, + } + actualResponse, err := rb.buildAdapterResponse() + assert.Equal(t, tc.expectedError, err, "error mismatch") + assert.Equal(t, tc.expectedResponse, actualResponse, "response mismatch") + }) + } +} + +func TestSetPrebidBidderResponse(t *testing.T) { + testCases := []struct { + name string + bidderResponse map[string]any + bidderResponseBytes []byte + responseParams map[string]bidderparams.BidderParamMapper + expectedError error + expectedResponse map[string]any + }{ + { + name: "Invalid bidder response, unmarshal failure", + bidderResponseBytes: []byte(`{"id":"bid-resp-id","cur":"USD","seatbid":[{"seat":"test_bidder","bid":[{"id":"bid-1", "ext":{"mtype":"video"}}]}`), + responseParams: map[string]bidderparams.BidderParamMapper{ + "Currency": { + Location: "cur", + }, + }, + expectedError: &errortypes.FailedToUnmarshal{Message: "expect ] in the end, but found \x00"}, + }, + { + name: "Invalid seatbid object in response", + bidderResponseBytes: []byte(`{"id":"bid-resp-id","cur":"USD","seatbid":"invalid"}`), + responseParams: map[string]bidderparams.BidderParamMapper{ + "Currency": { + Location: "cur", + }, + }, + expectedError: &errortypes.BadServerResponse{Message: "invalid seatbid array found in response, seatbids:[invalid]"}, + }, + { + name: "Invalid seatbid is seatbid arrays", + bidderResponseBytes: []byte(`{"id":"bid-resp-id","cur":"USD","seatbid":["invalid"]}`), + responseParams: map[string]bidderparams.BidderParamMapper{ + "Currency": { + Location: "cur", + }, + }, + expectedError: &errortypes.BadServerResponse{Message: "invalid seatbid found in seatbid array, seatbid:[invalid]"}, + }, + { + name: "Invalid bid in seatbid", + bidderResponseBytes: []byte(`{"id":"bid-resp-id","cur":"USD","seatbid":[{"seat":"test_bidder","bid":"invalid"}]}`), + responseParams: map[string]bidderparams.BidderParamMapper{ + "Currency": { + Location: "cur", + }, + }, + expectedError: &errortypes.BadServerResponse{Message: "invalid bid array found in seatbid, bids:[invalid]"}, + }, + { + name: "Invalid bid in bids array", + bidderResponseBytes: []byte(`{"id":"bid-resp-id","cur":"USD","seatbid":[{"seat":"test_bidder","bid":["invalid"]}]}`), + responseParams: map[string]bidderparams.BidderParamMapper{ + "Currency": { + Location: "cur", + }, + }, + expectedError: &errortypes.BadServerResponse{Message: "invalid bid found in bids array, bid:[invalid]"}, + }, + { + name: "Valid bidder respone, no bidder params", + bidderResponseBytes: []byte(`{"id":"bid-resp-id","cur":"USD","seatbid":[{"seat":"test_bidder","bid":[{"id":"123"}]}]}`), + responseParams: map[string]bidderparams.BidderParamMapper{}, + expectedError: nil, + expectedResponse: map[string]any{ + "Currency": "USD", + "Bids": []any{ + map[string]any{ + "Bid": map[string]any{ + "id": "123", + }, + }, + }, + }, + }, + { + name: "Valid bidder respone, no bidder params - bidtype populated", + bidderResponseBytes: []byte(`{"id":"bid-resp-id","cur":"USD","seatbid":[{"seat":"test_bidder","bid":[{"id":"123","mtype": 2}]}]}`), + responseParams: map[string]bidderparams.BidderParamMapper{}, + expectedError: nil, + expectedResponse: map[string]any{ + "Currency": "USD", + "Bids": []any{ + map[string]any{ + "Bid": map[string]any{ + "id": "123", + "mtype": float64(2), + }, + "BidType": openrtb_ext.BidType("video"), + }, + }, + }, + }, + { + name: "Valid bidder respone, with bidder params", + bidderResponseBytes: []byte(`{"id":"bid-resp-id","cur":"USD","seatbid":[{"seat":"test_bidder","bid":[{"id":"123","ext": {"bidtype": "video"}}]}]}`), + bidderResponse: map[string]any{ + "cur": "USD", + seatBidKey: []any{ + map[string]any{ + "bid": []any{ + map[string]any{ + "id": "123", + "ext": map[string]any{ + "bidtype": "video", + }, + }, + }, + }, + }, + }, + responseParams: map[string]bidderparams.BidderParamMapper{ + "currency": { + Location: "cur", + }, + "bidtype": { + Location: "seatbid.#.bid.#.ext.bidtype", + }, + }, + expectedError: nil, + expectedResponse: map[string]any{ + "Currency": "USD", + "Bids": []any{ + map[string]any{ + "Bid": map[string]any{ + "id": "123", + "ext": map[string]any{ + "bidtype": "video", + }, + }, + "BidType": "video", + }, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + rb := &responseBuilder{ + bidderResponse: tc.bidderResponse, + responseParams: tc.responseParams, + } + err := rb.setPrebidBidderResponse(tc.bidderResponseBytes) + assert.Equal(t, tc.expectedError, err) + assert.Equal(t, tc.expectedResponse, rb.adapterRespone) + }) + } +} diff --git a/adapters/ortbbidder/singleRequestBuilder.go b/adapters/ortbbidder/single_request_builder.go similarity index 100% rename from adapters/ortbbidder/singleRequestBuilder.go rename to adapters/ortbbidder/single_request_builder.go diff --git a/adapters/ortbbidder/singleRequestBuilder_test.go b/adapters/ortbbidder/single_request_builder_test.go similarity index 100% rename from adapters/ortbbidder/singleRequestBuilder_test.go rename to adapters/ortbbidder/single_request_builder_test.go diff --git a/adapters/ortbbidder/util.go b/adapters/ortbbidder/util/util.go similarity index 52% rename from adapters/ortbbidder/util.go rename to adapters/ortbbidder/util/util.go index 1aa5881b073..9a40ba49e8f 100644 --- a/adapters/ortbbidder/util.go +++ b/adapters/ortbbidder/util/util.go @@ -1,7 +1,19 @@ -package ortbbidder +package util import ( "strconv" + "strings" +) + +const ( + impKey = "imp" + extKey = "ext" + bidderKey = "bidder" + appsiteKey = "appsite" + siteKey = "site" + appKey = "app" + owOrtbPrefix = "owortb_" + locationMacro = "#" ) /* @@ -10,6 +22,20 @@ The location is a string that specifies a path through the node hierarchy, separated by dots ('.'). The value can be any type, and the function will 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. +- 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}}} +*/ +/* +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, +separated by dots ('.'). The value can be any type, and the function will +create intermediate nodes as necessary if they do not exist. + Arguments: - node: the root of the map in which to set the value - location: slice of strings indicating the path to set the value. @@ -18,7 +44,7 @@ Arguments: Example: - location = imp.0.ext.adunitid; value = 123 ==> {"imp": {"ext" : {"adunitid":123}}} */ -func setValue(node map[string]any, location []string, value any) bool { +func SetValue(node map[string]any, location []string, value any) bool { if node == nil || value == nil { return false } @@ -79,3 +105,54 @@ func getNode(requestNode map[string]any, key string) any { } return requestNode[key] } + +// getValueFromLocation retrieves a value from a map based on a specified location. +// getValueFromLocation retrieves a value from a map based on a specified location. +func GetValueFromLocation(souce interface{}, path string) (interface{}, bool) { + location := strings.Split(path, ".") + var ( + ok bool + next interface{} = souce + ) + for _, loc := range location { + switch nxt := next.(type) { + case map[string]interface{}: + next, ok = nxt[loc] + if !ok { + return nil, false + } + case []interface{}: + index, err := strconv.Atoi(loc) + if err != nil { + return nil, false + } + if index < 0 || index >= len(nxt) { + return nil, false + } + next = nxt[index] + default: + return nil, false + } + } + return next, true +} + +func ReplaceLocationMacro(path string, array []int) string { + parts := strings.Split(path, ".") + j := 0 + for i, part := range parts { + if part == locationMacro { + if j >= len(array) { + break + } + parts[i] = strconv.Itoa(array[j]) + j++ + } + } + return strings.Join(parts, ".") +} + +// IsORTBBidder returns true if the bidder is an oRTB bidder +func IsORTBBidder(bidderName string) bool { + return strings.HasPrefix(bidderName, "owortb_") +} diff --git a/adapters/ortbbidder/util_test.go b/adapters/ortbbidder/util/util_test.go similarity index 72% rename from adapters/ortbbidder/util_test.go rename to adapters/ortbbidder/util/util_test.go index 46792c7aa77..c5773fd5134 100644 --- a/adapters/ortbbidder/util_test.go +++ b/adapters/ortbbidder/util/util_test.go @@ -1,8 +1,9 @@ -package ortbbidder +package util import ( "testing" + "github.com/prebid/prebid-server/v2/util/jsonutil" "github.com/stretchr/testify/assert" ) @@ -329,7 +330,7 @@ func TestSetValue(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := setValue(tt.args.requestNode, tt.args.location, tt.args.value) + 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") }) @@ -405,3 +406,131 @@ func TestGetNode(t *testing.T) { }) } } + +func TestReplaceLocationMacro(t *testing.T) { + tests := []struct { + name string + path string + array []int + expectedValuePath string + }{ + { + name: "Empty path", + path: "", + array: []int{0, 1}, + expectedValuePath: "", + }, + { + name: "Replace # in path with array", + path: "seatbid.#.bid.#.ext.mtype", + array: []int{0, 1}, + expectedValuePath: "seatbid.0.bid.1.ext.mtype", + }, + { + name: "Array length less than # count in path", + path: "seatbid.#.bid.#.ext.mtype", + array: []int{0}, + expectedValuePath: "seatbid.0.bid.#.ext.mtype", + }, + { + name: "No # in path", + path: "seatbid.bid.ext.mtype", + array: []int{0, 1}, + expectedValuePath: "seatbid.bid.ext.mtype", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ReplaceLocationMacro(tt.path, tt.array) + assert.Equal(t, tt.expectedValuePath, result) + }) + } +} + +func TestGetValueFromLocation(t *testing.T) { + + jsonToMap := func(jsonStr string) (result map[string]any) { + jsonutil.Unmarshal([]byte(jsonStr), &result) + return + } + + node := jsonToMap(`{"seatbid":[{"bid":[{"ext":{"mtype":"video"}}]}]}`) + + tests := []struct { + name string + node interface{} + path string + expectedValue interface{} + ok bool + }{ + { + name: "Node is empty", + node: nil, + path: "seatbid.0.bid.0.ext.mtype", + expectedValue: nil, + ok: false, + }, + { + name: "Path is empty", + node: node, + path: "", + expectedValue: nil, + ok: false, + }, + { + name: "Value is present in node", + node: node, + path: "seatbid.0.bid.0.ext.mtype", + expectedValue: "video", + ok: true, + }, + { + name: "Value is not present in node", + node: node, + path: "seatbid.0.bid.0.ext.mtype1", + expectedValue: nil, + ok: false, + }, + { + name: "Value is not present in node due to invalid index", + node: jsonToMap(`{"seatbid":[{"bid":[{"ext":{"mtype":"video"}}]}]}`), + path: "seatbid.0.bid.1.ext.mtype1", + expectedValue: nil, + ok: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, ok := GetValueFromLocation(tt.node, tt.path) + assert.Equal(t, tt.ok, ok) + assert.Equal(t, tt.expectedValue, result) + }) + } +} + +func TestIsORTBBidder(t *testing.T) { + tests := []struct { + name string + bidder string + expected bool + }{ + { + name: "ORTB bidder", + bidder: "owortb_test", + expected: true, + }, + { + name: "Non-ORTB bidder", + bidder: "test", + expected: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsORTBBidder(tt.bidder) + assert.Equal(t, tt.expected, result) + }) + } +} diff --git a/main_ow.go b/main_ow.go index ae0d3d2c353..d7ad9564935 100644 --- a/main_ow.go +++ b/main_ow.go @@ -5,11 +5,14 @@ import ( "github.com/prebid/prebid-server/v2/adapters/ortbbidder" ) -const paramsDirectory = "./static/bidder-params" +const ( + requestParamsDirectory = "./static/bidder-params" + resposnseParamsDirectory = "./static/bidder-response-params" +) // main_ow will perform the openwrap specific initialisation tasks func main_ow() { - err := ortbbidder.InitBidderParamsConfig(paramsDirectory) + err := ortbbidder.InitBidderParamsConfig(requestParamsDirectory, resposnseParamsDirectory) if err != nil { glog.Exitf("Unable to initialise bidder-param mapper for oRTB bidders: %v", err) } diff --git a/openrtb_ext/bidders_validate_test.go b/openrtb_ext/bidders_validate_test.go index eedd4c402f1..1592c7730d9 100644 --- a/openrtb_ext/bidders_validate_test.go +++ b/openrtb_ext/bidders_validate_test.go @@ -4,7 +4,7 @@ import ( "os" "testing" - "github.com/prebid/prebid-server/v2/adapters/ortbbidder" + "github.com/prebid/prebid-server/v2/adapters/ortbbidder/util" "github.com/stretchr/testify/assert" "github.com/xeipuuv/gojsonschema" ) @@ -51,7 +51,7 @@ func TestBidderUniquenessGatekeeping(t *testing.T) { // - Exclude duplicates of adapters for the same bidder, as it's unlikely a publisher will use both. var bidders []string for _, bidder := range CoreBidderNames() { - if bidder != BidderSilverPush && bidder != BidderTripleliftNative && bidder != BidderAdkernelAdn && bidder != "freewheel-ssp" && bidder != "yahooAdvertising" && !ortbbidder.IsORTBBidder(string(bidder.String())) { + if bidder != BidderSilverPush && bidder != BidderTripleliftNative && bidder != BidderAdkernelAdn && bidder != "freewheel-ssp" && bidder != "yahooAdvertising" && !util.IsORTBBidder(bidder.String()) { bidders = append(bidders, string(bidder)) } } diff --git a/static/bidder-response-params/owortb_testbidder.json b/static/bidder-response-params/owortb_testbidder.json new file mode 100644 index 00000000000..236c0323834 --- /dev/null +++ b/static/bidder-response-params/owortb_testbidder.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "testbidder (oRTB Integration) Adapter Params", + "description": "A schema which validates params accepted by the testbidder (oRTB Integration)", + "type": "object", + "properties": { + "bidtype": { + "type": "string", + "description": "bidtype", + "location": "seat.#.bid.#.ext.bidtype" + } + } + } \ No newline at end of file