diff --git a/adapters/pwbid/params_test.go b/adapters/pwbid/params_test.go new file mode 100644 index 00000000000..e16fd13c4dc --- /dev/null +++ b/adapters/pwbid/params_test.go @@ -0,0 +1,49 @@ +package pwbid + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/openrtb_ext" +) + +func TestValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json schema. %v", err) + } + + for _, p := range validParams { + if err := validator.Validate(openrtb_ext.BidderPWBid, json.RawMessage(p)); err != nil { + t.Errorf("Schema rejected valid params: %s", p) + } + } +} + +func TestInvalidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json schema. %v", err) + } + + for _, p := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderPWBid, json.RawMessage(p)); err == nil { + t.Errorf("Schema allowed invalid params: %s", p) + } + } +} + +var validParams = []string{ + `{"siteId":"39f43a","bidFloor":0.10,"isTest":false}`, + `{"siteId":"39f43a","bidFloor":0.10,"isTest":true}`, + `{"siteId":"39f43a","bidFloor":0.10}`, + `{"siteId":"39f43a"}`, +} + +var invalidParams = []string{ + `{"siteId":42,"bidFloor":"asdf","isTest":123}`, + `{"siteId":}`, + `{"bidFloor":}`, + `{"bidFloor":0.10}`, + `null`, +} diff --git a/adapters/pwbid/pwbid.go b/adapters/pwbid/pwbid.go new file mode 100644 index 00000000000..bcb8c137b98 --- /dev/null +++ b/adapters/pwbid/pwbid.go @@ -0,0 +1,97 @@ +package pwbid + +import ( + "encoding/json" + "fmt" + + "github.com/prebid/openrtb/v17/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/prebid/prebid-server/util/httputil" +) + +type adapter struct { + endpoint string +} + +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + bidder := &adapter{ + endpoint: config.Endpoint, + } + + return bidder, nil +} + +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + requestJSON, err := json.Marshal(request) + if err != nil { + return nil, []error{err} + } + + requestData := &adapters.RequestData{ + Method: "POST", + Uri: a.endpoint, + Body: requestJSON, + } + + return []*adapters.RequestData{requestData}, nil +} + +func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) { + var errors []error + + if httputil.IsResponseStatusCodeNoContent(responseData) { + return nil, nil + } + + if err := httputil.CheckResponseStatusCodeForErrors(responseData); err != nil { + return nil, []error{err} + } + + var response openrtb2.BidResponse + if err := json.Unmarshal(responseData.Body, &response); err != nil { + return nil, []error{err} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp)) + + for _, seatBid := range response.SeatBid { + for i, bid := range seatBid.Bid { + bidType, typeErr := getMediaTypeForBid(request.Imp, bid) + if typeErr != nil { + errors = append(errors, typeErr) + continue + } + + b := &adapters.TypedBid{ + Bid: &seatBid.Bid[i], + BidType: bidType, + } + bidResponse.Bids = append(bidResponse.Bids, b) + } + } + + return bidResponse, errors +} + +func getMediaTypeForBid(impressions []openrtb2.Imp, bid openrtb2.Bid) (openrtb_ext.BidType, error) { + for _, impression := range impressions { + if impression.ID == bid.ImpID { + if impression.Banner != nil { + return openrtb_ext.BidTypeBanner, nil + } + if impression.Native != nil { + return openrtb_ext.BidTypeNative, nil + } + if impression.Video != nil { + return openrtb_ext.BidTypeVideo, nil + } + } + } + + return "", &errortypes.BadServerResponse{ + Message: fmt.Sprintf("The impression with ID %s is not present into the request", bid.ImpID), + } +} diff --git a/adapters/pwbid/pwbid_test.go b/adapters/pwbid/pwbid_test.go new file mode 100644 index 00000000000..21a4194249e --- /dev/null +++ b/adapters/pwbid/pwbid_test.go @@ -0,0 +1,21 @@ +package pwbid + +import ( + "testing" + + "github.com/prebid/prebid-server/adapters/adapterstest" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder(openrtb_ext.BidderPWBid, config.Adapter{ + Endpoint: "https://bid.pubwise.io/prebid"}, + config.Server{ExternalUrl: "http://hosturl.com", GvlID: 842, DataCenter: "2"}) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "pwbidtest", bidder) +} diff --git a/adapters/pwbid/pwbidtest/exemplary/banner.json b/adapters/pwbid/pwbidtest/exemplary/banner.json new file mode 100644 index 00000000000..ba618cb8cf1 --- /dev/null +++ b/adapters/pwbid/pwbidtest/exemplary/banner.json @@ -0,0 +1,93 @@ +{ + "mockBidRequest": { + "id": "test-request-id-banner", + "imp": [ + { + "id": "test-imp-id-banner", + "banner": { + "format": [ + { + "w": 728, + "h": 90 + } + ] + }, + "ext": { + "bidder": { + "siteId": "3943fa" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://bid.pubwise.io/prebid", + "body": { + "id": "test-request-id-banner", + "imp": [ + { + "id": "test-imp-id-banner", + "banner": { + "format": [ + { + "w": 728, + "h": 90 + } + ] + }, + "ext": { + "bidder": { + "siteId": "3943fa" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id-banner", + "seatbid": [ + { + "seat": "pwbid", + "bid": [ + { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id-banner", + "price": 0.500000, + "adm": "some-test-ad-banner", + "crid": "crid_10", + "w": 728, + "h": 90 + } + ] + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id-banner", + "price": 0.5, + "adm": "some-test-ad-banner", + "crid": "crid_10", + "w": 728, + "h": 90 + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/pwbid/pwbidtest/exemplary/native.json b/adapters/pwbid/pwbidtest/exemplary/native.json new file mode 100644 index 00000000000..907c16d467a --- /dev/null +++ b/adapters/pwbid/pwbidtest/exemplary/native.json @@ -0,0 +1,81 @@ +{ + "mockBidRequest": { + "id": "test-request-id-native", + "imp": [ + { + "id": "test-imp-id-native", + "native": { + "request": "" + }, + "ext": { + "bidder": { + "siteId": "39f43a" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://bid.pubwise.io/prebid", + "body": { + "id": "test-request-id-native", + "imp": [ + { + "id": "test-imp-id-native", + "native": { + "request": "" + }, + "ext": { + "bidder": { + "siteId": "39f43a" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id-native", + "seatbid": [ + { + "seat": "pwbid", + "bid": [{ + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id-native", + "price": 0.500000, + "adm": "some-test-ad-native", + "crid": "crid_10", + "w": 728, + "h": 90 + }] + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id-native", + "price": 0.5, + "adm": "some-test-ad-native", + "crid": "crid_10", + "w": 728, + "h": 90 + }, + "type": "native" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/pwbid/pwbidtest/exemplary/optional-params.json b/adapters/pwbid/pwbidtest/exemplary/optional-params.json new file mode 100644 index 00000000000..a080be90208 --- /dev/null +++ b/adapters/pwbid/pwbidtest/exemplary/optional-params.json @@ -0,0 +1,97 @@ +{ + "mockBidRequest": { + "id": "test-request-id-banner", + "imp": [ + { + "id": "test-imp-id-banner", + "banner": { + "format": [ + { + "w": 728, + "h": 90 + } + ] + }, + "ext": { + "bidder": { + "siteId": "3943fa", + "bidFloor": 0.10, + "isTest": false + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://bid.pubwise.io/prebid", + "body": { + "id": "test-request-id-banner", + "imp": [ + { + "id": "test-imp-id-banner", + "banner": { + "format": [ + { + "w": 728, + "h": 90 + } + ] + }, + "ext": { + "bidder": { + "siteId": "3943fa", + "bidFloor": 0.10, + "isTest": false + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id-banner", + "seatbid": [ + { + "seat": "pwbid", + "bid": [ + { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id-banner", + "price": 0.500000, + "adm": "some-test-ad-banner", + "crid": "crid_10", + "w": 728, + "h": 90 + } + ] + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id-banner", + "price": 0.5, + "adm": "some-test-ad-banner", + "crid": "crid_10", + "w": 728, + "h": 90 + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/pwbid/pwbidtest/exemplary/video.json b/adapters/pwbid/pwbidtest/exemplary/video.json new file mode 100644 index 00000000000..b74c780d0a9 --- /dev/null +++ b/adapters/pwbid/pwbidtest/exemplary/video.json @@ -0,0 +1,91 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-video-id", + "video": { + "mimes": ["video/mp4"], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "siteId": "39f43a" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://bid.pubwise.io/prebid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-video-id", + "video": { + "mimes": ["video/mp4"], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "siteId": "39f43a" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id-test-video-id", + "seatbid": [ + { + "seat": "test-seat", + "bid": [ + { + "id": "5dce6055-a93c-1fd0-8c29-14afc3e510fd", + "impid": "test-video-id", + "price": 0.1529, + "nurl": "test-win", + "adm": "test-video", + "adid": "92-288", + "adomain": ["advertiserdomain.com"], + "crid": "288", + "w": 300, + "h": 250 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "5dce6055-a93c-1fd0-8c29-14afc3e510fd", + "impid": "test-video-id", + "price": 0.1529, + "nurl": "test-win", + "adm": "test-video", + "adid": "92-288", + "adomain": ["advertiserdomain.com"], + "crid": "288", + "w": 300, + "h": 250 + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/pwbid/pwbidtest/supplemental/response-200-without-body.json b/adapters/pwbid/pwbidtest/supplemental/response-200-without-body.json new file mode 100644 index 00000000000..146ba93a27d --- /dev/null +++ b/adapters/pwbid/pwbidtest/supplemental/response-200-without-body.json @@ -0,0 +1,51 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [{"w": 728, "h": 90}] + }, + "ext": { + "bidder": { + "siteId": "39f43a" + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://bid.pubwise.io/prebid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [{"w": 728, "h": 90}] + }, + "ext": { + "bidder": { + "siteId": "39f43a" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200 + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "unexpected end of JSON input", + "comparison": "literal" + } + ] +} diff --git a/adapters/pwbid/pwbidtest/supplemental/response-204.json b/adapters/pwbid/pwbidtest/supplemental/response-204.json new file mode 100644 index 00000000000..5fff7ee32cc --- /dev/null +++ b/adapters/pwbid/pwbidtest/supplemental/response-204.json @@ -0,0 +1,46 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [{"w": 728, "h": 90}] + }, + "ext": { + "bidder": { + "siteId": "39f43a" + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://bid.pubwise.io/prebid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [{"w": 728, "h": 90}] + }, + "ext": { + "bidder": { + "siteId": "39f43a" + } + } + } + ] + } + }, + "mockResponse": { + "status": 204 + } + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/pwbid/pwbidtest/supplemental/response-400.json b/adapters/pwbid/pwbidtest/supplemental/response-400.json new file mode 100644 index 00000000000..d594e571243 --- /dev/null +++ b/adapters/pwbid/pwbidtest/supplemental/response-400.json @@ -0,0 +1,51 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [{"w": 728, "h": 90}] + }, + "ext": { + "bidder": { + "siteId": "39f43a" + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://bid.pubwise.io/prebid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [{"w": 728, "h": 90}] + }, + "ext": { + "bidder": { + "siteId": "39f43a" + } + } + } + ] + } + }, + "mockResponse": { + "status": 400 + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 400. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/adapters/pwbid/pwbidtest/supplemental/response-500.json b/adapters/pwbid/pwbidtest/supplemental/response-500.json new file mode 100644 index 00000000000..fa3d4d063a8 --- /dev/null +++ b/adapters/pwbid/pwbidtest/supplemental/response-500.json @@ -0,0 +1,51 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [{"w": 728, "h": 90}] + }, + "ext": { + "bidder": { + "siteId": "39f43a" + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://bid.pubwise.io/prebid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [{"w": 728, "h": 90}] + }, + "ext": { + "bidder": { + "siteId": "39f43a" + } + } + } + ] + } + }, + "mockResponse": { + "status": 500 + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 500. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go index d3536a1be1d..ef052a2ff89 100755 --- a/exchange/adapter_builders.go +++ b/exchange/adapter_builders.go @@ -123,6 +123,7 @@ import ( "github.com/prebid/prebid-server/adapters/pubmatic" "github.com/prebid/prebid-server/adapters/pubnative" "github.com/prebid/prebid-server/adapters/pulsepoint" + "github.com/prebid/prebid-server/adapters/pwbid" "github.com/prebid/prebid-server/adapters/revcontent" "github.com/prebid/prebid-server/adapters/rhythmone" "github.com/prebid/prebid-server/adapters/richaudience" @@ -302,6 +303,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderPubmatic: pubmatic.Builder, openrtb_ext.BidderPubnative: pubnative.Builder, openrtb_ext.BidderPulsepoint: pulsepoint.Builder, + openrtb_ext.BidderPWBid: pwbid.Builder, openrtb_ext.BidderQuantumdex: apacdex.Builder, openrtb_ext.BidderRevcontent: revcontent.Builder, openrtb_ext.BidderRhythmone: rhythmone.Builder, diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index a8a7db830c1..2a0efd097eb 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -215,6 +215,7 @@ const ( BidderPubmatic BidderName = "pubmatic" BidderPubnative BidderName = "pubnative" BidderPulsepoint BidderName = "pulsepoint" + BidderPWBid BidderName = "pwbid" BidderQuantumdex BidderName = "quantumdex" BidderRevcontent BidderName = "revcontent" BidderRhythmone BidderName = "rhythmone" @@ -398,6 +399,7 @@ func CoreBidderNames() []BidderName { BidderPubmatic, BidderPubnative, BidderPulsepoint, + BidderPWBid, BidderQuantumdex, BidderRevcontent, BidderRhythmone, diff --git a/openrtb_ext/imp_pwbid.go b/openrtb_ext/imp_pwbid.go new file mode 100644 index 00000000000..d3f0da05dd6 --- /dev/null +++ b/openrtb_ext/imp_pwbid.go @@ -0,0 +1,7 @@ +package openrtb_ext + +type ExtImpPwbid struct { + SiteId string `json:"siteId"` + BidFloor float32 `json:"bidFloor"` + IsTest bool `json:"isTest"` +} diff --git a/static/bidder-info/pwbid.yaml b/static/bidder-info/pwbid.yaml new file mode 100644 index 00000000000..ac6872738b4 --- /dev/null +++ b/static/bidder-info/pwbid.yaml @@ -0,0 +1,20 @@ +endpoint: "https://bid.pubwise.io/prebid" +maintainer: + email: info@pubwise.io +gvlVendorID: 842 +capabilities: + app: + mediaTypes: + - banner + - native + - video + site: + mediaTypes: + - banner + - native + - video +userSync: + # PubWise supports user syncing, but requires configuration by the host. contact this + # bidder directly at the email address in this file to ask about enabling user sync. + supports: + - redirect \ No newline at end of file diff --git a/static/bidder-params/pwbid.json b/static/bidder-params/pwbid.json new file mode 100644 index 00000000000..e973d156223 --- /dev/null +++ b/static/bidder-params/pwbid.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "PubWise (pwbid) Adapter Params", + "description": "A schema which validates params accepted by the PubWise (pwbid) adapter", + "type": "object", + "properties": { + "siteId": { + "type": "string", + "description": "The site ID provided by the PubWise system" + }, + "bidFloor": { + "type": "number", + "description": "Value to pass as the bidfloor for this bid" + }, + "isTest": { + "type": "boolean", + "description": "A boolean to indicate 100% fill test placement request" + } + }, + "required": ["siteId"] +}