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 #4117

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
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
}
129 changes: 129 additions & 0 deletions adapters/pixfuture/pixfuture.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package pixfuture

import (
"encoding/json"
"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 adapter 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) {
return &adapter{
endpoint: config.Endpoint,
}, nil
}

// MakeRequests prepares and serializes HTTP requests to be sent to the Pixfuture endpoint.
func (a *adapter) MakeRequests(bidRequest *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
var errs []error

// Validate the bid request
if bidRequest == nil || len(bidRequest.Imp) == 0 {
errs = append(errs, fmt.Errorf("no valid impressions in bid request"))
return nil, errs
}

// Process impressions
var validImpressions []openrtb2.Imp
for _, imp := range bidRequest.Imp {
if imp.Banner == nil && imp.Video == nil {
errs = append(errs, fmt.Errorf("unsupported impression type for impID: %s", imp.ID))
continue
}
validImpressions = append(validImpressions, imp)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This check is not required here as this is being checked in the core codebase already. Here is the link -

if req.LenImp() < 1 {
return []error{errors.New("request.imp must contain at least one element.")}
}


if len(validImpressions) == 0 {
errs = append(errs, fmt.Errorf("no valid impressions after filtering"))
return nil, errs
}

// Create the outgoing request
bidRequest.Imp = validImpressions
body, err := json.Marshal(bidRequest)
if err != nil {
errs = append(errs, fmt.Errorf("failed to marshal bid request: %v", err))
return nil, errs
}

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

return []*adapters.RequestData{request}, errs
}

// 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 *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) {
// Handle No Content response
if adapters.IsResponseStatusCodeNoContent(responseData) {
return nil, nil
}

// Check for errors in response status code
if err := adapters.CheckResponseStatusCodeForErrors(responseData); err != nil {
return nil, []error{err}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This is correct but you could also use the common code as below -

if adapters.IsResponseStatusCodeNoContent(responseData) {
		return nil, nil
}

if err := adapters.CheckResponseStatusCodeForErrors(responseData); err != nil {
		return nil, []error{err}
}


// Parse the response body
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
}

// Set the MType explicitly in the bid
//mType := openrtb2.MType(bidType)
//seatBid.Bid[i].MType = mType

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: "https://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 TestAdapter_MakeRequests(t *testing.T) {
adapter := &adapter{endpoint: "https://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, "https://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 TestAdapter_MakeBids(t *testing.T) {
adapter := &adapter{}

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"
}
}
24 changes: 24 additions & 0 deletions adapters/pixfuture/testdata/valid-bid-response.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"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"
}
Loading
Loading