Skip to content
198 changes: 198 additions & 0 deletions adapters/floxis/floxis.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
package floxis

import (
"fmt"
"net/http"
"net/url"
"text/template"

"github.com/prebid/openrtb/v20/openrtb2"
"github.com/prebid/prebid-server/v4/adapters"
"github.com/prebid/prebid-server/v4/config"
"github.com/prebid/prebid-server/v4/errortypes"
"github.com/prebid/prebid-server/v4/macros"
"github.com/prebid/prebid-server/v4/openrtb_ext"
"github.com/prebid/prebid-server/v4/util/jsonutil"
)

type adapter struct {
endpoint *template.Template
}

const (
defaultRegion = "us-e"
defaultPartner = "floxis"
)

func resolveBidHost(region, partner string) string {
if region == "" {
region = defaultRegion
}
if partner == "" {
partner = defaultPartner
}
if partner == defaultPartner {
return region
}
return partner + "-" + region
}

// Builder builds a new instance of the Floxis adapter for the given bidder with the given config.
func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
tmpl, err := template.New("endpointTemplate").Parse(config.Endpoint)
if err != nil {
return nil, fmt.Errorf("unable to parse endpoint url template: %v", err)
}
return &adapter{endpoint: tmpl}, nil
}

func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
ext, err := parseImpExt(request.Imp[0])
if err != nil {
return nil, []error{err}
}

for i := 1; i < len(request.Imp); i++ {
impExt, err := parseImpExt(request.Imp[i])
if err != nil {
return nil, []error{err}
}
if impExt.Seat != ext.Seat || impExt.Region != ext.Region || impExt.Partner != ext.Partner {
return nil, []error{&errortypes.BadInput{Message: fmt.Sprintf(
"imp %s seat/region/partner (%q/%q/%q) differs from imp %s (%q/%q/%q); split into separate requests",
request.Imp[i].ID, impExt.Seat, impExt.Region, impExt.Partner,
request.Imp[0].ID, ext.Seat, ext.Region, ext.Partner)}}
}
}
Comment thread
przemkaczmarek marked this conversation as resolved.
Outdated

endpoint, err := macros.ResolveMacros(a.endpoint, macros.EndpointTemplateParams{Host: resolveBidHost(ext.Region, ext.Partner)})
if err != nil {
return nil, []error{err}
}
uri := fmt.Sprintf("%s?seat=%s", endpoint, url.QueryEscape(ext.Seat))

body, err := jsonutil.Marshal(request)
if err != nil {
return nil, []error{err}
}

headers := http.Header{}
headers.Add("Content-Type", "application/json;charset=utf-8")
headers.Add("Accept", "application/json")

return []*adapters.RequestData{{
Method: "POST",
Uri: uri,
Body: body,
Headers: headers,
ImpIDs: openrtb_ext.GetImpIDs(request.Imp),
}}, nil
}

func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) {
if adapters.IsResponseStatusCodeNoContent(responseData) {
return nil, nil
}
if err := adapters.CheckResponseStatusCodeForErrors(responseData); err != nil {
return nil, []error{err}
}

var response openrtb2.BidResponse
if err := jsonutil.Unmarshal(responseData.Body, &response); err != nil {
return nil, []error{err}
}

var errs []error
bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp))
if response.Cur != "" {
bidResponse.Currency = response.Cur
}
for _, seatBid := range response.SeatBid {
for i := range seatBid.Bid {
bidType, typeErr := getMediaTypeForBid(request.Imp, seatBid.Bid[i])
if typeErr != nil {
errs = append(errs, typeErr)
continue
}
bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
Bid: &seatBid.Bid[i],
BidType: bidType,
})
}
}
return bidResponse, errs
}

func parseImpExt(imp openrtb2.Imp) (openrtb_ext.ExtImpFloxis, error) {
var bidderExt adapters.ExtImpBidder
if err := jsonutil.Unmarshal(imp.Ext, &bidderExt); err != nil {
return openrtb_ext.ExtImpFloxis{}, &errortypes.BadInput{Message: fmt.Sprintf("invalid imp.ext for imp %s: %s", imp.ID, err)}
}
var floxisExt openrtb_ext.ExtImpFloxis
if err := jsonutil.Unmarshal(bidderExt.Bidder, &floxisExt); err != nil {
return openrtb_ext.ExtImpFloxis{}, &errortypes.BadInput{Message: fmt.Sprintf("invalid imp.ext.bidder for imp %s: %s", imp.ID, err)}
}
return floxisExt, nil
}

