diff --git a/adapters/orbidder/orbidder.go b/adapters/orbidder/orbidder.go index 77985c8dae0..f2dda85e290 100644 --- a/adapters/orbidder/orbidder.go +++ b/adapters/orbidder/orbidder.go @@ -3,13 +3,13 @@ package orbidder import ( "encoding/json" "fmt" - "net/http" - "github.com/mxmCherry/openrtb/v15/openrtb2" "github.com/prebid/prebid-server/adapters" "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/errortypes" "github.com/prebid/prebid-server/openrtb_ext" + "net/http" + "strings" ) type OrbidderAdapter struct { @@ -18,30 +18,11 @@ type OrbidderAdapter struct { // MakeRequests makes the HTTP requests which should be made to fetch bids from orbidder. func (rcv *OrbidderAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { - var errs []error - var validImps []openrtb2.Imp - - // check if imps exists, if not return error and do send request to orbidder. - if len(request.Imp) == 0 { - return nil, []error{&errortypes.BadInput{ - Message: "No impressions in request", - }} - } - - // validate imps - for _, imp := range request.Imp { - if err := preprocess(&imp); err != nil { - errs = append(errs, err) - continue - } - validImps = append(validImps, imp) - } - + validImps, errs := getValidImpressions(request, reqInfo) if len(validImps) == 0 { return nil, errs } - //set imp array to only valid imps request.Imp = validImps requestBodyJSON, err := json.Marshal(request) @@ -55,14 +36,34 @@ func (rcv *OrbidderAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo * headers.Add("Accept", "application/json") return []*adapters.RequestData{{ - Method: "POST", + Method: http.MethodPost, Uri: rcv.endpoint, Body: requestBodyJSON, Headers: headers, }}, errs } -func preprocess(imp *openrtb2.Imp) error { +// getValidImpressions validate imps and check for bid floor currency. Convert to EUR if necessary +func getValidImpressions(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]openrtb2.Imp, []error) { + var errs []error + var validImps []openrtb2.Imp + + for _, imp := range request.Imp { + if err := preprocessBidFloorCurrency(&imp, reqInfo); err != nil { + errs = append(errs, err) + continue + } + + if err := preprocessExtensions(&imp); err != nil { + errs = append(errs, err) + continue + } + validImps = append(validImps, imp) + } + return validImps, errs +} + +func preprocessExtensions(imp *openrtb2.Imp) error { var bidderExt adapters.ExtImpBidder if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { return &errortypes.BadInput{ @@ -80,8 +81,21 @@ func preprocess(imp *openrtb2.Imp) error { return nil } +func preprocessBidFloorCurrency(imp *openrtb2.Imp, reqInfo *adapters.ExtraRequestInfo) error { + // we expect every currency related data to be EUR + if imp.BidFloor > 0 && strings.ToUpper(imp.BidFloorCur) != "EUR" && imp.BidFloorCur != "" { + if convertedValue, err := reqInfo.ConvertCurrency(imp.BidFloor, imp.BidFloorCur, "EUR"); err != nil { + return err + } else { + imp.BidFloor = convertedValue + } + } + imp.BidFloorCur = "EUR" + return nil +} + // MakeBids unpacks server response into Bids. -func (rcv OrbidderAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { +func (rcv OrbidderAdapter) MakeBids(_ *openrtb2.BidRequest, _ *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { if response.StatusCode == http.StatusNoContent { return nil, nil } @@ -125,7 +139,7 @@ func (rcv OrbidderAdapter) MakeBids(internalRequest *openrtb2.BidRequest, extern } // Builder builds a new instance of the Orbidder adapter for the given bidder with the given config. -func Builder(bidderName openrtb_ext.BidderName, config config.Adapter) (adapters.Bidder, error) { +func Builder(_ openrtb_ext.BidderName, config config.Adapter) (adapters.Bidder, error) { bidder := &OrbidderAdapter{ endpoint: config.Endpoint, } diff --git a/adapters/orbidder/orbidder_test.go b/adapters/orbidder/orbidder_test.go index 0eaed23a971..bbda1c06223 100644 --- a/adapters/orbidder/orbidder_test.go +++ b/adapters/orbidder/orbidder_test.go @@ -2,6 +2,10 @@ package orbidder import ( "encoding/json" + "errors" + "github.com/mxmCherry/openrtb/v15/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/stretchr/testify/mock" "testing" "github.com/prebid/prebid-server/adapters/adapterstest" @@ -22,6 +26,16 @@ func TestUnmarshalOrbidderExtImp(t *testing.T) { }, impExt) } +func TestPreprocessExtensions(t *testing.T) { + for name, tc := range testCasesExtension { + t.Run(name, func(t *testing.T) { + imp := tc.imp + err := preprocessExtensions(&imp) + tc.assertError(t, err) + }) + } +} + func TestJsonSamples(t *testing.T) { bidder, buildErr := Builder(openrtb_ext.BidderOrbidder, config.Adapter{ Endpoint: "https://orbidder-test"}) @@ -32,3 +46,136 @@ func TestJsonSamples(t *testing.T) { adapterstest.RunJSONBidderTest(t, "orbiddertest", bidder) } + +var testCasesCurrency = map[string]struct { + imp openrtb2.Imp + setMock func(m *mock.Mock) + expectedImp openrtb2.Imp + assertError func(t assert.TestingT, err error, msgAndArgs ...interface{}) bool +}{ + "EUR: no bidfloor, no currency": { + imp: openrtb2.Imp{ + BidFloor: 0, + BidFloorCur: "", + }, + setMock: func(m *mock.Mock) {}, + expectedImp: openrtb2.Imp{ + BidFloor: 0, + BidFloorCur: "EUR", + }, + assertError: assert.NoError, + }, + "EUR: bidfloor, no currency": { + imp: openrtb2.Imp{ + BidFloor: 1, + BidFloorCur: "", + }, + setMock: func(m *mock.Mock) {}, + expectedImp: openrtb2.Imp{ + BidFloor: 1, + BidFloorCur: "EUR", + }, + assertError: assert.NoError, + }, + "EUR: bidfloor and currency": { + imp: openrtb2.Imp{ + BidFloor: 1, + BidFloorCur: "EUR", + }, + setMock: func(m *mock.Mock) {}, + expectedImp: openrtb2.Imp{ + BidFloor: 1, + BidFloorCur: "EUR", + }, + assertError: assert.NoError, + }, + "USD: bidfloor with currency": { + imp: openrtb2.Imp{ + BidFloor: 1, + BidFloorCur: "USD", + }, + setMock: func(m *mock.Mock) { + m.On("GetRate", "USD", "EUR").Return(2.5, nil) + }, + expectedImp: openrtb2.Imp{ + BidFloor: 2.5, + BidFloorCur: "EUR", + }, + assertError: assert.NoError, + }, + "USD: no bidfloor": { + imp: openrtb2.Imp{ + BidFloor: 0, + BidFloorCur: "USD", + }, + setMock: func(m *mock.Mock) {}, + expectedImp: openrtb2.Imp{ + BidFloor: 0, + BidFloorCur: "EUR", + }, + assertError: assert.NoError, + }, + "ABC: invalid currency code": { + imp: openrtb2.Imp{ + BidFloor: 1, + BidFloorCur: "ABC", + }, + setMock: func(m *mock.Mock) { + m.On("GetRate", "ABC", "EUR").Return(0.0, errors.New("currency conversion error")) + }, + expectedImp: openrtb2.Imp{ + BidFloor: 1, + BidFloorCur: "ABC", + }, + assertError: assert.Error, + }, +} + +var testCasesExtension = map[string]struct { + imp openrtb2.Imp + assertError func(t assert.TestingT, err error, msgAndArgs ...interface{}) bool +}{ + "Valid Orbidder Extension": { + imp: openrtb2.Imp{ + Ext: json.RawMessage(`{"bidder":{"accountId":"orbidder-test", "placementId":"center-banner", "bidfloor": 0.1}}`), + }, + assertError: assert.NoError, + }, + "Invalid Orbidder Extension": { + imp: openrtb2.Imp{ + Ext: json.RawMessage(`{"there's'":{"something":"strange", "in the":"neighbourhood", "who you gonna call?": 0.1}}`), + }, + assertError: assert.Error, + }, +} + +func TestPreprocessBidFloorCurrency(t *testing.T) { + for name, tc := range testCasesCurrency { + t.Run(name, func(t *testing.T) { + imp := tc.imp + mockConversions := &mockCurrencyConversion{} + tc.setMock(&mockConversions.Mock) + extraRequestInfo := adapters.ExtraRequestInfo{ + CurrencyConversions: mockConversions, + } + err := preprocessBidFloorCurrency(&imp, &extraRequestInfo) + assert.True(t, mockConversions.AssertExpectations(t)) + tc.assertError(t, err) + assert.Equal(t, tc.expectedImp, imp) + }) + } +} + +type mockCurrencyConversion struct { + mock.Mock +} + +func (m *mockCurrencyConversion) GetRate(from string, to string) (float64, error) { + args := m.Called(from, to) + return args.Get(0).(float64), args.Error(1) +} + +func (m *mockCurrencyConversion) GetRates() *map[string]map[string]float64 { + args := m.Called() + return args.Get(0).(*map[string]map[string]float64) +} diff --git a/adapters/orbidder/orbiddertest/exemplary/simple-app-banner.json b/adapters/orbidder/orbiddertest/exemplary/simple-app-banner.json index 8697bff3a92..2315a52c130 100644 --- a/adapters/orbidder/orbiddertest/exemplary/simple-app-banner.json +++ b/adapters/orbidder/orbiddertest/exemplary/simple-app-banner.json @@ -51,6 +51,7 @@ } ] }, + "bidfloorcur": "EUR", "ext": { "bidder": { "accountId": "orbidder-test", diff --git a/adapters/orbidder/orbiddertest/supplemental/dsp-bad-request-example.json b/adapters/orbidder/orbiddertest/supplemental/dsp-bad-request-example.json index 69496c4ff3f..dc199d24ead 100644 --- a/adapters/orbidder/orbiddertest/supplemental/dsp-bad-request-example.json +++ b/adapters/orbidder/orbiddertest/supplemental/dsp-bad-request-example.json @@ -51,6 +51,7 @@ } ] }, + "bidfloorcur": "EUR", "ext": { "bidder": { "accountId": "orbidder-test", diff --git a/adapters/orbidder/orbiddertest/supplemental/dsp-bad-response-example.json b/adapters/orbidder/orbiddertest/supplemental/dsp-bad-response-example.json index d1c57a54a9e..61e70d315f7 100644 --- a/adapters/orbidder/orbiddertest/supplemental/dsp-bad-response-example.json +++ b/adapters/orbidder/orbiddertest/supplemental/dsp-bad-response-example.json @@ -51,6 +51,7 @@ } ] }, + "bidfloorcur": "EUR", "ext": { "bidder": { "accountId": "orbidder-test", diff --git a/adapters/orbidder/orbiddertest/supplemental/dsp-internal-server-error-example.json b/adapters/orbidder/orbiddertest/supplemental/dsp-internal-server-error-example.json index 20ea36ab38c..66a766a9261 100644 --- a/adapters/orbidder/orbiddertest/supplemental/dsp-internal-server-error-example.json +++ b/adapters/orbidder/orbiddertest/supplemental/dsp-internal-server-error-example.json @@ -51,6 +51,7 @@ } ] }, + "bidfloorcur": "EUR", "ext": { "bidder": { "accountId": "orbidder-test", diff --git a/adapters/orbidder/orbiddertest/supplemental/dsp-invalid-accountid-example.json b/adapters/orbidder/orbiddertest/supplemental/dsp-invalid-accountid-example.json index 6bc0482dd0c..7944db44bce 100644 --- a/adapters/orbidder/orbiddertest/supplemental/dsp-invalid-accountid-example.json +++ b/adapters/orbidder/orbiddertest/supplemental/dsp-invalid-accountid-example.json @@ -51,6 +51,7 @@ } ] }, + "bidfloorcur": "EUR", "ext": { "bidder": { "accountId": "orbidder-test", diff --git a/adapters/orbidder/orbiddertest/supplemental/empty-imp-request-error.json b/adapters/orbidder/orbiddertest/supplemental/empty-imp-request-error.json deleted file mode 100644 index 0c5cf6d2faa..00000000000 --- a/adapters/orbidder/orbiddertest/supplemental/empty-imp-request-error.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "mockBidRequest": { - "id": "test-request-id", - "imp": [ - ], - "app": { - "bundle": "com.prebid" - }, - "device": { - "ifa":"87857b31-8942-4646-ae80-ab9c95bf3fab" - } - }, - "expectedMakeRequestsErrors": [ - { - "value": "No impressions in request", - "comparison": "literal" - } - ] -} diff --git a/adapters/orbidder/orbiddertest/supplemental/no-content-response.json b/adapters/orbidder/orbiddertest/supplemental/no-content-response.json index f3b1b287da7..0a1e5f0fc4d 100644 --- a/adapters/orbidder/orbiddertest/supplemental/no-content-response.json +++ b/adapters/orbidder/orbiddertest/supplemental/no-content-response.json @@ -51,6 +51,7 @@ } ] }, + "bidfloorcur": "EUR", "ext": { "bidder": { "accountId": "orbidder-test", diff --git a/adapters/orbidder/orbiddertest/supplemental/valid-and-invalid-imps.json b/adapters/orbidder/orbiddertest/supplemental/valid-and-invalid-imps.json index b6db9f48ee3..1ea133ab1d3 100644 --- a/adapters/orbidder/orbiddertest/supplemental/valid-and-invalid-imps.json +++ b/adapters/orbidder/orbiddertest/supplemental/valid-and-invalid-imps.json @@ -57,6 +57,7 @@ } ] }, + "bidfloorcur": "EUR", "ext": { "bidder": { "accountId": "orbidder-test",