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 14 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
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,24.2 1 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:27.137,28.27 1 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:28.27,30.3 1 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:31.2,32.16 2 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:32.16,34.3 1 0
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:36.2,44.50 2 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:48.72,49.20 1 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:49.20,52.41 3 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:52.41,54.4 1 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:57.2,59.3 1 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:63.166,64.53 1 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:64.53,66.3 1 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:68.2,68.54 1 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:68.54,72.3 1 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:74.2,74.46 1 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:74.46,78.3 1 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:80.2,81.73 2 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:81.73,83.3 1 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:85.2,89.43 4 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:89.43,90.35 1 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:90.35,92.18 2 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:92.18,94.13 2 0
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:96.4,99.6 1 1
github.com/prebid/prebid-server/v3/adapters/pixfuture/pixfuture.go:103.2,103.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
}
104 changes: 104 additions & 0 deletions adapters/pixfuture/pixfuture.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
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 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(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"}}
}
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.")}
}

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 *adapter) 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),
}}
}
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}
}


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: "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