func getMediaTypeForBid(imps []openrtb2.Imp, bid openrtb2.Bid) (openrtb_ext.BidType, error) {
if bid.MType != 0 {
switch bid.MType {
case openrtb2.MarkupBanner:
return openrtb_ext.BidTypeBanner, nil
case openrtb2.MarkupVideo:
return openrtb_ext.BidTypeVideo, nil
case openrtb2.MarkupAudio:
return openrtb_ext.BidTypeAudio, nil
case openrtb2.MarkupNative:
return openrtb_ext.BidTypeNative, nil
default:
return "", &errortypes.BadServerResponse{
Message: fmt.Sprintf("unsupported bid.mtype %d for impression %s", bid.MType, bid.ImpID),
}
}
}
for _, imp := range imps {
if imp.ID != bid.ImpID {
continue
}
formats, resolved := countFormats(imp)
switch {
case formats == 1:
return resolved, nil
case formats > 1:
return "", &errortypes.BadServerResponse{
Message: fmt.Sprintf("bid for multi-format imp %s requires bid.mtype to disambiguate", bid.ImpID),
}
default:
return "", &errortypes.BadServerResponse{
Message: fmt.Sprintf("unable to resolve media type for impression %s", bid.ImpID),
}
}
}
return "", &errortypes.BadServerResponse{
Message: fmt.Sprintf("unable to find impression %s for bid", bid.ImpID),
}
}

func countFormats(imp openrtb2.Imp) (int, openrtb_ext.BidType) {
formats := 0
var resolved openrtb_ext.BidType
if imp.Banner != nil {
formats++
resolved = openrtb_ext.BidTypeBanner
}
if imp.Video != nil {
formats++
resolved = openrtb_ext.BidTypeVideo
}
if imp.Audio != nil {
formats++
resolved = openrtb_ext.BidTypeAudio
}
if imp.Native != nil {
formats++
resolved = openrtb_ext.BidTypeNative
}
return formats, resolved
}
200 changes: 200 additions & 0 deletions adapters/floxis/floxis_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
package floxis

import (
"encoding/json"
"testing"
"text/template"

"github.com/prebid/openrtb/v20/openrtb2"
"github.com/prebid/prebid-server/v4/adapters"
"github.com/prebid/prebid-server/v4/adapters/adapterstest"
"github.com/prebid/prebid-server/v4/config"
"github.com/prebid/prebid-server/v4/openrtb_ext"
"github.com/stretchr/testify/assert"
)

func TestJsonSamples(t *testing.T) {
bidder, buildErr := Builder(openrtb_ext.BidderFloxis, config.Adapter{
Endpoint: "https://{{.Host}}.floxis.tech/pbs"},
config.Server{ExternalUrl: "http://hosturl.com", DataCenter: "2"})

if buildErr != nil {
t.Fatalf("Builder returned unexpected error %v", buildErr)
}

adapterstest.RunJSONBidderTest(t, "floxistest", bidder)
}

func newAdapter() *adapter {
return &adapter{endpoint: template.Must(template.New("endpointTemplate").Parse("https://{{.Host}}.floxis.tech/pbs"))}
}

func bannerImp(ext string) openrtb2.Imp {
return openrtb2.Imp{
ID: "imp-1",
Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}},
Ext: json.RawMessage(ext),
}
}

func TestResolveBidHost(t *testing.T) {
cases := []struct {
region string
partner string
want string
}{
{"us-e", "floxis", "us-e"},
{"eu", "floxis", "eu"},
{"apac", "floxis", "apac"},
{"", "", "us-e"},
{"", "floxis", "us-e"},
{"eu", "", "eu"},
{"mars", "floxis", "mars"},
{"us-e", "acme", "acme-us-e"},
{"eu", "acme", "acme-eu"},
}
for _, c := range cases {
assert.Equal(t, c.want, resolveBidHost(c.region, c.partner), "region %q partner %q", c.region, c.partner)
}
}

func TestSeatIsURLEscaped(t *testing.T) {
req := &openrtb2.BidRequest{
ID: "req-1",
Imp: []openrtb2.Imp{bannerImp(`{"bidder":{"seat":"a b&c","region":"eu"}}`)},
Site: &openrtb2.Site{ID: "271"},
}
reqData, errs := newAdapter().MakeRequests(req, &adapters.ExtraRequestInfo{})
assert.Empty(t, errs)
assert.Len(t, reqData, 1)
assert.Equal(t, "https://eu.floxis.tech/pbs?seat=a+b%26c", reqData[0].Uri)
}

func TestPartnerPrefixesHost(t *testing.T) {
req := &openrtb2.BidRequest{
ID: "req-1",
Imp: []openrtb2.Imp{bannerImp(`{"bidder":{"seat":"abc","region":"us-e","partner":"acme"}}`)},
Site: &openrtb2.Site{ID: "271"},
}
reqData, errs := newAdapter().MakeRequests(req, &adapters.ExtraRequestInfo{})
assert.Empty(t, errs)
assert.Equal(t, "https://acme-us-e.floxis.tech/pbs?seat=abc", reqData[0].Uri)
}

func TestValidNonStandardRegionPassesThrough(t *testing.T) {
req := &openrtb2.BidRequest{
ID: "req-1",
Imp: []openrtb2.Imp{bannerImp(`{"bidder":{"seat":"abc","region":"mars"}}`)},
Site: &openrtb2.Site{ID: "271"},
}
reqData, errs := newAdapter().MakeRequests(req, &adapters.ExtraRequestInfo{})
assert.Empty(t, errs)
assert.Equal(t, "https://mars.floxis.tech/pbs?seat=abc", reqData[0].Uri)
}

