Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Adapter: Pixfuture #4105

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
25 changes: 25 additions & 0 deletions adapters/pixfuture/coverage.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
mode: set
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:20.119,25.2 2 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:28.146,29.27 1 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:29.27,31.3 1 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:32.2,33.16 2 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:33.16,35.3 1 0
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:37.2,45.50 2 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:49.72,50.20 1 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:50.20,53.41 3 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:53.41,55.4 1 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:58.2,60.3 1 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:64.175,65.53 1 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:65.53,67.3 1 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:69.2,69.54 1 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:69.54,73.3 1 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:75.2,75.46 1 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:75.46,79.3 1 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:81.2,82.73 2 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:82.73,84.3 1 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:86.2,90.43 4 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:90.43,91.35 1 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:91.35,93.18 2 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:93.18,95.13 2 0
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:97.4,100.6 1 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:104.2,104.28 1 1
50 changes: 50 additions & 0 deletions adapters/pixfuture/params_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package pixfuture

import (
"fmt"
"testing"

"github.com/prebid/prebid-server/v3/openrtb_ext"
"github.com/stretchr/testify/assert"
)

func TestPixfutureParams(t *testing.T) {
testCases := []struct {
name string
params openrtb_ext.ImpExtPixfuture
expectedError string
}{
{
name: "Valid Params",
params: openrtb_ext.ImpExtPixfuture{
PlacementID: "123",
},
expectedError: "",
},
{
name: "Missing PlacementID",
params: openrtb_ext.ImpExtPixfuture{
PlacementID: "",
},
expectedError: "PlacementID is required",
},
}

for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
err := validatePixfutureParams(test.params)
if test.expectedError == "" {
assert.NoError(t, err)
} else {
assert.EqualError(t, err, test.expectedError)
}
})
}
}

func validatePixfutureParams(params openrtb_ext.ImpExtPixfuture) error {
if params.PlacementID == "" {
return fmt.Errorf("PlacementID is required")
}
return nil
}
105 changes: 105 additions & 0 deletions adapters/pixfuture/pixfuture.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package pixfuture

import (
"fmt"
"net/http"

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

type PixfutureAdapter struct {
endpoint string
}

// Builder builds a new instance of the Pixfuture adapter.
func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
bidder := &PixfutureAdapter{

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can call this simply "adapter", the PixfutureAdapter identification is already supplied by the package name. As you have it, referencing your adapter from outside the package would be PixfutureAdapter.PixfutureAdapter which looks a little redundant. See example below:

  package foo

  type adapter struct {
    endpoint string
  }
  
  func  Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
    return &adapter{endpoint: "https://www.foo.com"}, nil
  }

endpoint: config.Endpoint,
}
return bidder, nil
}

// MakeRequests prepares and serializes HTTP requests to be sent to the Pixfuture endpoint.
func (a *PixfutureAdapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
if len(request.Imp) == 0 {
return nil, []error{&errortypes.BadInput{Message: "No impressions in the bid request"}}
}
requestJSON, err := jsonutil.Marshal(request)
if err != nil {
return nil, []error{err}
}

requestData := &adapters.RequestData{
Method: "POST",
Uri: a.endpoint,
Body: requestJSON,
Headers: http.Header{
"Content-Type": []string{"application/json"},
},
}
return []*adapters.RequestData{requestData}, nil
}

// getMediaTypeForBid extracts the bid type based on the bid extension data.
func getMediaTypeForBid(bid openrtb2.Bid) (openrtb_ext.BidType, error) {
if bid.Ext != nil {
var bidExt openrtb_ext.ExtBid
err := jsonutil.Unmarshal(bid.Ext, &bidExt)
if err == nil && bidExt.Prebid != nil {
return openrtb_ext.ParseBidType(string(bidExt.Prebid.Type))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider this as a suggestion. Prebid server expects the media type to be explicitly set in the adapter response. Therefore, recommends implementing a pattern where the adapter server sets the MType field in the response to accurately determine the media type for the impression.

}
}

return "", &errortypes.BadServerResponse{
Message: fmt.Sprintf("Failed to parse impression \"%s\" mediatype", bid.ImpID),
}
}

