Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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