func TestMissingRegionDefaultsToUSE(t *testing.T) {
req := &openrtb2.BidRequest{
ID: "req-1",
Imp: []openrtb2.Imp{bannerImp(`{"bidder":{"seat":"abc"}}`)},
Site: &openrtb2.Site{ID: "271"},
}
reqData, errs := newAdapter().MakeRequests(req, &adapters.ExtraRequestInfo{})
assert.Empty(t, errs)
assert.Equal(t, "https://us-e.floxis.tech/pbs?seat=abc", reqData[0].Uri)
}

func TestInvalidImpExt(t *testing.T) {
req := &openrtb2.BidRequest{
ID: "req-1",
Imp: []openrtb2.Imp{{ID: "imp-1", Banner: &openrtb2.Banner{}, Ext: json.RawMessage(`"not-an-object"`)}},
Site: &openrtb2.Site{ID: "271"},
}
_, errs := newAdapter().MakeRequests(req, &adapters.ExtraRequestInfo{})
assert.Len(t, errs, 1)
assert.Contains(t, errs[0].Error(), "imp.ext")
}

func TestCallerRequestNotMutated(t *testing.T) {
imp := openrtb2.Imp{
ID: "imp-1",
Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}},
Ext: json.RawMessage(`{"bidder":{"seat":"abc","region":"eu"}}`),
}
req := &openrtb2.BidRequest{
ID: "req-1",
Imp: []openrtb2.Imp{imp},
Site: &openrtb2.Site{ID: "271", Ext: json.RawMessage(`{"amp":0}`)},
}
before, _ := json.Marshal(req)

_, errs := newAdapter().MakeRequests(req, &adapters.ExtraRequestInfo{})
assert.Empty(t, errs)

after, _ := json.Marshal(req)
assert.JSONEq(t, string(before), string(after), "caller's request must not be mutated")
assert.Nil(t, req.Imp[0].Secure, "caller's imp[0].Secure must not be mutated")
}

func TestGetMediaTypeForBidByMType(t *testing.T) {
cases := []struct {
mtype openrtb2.MarkupType
want openrtb_ext.BidType
}{
{openrtb2.MarkupBanner, openrtb_ext.BidTypeBanner},
{openrtb2.MarkupVideo, openrtb_ext.BidTypeVideo},
{openrtb2.MarkupAudio, openrtb_ext.BidTypeAudio},
{openrtb2.MarkupNative, openrtb_ext.BidTypeNative},
}
for _, c := range cases {
bt, err := getMediaTypeForBid(nil, openrtb2.Bid{ImpID: "x", MType: c.mtype})
assert.NoError(t, err)
assert.Equal(t, c.want, bt)
}
}

func TestGetMediaTypeForBidUnsupportedMType(t *testing.T) {
_, err := getMediaTypeForBid(nil, openrtb2.Bid{ImpID: "x", MType: openrtb2.MarkupType(99)})
assert.Error(t, err)
assert.Contains(t, err.Error(), "unsupported bid.mtype")
}

func TestGetMediaTypeForBidSingleFormatFallback(t *testing.T) {
imps := []openrtb2.Imp{
{ID: "b", Banner: &openrtb2.Banner{}},
{ID: "v", Video: &openrtb2.Video{}},
{ID: "a", Audio: &openrtb2.Audio{}},
{ID: "n", Native: &openrtb2.Native{}},
}
expected := map[string]openrtb_ext.BidType{
"b": openrtb_ext.BidTypeBanner,
"v": openrtb_ext.BidTypeVideo,
"a": openrtb_ext.BidTypeAudio,
"n": openrtb_ext.BidTypeNative,
}
for impID, want := range expected {
bt, err := getMediaTypeForBid(imps, openrtb2.Bid{ImpID: impID})
assert.NoError(t, err, impID)
assert.Equal(t, want, bt, impID)
}
}

func TestGetMediaTypeForBidMultiFormatNeedsMType(t *testing.T) {
imps := []openrtb2.Imp{{ID: "m", Banner: &openrtb2.Banner{}, Video: &openrtb2.Video{}}}
_, err := getMediaTypeForBid(imps, openrtb2.Bid{ImpID: "m"})
assert.Error(t, err)
assert.Contains(t, err.Error(), "requires bid.mtype to disambiguate")
}

func TestGetMediaTypeForBidImpWithoutFormat(t *testing.T) {
imps := []openrtb2.Imp{{ID: "x"}}
_, err := getMediaTypeForBid(imps, openrtb2.Bid{ImpID: "x"})
assert.Error(t, err)
assert.Contains(t, err.Error(), "unable to resolve media type")
}

func TestGetMediaTypeForBidUnknownImp(t *testing.T) {
imps := []openrtb2.Imp{{ID: "x", Banner: &openrtb2.Banner{}}}
_, err := getMediaTypeForBid(imps, openrtb2.Bid{ImpID: "missing"})
assert.Error(t, err)
assert.Contains(t, err.Error(), "unable to find impression")
}
Loading
Loading