// MakeBids parses the HTTP response from the Pixfuture endpoint and generates a BidderResponse.
func (a *PixfutureAdapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) {
if responseData.StatusCode == http.StatusNoContent {
return nil, nil
}

if responseData.StatusCode == http.StatusBadRequest {
return nil, []error{&errortypes.BadInput{
Message: "Unexpected status code: 400. Bad request from publisher. Run with request.debug = 1 for more info.",
}}
}

if responseData.StatusCode != http.StatusOK {
return nil, []error{&errortypes.BadServerResponse{
Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info.", responseData.StatusCode),
}}
}

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

bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp))
bidResponse.Currency = response.Cur
var errors []error

for _, seatBid := range response.SeatBid {
for i, bid := range seatBid.Bid {
bidType, err := getMediaTypeForBid(bid)
if err != nil {
errors = append(errors, err)
continue
}
bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
Bid: &seatBid.Bid[i],
BidType: bidType,
})
}
}

return bidResponse, errors
}
167 changes: 167 additions & 0 deletions adapters/pixfuture/pixfuture_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package pixfuture

import (
"encoding/json"
"net/http"
"testing"

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

func TestBuilder(t *testing.T) {
adapter, err := Builder("pixfuture", config.Adapter{Endpoint: "http://mock-endpoint.com"}, config.Server{})
assert.NoError(t, err, "unexpected error during Builder execution")
assert.NotNil(t, adapter, "expected a non-nil adapter instance")
}

func TestPixfutureAdapter_MakeRequests(t *testing.T) {
adapter := &PixfutureAdapter{endpoint: "http://mock-pixfuture-endpoint.com"}

t.Run("Valid Request", func(t *testing.T) {
bidRequest := &openrtb2.BidRequest{
ID: "test-request-id",
Imp: []openrtb2.Imp{
{
ID: "test-imp-id",
Banner: &openrtb2.Banner{W: int64Ptr(300), H: int64Ptr(250)},
Ext: jsonRawExt(`{"bidder":{"siteId":"123"}}`),
},
},
}

requests, errs := adapter.MakeRequests(bidRequest, nil)
assert.Empty(t, errs, "unexpected errors in MakeRequests")
assert.Equal(t, 1, len(requests), "expected exactly one request")

request := requests[0]
assert.Equal(t, "POST", request.Method, "unexpected HTTP method")
assert.Equal(t, "http://mock-pixfuture-endpoint.com", request.Uri, "unexpected request URI")
assert.Contains(t, string(request.Body), `"id":"test-request-id"`, "unexpected request body")
assert.Equal(t, "application/json", request.Headers.Get("Content-Type"), "unexpected content-type")
})

t.Run("No Impressions", func(t *testing.T) {
bidRequest := &openrtb2.BidRequest{ID: "test-request-id"}

requests, errs := adapter.MakeRequests(bidRequest, nil)
assert.NotEmpty(t, errs, "expected error for request with no impressions")
assert.Nil(t, requests, "expected no requests for request with no impressions")
})

t.Run("Malformed BidRequest", func(t *testing.T) {
bidRequest := &openrtb2.BidRequest{}

requests, errs := adapter.MakeRequests(bidRequest, nil)
assert.NotEmpty(t, errs, "expected error for malformed request")
assert.Nil(t, requests, "expected no requests for malformed request")
})
}

func TestPixfutureAdapter_MakeBids(t *testing.T) {
adapter := &PixfutureAdapter{}

t.Run("Valid Response", func(t *testing.T) {
responseData := &adapters.ResponseData{
StatusCode: http.StatusOK,
Body: []byte(`{
"id": "test-response-id",
"seatbid": [{
"bid": [{
"id": "test-bid-id",
"impid": "test-imp-id",
"price": 1.23,
"adm": "<html>Ad Content</html>",
"crid": "creative-123",
"w": 300,
"h": 250,
"ext": {"prebid":{"type":"banner"}}
}]
}],
"cur": "USD"
}`),
}

bidRequest := &openrtb2.BidRequest{ID: "test-request-id", Imp: []openrtb2.Imp{{ID: "test-imp-id"}}}
bidResponse, errs := adapter.MakeBids(bidRequest, nil, responseData)

assert.Empty(t, errs, "unexpected errors in MakeBids")
assert.NotNil(t, bidResponse, "expected bid response")
assert.Equal(t, "USD", bidResponse.Currency, "unexpected currency")
assert.Equal(t, 1, len(bidResponse.Bids), "expected one bid")

bid := bidResponse.Bids[0]
assert.Equal(t, "test-bid-id", bid.Bid.ID, "unexpected bid ID")
assert.Equal(t, "test-imp-id", bid.Bid.ImpID, "unexpected impression ID")
assert.Equal(t, 1.23, bid.Bid.Price, "unexpected bid price")
assert.Equal(t, openrtb_ext.BidTypeBanner, bid.BidType, "unexpected bid type")
})

t.Run("No Content Response", func(t *testing.T) {
responseData := &adapters.ResponseData{StatusCode: http.StatusNoContent}
bidRequest := &openrtb2.BidRequest{}
bidResponse, errs := adapter.MakeBids(bidRequest, nil, responseData)
assert.Nil(t, bidResponse, "expected no bid response")
assert.Empty(t, errs, "unexpected errors for no content response")
})

t.Run("Bad Request Response", func(t *testing.T) {
responseData := &adapters.ResponseData{StatusCode: http.StatusBadRequest}
bidRequest := &openrtb2.BidRequest{}
bidResponse, errs := adapter.MakeBids(bidRequest, nil, responseData)
assert.Nil(t, bidResponse, "expected no bid response")
assert.NotEmpty(t, errs, "expected errors for bad request response")
})

t.Run("Unexpected Status Code", func(t *testing.T) {
responseData := &adapters.ResponseData{StatusCode: http.StatusInternalServerError}
bidRequest := &openrtb2.BidRequest{}
bidResponse, errs := adapter.MakeBids(bidRequest, nil, responseData)
assert.Nil(t, bidResponse, "expected no bid response")
assert.NotEmpty(t, errs, "expected errors for unexpected status code")
})

t.Run("Malformed Response Body", func(t *testing.T) {
responseData := &adapters.ResponseData{
StatusCode: http.StatusOK,
Body: []byte(`malformed response`),
}
bidRequest := &openrtb2.BidRequest{}
bidResponse, errs := adapter.MakeBids(bidRequest, nil, responseData)
assert.Nil(t, bidResponse, "expected no bid response")
assert.NotEmpty(t, errs, "expected errors for malformed response body")
})
}

func TestGetMediaTypeForBid(t *testing.T) {
t.Run("Valid Bid Ext", func(t *testing.T) {
bid := openrtb2.Bid{
ID: "test-bid",
Ext: json.RawMessage(`{"prebid":{"type":"banner"}}`),
}
bidType, err := getMediaTypeForBid(bid)
assert.NoError(t, err, "unexpected error in getMediaTypeForBid")
assert.Equal(t, openrtb_ext.BidTypeBanner, bidType, "unexpected bid type")
})

t.Run("Invalid Bid Ext", func(t *testing.T) {
bid := openrtb2.Bid{
ID: "test-bid",
Ext: json.RawMessage(`{"invalid":"data"}`),
}
bidType, err := getMediaTypeForBid(bid)
assert.Error(t, err, "expected error for invalid bid ext")
assert.Equal(t, openrtb_ext.BidType(""), bidType, "expected empty bid type for invalid bid ext")
})
}

func int64Ptr(i int64) *int64 {
return &i
}

func jsonRawExt(jsonStr string) json.RawMessage {
return json.RawMessage(jsonStr)
}
24 changes: 24 additions & 0 deletions adapters/pixfuture/testdata/valid-bid-request.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"id": "test-request-id",
"imp": [
{
"id": "test-imp-id",
"banner": {
"w": 300,
"h": 250
},
"ext": {
"bidder": {
"siteId": "123"
}
}
}
],
"site": {
"page": "http://example.com"
},
"device": {
"ua": "Mozilla/5.0",
"ip": "192.168.0.1"
}
}
Loading
Loading